设计模式中的设计原则
先来抛一个问题,设计模式到底有几个原则?
翻了三本书《设计模式之禅》、《设计模式:可复用的面向对象软件元素》、《Head First 设计模式》,也看了不少博客和关于设计模式原则的文章。关于设计模式有几大原则,似乎没有严格的定论,有的说6大设计原则,有的说7大设计原则,《Head First》中更是提到了9个设计原则。
不管是多少个设计原则,最终都是希望程序达到 “高内聚,低耦合” ,代码高度复用,具有可维护性的目的。所以多少个设计原则已经不重要了,重要的是达到怎样的目标!
设计原则
我觉得7大设计原则都有必要了解和尽量向其靠拢,但是程序设计肯定是不可能完全遵守这些设计原则,但是我们的设计可以让程序更好扩展和更容易维护。
1.开闭原则(Open Closed Principle,OCP)
开闭原则的原文定义是:Software entities should be open for extension,but closed for modification.(软件实体应该对扩展开放,对修改关闭。)
其意思是说一个软件程序应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。但是并不意味着不对代码做任何的修改,底层模块的变更,必然要有高层模块进行耦合,我们只能尽量预测变化,但是并不能保证所有的变更都不需要修改代码。
所以说,开闭原则具有理想主义的色彩,它是面向对象设计的终极目标。
开闭原则是面向对象设计中最基础的设计原则,它也被称为设计总则,它指导我们如何建立稳定灵活的系统。
2.单一职责原则(Single Responsiblity Principle ,SRP)
单一职责原则的原文定义是:There should never be more than one reason for a class to change.(应该有且仅有一个原因引起类的变更。)
在《设计模式之禅》中举了一个电话接口的例子。电话通话的时候有4个过程发生:拨号、通话、回应、挂机。那么写一个接口:
但是,这个IPhone 接口,并不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。dial()和hangup()实现的是协议管理,分别负责拨号和挂机;chat()实现的是数据的传送。如果协议接通的变化肯定会引起接口或者实现类的变化;而数据传送的变化(电话不仅仅为了通话传送数据,还可以为上网传送数据)肯定也会引起接口或者实现类的变化,所以这里就有两个原因会引起接口或者类的变化。分析后,我们应该考虑拆分连个接口:
注意
单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或者类设计的是否优良,但是“职责”或“变化原因”都是不可度量的,因项目而异,因环境而异。
因为“职责”没有一个量化的标准,一个类到底要负责哪些职责?这些职责该怎么细化?而项目后期职责发生扩展,可能一个职责要衍生出两个职责出来,该怎么拆分?在项目时间紧迫,接口或者类非常简单,考虑人工和事件成本时,是否还要坚持 单一职责原则?这些都要根据实际情况来考量。
3.里氏替换原则(Liskov Substitution Principle,LSP)
为什么这个原则的名字这么奇怪呢?
因为这个原则是由麻省理工学院的一位叫Barbara Liskov的女士提出来的,所以就叫里氏替换原则了。国外用发现或者创造定理、原则等是很正常的事,比如牛顿定理、法拉第、欧拉、XXX彗星等等。
里氏替换原则 有两种定义:
第一种定义: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 substitued 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.(所有引用基类的地方必须能透明地使用其子类的对象。)
用通俗的话讲,就是 所有父类能出现的地方子类就可以出现,并且替换为子类也不会产生任何的错误或异常,使用者可能根本就不需要知道是父类还是子类。要做到这样,那么子类就只能扩展父类的功能,但不能修改父类原有的功能。
这一原则主要是为了规范面向对象语言的* 继承 * 这一特性。
里氏替换原则为良好的继承定义了一个规范,其包含了4层含义:
* 1.子类可以实现父类的抽象方法,但是不能覆写父类的非抽象方法。
* 2.子类可以添加自己特有的属性或者方法。
* 3.子类覆写或者实现父类的方法时,输入的参数应该比父类的参数条件更宽松。
* 4.子类覆写或实现父类的方法时,返回的结果应该比父类的返回结果更严格。
提醒
1. 在使用父类的地方可以替换为子类,但是反过来在使用子类的地方却不一定能替换成父类。
2. 如果父类的某些方法在子类中发生了“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合灯关系代替继承。
4.依赖倒置原则(Dependence Inversion Principle)
依赖倒置原则的原始定义是:
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 语言中,抽象就是指接口或者抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,其特点就是可以直接被实例化。在OC 中,抽象就是协议啦,细节就是实现协议的类。
依赖倒置原则在Java 语言中的表现就是:
* 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
* 接口或抽象类不依赖实现类;
* 实现类依赖接口或抽象类。
具体到写代码时,那就是在使用到具体类时,不直接使用具体类,而使用具体类的抽象类或接口代替。
5.接口隔离原则(Interface Segregation Principle,ISP)
接口隔离原则有两种定义:
* 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.(类间的依赖关系应该建立在最小的接口上。)
每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。
举个例子就是如果接口A 中有10个接口,而实现类B 使用到了接口A 中的 5个,实现类C 使用到了接口A 中的另外 5个,那么我们应该将接口A 拆分成接口A1和接口A2。然后让实现类B 实现接口A1中的接口,实现类C 实现接口A2中的接口。
错误的设计如下图所示:
经过修改后的关系如下:
将一个臃肿的接口拆分为两个独立的接口,所依赖的原则就是接口隔离原则,使用多个隔离的接口,比使用一个臃肿的接口要好的多。很多IM SDK都遵守了这种原则来使某些实现来具有单聊、群聊、音视频通话 等功能。可以参考容量、云之讯、网易云信等SDK。
接口隔离原则是对接口进行规范的约束,其包含了4层含义:
* 1.接口要尽量小,但是也有一定的限度。因为过度小的接口会使接口变多,让程序变得复杂(我们总不能把每一个接口方法都定义在一个接口类里面吧)。
* 2.接口要高内聚。高内聚可以提高接口、类、模块的处理能力,减少对外的交互。具体到实际开发,就是在接口中尽量少公布 方法。
* 3.为依赖接口的类定制服务,只暴漏给它需要的方法,它不需要的方法则隐藏起来。
* 4.接口的设计要有限度。接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也使得接口类增加,是项目接口变得复杂。所以我们设计接口时,要把握一个“度”,接口不能太臃肿,也不能太小。
6.迪米特法则(Law of Demeter, LoD)
迪米特法则也称为最小知识原则(Principle of Least Knowledge,PLK)。
一个对象应该对其他对象有最少的了解。简单说来,就是一个类应该对自己需要耦合或调用的类知道得最少,被耦合或调用的类的内部是如何复杂与我都没关系,我只关系呗耦合或被调用的类提供给我的方法。
迪米特法则还有一个英文解释:Only talk to your immediate friends。
每个对象都会与其他对象有耦合关系,两个对象之间的耦合就成为了朋友关系。这种关系的类型有很多,例如组合、聚合、依赖等。 简单的说,只要两个类之间有交互或关联,那么它们就是朋友关系。
7.合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
合成/聚合复用原则经常又叫做合成复用原则,它的设计原则是:要尽量使用合成/聚合,尽量不要使用继承。也就是说,我们要优先考虑使用合成、聚合来实现功能,在使用合成、聚合无法实现的情况下,才考虑使用继承来实现。
其实这里最重要的地方就是区分“has-a”和“is-a”的区别。
相对于合成和聚合,继承的缺点在于:父类的方法全部暴露给子类。父类如果发生变化,子类也得发生变化;聚合的复用的时候就对另外的类依赖的比较的少。
其他的设计原则整理
在《Head First 设计模式》一书中整理的设计原则有:
* 封装变化
* 多用组合,少用继承
* 针对接口编程,不针对实现编程
* 为交互对象之间的松耦合设计而努力
* 类应该对扩展开放,对修改关闭。
* 依赖抽象,不要依赖具体类。
* 只和朋友谈
* 别找我,我会找你。
* 类应该只有一个改变的理由。
可以看出这里的设计原则其实也是用更通俗简单的话描述了上面的7大原则,或者扩展等。正所谓一千个读者眼中就有一千个哈姆雷特,我们不应该拘泥于多少个设计原则或者设计模式,应该将重点放在如何设计出高内聚,低耦合,代码能够高度复用,具有高度可维护性,健壮的程序上。毕竟这些原则或模式都是为了我们设计程序代码,实现某些功能服务的,不是吗?
参考:
百度百科-设计模式
http://www.runoob.com/design-pattern/design-pattern-intro.html
http://www.uml.org.cn/sjms/201211023.asp#6
书籍:
《Head First 设计模式》
《设计模式 - 可复用的面向对象软件元素》
《设计模式之禅》