说明:本文为《设计模式之禅》的阅读笔记,主要总结精华和记录自己的部分理解,主要用于日后复习,不清楚的地方可翻书看下具体的示例代码,帮助理解。
1. 单一职责原则(Single Responsibility Principle,SRP)
1.1 定义
There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更。
1.2 优点
- 类的复杂性降低(类实现的职责有清晰明确的定义);
- 可读性提高(复杂性降低就意味着可读性提高);
- 可维护性提高(可读性提高当然更容易维护);
- 变更引起的风险降低(如果一个接口的单一职责做的好,那么接口的修改只对相应的实现类有影响,对其他接口无影响,这对系统的扩展性、维护性都有非常大的帮助);
1.3 最佳实践
-
接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。
接口在设计的时候一定要做到单一。但是实现类就要多方面考虑,生搬硬套单一职责原则,会引起类的剧增,给维护带来麻烦,过分细分类的职责也会人为地增加系统的复杂性。(如:本来一个类可以实现的行为硬拆成两个类,然后再使用聚合或组合的方式耦合在一起,人为的制造了系统的复杂性。原则是死的,人是活的。)
-
单一职责适用于接口、类,同样也适用于方法。
一个方法尽可能只做一件事情(比如修改用户名
changeUserName()
,不要把这个方法放到”修改用户信息“方法changeUser()
中)。
不要让别人猜这个方法可能是用来处理什么逻辑的。(比如上述例子中,方法职责不清晰,不单一,可能还修改了用户别的信息) 类的单一职责受非常多因素的制约,基本很难实现。
2. 里氏替换原则(Liskov Substitution Principle,LSP)
2.1 定义
定义一(最正宗的定义):If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.(如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。)
定义二(最清晰明确的定义):Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.(所有引用基类的地方必须能透明地使用其子类的对象。)
通俗理解:子类对象可替换父类对象,替换后不会产生错误或异常。或者说替换前后逻辑一样。
只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。
2.2 4层含义
- 子类必须完全实现父类的方法
在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。 - 子类可以有自己的个性
里氏替换原则可以正着用,但是不能反过来用。有子类出现的地方父类未必就可以出现。 - 覆盖或实现父类的方法时输入参数可以被放大
- 覆写或实现父类的方法时输出结果可以被缩小
父类的方法子类可以重载(Overload)(方法名相同,但是方法签名不同,就是参数列表不同)。
例如,父类中方法的参数是HashMap类型,子类中重载的方法参数是Map类型,这是正确的。但这种情况下,子类的方法永远不会被执行。如果你想让子类的方法被执行,就必须覆写(Override)父类的方法。
反过来,如果子类的方法参数为HashMap,父类为Map,则在父类出现的地方替换子类后,调用的是子类的方法!!子类在没有覆写父类方法的前提下,子类方法被执行了!!这会引起业务逻辑的混乱!所以子类中方法的前置条件必须与超类中杯覆写的方法的前置条件相同或者更宽松。
2.3 最佳实践
采用里氏替换原则的目的:增强程序的健壮性。
3. 依赖倒置原则(Dependence Inversion Principle,DIP)
3.1 定义
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.
翻译过来,包含三层含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
高层模块就是具体的业务场景实现类,里面有对底层模块实现类的创建和调用;低层模块就是抽象类的实现类。
依赖倒置原则在Java语言中的表现就是:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过 接口或抽象类产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类。
更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。
3.2 优点
减少类之间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。
3.3 注意点
发生变更时,才能发现我们的程序是否是松耦合。
在Java中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型。
例如:zhangSan的 表面类型是IDriver,实际类型是Driver。
两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立地运行,而 TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。
3.4 依赖的三种写法
- 构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入。 - Setter方法传递依赖对象
在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入。 - 接口声明依赖对象
在接口的方法中声明依赖对象,该方法也叫做接口注入。
3.5 最佳实践
依赖倒置原则的本质:通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合。
- 使用依赖倒置须遵循规则
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。 - 变量的表面类型尽量是接口或者是抽象类
注意不是所有,例如xxxUtils一般是不需要接口或是抽象类的。 - 任何类都不应该从具体类派生
不是绝对的,只要不超过两层的继承都是可以忍受的。(特别是负责项目维护的同志,基本上可以不考虑这个规则,维护工作基本上都是进行扩展开发,修复行为, 有时通过一个继承关系,覆写一个方法就可以修正一个很大的Bug。) - 尽量不要覆写基类的方法
如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。 - 结合里氏替换原则使用
结合里氏替换原则(父类出现的地方子类就能出现),可以得出这样一个通俗的规则:接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒置原则的核心。
我们在实际的项目中使用依赖倒置原则时需要审时度势, 不要抓住一个原则不放,每一个原则的优点都是有限度的,并不是放之四海而皆准的真理, 所以别为了遵循一个原则而放弃了一个项目的终极目标:投产上线和盈利。
4. 接口隔离原则(Interface Segregation Principle)
4.1 定义
接口
- 实例接口(Object Interface),在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口。比如你定义Person这个类,然后使用 Person zhangSan=new Person()产生了一个实例,这个实例要遵从的标准就是Person这个类,Person类就是zhangSan的接口。疑惑?看不懂?不要紧,那是因为让Java语言浸染的时间太长了,只要知道从这个角度来看,Java中的类也是一种接口。
- 类接口(Class Interface),Java中经常使用的interface关键字定义的接口。
隔离
- Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口。)
- The dependency of one class to another one should depend on the smallest possible interface. (类间的依赖关系应该建立在最小的接口上。)
- 建立单一接口,不要建立臃肿庞大的接口,接口尽量细化,同时接口中的方法尽量少。
4.2 接口隔离原则是对接口进行规范约束
接口要尽量小
这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface), 但是“小”是有限度的,首先就是不能违反单一职责原则。接口要高内聚
什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互。 具体到接口隔离原则就是,要求在接口中尽量 少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。定制服务 - 只提供访问者需要的方法
一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口(并不一定就是Java中定义的Interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各个访问者(即客户端)定制服务。接口设计是有限度的
接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低。
4.3 最佳实践
- 一个接口只服务于一个子模块或业务逻辑;
- 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
- 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
- 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你的手中!
5. 迪米特法则(Law of Demeter,LoD)
也称为最少知识原则(Least Knowledge Principle,LKP)
5.1 定义
一个对象应该对其他对象有最少的了解。
通俗理解:一个类应该对自己需要耦合或调用的类知道得最少。
5.2 迪米特法则对类的低耦合要求
- 只和朋友交流
一个方法尽量不引入一个类中不存在的对象。
迪米特法则还有一个英文解释:Only talk to your immediate friends(只与直接的朋友通信。)
什么叫做直接的朋友呢?每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系。例如组合、聚合、依赖等。
朋友类的定义是这样的:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,而出现在方法体内部的类不属于朋友类。(如依赖)
注意:一个类只和朋友交流,不与陌生类交流,不要出现getA().getB().getC().getD()这种情况(在一种极端的情况下允许出现这种访问,即每一个点号后面的返回类型都相同),类与类之间的关系是建立在类间的,而不是方法间,因此一个方法尽量不引入一个类中不存在的对象,当然,JDK API提供的类除外。
朋友间也是有距离的
尽量不要对外公布太多的public方法和非静态的 public变量,尽量内敛,多使用private、package-private、protected等访问权限。是自己的就是自己的
在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。谨慎使用Serializable
在实际应用中,这个问题是很少出现的,即使出现也会立即被发现并得到解决。是怎么回事呢?举个例子来说,在一个项目中使用RMI(Remote Method Invocation,远程方法调用)方式传递一个VO(Value Object,值对象),这个对象就必须实现Serializable接口(仅仅是一个标志性接口,不需要实现具体的方法),也就是把需要网络传输的对象进行序列化,否则就会出现NotSerializableException异常。突然有一天,客户端的VO修改了一个属性的访问权限,从private变更为public,访问权限扩大了,如果服务器上没有做出相应的变更,就会报序列化失败,就这么简单。但是这个问题的产生应该属于项目管理范畴,一个类或接口在客户端已经变更了,而服务器端却没有同步更新,难道不是项目管理的失职吗?
5.3 最佳实践
- 迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。在采用迪米特法则时需要反复权衡,既做到让结构清晰,又做到高内聚低耦合。
- 如果一个类需要跳转两次以上才能访问到另一个类,则需要考虑重构。
为什么是两次以上呢?因为一个系统的成功不仅仅是一个标准或是原则就能够决定的,有非常多的外在因素决定,跳转次数越多,系统越复杂,维护就越困难,所以只要跳转不超过两次都是可以忍受的,这需要具体问题具体分析。
6. 开闭原则(Open Closed Principle)
6.1 定义
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。)
什么是软件实体?软件实体包括以下几个部分:
- 项目或软件产品中按照一定的逻辑规则划分的模块;
- 抽象和类;
- 方法。
开闭原则告诉我们应尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来完成变化,它是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
通俗高度总结:对扩展开放,对修改关闭。
6.2 注意点
注意:把价格定义为int类型并不是错误,在非金融类项目中对货币处理时,一般取2位精度,通常的设计方法是在运算过程中扩大100倍,在需要展示时再缩小100倍,减少精度带来的误差。
注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。
一个项目的基本路径应该是这样 的:项目开发、重构、测试、投产、运维,其中的重构可以对原有的设计和代码进行修改, 运维尽量减少对原有代码的修改,保持历史代码的纯洁性,提高系统的稳定性。
6.3 为什么要采用开闭原则
开闭原则是最基础的一个原则,前面的原则都是开闭原则的具体形态, 也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
开闭原则对测试的影响
新增加的类,新增加的测试方法,只要保证新增加类是正确的就可以了。开闭原则可以提高复用性
那为什么要复用呢?减少代码量,避免相同的逻辑分散在多个角落,避免日后的维护人员为了修改一个微小的缺陷或增加新功能而要在整个项目中到处查找相关的代码,然后发出对开发人 员“极度失望”的感慨。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分为止。开闭原则可以提高可维护性
维护人员最乐意做的事情就是扩展一个类,而不是修改一个类,甭管原有的代码写得多么优秀还是多么糟糕,让维护人员读懂原有的代码,然后再修改,是一件很痛苦的事情,不要让他在原有的代码海洋里游弋完毕后再修改,那是对维护人员的一种折磨和摧残。面向对象开发的要求
万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物皆运动,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
6.4 如何使用开闭原则
开闭原则是一个非常虚的原则,前面5个原则是对开闭原则的具体解释。
- 抽象约束
通过接口或抽象类可以约束一组可能变化的行为,并且能够实现对扩展开放。
- 第一,通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中不存在的public方法;
- 第二,参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;
- 第三,抽象层尽量保持稳定,一旦确定即不允许修改。
元数据(metadata)控制模块行为
什么是元数据?用来描述环境和数据的数据,通俗地说就是配置参数,参数可以从文件中获得,也可以从数据库中获得。
其中达到极致的就是控制反转(Inversion of Control), 使用最多的就是Spring容器。制定项目章程
建立项目章程是非常重要的,因为章程中指定了所有人员都必须遵守的约定,对项目来说,约定优于配置。封装变化
第一,将相同的变化封装到一个接口或抽象类中;第二, 将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一个接口或 抽象类中。
封装变化,也就是受保护的变化(protected variations),找出预计有变化或不稳定的点,我们为这些变化点创建稳定的接口,准确地讲是封装可能发生的变化,一旦预测到 或“第六感”发觉有变化,就可以进行封装,23个设计模式都是从各个不同的角度对变化进行封装的。
6.5 最佳实践
Single Responsibility Principle:单一职责原则
Open Closed Principle:开闭原则
Liskov Substitution Principle:里氏替换原则
Law of Demeter:迪米特法则
Interface Segregation Principle:接口隔离原则
Dependence Inversion Principle:依赖倒置原则
把这6个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是SOLID(solid,稳定的)。
开闭原则也只是一个原则
开闭原则只是精神口号,实现拥抱变化的方法非常多,并不局限于这6大设计原则,但是遵循这6大设计原则基本上可以应对大多数变化。项目规章非常重要
预知变化
在实践中过程中,架构师或项目经理一旦发现有发生变化的可能,或者变化曾经发生过,则需要考虑现有的架构是否可以轻松地实现这一变化。架构师设计一套系统不仅要符合现有的需求,还要适应可能发生的变化,这才是一个优良的架构。
最后附一张思维导图: