读《设计模式之禅》笔记一(六大设计原则)
单一职责(Single Responsibility Principle SRP)
There should never be more than one reason for a class to change
一个类的变化不应该有一个以上的原因
原则演示例子
简单的电话功能:
这个电话接口有两个职责:一个是协议管理(拨通电话、挂电话),另一个是数据传输(通话)。且其中一个职责的变化不会引起另一个职责的变化。则需要考虑将两个职责拆分开来,符合SRP。
进行SRP设计的简单电话功能
单一职责最难的就是划分职责,一个职责一个接口,但是职责没有一个量化的标准,一个类到底负责几个职责,职责如何细化这些问题都是需要根据实际项目和环境考虑的。
单一职责不但适合接口,类,还适合方法
一个修改用户信息的例子:
这个方法十分实用,根据参数可以修改用户的信息。但是从单一职责角度出发,这个方法有很大问题:方法职责不清晰,不单一。每当涉及到用户某一项信息功能变化时,此方法都要受到影响。
单一职责改良后:
这样可读性和扩展性都有提高。
类的单一职责原则的建议
在实际的项目开发中,一个类很难做到单一职责原则,书中作者的建议是接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
里氏替换原则(Liskov Substitution Principle LSP)
父类能出现的地方子类就可以出现。
要做此原则,有如下要求:
- 子类必须完全实现父类的方法。
- 子类可以有自己的个性。(LSP可以正着用,即子代替父,不能反着用,父代替子)
- 实现或覆盖父类的方法时输入参数可以被放大。(即子类输入参数类可以是父类输入参数类的父类)
- 实现或覆盖父类的方法时输出结果可以被缩小。
图书作者建议
在项目中采用里氏替换原则,尽量避免子类的“个性”,当子类拥有个性,子类与父类的关系就难以调和,把子类当父类用,子类的个性被抹杀;子类单独作为业务用,又让代码间耦合关系变得扑朔迷离,缺乏类替换的标准。
依赖倒置原则(Dependence Inversion Principle DIP)
原则要求
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象。
- 抽象不应该依赖细节。
- 细节应该依赖抽象。
低层与高层模块解释:
每一个逻辑的实现都由原子逻辑组成,不可分割的原子逻辑就是底层模块。
原子逻辑的再组装就是高层模块。
在JAVA语言中表现:
- 模块间的依赖通过抽象产生。
- 接口或抽象类不依赖实现类。
- 实现类依赖接口或抽象类。
这就是面向接口编程OOD(Object-Oriented Design)
接口隔离原则(Interface Segregation Principle)
先讲接口,接口分两种
- 实例接口:在JAVA中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事务描述,这是一种接口。可能有人会说,这不是类吗?这是在Java语言浸染时间太长了。只要知道,从这个角度来说,Java的类也是一种接口。
- 类接口:Java中interface关键词定义的接口。
什么是隔离?(两种定义)
- 客户端不应该依赖它不需要的接口。
- 类间的依赖关系应该建立在最小的接口上。
图书作者理解:
把两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗点讲就是:接口尽量细化,同时接口中的方法尽量少。有人可能会疑惑:这不和单一职责要求一样吗?错,接口隔离与单一职责的审视角度不相同的,单一职责要求的是类和接口职责单一,注重的是职责,这是从业务逻辑上划分的。而接口隔离原则则要求接口的方法尽量少。
当单一职责与接口隔离矛盾
在讲单一职责时,仔细分析一下IConnectionManager接口是否可以继续拆分下去?
挂电话有两种方式:
- 正常挂断。
- 异常挂断。(例如手机没电)
看到这里是不是要把IConnectionManager接口拆封成两个,一个负责连接,一个负责挂断。且慢,再思考一下。如果拆分就不符合单一职责了,从业务逻辑讲:通信的建立与关闭已是最小的业务单位。如果再拆分,就算对业务或是协议的拆分,这样的设计就是失败的设计。但是一个原则要拆,一个原则又不拆,怎么办?好办,根据接口原则拆分接口时,首先满足单一职责原则。
图书作者建议:
- 一个接口只服务与一个子模块或业务逻辑。
- 通过业务逻辑压缩接口中public方法。
- 已经污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。
- 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素。看到大师是这样做的你就照抄是不行的。环境不同,接口拆分就不同,深入了解业务逻辑,最好的接口设计就出自你的手中!
迪米特法则(Law of Demeter LoD)
原则要求:
一个对象应该对其他对象有最少的了解。通俗的讲就是对类的低耦合提出要求,其包含以下4层含义:
1. 只与朋友类交流。(朋友类的定义:出现在成员变量、方法的输入输出参数中的类称为成员朋友类)
2. 朋友间也是有距离的。
讲一个软件安装的例子:在安装软件时,经常有导向动作,第一步是确认是否安装,第二步确认License,第三步选择安装目录等等流程。先设计出类图:
Wizard导向类
public class Wizard {
private Random rand = new Random(System.currentTimeMillis());
//第一步
public int first(){
System.out.println("执行第一个方法...");
return rand.nextInt(100);
}
//第二步
public int second(){
System.out.println("执行第二个方法...");
return rand.nextInt(100);
}
//第三步
public int third(){
System.out.println("执行第三个方法...");
return rand.nextInt(100);
}
}
InstallSoftware类
public class InstallSoftware{
public boolean installWizard(Wizard wizard){
int first = wizard.first();
//根据first返回的结果,看是否需要执行second
if(first > 50){
int second = wizard.second();
if(second > 50){
int third = wizard.third();
return true;
}
}
return false;
}
}
仔细思考一下,上述设计是否符合第二条“朋友间也是有距离的”?Wizard类把太多的方法暴露给了InstallSoftware类,InstallSoftware类只用关心是否安装好,并不在意流程,两者的朋友关系太过亲密。如果要将Wizard类中的first方法返回值由int改为boolean,就需要修改InstallSoftware类。我们重新设计一下:
具体的代码我就不写了,通过重构,类间的耦合关系变弱了,结构也清晰了,变更引起的风险也变小了。
3. 是自己的就是自己的
在实际应用中经常会出现这样一个方法:放在本类也可以,放在其他类中也没有错,那该怎么办?可以坚持一个原则:如果一个方法放在本类中,既不增加类间的关系,也对本类不产生负面影响,那就放置在本类中。
4. 谨慎使用Serializable
请自行百度。
图书作者建议:
迪米特法则的核心观念就是类间解耦,弱耦合。但是解耦有限度,在项目中须适度的考虑此原则。
开闭原则(Open Closed Principle)
原则要求:
一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。
书店销售书籍的例子:
项目投产了,书籍正常销售出去了,当书店开了一周年,想举行图书打折活动,对于项目来说,这就是个变化,如何应对这样的变化,有三种做法:
- 修改接口: 在IBook上新增一个方法getOfficePrice(),专门用于进行打折处理,所有的实现类实现该方法。这样做的结果是NovelBook类要修改,BookStore中的main方法也要修改,同时IBook作为接口应该是稳定且可靠的,不应该经常发生变化,所以这个方案Pass。
- 修改实现类 修改NovelBook类中的方法,直接在getPrice()中实现打折处理,这的确算是个好办法,但是该方法也有缺陷,例如采购书籍人员也要看图书原价,而该方法导致看到的价格就是书籍折后价格,容易引起信息上的误会,所以该方案也不是个最优的方案,
- 通过扩展实现变化 增加一个子类OffNovelBook, 覆写getPrice方法,高层次的模块通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改也少,风险也小。修改后的类图:
要注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,底层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
如何使用开闭原则:
- 抽象约束。
- 元数据控制模块行为。(通俗点就是配置参数,从配置文件中获取参数和依赖)
- 制定项目章程。
- 封装变化。(两层含义:将相同的变化封装到一个接口或抽象中;第二将不同的变化封装到不同的接口和抽象中。)
博主言:
此书是我认为讲的通俗易懂的好书,以上内容多是书中原话,感兴趣的人可以自行去购买阅读,我比较偷懒书中的大多数代码没有敲上来,我本着服务自己的态度写了这篇博客,如有不好的地方,值得去改进的地方,希望能与大家探讨,共同进步!