单一职责原则
单一职责,顾名思义,只做一件事。定义是:There should never be more than one reason for a class for change。在对类或者接口设计的时候,尽量保证一个类或者一个接口只做同一种类型的事情。
example
原有设计:
public interface UserManager {
void UserManager(UserInfo userinfo,UserAdd useradd,UserWork userWork.....)
}
改进设计:
public interface UserManager {
void changeUserName(String name....)
void changeAdd(String add.....)
void changeWork(String work.....)
}
Notice: 在单一原则设计时,可能对于职责的定义会根据业务的不同,单一原则无法确定。但是应该保证接口是单一职责,而类的设计可以尽量单一。
里氏替换原则
里氏替换原则的定义,也就是说所有可以用基类的地方,就可以用从基类继承的子类对象。
原版定义是: Functions that use pointers or references to base classes must be able to use object of derived classes without knowing it.
虽然定义是简单的,但是可以提炼出四层含义
- 子类必须完全实现父类的方法
当我们设计了接口或者抽象类的时候,都需要进行具体的实现。这里有一个原则是使接口或者抽象类作为参数。这样就能保证任意的实现自基类或者接口的子类,都可以进行调用,这是一种松耦合的设计。
子类必须完全实现父类的方法,这是肯定的。假如需要调用父类的某一个方法,然而子类没有实现,那么就必然违反了这个原则
-子类可以有自己个性
子类继承父类后,一般会有自己个性的方法或者参数。扩展了父类后,子类能出现的地方父类就不一定能出现。这是明显的。
-覆盖或实现父类方法时输入参数可以被放大
当子类override父类方法时,本着父类出现的地方子类可以出现的原则,因此要去该方法的参数应该大于等于父类的方法,这样才能被替换。
- 覆写或实现父类的方法时输出结果可以被缩小
当父类的一个方法返回类型为T,子类覆写该方法时,返回类型S,要求S类型小于等于T。还是抓住替换原则的根本去理解,就很好理解了。当父类出现的地方,如果替换成子类后,返回的类型大于父类的类型,那么就无法转换到低类型,不符合里氏替换原则。
里氏替换原则的目的:增强程序健壮性,版本升级时也能保持很好兼容性,即使增加了子类,原有的系统还能正确运行。
依赖倒置原则
这个原则大家就比较熟悉了。JAVA开发的同学都知道Spring的IoC(控制反转),和这里的依赖倒置的思想还是比较相近的。先说说书面的定义:
High level modules should not depend upon low level modules. Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstractions.
意思就是:高层模块不应该依赖于底层模块,都应该依赖于抽象。抽象不能依赖于具体实现。具体实现应依赖于抽象。这也是面向接口编程(OOD)的精髓之一。
举个栗子:
在开车驾照的设计中,我们有A照,B照,C照等等。规定是假如你持有C照(抽象类或接口),那么你就可以开手动小汽车、自动小汽车、三轮车、小货车等等(抽象的具体实现)。但是假如驾照的设计是一种类型的车就得考对应的驾照,那么有的人可能需要随时带上一堆的驾照。一个统一的C照,就可以避免了我们单独每一种类型的车都去考驾照。是不是很方便?假如以后家里买了小三轮,就不用去考一个三轮驾照,直接用C照就行了(三轮驾驶资格属于C照允许范围内),是不是很方便(方便扩展)。
也就是说,依赖倒置减少类之间的耦合性,提高了系统的稳定性,降低了并行开发的风险。因为大家依赖的是抽象,而不是具体实现,就不需要等待别人具体实现后才能开发其他模块。
这里有几点最佳实践需要注意:
- 每个类都尽量有接口或者抽象,或都有
- 变量的表面类型尽量是接口或在抽象类
- 任何类都不应该从具体类派生,都应该出自抽象
- 尽量不要覆写基类方法
- 结合里氏替换原则
接口隔离原则
接口隔离原则,顾名思义,就是接口需要隔离。(好吧……笑话好冷……)首先我们需要明确接口的定义。大家基本都知道,以interface定义的类接口(Class interface),这只是一类接口。但是普通的类(Object interface)也是一类接口。
那我们所说的接口隔离是什么意思呢?总结来说,就是两点:1.使用者不应该依赖它不需要的接口。2.类间的依赖应该建立在最小的接口上。解释一下,就是我们在设计接口的时候,尽量细化接口,接口中的方法尽量少,使得接口的职责尽量单一。
但是接口隔离和我们上面说的单一原则有什么不同呢?接口隔离所强调的是接口本身的方法尽量少,单一原则强调的是业务的单一。
举个栗子:
假如有一个美女接口,设计如下
public interface BeautifulGirl {
public void goodLooking(); //颜值高
public void goodBody(); //身材瘦
public void goodTemperament() //气质好
}
感觉好像也没什么问题。但是假如我们现在的审美变了,身材越胖越好看(梦回唐朝)。那么我们就需要改变这个接口。身材这个部分可能单独会变化,因此我们需要拆分成两个接口。
public interface BeautifulWithBody {
public void goodBody(); //身材瘦
}
public interface BeautifulWithOther {
public void goodLooking(); //颜值高
public void goodTemperament() //气质好
}
我们在设计接口的时候,需要尽量使一个接口只服务于一个子模块或者业务逻辑,通过业务逻辑压缩接口中的public方法,缩小接口提供的服务数量。但是当接口已经很乱了,那么就采用适配器模式去转换处理。
迪米特法则
迪米特法则,也称最小知识原则。就是一个类应该对自己需要耦合或调用的类知道的越少越好。这样可以使得模块的设计松耦合。
举个栗子
public class A {
public functionTest(ClassB B) {
List<ClassC> listC = new ArrayList();
......
}
}
这个栗子中,在类A的方法functionTest中,传入类B,同时使用类C。因此类A中就包含了类C的使用。假如类C需要改变,那么类A也需要改变,不符合我们的松耦合设计。
那我们按照最小知识原则进行一下优化:
public class A {
public functionTest(ClassB B,ClassC C) {
List<ClassC> listC = new ArrayList();
......
}
}
这样,A就不用包含C。当C变化的时候,A不用变。而且一般参数我们可以传入接口或者抽象类,这样使得耦合度更低。
但是并不是越隔离越好。我们应该遵守尽量减少public方法,尽量少暴露接口。
开闭原则
开闭原则是所有原则的核心。首先介绍什么是开闭原则。
开闭原则:对扩展开放,对修改关闭
简单理解,就是说一个软件实体(功能模块、抽象和类、方法),有新的需求,需要扩展的时候,尽量不能去修改他本身代码,应该对他进行继承扩展。
在软件的开发过程中,需求变化是时有的事情。因此我们在开发软件的过程中,需要保持软件的可扩展性,时刻准备拥抱变化。
至于怎样具体去实现开闭原则,有很多方式,比如采用抽象、接口设计,以及我们所说的23种设计模式,都是开闭原则的具体实现。
开闭原则的使用,可以使得扩展后的测试很方便,同时可以提高软件复用性、可维护性。