到底我们为什么要用设计模式呢?这么多设计模式为什么要这么设计呢?为什么要提倡"Design Pattern"呢?根本原因是为了代码复用,增加可维护性。那么怎么才能实现代码复用呢?OO界有前辈的七大原则:"开-闭"原则(Open Closed Principal)、里氏代换原则、合成复用原则,依赖倒转原则,接口隔离原则,抽象类,迪米特法则。设计模式就是实现了这些原则,从而达到了代码复用、增加可维护性的目的。
1、"开-闭"原则
这些OOD原则的一个基石就是"开-闭原则"(Open-Closed Principle OCP).这个原则最早是由Bertrand Meyer提出,英文的原文是:Software entities should be open for extension,but closed for modification.意思是说,一个软件实体应当对扩展开放,对修改关闭.也就是说,我们在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,换句话说就是,应当可以在不必修改源代码的情况下改变这个模块的行为.
满足OCP的设计给系统带来两个无可比拟的优越性.
1.通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性.
2.已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使变化中的软件系统有一定的稳定性和延续性.
具有这两个优点的软件系统是一个高层次上实现了复用的系统,也是一个易于维护的系统.那么,我们如何才能做到这个原则呢?不能修改而可以扩展,这个看起来是自相矛盾的.其实这个是可以做到的,按面向对象的说法,这个就是不允许更改系统的抽象层,而允许扩展的是系统的实现层.
解决问题的关键在:抽象化.我们让模块依赖于一个固定的抽象体,这样它就是不可以修改的;同时,通过这个抽象体派生,我们就可以扩展此模块的行为功能.如此,这样设计的程序只通过增加代码来变化而不是通过更改现有代码来变化,前面提到的修改的副作用就没有了.
"开-闭"原则如果从另外一个角度讲述,就是所谓的"对可变性封装原则"(Principle of Encapsulation of Variation, EVP).讲的是找到一个系统的可变因素,将之封装起来.在我们考虑一个系统的时候,我们不要把关注的焦点放在什么会导致设计发生变化上,而是考虑允许什么发生变化而不让这一变化导致重新设计.也就是说,我们要积极的面对变化,积极的包容变化,而不是逃避.
2、里氏代换原则
指导我们如何去构建一个extends(继承、派生)结构。
子类与父类的关系必须是is-A,即,子类必须在任何场合都敢于大声宣称自己起码(至少)是一个父类。比如,假设某类结构,“男人”、“女人”从“人”派生出来,看起来就是满足里氏代换原则的,因为无论“男人”还是“女人”,在任何场合都是“人”。这个原则大多数情况下,可以用现实世界中的概念来思考,但软件世界与显示世界毕竟有区别,比如,书中的例子,“正方形是不是矩形”问题。
此外,很多问题需要利用OO核心思想来灵活考虑,还是书中的例子(我只说大概意思,可能与书中描述存在差异),一个类结构,“职工、普通职员、项目经理、科长、部长……”,从“员工”类派生出来,从一般概念来看还不错,职工和部长都是员工嘛,但这里隐含问题,现实世界中,普通职员可能变成项目经理,但软件世界中,普通职员和项目经理被规划成两个类了,于是一个普通职员instance是很难变成项目经理instance的,这说明我们把显示世界映射为软件世界时,出现了问题。更加合理的做法是,把这些职务(角色)抽象出来,比如叫做“职务”类(interface/abstract class),所有职务从其派生,员工类、职务类是关联关系,任何一个员工实例都一个职务实例作为它的属性,这样就对了,员工的职务是可以变化的。实现这个重构的依据,是OO中的封装变化思想,以及从中演化出来的依赖倒换原则、合成/聚合复用原则。
但凡涉及到extends结构的设计模式都符合着里氏代换原则:
策略模式:一组算法,把他们封装成对象,使之可以互换(满足同一接口,即都is-A这个接口)。
合成模式:Leaf和Composite都is-A Component,于是才可能方便地层层嵌套。
代理模式:Proxy与RealSubject都is-A父类Subject,于是可以插入代理,完成附加功能。
3、合成复用原则
合成和聚合都是关联的特殊种类。聚合表示“拥有”关系或整体与部分的关系;而合成则是用来表示一种强的多的 “拥有”。在一个合成的关系里,部分和整体的生命周期是一样的。一个合成的新的对象完全用又对其他组成部分的支配权。包括他们的创建和湮灭。组合而成的对象对组成部分的内存分配、内存释放有绝对的责任。 进一步讲。一个合成的多重性不能超过1,换言之,一个合成的关系中,部分对象不能于另一个合成关系对象共享。
合成通常理解为:值的聚合。而聚合则是:引用的聚合。
合成和聚合是将已有的对象纳入到新的对象中,使之成为新对象的一部分,因此新的对象可以调用已由对象的功能。达到复用的目的。
优点:
l 新对象存取成分对象的唯一方法是通过成分对象的接口。
这种复用是黑箱复用,隐藏了成分对象的内部细节。
这种复用支持包装。
这种复用依赖性较小。(耦合度低)
每一个新类可以将焦点集中在一个任务上。
这种复用可以在运行时动态进行。新对象可以动态引用于成分对象类型的子对象。
缺点就是:
通过这种复用建造的系统通常会有较多的对象需要管理。
“合成聚合复用可以替代继承复用完成任一项功能。”
4 依赖倒转原则
很具体地指导我们对抽象类(接口)、实现类的使用。
依赖于抽象的实体(interface/abstract class),才能够更具有可插入性(但凡实现既有接口的实现类实例都可以在依赖此接口的地方以此接口实例的角色插入进来),更容易满足Open-Close原则(抽象的层次不变化、实现的层次由于使用不同的类来封装不同的变化,于是可以在增加新类作为扩展的同时不需要修改已有实现类)。
5 接口隔离原则
含 义:使用多个专门的接口要比使用单一的总接口要好!
从客户类角度上看:一个类对另外一个类的依赖性应当是建立在最小的接口之上。
接口的划分直接带来类型的划分。
目标就是不向客户类提供不必要的行为。
6 抽象类
抽象类不会有实例,一般作为父类为子类继承,一般包含这个系的共同属性和方法。
注意:好的继承关系中,只有叶节点是具体类,其他节点应该都是抽象类,也就是说具体类
是不被继承的。将尽可能多的共同代码放到抽象类中。
7 迪米特法则
用于解开类之间的不必要的耦合。“不要与陌生人说话”。说起来容易,实际操作的时候很可能出现些无所适从的问题,而解开类之间的耦合是比较重要的,目前我们设计类的时候,增加属性、增加对其他类的调用都比较随意,不太好。
迪米特法则要求:一个对象应当对其他对象有尽可能少的了解。几种其他的表述:只与你直接的朋友通信、不要跟“陌生人”说话(冯远征?)
每个软件单位对其他单位都具有最少的知识,而且局限于那些与本单位密切相关的软件单位。
狭义迪米特法则-规定了谁是Friends、谁是Stranger。Friends圈子包括:
当前对象自身(this)、Instance field(如果field是Map、List等容器类型,则容器内的对象也是朋友)、当前对象创建的对象(调用其构造方法)、当前对象方法的参数对象。
只跟Friends说话,跟Stranger说的话由Friends转述。
狭义迪米特法则的弊端:传递间接调用的小方法太多!解决办法:遵循依赖倒换原则做些折衷处理,让对象依赖于Stranger的抽象层。虽然没有完全断开耦合,至少降低了耦合。
总结: 软件系统中,一个模块设计的好不好最主要、最重要的标志,就是该模块在多大程度上将自己的内部数据和其他实现细节隐藏起来。一个设计好的模块可以将它所有的实现细节隐藏起来,很彻底的将提供给外界的API和自己的实现分离开来。这样一来模块于模块之间的通信仅仅通过彼此的API,而不理会模块内部的工作细节。这便是:“信息的隐藏”。