名称 | 英文名称 | 解释 |
---|---|---|
单一职责原则 | Single Responsibility Principle | 一个类只负责一项职责 |
里氏替换原则 | Liskov Substitution Principle | 父类能出现的地方都可以替换为子类,反之则不一定 |
依赖倒置原则 | Dependence Inversion Principle | 抽象不应该依赖于细节,细节应该依赖于抽象 |
接口隔离原则 | Interface Segregation Principle | 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。 |
迪米特法则 | Law of Demeter | 一个对象应该对其他对象保持最少的了解。降低类与类之间的耦合(高内聚,低耦合) |
开闭原则 | Open Closed Principle | 软件实体应当对扩展开放,对修改关闭,即软件实体应当在不修改的前提下扩展。 |
单一职责原则
一个类为什么要只负责一项职责呢?因为如果一个类承担的职责过多,就等于把这些职责耦合在一起,当其中一个职责发生变化时,会造成意想不到的破坏。单一职责并不是说一个类只有一个函数,而是说这个类中的函数都是高度相关的,也就是高内聚。单一职责原则关注的是职责,最难的也是职责的划分,上图中的发邮件和发短信其实放到一起也是可以的,这两个行为是否相关的界定是需要根据项际目的实情况来判断的。
里氏替换原则
父类能出现的地方都可以替换为子类,换句话说,子类可以扩展父类功能,但不能改变父类原有功能。
public class Father {
public void sendEmail(){
//发送了邮件
}
}
public class Son extends Father{
@Override
public void sendEmail(){
//发送了短信
}
}
上面这个例子中,父类定义了发邮件的方法,但是子类却发了短信,父类和子类的行为不一致,明显违背了里氏替换原则。父类的sendEmail方法设定了规范,子类不能任意修改。
依赖倒置原则
依赖倒置原则的解释是抽象不应该依赖细节,细节应该依赖于抽象,通俗的说,就是面向接口编程而不是面向实现。举个小明开枪射击的例子。
//手枪
class Pistol{
public void fire(){
//单发射击
}
}
//小明
class Xiaoming{
public void shoot(Pistol pistol){
pistol.fire();
}
}
//场景类
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
xiaoming.shoot(new Pistol());
}
}
小明用手枪射击没有问题,但是有一天,需求改成让小明用步枪射击怎么办?
//增加一个步枪类
class Rifle{
public void fire(){
//连发射击
}
}
//修改小明的射击方法
class Xiaoming{
public void shoot(Rifle rifle){
rifle.fire();
}
}
//修改场景类
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
xiaoming.shoot(new Rifle());
}
}
因为手枪和小明是强耦合关系,所以换成步枪必须修改小明类和场景类才行,以后换成其他枪,还是要不断修改,这显然不是个好设计。如何降低手枪和小明的耦合呢?添加一个抽象接口不就行了嘛!改造后如下:
//抽象一个枪的接口
interface IGUN{
void fire();
}
//手枪
class Pistol implements IGUN{
public void fire(){
//单发射击
}
}
//步枪类
class Rifle implements IGUN{
public void fire(){
//连发射击
}
}
//小明
class Xiaoming{
public void shoot(IGUN gun){
rifle.fire();
}
}
//场景类
public class Client{
public static void main(String[] args){
Xiaoming xiaoming = new Xiaoming();
//用手枪射击
xiaoming.shoot(new Pistol());
//用步枪射击
xiaoming.shoot(new Rifle());
}
}
这样是不是就好多了,以后需求再让小明用别的枪,就不用那么麻烦了。
接口隔离原则
接口隔离原则是说要避免臃肿的接口,尽量细化接口,其实也是为了解耦。举例说明一下:
首先,定义一个功能聚合的(臃肿的)接口
//各种功能定义到一个接口中
interface IFuns{
void playAudio(); //播放音频
void playVideo(); //播放视频
void readBook(); //阅读书籍
void readComic(); //浏览漫画
//...
}
现在要生产两个产品,一个是mp4,一个是kindle,代码如下
//mp4目前只能播放音频和视频
class Mp4 implements IFuns{
public void fire(){
public void playAudio(){
//播放音频
};
public void playVideo(){
//播放视频
};
public void readBook(){
//不支持
};
public void readComic(){
//不支持
};
}
}
//kindle的设计只是为了阅读书籍和浏览漫画
class Kindle implements IFuns{
public void fire(){
public void playAudio(){
//不支持
};
public void playVideo(){
//不支持
};
public void readBook(){
//阅读书籍
};
public void readComic(){
//浏览漫画
};
}
}
要实例化产品出来,每个产品必须实现IFuns中所有的功能,但是呢,对于mp4,目前只研发出了看视频和音频的功能,其他功能还在研发中,对于kindle,这个产品是专注于阅读书籍和浏览漫画的,不想具备其他功能。现在的设计要求必须实现产品不需要的功能,这是不满足需求的。于是IFuns接口被拆分为:
interface IFunsA{
void playAudio(); //播放音频
void playVideo(); //播放视频
}
interface IFunsB{
void readBook(); //阅读书籍
void readComic(); //浏览漫画
}
//Mp4实现IFunsA;Kindle实现IFunsB 代码省略
这样拆分满足了kindle专注于阅读书籍和浏览漫画而不想具备其他功能。但是呢,对于Mp4来说,并不是不想具备其他功能,只是现在还没研发出来而已,以后mp4还是需要实现阅读书籍和浏览漫画的。于是还需要进行拆分:
interface IFunsA{
void playAudio(); //播放音频
}
interface IFunsB{
void playVideo(); //播放视频
}
interface IFunsC{
void readBook(); //阅读书籍
}
interface IFunsD{
void readComic(); //浏览漫画
}
//Mp4实现IFunsA,IFunsB;Kindle实现IFunsC,IFunsD 代码省略
这样拆分后,产品需要什么功能,实现对应的接口就行了。
这个结果看上去很像单一职责原则啊?虽然看起来差不来,其实是不一样的。单一职责原则注重的是职责,与接口中的方法数量无关,而接口隔离原则注重于对接口的解耦,接口隔离是在满足单一职责的基础上,尽可能减少方法。
迪米特法则
又称为最少知道原则。定义是一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。通俗的说就是一个类对于它需要调用的类,知道的越少越好,如果知道的越多,类之间的关系就会越密切,耦合度就会变得越来越高,耦合度高了,类的复用率就降低了。
有篇文章对迪米特法则解释很好,可以点进去看看
开闭原则
软件实体应当在不修改的前提下扩展,就是说,需求变化时,不要修改已有的稳定的代码,可以通过扩展的方式满足需求,以避免引入新的bug。
这个原则感觉没什么好解释的了,因为前面5个原则遵守好了,设计出来的软件就是符合开闭原则的。换句话说,开闭原则是其他5个原则的抽象。
软件设计最大的难题就是应对需求的不断变化,开闭原则应该说是软件设计的最终目标了。