1.单一职责原则(Single Responsibility Principle)
There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更。这里也包括接口、方法。
优点
- 降低类的复杂性。职责单一,定义明确,自然就变得简单。由此也陆续引出以下的优点。
- 提高可读性
- 提高可维护性。职责单一,将不会出现,修改职责A的逻辑会影响到不需要改变的职责B的逻辑。
- 提高扩展性
类的单一职责原则
比如,对象的属性和行为,就应该放到两个类里处理。比如对于用户,用户的属性:id、name、age就应该放到UserBO中。而对于用户行为的处理:添加权限、修改密码,就应该放到UserBiz类中处理。因为如果都放到同一个类中处理,则属性和行为的变更都会引起类的变更,便不符合有且只有一个原因引起变更。
方法的单一职责原则
原方法:changeUserInfo()。其中,修改用户信息,同时修改了用户的名称、密码、联系方式等等。应改为:
- changeUserName()
- changeUserPassWord()
- changeUserPhone()
便于明确职责。
个人理解
如果职责划分过细,或者过于追求单一职责原则,会导致类和方法过多。因此,在贯彻单一职责原则的过程中,我们应该首先确认合适的职责范围,然后在按照单一职责原则进行设计。
2.里氏替换原则(Liskov Substitution Principle,LSP)
定义1:
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型。
定义2:
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
所有引用基类的地方必须透明的使用其子类的对象。即:只要父类能出现的地方子类也可以出现,而且替换为子类不会产生任何错误或异常,但是反过来就不行,有子类出现的地方,父类未必就能适应
关于继承
实质上,里氏替换原则是对继承的使用做出了一定的规范。继承的优点在于,减少类创建的工作量、提高代码复用性、提高类的可扩展性等。但继承也有缺点,即:继承是侵入性的,只要继承,就必须拥有父类的所有方法和属性;降低了代码的灵活性,子类必须拥有父类的属性和方法,让子类有了一些约束;增加了耦合性,当父类的常量,变量和方法被修改了,需要考虑子类的修改,这种修改可能带来非常糟糕的结果,要重构大量的代码。
里氏替换原则的四个规范
简单来说,就是子类可以扩展父类的功能,但不能改变父类原有的功能。
对于继承会导致的问题,里氏替换原则规范了四个规范。
- 子类必须完全实现父类的方法。(即正向,引用基类的地方必须能透明地使用子类对象,且子类不能随意覆盖父类的方法,这样导致子类无法完全替换父类)
- 子类可以有自己的个性。(即反向,必须引用子类的地方不能以基类代替)
- 覆盖或者实现父类的方法时输入参数可以被放大。(一旦输入参数不相同,那么就不是重写而是重载,那么如果想实现里氏替换原则,就必须让父类的参数更小,因为这样才可以让替换子类的时候仍使用该方法。例如:父类方法入参为实现类(HashMap),子类方法入参可以为接口(Map),或者子类方法入参(HashMap)为父类方法入参(ConcurrentHashMap)的父类)
- 覆盖或者实现父类的方法时输出结果可以被缩小。
反例
java.sql.Time类,继承了java.util.Date类。但是他重写了getDate()、getDay()等方法,直接抛出了java.lang.IllegalArgumentException()异常,破坏了里氏替换原则。
3.依赖倒置原则(Dependence Inversion Principle ,DIP)
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 abstracts.
- 高层模块不应该依赖低层模块,两者都应该依赖抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象。
依赖倒置原则在java语言中,表现为:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。
- 接口或抽象类不依赖实现类
- 实现类依赖接口或抽象类
依赖倒置原则的核心思想是面向接口编程!
举个例子
比如,作为一个苦逼的码农(Coder类),每天下班回家(goHome)都要骑摩拜。
public class Mobike {
public void drive(){
System.out.print("骑摩拜");
}
}
public class Coder {
public void goHome(Mobike mobike){
mobike.drive();
System.out.println("回家!");
}
}
public void offDuty() {
Coder coder = new Coder();
Mobike mobike = new Mobike();
coder.goHome(mobike);
}
运行dip方法,则会输出“骑摩拜回家!”
然而有一台,公司上市了!苦逼码农翻身做主人,财务自由了!于是他买了辆车,以后每天开车回家,美滋滋。
public class Car {
public void drive(){
System.out.print("开车");
}
}
但是goHome方法的入参只能接受Mobike类型的交通工具,Coder和Mobike的耦合性过高,产生了强依赖,所以买了车也没法马上开。
如果我们的Coder一开始拥有这样的驾驶能力,小汽车和摩拜也这样设计呢?
public interface Vehicle {
void drive();
}
public class Mobike implements Vehicle{
public void drive(){
System.out.print("骑摩拜");
}
}
public class Car implements Vehicle{
public void drive() {
System.out.print("开车");
}
}
public class Coder {
public void goHome(Vehicle vehicle){
vehicle.drive();
System.out.println("回家!");
}
}
public void offDuty() {
Coder coder = new Coder();
Car mobike = new Car();
coder.goHome(mobike);
}
这样的话,我们的高端码农就可以肆意妄为的想骑车就骑车想开车就开车嘞。
依赖倒置原则的优点
- 降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险
- 降低并行开发引起的风险与难度
依赖倒置原则的经验
- 每个类尽量都有接口或者抽象类,或者抽象类和接口两都具备。在使用时尽量依赖接口或抽象类使用。
- 变量的表面类型尽量是接口或者抽象类
- 任何类都不应该从具体类派生(视情况而定)
- 尽量不要覆写基类的方法 。覆盖基类的方法会影响依赖的稳定性。
- 结合里氏替换原则使用
4.接口隔离原则
-
定义1:Clients should not be forced to depend upon interfaces that they
don’t use.
客户端不应该依赖它不需要的接口。
那依赖什么呢?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除,那就需要对接口进行细化,保证其纯洁性。 -
定义2:The dependency of one class to another one should depend on the
smallest possible interface.
类间的依赖关系应该建立在最小的接口上。
它要求是最小的接口,也是要求接口细化,接口纯洁。
这里的接口指的是:
- 实例接口(Object Interface) :在 Java 中声明一个类,然后用 new 关键字产生一个实例,它是对一类事物的描述,可以看成是一个接口
- 类接口(Class Interface):Java中常使用的interface关键字定义的接口
即:建立单一接口,不要建立臃肿庞大的接口。再通俗的说就是接口尽量细化,同时接口中的方法尽量少。我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。
接口隔离即注意控制接口粒度大小,是为了防止我们封装过度,但是同时设计也是有限度的,我们在实现接口隔离原则时,一定要先满足单一职责原则
5.迪米特法则(Law of Demeter,LoD)
一个对象应该对其他对象有最少的了解。也叫最少知识原则(Low knowledge Principle,LKP),即一个类对自己需要耦合或调用的类知道的越少越好。或者用另一个解释:Only talk to your immediate friends(只与直接朋友通信)。
迪米特法则是对对象之间的耦合度进行限制,尽量降低类与类之间的耦合。
什么是朋友
直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友。
而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
注意
既然要避免与非直接的朋友通信,那就势必要通过直接的朋友作为中介来间接的与非直接的朋友通信,这样会导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。
6.开闭原则
Software entities like classes,modules and functions should be open for extension but closed for modifications.
一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。
即:一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的
为什么使用开闭原则
- 开闭原则是最基础的设计原则,其它的五个设计原则都是开闭原则的具体形态。
也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。依照java语言的称谓,开闭原则是抽象类,而其它的五个原则是具体的实现类。 - 开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来,不是在一个类中独立实现一个业务逻辑。只有这样的代码才可以复用,粒度越小,被复用的可能性越大。 - 开闭原则可以提高维护性
扩展一个类比修改一个类要好的多 - 面向对象开发的要求
总结
- 单一职责原则告诉我们实现类要职责单一
- 里氏替换原则告诉我们不要破坏继承体系
- 依赖倒置原则告诉我们要面向接口编程
- 接口隔离原则告诉我们在设计接口的时候要精简单一
- 迪米特法则告诉我们要降低耦合
- 开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭
参考文档
专栏:6大设计原则详解
以及一些内部资料