1.单一职责(SRP)
定义
单一职责原则(Single Responsibility Principle)中的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。简单来说:一个类只负责一项职责。
优点
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点:
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性高了,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
出现的原因
比如一个类T负责两个不同的职责:职责P1、职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。这种问题的出现就是因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
因此在设计一个类 的时候,可以先从粗粒度的类开始设计,等到业务发展到一定规模,我们发现这个粗粒度的类方法和属性太多,且经常修改的时候,我们就可以对这个类进行重构了,将这个类拆分为粒度更细的类,这就是所谓的持续重构。
比如:类T只负责一个职责P,这样的设计时符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1、P2,这时候如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。这样做的风险在于职责扩散的不确定性,因为我们不会想到这个职责P,在未来可能会扩展为P1、P2、P3、P4…Pn。所以记住,在职责扩散到我们无法控制的程度之前,立刻对代码进行重构。
例子
1.类单一职责和方法单一职责
一个类描述动物呼吸这个场景。
运行结果:
程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrrestrial,水生动物Aquatic,代码如下:
运行结果:
此时发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
可以看到,这种修改方式要简单的多。但是却存在这隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”、“牛”、“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式指直接在方法级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。
还有一种修改方式:
可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为他并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一种呢实这个比较难说,需要根据实际情况来确定。如果逻辑足够简单,可以在方法内部违反单一职责原则,如果类中方法数量足够少,可以在方法毕节上违反单一职责原则。
2.接口单一职责
初始接口里面既有获取信息行为,又有对课程的操作行为。
我们就可以对接口中的行为进行分类,使得每个接口都具有单一职责。实现类可以实现多个接口就行了。
在JDK中的应用
接口单一职责:
在JDK1.8中的新特性CompletableFuture就实现了两个接口
CompletableFuture功能是进行异步操作与获取返回结果。
其中CompletionStage接口定义的是异步操作的触发与操作函数,future接口定义的是返回结果相关的函数。
类单一职责:
atomic相关类,包装类,都是类单一职责的体现。
2.里氏替换原则(LSP)
定义
里氏替换原则(Liskov Substitution Principle)是面向对象设计的基本原则之一。里氏替换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能在基类的基础上增加新的行为。
可以解刨为以下描述:
- 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
- 所有引用基类的地方必须能透明地使用其子类的对象。
通俗来讲:子类可以扩展父类的功能,但不能改变父类原有的功能。
优点
- 克服了继承中重写父类造成的可复用性变差的缺点。
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展新,降低需求变更时引入的风险。
出现的原因
有一功能P1,由类A完成,现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
解决方法:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法,父类可以使用final的手段强制子类来遵守。
总结来说:
1.子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
2.子类中可以增加自己特有的方法
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
4.当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类的方法更严格或相等。
例子
1.子类实现时可以增加自己特有的方法,但是不能覆盖父类的非抽象方法
类A完成两数相减的功能
运行结果:
后来,我们需要增加一个新功能:完成两数相加,然后再与100求和,由类B来负责。
即类B需要完成两个功能:
1.两数相减。
2.两数相加,然后再加100。
由于类A已经实现了第一个功能【两数相减】,所以类B继承类A后,只需要再完成第二个功能【两数相加,然后再加100】就可以了。
类B完成后,运行结果:
可以发现,原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。
在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合、组合更关系代替。
2.子类方法入参要比父类宽松或相等,输出/返回值要比父类严格或相等。
鸟一般都会飞行,如燕子的飞行速度大概是每小时120千米,但是新西兰的几维鸟由于翅膀退化无法飞行。现在设计一个实例,计算这两种鸟飞行300千米要花费的时间。
运行结果:
使用燕子进行测试的时候,结果正确,能计算出所需要的时间。
但是等使用几维鸟的时候,却发现出现问题了
运行结果:
这个问题就出在几维鸟的对飞行速度的设置不符合父类对飞行速度的要求。父类对飞行速度的要求肯定是大于0的,而子类却设置为0,不符合子类对父类属性的操作要和父类相等或更严格这一准则。
此时应该取消几维鸟原来的继承关系,定义鸟和几维鸟一般的父类,如动物类。他们都有前进的速度。几维鸟飞行速度虽然为0,但是奔跑速度不为0。
在JDK中的应用
1.抽象类中的非抽象非私有方法都用final修饰
JUC包下的抽象类,例如AbstractQueuedSynchronizer抽象类里面的所有非抽象方法,不是final修饰,就是private修饰。
2.子类方法入参和父类相等或者更宽松,子类输出/返回和父类相等或者更严格。
基于AbstractQueuedSynchronizer实现的锁对state锁资源的获取与释放的要求比AbstractQueuedSynchronizer更严格。
3.依赖倒置原则(DIP)
定义
依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖于具体。实现开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则的关键是抽象化,并且从抽象化导出具体化实现,如果说开闭原则是面向对象设计的目标的话,那么依赖倒置原则就是面向对象设计的主要手段。
总体来说:高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
通俗来说:要求对抽象编程,不要对实现进行编程,这样就降低了客户与实现模块之间的耦合。
优点
- 降低类间的耦合性。
- 提高系统的稳定性。
- 依减少并行开发引起的风险。
- 提高代码的可读性和可维护性。
出现的原因
类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是底层模块,负责基本的原子操作;加入修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置原则的核心思想是面向接口编程。
总体来说,在实际编程中遵循以下四点,就能满足这个原则:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
例子
母亲给孩子讲故事,只要给她一本书,她就可以照着书该孩子讲故事。
运行结果:
上述是面向实现的编程,即依赖的是Book这个具体的实现类;看起来功能都很OK,也没有什么问题。
假如有一天,需求变成这样:不是给书而是给一份 纸,让这位母亲讲一下 纸上的故事, 纸的代码如下:
而这位母亲却办不到,因为她不会读 纸上的故事。如果要将书换成 纸,必须要修改Mother才能读。假如以后需求换成杂志呢成 页呢?
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!