设计模式之禅学习总结
单一职责原则 SRP
单一职责原则的定义是:应该有且仅有一个原因引起类的变更。
单一职责原则的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义。
- 可读性提高,复杂性降低,那当然可读性提高了。
- 可维护性提高,可读性提高,那当然更容易维护了。
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,对其他对接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
注意:单一职责原则提出了一个编写程序的标准,用“职责”和“变化原因”来衡量接口或类设计是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。
对于单一职责原则,建议接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化!
里式替换原则 LSP
在面向对象的语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:
-
代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
-
提高代码的重用性;
-
子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有 父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
-
提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的 扩展接口都是通过继承父类来完成的;
-
提高产品或项目的开放性。 自然界的所有事物都是优点和缺点并存的,即使是鸡蛋,有时候也能挑出骨头来,继承的缺点如下:
继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构;
里氏替换原则定义:所有引用基类的地方必须能透明地使用其子类的对象
通俗的讲,只要父类出现的地方子类就可以出现,而且替换为子类不会产生任何错误或异常,使用者根本不需要知道是父类还是子类。但是反过来,就不行了,有子类出现的地方,父类未必就能适应。
里氏替换原则为良好的继承定义了一个规范,包含了4个层次:
- 子类必须完全实现父类的方法。例如 继承抽象类,则子类必须完全实现父类的方法。
- 子类可以有自己的方法和属性。
- 覆盖或实现父类的方法时输入参数可以被放大。
- 覆盖或实现父类的方法时输出参数可以被缩小。
依赖倒置原则 DIP
定义包含3层含义:
- 高层模块不应该依赖低层模块,两者之间都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象;
注:高层模块和低层模块容易理解,每一个逻辑的实现都是由原子逻辑组成,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。那么抽象是什么呢?在Java、PHP中抽象就是指抽象类和接口,两个都是不能直接实例化的。细节就是实现类,实现接口或者继承抽象类而产生的类就是细节,其特点就是可以被直接实例化,也就是说可以new。
依赖导致原则在Java、PHP中的表现是:
- 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类而产生的;
- 接口或抽象类不依赖于实现类;
- 实现类依赖接口或抽象类;
依赖的三种写法:
依赖是可以传递的,A对象依赖B对象、B对象依赖C对象、C对象依赖D…生生不息,依赖不止。但是记住一点:只要做到抽象依赖,即使多层依赖传递也无所畏惧。
对象的依赖关系有三种方式传递:
- 构造函数传递对象;
- Setter方法传递依赖对象;
- 接口声明依赖对象
依赖倒置原则本质就是通过抽象(抽象类或接口)使各个类或模块的实现彼此独立,不相互影响,实现模块间的松耦合,我们怎么在项目中使用这个规则呢?只要遵循以下几个规则就可以了:
- 每个类都尽量有接口或抽象类,或者抽象类和接口都有;
- 变量表面类型尽量是接口或者抽象类;
- 任何类都不应该从具体类派生;
- 尽量不要覆写父类的方法;
接口隔离原则
定义:
接口分为2种:
- 实例接口:声明一个类,然后new产生了一个实例,它是对一个类型事物的描述,这是一种接口。
- 类接口:使用interface关键字声明定义的接口
什么是隔离呢?它有两种定义:
-
客户端不应该依赖它不需要的接口
注:“客户端不依赖它不需要的接口”。那依赖什么,依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性。
-
类之间的依赖关系应该建立在最小接口上
注:它要求是最小的接口,也就是接口细化,接口纯洁。与第一个定义如出一辙,只是一个事物的两种不同描述。
我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。
接口隔离原则是对接口进行规范约束,其包含以下4层含义:
-
接口尽量要小
注:这是接口隔离原则的核心定义,不出现臃肿的接口,但是“小”是有限度的,首先不能违反单一职责原则!根据接口隔离原则拆分时,首先必须要满足单一职责原则。
-
接口要高内聚
注:什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互。具体到接口隔离原则就是,要求接口中尽量少公布public方法,接口是对外的承诺,承诺的越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
-
定制服务
注:一个系统内的模块之间必然有耦合,有耦合就要有相互访问的接口。我们设计时就需要为每个访问者(即客户端)定制服务?定制服务就是单独为一个 个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用定制服务有一个要求:只提供访问者需要的方法。
-
接口设计是有限度的
注:接口设计的越小,系统就越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化开发难度增加,可维护性降低,这不是项目或产品希望看到的,所以接口设计一定要注意适度。这个度如何来判断呢?根据经验和常识、项目来判断,没有一个固化的标准。
接口隔离原则是对接口的定义,也是对类的定义。接口和类尽量使用原子接口或原子类来组装。但是这个原子该怎么划分是设计模式中的一大难题,在实践中可以根据以下几个规则衡量:
- 一个接口只服务于一个子模块或业务逻辑;
- 通过业务逻辑压缩接口的public方法,接口时常去回顾,尽量让接口达到“满身筋骨”,而不是“肥嘟嘟”的一大堆方法;
- 已经被污染了的接口,尽量去修改,若更改风险较大,则采用适配器模式进行处理;
- 了解环境,拒绝盲从!不同的环境,接口拆分的标准就不同。深入了解业务逻辑,才能设计出最好的方式;
怎么准确的实践接口隔离原则?实践、经验、领悟!
迪米特法则LoD
定义:
迪米特法则也被称为最少知识原则,虽然名字不同,但描述的是同一个规则:一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合和调用的类知道得最少,被调用或耦合的内部是如何复杂,和本类没关系,只调用提供的public方法,其他的一概不关心。
迪米特法则对类的低耦合提出了明确的要求:
-
只和朋友通信
注:每个对象都会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,例如组合、聚合、依赖等。
-
朋友之间也是有距离的
注:即使是朋友类之间也不能无话不说,无所不知。
-
是自己的就是自己的
注:可以坚持这样一个原则:如果一个方法放在本类中,即不增加类之间关系,也不会对本类产生负面影响,那就放置本类中。
-
谨慎使用Serializable
注:Serializable接口是启用其序列化功能的接口。
迪米特法则的核心概念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才会提高。其要求的结果就是产生了大量的中转或跳转类,导致系统的复杂性提高,同时也为维护带来了难度。
迪米特法则要求类间解耦,但解耦是有限度的,除非是计算机的最小单元——二进制的 0和1。那才是完全解耦,在实际的项目中,需要适度地考虑这个原则,别为了套用原则而做项目。原则只是供参考,如果违背了这个原则,项目也未必会失败,这就需要大家在采用原则时反复度量,不遵循是不对的,严格执行就是“过犹不及”。
开闭原则:
其定义如下:
一个软件实体如类、模块、函数应该对扩展开放,对修改关闭。其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。那么软件实体又是什么呢?软件实体包含以下几个部分:
- 项目或软件产品中按照一定逻辑规划划分的模块
- 抽象和类
- 方法
注:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层次模块的变更,必然要有更高层模块进行耦合,否则就是一个孤立无意义的代码片段。
可以把变化归纳为以下三种类型:
- 逻辑变化
- 子模块变化
- 可见视图变化
开闭原则是最基础的原则,其他五个原则都是开闭原则的具体形态。也就是说前五个原则就是指导设计的工具和方法,而开闭原则才是其精神领袖。
如何使用开闭原则:
- 抽象约束
- 元数据(metadata)控制模块行为
- 制定项目章程
- 封装变化
单例模式
单例模式(Singleton Pattern)是一个比较简单的模式,其定义如下:
确保一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例
单例模式的通用类图如下图
注:ingleton类称为单例类,通过使用private的构造函数确保了在一个应用中只产生一个实 例,并且是自行实例化的(在Singleton中自己使用new Singleton())。
单例模式的优点
- 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建,销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就特别明显。
- 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置,连接数据库,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留在内存的方式来解决。
- 单例模式可以避免对资源的多重占用,例如一个写文件操作,只有一个实例在内存中,避免对同一个资源文件的同时写操作。
- 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点
- 单例模式一般没有接口,扩展很困难若要扩展,除了修改代码基本上没有其他途径可以实现。为什么单例模式不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且只提供一个实例,接口或抽象类是不可能被实例化的。当然在特殊情况下,单例单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
- 单例模式对测试是不利的,在并行开发环境中,如果单例模式没有开发完成,是不能进行测试的。
- 单例模式与单一职责原则有冲突,一个类应该只实现一个逻辑,而不关心它是否是单例的,是不是单例取决于环境,单例模式把要“单例”和业务逻辑融合在一个类中。
单例模式的使用场景
在一个系统中,要求类只有一个对象,如果出现多个对象就会出现不良反应,可以采用单例模式。
例如:
要求生成唯一序号的环境。
在项目中需要一个共享访问点,或共享数据,例如WEB页面上的计数器,可以不用把每次刷新都记录到数据库中,使用单例模式保持计数器的值,并确保线程是安全的。
创建一个对象需要消耗的资源过多,如要访问IO和数据库资源。
需要定义大量的静态常量和静态方法,可以采用单例模式。
工厂模式
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
工厂方法模式的通用类图
在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定 义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂 ConcreteCreator完成的。
工厂模式的优点
- 首先是良好的封装性,代码结构清晰,一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。
- 其次,工厂方法模式的扩展性非常优秀。在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化”。
- 再次,屏蔽产品类。这一特点非常重要,产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。因为产品类的实例化工作是由工厂类负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的。在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式活性的一个直接案例。
- 最后,工厂方法模式是典型的解耦框架。高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类,没问题!
工厂方法模式的使用场景
- 首先,工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。
- 其次,需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。万物皆对象,那万物也就皆产品类,例如需要设计一个连接邮件服务器的框架,有三种网络协议可供选择:POP3、IMAP、HTTP,我们就可以把这三种连接方法作为产品类,定义一个接口如IConnectMail,然后定义对邮件的操作方法,用不同的方法实现三个具体的产品类(也就是连接方式)再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。如此设计,可以做到完美的扩展,如某些邮件服务器提供了WebService接口,很好,们只要增加一个产品类就可以了。
- 再次,工厂方法模式可以用在异构项目中,例如通过WebService与一个非Java的项目交互,虽然WebService号称是可以做到异构系统的同构化,但是在实际的开发中,还是会碰到很多问题,如类型问题、WSDL文件的支持问题,等等。从WSDL中产生的对象都认为是一个产品,然后由一个具体的工厂类进行管理,减少与外围系统的耦合。
- 最后,可以使用在测试驱动开发的框架下。例如,测试一个类A,就需要把与类A有关联关系的类B也同时产生出来,我们可以使用工厂方法模式把类B虚拟出来避免类A与类B的耦合。
工厂方法模式的扩展
工厂方法模式有很多扩展,而且与其他模式结合使用威力更大,下面将介绍4种扩展。
-
缩小为简单工厂模式
我们这样考虑一个问题:一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法就可以了。
-
升级为多个工厂类
当我们在做一个比较复杂的项目时,经常会遇到初始化一个对象很耗费精力的情况,所有的产品类都放到一个工厂方法中进行初始化会使代码结构不清晰。例如,一个产品类有5个具体实现,每个实现类的初始化(不仅仅是new,初始化包括new一个对象,并对对象设置一定的初始值)方法都不相同,如果写在一个工厂方法中,势必会导致方法巨大无比,那该怎么办?考虑到需要结构清晰,我们就为每个产品定义一个创造者,然后由调用者自己去选择与哪个工厂方法关联。
-
替代单例模式
单例模式以及扩展出的多例模式,并且指出了单例和多例的一些缺点,我们是不是可以采用工厂方法模式实现单例模式的功能呢?单例模式的核心要求就是在内存中只有一个对象,通过工厂方法模式也可以只在内存中生产一个对象。
-
延迟初始化
何为延迟初始化(Lazy initialization)?一个对象被消费完毕后,并不立刻释放,工厂类保持其初始状态,等待再次被使用。延迟初始化是工厂方法模式的一个扩展应用。
延迟加载框架是可以扩展的,例如限制某一个产品类的最大实例化数量,可以通过判断Map中已有的对象数量来实现,这样的处理是非常有意义的,例如JDBC连接数据库,都会要求设置一个MaxConnections最大连接数量,该数量就是内存中最大实例化的数量。延迟加载还可以用在对象初始化比较复杂的情况下,例如硬件访问,涉及多方面的交互,则可以通过延迟加载降低对象的产生和销毁带来的复杂性。
抽象工厂模式
定义:为创建一组相关或相互依赖的对象提供一个接口,而且无须指定它们的具体类。
抽象工厂模式的通用类图
抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。
抽象工厂模式的优点
- 封装性,每个产品的实现类不是高层模块要关心的,它要关心的是什么?是接口,是抽象,它不关心对象是如何创建出来,这由谁负责呢?工厂类,只要知道工厂类是谁,我就能创建出一个需要的对象,省时省力,优秀设计就应该如此。
- 产品族内的约束为非公开状态。例如生产男女比例的问题上,猜想女娲娘娘肯定有自己的打算,不能让女盛男衰,否则女性的优点不就体现不出来了吗?那在抽象工厂模式,就应该有这样的一个约束:每生产1个女性,就同时生产出1.2个男性,这样的生产过程对调用 工厂类的高层模块来说是透明的,它不需要知道这个约束,我就是要一个黄色女性产品就可以了,具体的产品族内的约束是在工厂内实现的。
抽象工厂模式的缺点
抽象工厂模式的最大缺点就是产品族扩展非常困难,为什么这么说呢?我们以通用代码 为例,如果要增加一个产品C,也就是说产品家族由原来的2个增加到3个,看看我们的程序有多大改动吧!抽象类AbstractCreator要增加一个方法createProductC(),然后两个实现类都要修改,想想看,这严重违反了开闭原则,而且我们一直说明抽象类和接口是一个契约。改变契约,所有与契约有关系的代码都要修改,那么这段代码叫什么?叫“有毒代码”,——只要与这段代码有关系,就可能产生侵害的危险!
抽象工厂模式的使用场景
抽象工厂模式的使用场景定义非常简单:一个对象族(或是一组没有任何关系的对象) 都有相同的约束,则可以使用抽象工厂模式。什么意思呢?例如一个文本编辑器和一个图片处理器,都是软件实体,但是Linux下的文本编辑器和Windows下的文本编辑器虽然功能和界面都相同,但是代码实现是不同的,图片处理器也有类似情况。也就是具有了共同的约束条件:操作系统类型。于是我们可以使用抽象工厂模式,
产生不同操作系统下的编辑器和图片处理器。
抽象工厂模式的注意事项
在抽象工厂模式的缺点中,我们提到抽象工厂模式的产品族扩展比较困难,但是一定要清楚,是产品族扩展困难,而不是产品等级。在该模式下,产品等级是非常容易扩展的,增 加一个产品等级,只要增加一个工厂类负责新增加出来的产品生产任务即可。也就是说横向 扩展容易,纵向扩展困难。
最佳实践
一个模式在什么情况下才能够使用,是很多人比较困惑的地方。抽象工厂模式是一个简单的模式,使用的场景非常多,大家在软件产品开发过程中,涉及不同操作系统的时候, 都可以考虑使用抽象工厂模式,例如一个应用,需要在三个不同平台(Windows、Linux、 Android(Google发布的智能终端操作系统))上运行,你会怎么设计?分别设计三套不同的应用?非也,通过抽象工厂模式屏蔽掉操作系统对应用的影响。三个不同操作系统上的软件功能、应用逻辑、UI都应该是非常类似的,唯一不同的是调用不同的工厂方法,由不同的产品类去处理与操作系统交互的信息。
模板方法模式
模板方法模式的定义
定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤 。
模板方法模式的通用类图:
模板方法模式确实非常简单,仅仅使用面向对象的继承机制,但它是一个应用非常广泛的模式。其中,AbstractClass叫做抽象模板,它的方法分为两类:
-
基本方法
基本方法也叫做基本操作,是由子类实现的方法,并且在模板方法中被调用。
-
模板方法
可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度, 完成固定的逻辑。
注:为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写 。抽象模板中的基本方法尽量设计为protected类型,符合迪米特法则,不需要暴露的属性或方法尽量不要设置为protected类型。实现类若非必要,尽量不要扩大父类中的访问权限。
模板方法模式的优点
-
封装不变部分,扩展可变部分
注:把认为是不变部分的算法封装到父类实现,而可变部分的则可以通过继承来继续扩展。
-
提取公共部分代码,便于维护
-
行为由父类控制,子类实现
注:基本方法是由子类实现的,因此子类可以通过扩展的方式增加相应的功能,符合开闭原则。
模板方法模式的缺点
按照我们的设计习惯,抽象类负责声明最抽象、最一般的事物属性和方法,实现类完成具体的事物属性和方法。但是模板方法模式却颠倒了,抽象类定义了部分抽象方法,由子类实现,子类执行的结果影响了父类的结果,也就是子类对父类产生了影响,这在复杂的项目 中,会带来代码阅读的难度,而且也会让新手产生不适感。
模板方法模式的使用场景
- 多个子类有公有的方法,并且逻辑基本相同时
- 重要、复杂的算法,可以把核心算法设计为模板方法,周边的相关细节功能则由各个子类实现
- 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通 过钩子函数(见“模板方法模式的扩展”)约束其行为
最佳实践
初级程序员在写程序的时候经常会问高手“父类怎么调用子类的方法”。这个问题很有普 遍性,反正我是被问过好几回,那么父类是否可以调用子类的方法呢?我的回答是能,但强 烈地、极度地不建议这么做,那该怎么做呢?
- 把子类传递到父类的有参构造中,然后调用
- 使用反射的方式调用,你使用了反射还有谁不能调用的?
- 父类调用子类的静态方法
这三种都是父类直接调用子类的方法,好用不?好用!解决问题了吗?解决了!项目中允许使用不?不允许!我就一直没有搞懂为什么要用父类调用子类的方法。如果一定要调用子类,那为什么要继承它呢?搞不懂。其实这个问题可以换个角度去理解,父类建立框架, 子类在重写了父类部分的方法后,再调用从父类继承的方法,产生不同的结果(而这正是模板方法模式)。这是不是也可以理解为父类调用了子类的方法呢?你修改了子类,影响了父类行为的结果,曲线救国的方式实现了父类依赖子类的场景,模板方法模式就是这种效果。
模板方法在一些开源框架中应用非常多,它提供了一个抽象类,然后开源框架写了一堆子类。在《××× In Action》中就说明了,如果你需要扩展功能,可以继承这个抽象类,然后覆写protected方法,再然后就是调用一个类似execute方法,就完成你的扩展开发,非常容易扩展的一种模式。
建造者模式
建造者模式的定义
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式的通用类图如图:
在建造者模式中,有如下4个角色:
-
Product产品类
通常是实现了模板方法模式,也就是有模板方法和基本方法。
-
Builder抽象建造者
规范产品的组建,一般是由子类实现。
-
ConcreteBuilder具体建造者
实现抽象类定义的所有方法,并且返回一个组建好的对象。
-
Director导演类
负责安排已有模块的顺序,然后告诉Builder开始建造。
注:需要注意的是,如果有多个产品类就有几个具体的建造者,而且这多个产品类具有相同 接口或抽象类。导演类起到封装的作用,避免高层模块深入到建造者内部的实现类。当然,在建造者模式比较庞大时,导演类可以有多个。
建造者模式的优点
-
封装性
使用建造者模式可以使客户端不必知道产品内部组成的细节,如例子中我们就不需要关心每一个具体的模型内部是如何实现的,产生的对象类型就是CarModel。
-
建造者独立,容易扩展
建造者类之间是相互独立的,对系统的扩展非常有利。
-
便于控制细节风险
由于具体的建造者是独立的,因此可以对建造过程逐步细化,而不对其他的模块产生任何影响。
建造者模式的使用场景
- 相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式。
- 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
- 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适。
- 在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程。该种场景只能是一个补偿方法,因为一个对象不容易获得,而在设计阶段竟然没有发觉,而要通过创建者模式柔化创建程,本身已经违反设计的最初目标。
建造者模式的注意事项
建造者模式关注的是零件类型和装配工艺(顺序),这是它与工厂方法模式最大不同的地方,虽然同为创建类模式,但是注重点不同。
建造者模式的扩展
不需要扩展了,建造者模式中还有一个角色没有说明,就是零件,建造者怎么去建造一个对象?是零件的组装,组装顺序不同对象效能也不同,这才是建造者模式要表达的核心意义,而怎么才能更好地达到这种效果呢?引入模板方法模式是一个非常简单而有效的办法。
大家看到这里估计就开始犯嘀咕了,这个建造者模式和工厂模式非常相似呀,是的,非常相似,但是记住一点你就可以游刃有余地使用了:建造者模式最主要的功能是基本方法的调用顺序安排,也就是这些基本方法已经实现了,通俗地说就是零件的装配,顺序不同产生 的对象也不同;而工厂方法则重点是创建,创建零件是它的主要职责,组装顺序则不是它关心的。
最佳实践
在使用建造者模式的时候考虑一下模板方法模式,别孤立地思考一个模式, 僵化地套用一个模式会让你受害无穷!
代理模式
代理模式的定义
代理模式(Proxy Pattern)是一个使用率非常高的模式,其定义是: 为其他对象提供一种代理以控制对这个对象的访问。
代理模式的通用类图:
代理模式也叫做委托模式,它是一项基本设计技巧。许多其他的模式,如状态模式、策略模式、访问者模式本质上是在更特殊的场合采用了委托模式,而且在日常的应用中,代理模式可以提供非常好的访问控制。 看一下类图中的三个角色的定义:
-
Subject抽象主题角色
抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求。
-
RealSubject具体主题角色
也叫做被委托角色、被代理角色。它才是冤大头,是业务逻辑的具体执行者。
-
Proxy代理主题角色
也叫做委托类、代理类。它负责对真实角色的应用,把所有抽象主题类定义的方法限制 委托给真实主题角色实现,并且在真实主题角色处理完毕前后做预处理和善后处理工作。
一个代理类可以代理多个被委托者或被代理者,因此一个代理类具体代理哪个真实主题角色,是由场景类决定的。当然,最简单的情况就是一个主题类和一个代理类,这是最简洁的代理模式。在通常情况下,一个接口只需要一个代理类就可以了,具体代理哪个实现类由高层模块来决定,也就是在代理类的构造函数中传递被代理者。
想要代理谁就产生该代理的实例,然后把被代理者传递进来,该模式在实际的项目应用中比较广泛。
代理模式的优点
-
职责清晰
真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件事务,附带的结果就是编程简洁清晰。
-
高扩展性
具体主题角色是随时都会发生变化的,只要它实现了接口,甭管它如何变化,都逃不脱如来佛的手掌(接口),那我们的代理类完全就可以在不做任何修改的情况下使用。
-
智能化
代理模式的使用场景
相信第一次接触到代理模式的读者肯定很郁闷,为什么要用代理呀?想想现实世界吧,打官司为什么要找个律师?因为你不想参与中间过程的是是非非,只要完成自己的答辩就成,其他的比如事前调查、事后追查都由律师来搞定,这就是为了减轻你的负担。代理模式的使用场景非常多,大家可以看看Spring AOP,这是一个非常典型的动态代理。
代理模式的扩展
-
普通代理
首先说普通代理,它的要求就是客户端只能访问代理角色,而不能访问真实角色,这是比较简单的。
注意 :普通代理模式的约束问题,尽量通过团队内的编程规范类约束,因为每一个主题类是可被重用的和可维护的,使用技术约束的方式对系统维护是一种非常不利的因素。
-
强制代理
强制代理在设计模式中比较另类,为什么这么说呢?一般的思维都是通过代理找到真实 的角色,但是强制代理却是要“强制”,你必须通过真实角色查找到代理角色,否则你不能访 问。不管你是通过代理类还是通过直接new一个主题角色类,都不能访问,只有通过真实角色指定的代理类才可以访问,也就是说由真实角色管理代理角色。这么说吧,高层模块new 了一个真实角色的对象,返回的却是代理角色,这就好比是你和一个明星比较熟,相互认 识,有件事情你需要向她确认一下,于是你就直接拨通了明星的电话:“喂,沙比呀,我要见一下×××导演,你帮下忙了!” “不行呀衰哥,我这几天很忙呀,你找我的经纪人吧…” 。郁闷了吧,你是想直接绕过她的代理,谁知道返回的还是她的代理,这就是强制代理, 你可以不用知道代理存在,但是你的所作所为还是需要代理为你提供。
代理是有个性的
一个类可以实现多个接口,完成不同任务的整合。也就是说代理类不仅仅可以实现主题 接口,也可以实现其他接口完成不同的任务,而且代理的目的是在目标对象方法的基础上作增强,这种增强的本质通常就是对目标对象的方法进行拦截和过滤。
动态代理
什么是动态代理?动态代理是在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。 现在有一个非常流行的名称叫做面向横切面编程,也就是AOP(Aspect Oriented Programming),其核心就是采用了动态代理机制。
注意:要实现动态代理的首要条件是:被代理类必须实现一个接口,回想一下前面的分析吧。当然了,现在也有很多技术如CGLIB可以实现不需要接口也可以实现动态代理的方式。
原型模式
原型模式的定义
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的通用类图:
注:原型模式的核心是一个clone方法,通过该方法进行对象的拷贝。
原型模式的优点
-
性能优良
原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环体内产生大量的对象时,原型模式可以更好地体现其优点。
-
逃避构造函数的约束
这既是它的优点也是缺点,直接在内存中拷贝,构造函数是不会执行的。优点就是减少了约束,缺点也是减少了约束,需要大家在实际应用时考虑。
原型模式的使用场景
-
资源优化场景
类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
-
性能和安全要求的场景
通过new产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
-
一个对象多个修改者的场景
一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑 使用原型模式拷贝多个对象供调用者使用。
在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。
原型模式的注意事项
- 构造函数不会被执行
- 浅拷贝和深拷贝
注意:使用原型模式时,引用的成员变量必须满足两个条件才不会被拷贝:一是类的成员变量,而不是方法内变量;二是必须是一个可变的引用对象,而不是一个原始类型或不可变对象。
注意:深拷贝和浅拷贝建议不要混合使用,特别是在涉及类的继承时,父类有多个引用的情况就非常复杂,建议的方案是深拷贝和浅拷贝分开实现。
中介者模式
中介者模式的定义
用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
中介者模式通用类图:
从类图中看,中介者模式由以下几部分组成:
-
Mediator 抽象中介者角色
抽象中介者角色定义统一的接口,用于各同事角色之间的通信。
-
Concrete Mediator 具体中介者角色
具体中介者角色通过协调各同事角色实现协作行为,因此它必须依赖于各个同事角色。
-
Colleague 同事角色
每一个同事角色都知道中介者角色,而且与其他的同事角色通信的时候,一定要通过中 介者角色协作。每个同事类的行为分为两种:一种是同事本身的行为,比如改变对象本身的 状态,处理自己的行为等,这种行为叫做自发行为(Self-Method),与其他的同事类或中介 者没有任何的依赖;第二种是必须依赖中介者才能完成的行为,叫做依赖方法(Dep- Method)。
中介者模式的优点:
中介者模式的优点就是减少类间的依赖,把原有的一对多的依赖变成了一对一的依赖, 同事类只依赖中介者,减少了依赖,当然同时也降低了类间的耦合。
中介者模式的缺点
中介者模式的缺点就是中介者会膨胀得很大,而且逻辑复杂,原本N个对象直接的相互依赖关系转换为中介者和同事类的依赖关系,同事类越多,中介者的逻辑就越复杂。
中介者模式的使用场景
中介者模式简单,但是简单不代表容易使用,很容易被误用。在面向对象的编程中,对 象和对象之间必然会有依赖关系,如果某个类和其他类没有任何相互依赖的关系,那这个类 就是一个“孤岛”,在项目中就没有存在的必要了!就像是某个人如果永远独立生活,与任何人都没有关系,那这个人基本上就算是野人了——排除在人类这个定义之外。
类之间的依赖关系是必然存在的,一个类依赖多个类的情况也是存在的,存在即合理, 那是否可以说只要有多个依赖关系就考虑使用中介者模式呢?答案是否定的。中介者模式未 必能帮你把原本凌乱的逻辑整理得清清楚楚,而且中介者模式也是有缺点的,这个缺点在使 用不当时会被放大,比如原本就简单的几个对象依赖关系,如果为了使用模式而加入了中介 者,必然导致中介者的逻辑复杂化,因此中介者模式的使用需要“量力而行”!中介者模式适 用于多个对象之间紧密耦合的情况,紧密耦合的标准是:在类图中出现了蜘蛛网状结构。在 这种情况下一定要考虑使用中介者模式,这有利于把蜘蛛网梳理为星型结构,使原本复杂混 乱的关系变得清晰简单。
中介者模式的实际应用
中介者模式也叫做调停者模式,是什么意思呢?一个对象要和N多个对象交流,就像对象间的战争,很混乱。这时,需要加入一个中心,所有的类都和中心交流,中心说怎么处理 就怎么处理,我们举一些在开发和生活中经常会碰到的例子。
-
机场调度中心
大家在每个机场都会看到有一个“××机场调度中心”,它就是具体的中介者,用来调度每一架要降落和起飞的飞机。比如,某架飞机(同事类)飞到机场上空了,就询问调度中心 (中介者)“我是否可以降落”以及“降落到哪个跑道”,调度中心(中介者)查看其他飞机 (同事类)情况,然后通知飞机降落。如果没有机场调度中心,飞机飞到机场了,飞行员要先看看有没有飞机和自己一起降落的,有没有空跑道,停机位是否具备等情况,这种局面是难以想象的!
-
MVC框架
大家都应该使用过MVC模式框架,其中的C(Controller)就是一个中介者,叫做前端控制器(Front Controller),它的作用就是把M(Model,业务逻辑)和V(View,视图)隔离开, 协调M和V协同工作,把M运行的结果和V代表的视图融合成一个前端可以展示的页面,减少 M和V的依赖关系。MVC框架已经成为一个非常流行、成熟的开发框架,这也是中介者模式的优点的一个体现。
-
媒体网关
媒体网关也是一个典型的中介者模式,比如使用MSN时,张三发消息给李四,其过程应 该是这样的:张三发送消息,MSN服务器(中介者)接收到消息,查找李四,把消息发送到李四,同时通知张三,消息已经发送。在这里,MSN服务器就是一个中转站,负责协调两个客户端的信息交流,与此相反的就是IPMsg(也叫飞鸽),它没有使用中介者,而直接使用了UDP广播的方式,每个客户端既是客户端也是服务器端。
最佳实践
中介者模式很少用到接口或者抽象类,这与依赖倒置原则是冲突的,这是什么原因呢?首先,既然是同事类而不是兄弟类(有相同的血缘),那就说明这些类之间是协作关系,完成不同的任务,处理不同的业务,所以不能在抽象类或接口中严格定义同事类必须具有的方法(从这点也可以看出继承是高侵入性的)。
中介者模式是一个非常好的封装模式,也是一个很容易被滥用的模式,一个对象依赖几 个对象是再正常不过的事情,但是纯理论家就会要求使用中介者模式来封装这种依赖关系, 这是非常危险的!使用中介模式就必然会带来中介者的膨胀问题,这在一个项目中是很不恰 当的。大家可以在如下的情况下尝试使用中介者模式:
- N个对象之间产生了相互的依赖关系(N>2)。
- 多个对象有依赖关系,但是依赖的行为尚不确定或者有发生改变的可能,在这种情况下一般建议采用中介者模式,降低变更引起的风险扩散。
- 产品开发。一个明显的例子就是MVC框架,把中介者模式应用到产品中,可以提升产 品的性能和扩展性,但是对于项目开发就未必,因为项目是以交付投产为目标,而产品则是以稳定、高效、扩展为宗旨。
命令模式
命令模式的定义
命令模式是一个高内聚的模式,其定义为:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
命令模式的通用类图:
在该类图中,我们看到三个角色:
-
Receiver接收者角色
该角色就是干活的角色,命令传递到这里是应该被执行的。
-
Command命令角色
需要执行的所有命令都在这里声明。
-
Invoker调用者角色
接收到命令,并执行命令。在例子中,我(项目经理)就是这个角色。
命令模式比较简单,但是在项目中非常频繁地使用,因为它的封装性非常好,把请求方 (Invoker)和执行方(Receiver)分开了,扩展性也有很好的保障。
命令模式的优点
-
命令模式的优点
调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只需调用Command 抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。
-
可扩展性
Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合。
-
命令模式结合其他模式会更优秀
命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少 Command子类的膨胀问题。
命令模式的缺点
命令模式也是有缺点的,请看Command的子类:如果有N个命令,问题就出来了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大,这个就需要读者在项 目中慎重考虑使用。
命令模式的使用场景
只要你认为是命令的地方就可以采用命令模式,例如,在GUI开发中,一个按钮的点击 是一个命令,可以采用命令模式;模拟SHELL命令的时候,当然也要采用命令模式;触发-反馈机制的处理等。
最佳实践
每一个模式到实际应用的时候都有一些变形,命令模式的Receiver在实际应用中一般都会被封装掉(除非非常必要, 例如撤销处理),那是因为在项目中:约定的优先级最高,每一个命令是对一个或多个 Receiver的封装,我们可以在项目中通过有意义的类名或命令名处理命令角色和接收者角色的耦合关系(这就是约定),减少高层模块(Client类)对低层模块(Receiver角色类)的依赖关系,提高系统整体的稳定性。
责任链模式
责任链模式定义
使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。责任链模式的重点是在“链”上,由一条链去处理相似的请求在链中决定谁来处理这个请求,并返回相应的结果。
通用类图如下图:
责任链模式的核心在“链”上,“链”是由多个处理者ConcreteHandler组成的。
注意:在责任链模式中一个请求发送到链中后,前一节点消费部分消息,然后交由后续节点继续处理,最终可以有处理结果也可以没有处理结果,可以不用理会什么纯的、不纯的责任链模式。
在实际应用中,一般会有一个封装类对责任模式进行封装,也就是替代Client类,直接 返回链中的第一个处理者,具体链的设置不需要高层次模块关系,这样更简化了高层次模 块的调用,减少模块间的耦合,提高系统的灵活性。
责任链模式的优点
责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处 理者可以不用知道请求的全貌(例如在J2EE项目开发中,可以剥离出无状态Bean由责任链处理),两者解耦,提高系统的灵活性。
责任链模式的缺点
责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别 是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长, 环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。
责任链模式的注意事项
链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个 最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。
最佳实践
想想单一职责原则和迪米特法则吧,通过融合模板方法模式,各个实现类只要关注的自己业务逻辑就成了,至于说什么事要自己处理,那就让父类去决定好了,也就是说父类实现了请求传递的功能,子类实现请求的处理,符合单一职责原则,各个实现类只完成一个动作或逻辑,也就是只有一个原因引起类的改变,建议大家在使用的时候用这种方法,好处是非常明显的了,子类的实现非常简单,责任链的建立也是非常灵活的。
责任链模式屏蔽了请求的处理过程,你发起一个请求到底是谁处理的,这个你不用关 心,只要你把请求抛给责任链的第一个处理者,最终会返回一个处理结果(当然也可以不做任何处理),作为请求者可以不用知道到底是需要谁来处理的,这是责任链模式的核心,同时责任链模式也可以作为一种补救模式来使用。
装饰模式
装饰模式的定义
装饰模式(Decorator Pattern)是一种比较常见的模式,其定义如下:动态地给一个对象添加一些额外的职责。 就增加功能来说,装饰模式相比生成子类更为灵活。
装饰模式的通用类图:
在类图中,有四个角色需要说明:
-
Component抽象构件
Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对 象,如上面的成绩单。
注意:在装饰模式中,必然有一个最基本、最核心、最原始的接口或抽象类充当Component抽象构件。
-
ConcreteComponent 具体构件
ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它。
-
Decorator装饰角色
一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方 法呀,在它的属性里必然有一个private变量指向Component抽象构件。
-
具体装饰角色
ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西。
装饰模式的优点
- 装饰类和被装饰类可以独立发展,而不会相互耦合。换句话说,Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
- 装饰模式是继承关系的一个替代方案。我们看装饰类Decorator,不管装饰多少层,返回的对象还是Component,实现的还是is-a的关系。
- 装饰模式可以动态地扩展一个实现类的功能,这不需要多说,装饰模式的定义就是如此。
装饰模式的缺点
对于装饰模式记住一点就足够了:多层的装饰是比较复杂的。为什么会复杂呢?你想想 看,就像剥洋葱一样,你剥到了最后才发现是最里层的装饰出现了问题,想象一下工作量 吧,因此,尽量减少装饰类的数量,以便降低系统的复杂度。
装饰模式的使用场景
- 需要扩展一个类的功能,或给一个类增加附加功能。
- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
- 需要为一批的兄弟类进行改装或加装功能,当然是首选装饰模式。
最佳实践
装饰模式是对继承的有力补充。你要知道继承不是万能的,继承可以解决实际的问题, 但是在项目中你要考虑诸如易维护、易扩展、易复用等,而且在一些情况下(比如上面那个 成绩单例子)你要是用继承就会增加很多子类,而且灵活性非常差,那当然维护也不容易 了,也就是说装饰模式可以替代继承,解决我们类膨胀的问题。同时,你还要知道继承是静 态地给类增加功能,而装饰模式则是动态地增加功能。
装饰模式还有一个非常好的优点:扩展性非常好。在一个项目中,你会有非常多的因素 考虑不到,特别是业务的变更,不时地冒出一个需求,尤其是提出一个令项目大量延迟的需 求时,那种心情是相当的难受!装饰模式可以给我们很好的帮助,通过装饰模式重新封装一 个类,而不是通过继承来完成,简单点说,三个继承关系Father、Son、GrandSon三个类,我 要在Son类上增强一些功能怎么办?我想你会坚决地顶回去!不允许,对了,为什么呢?你 增强的功能是修改Son类中的方法吗?增加方法吗?对GrandSon的影响呢?特别是GrandSon 有多个的情况,你会怎么办?这个评估的工作量就够你受的,所以这是不允许的,那还是要解决问题的呀,怎么办?通过建立SonDecorator类来修饰Son,相当于创建了一个新的类,这个对原有程序没有变更,通过扩展很好地完成了这次变更。
策略模式
策略模式的定义
策略模式(Strategy Pattern)是一种比较简单的模式,也叫做政策模式(Policy Pattern)。其定义如下:定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。
这个定义是非常明确、清晰的,“定义一组算法”,看看我们的三个计谋是不是三个算 法?“将每个算法都封装起来”,封装类Context不就是这个作用吗?“使它们可以互换”当然可以互换了,都实现是相同的接口,那当然可以相互转化了。
策略模式的通用类图:
策略模式使用的就是面向对象的继承和多态机制,非常容易理解和掌握,我们再来看看策略模式的三个角色:
-
Context封装角色
它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问, 封装可能存在的变化。
-
Strategy抽象策略角色
策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。各 位看官可能要问了,类图中的AlgorithmInterface是什么意思,嘿嘿,algorithm是“运算法则”的意思,结合起来意思就明白了吧。
-
ConcreteStrategy具体策略角色
实现抽象策略中的操作,该类含有具体的算法。
注:策略模式的重点就是封装角色,它是借用了代理模式的思路,大家可以想想,它和代理 模式有什么差别,差别就是策略模式的封装角色和被封装的策略类不用是同一个接口,如果 是同一个接口那就成为了代理模式。
策略模式就是这么简单,它就是采用了面向对象的继承和多态机制,其他没什么玄机。想想看,你真实的业务环境有这么简单吗?一个类实现多个接口很正常,你要有火眼金睛看清楚哪个接口是抽象策略接口,哪些是和策略模式没有任何关系,这就是你作为系统分析师的价值所在。
策略模式的优点
-
算法可以自由切换
这是策略模式本身定义的,只要实现抽象策略,它就成为策略家族的一个成员,通过封 装角色对其进行封装,保证对外提供“可自由切换”的策略。
-
避免使用多重条件判断
如果没有策略模式,我们想想看会是什么样子?一个策略家族有5个策略算法,一会要 使用A策略,一会要使用B策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
-
扩展性良好
这甚至都不用说是它的优点,因为它太明显了。在现有的系统中增加一个策略太容易 了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大地符合了OCP原则。
策略模式的缺点
-
策略类数量增多
每一个策略都是一个类,复用的可能性很小,类数量增多。
-
所有的策略类都需要对外暴露
上层模块必须知道有哪些策略,然后才能决定使用哪一个策略,这与迪米特法则是相违 背的,我只是想使用了一个策略,我凭什么就要了解这个策略呢?那要你的封装类还有什么 意义?这是原装策略模式的一个缺点,幸运的是,我们可以使用其他模式来修正这个缺陷, 如工厂方法模式、代理模式或享元模式。
策略模式的使用场景
-
多个类只有在算法或行为上稍有不同的场景。
-
算法需要自由切换的场景。
例如,算法的选择是由使用者决定的,或者算法始终在进化,特别是一些站在技术前沿 的行业,连业务专家都无法给你保证这样的系统规则能够存在多长时间,在这种情况下策略 模式是你最好的助手。
-
需要屏蔽算法规则的场景
现在的科技发展得很快,人脑的记忆是有限的(就目前来说是有限的),太多的算法你 只要知道一个名字就可以了,传递相关的数字进来,反馈一个运算结果,万事大吉。
策略模式的注意事项
如果系统中的一个策略家族的具体策略数量超过4个,则需要考虑使用混合模式,解决 策略类膨胀和对外暴露的问题,否则日后的系统维护就会成为一个烫手山芋,谁都不想接。
最佳实践
策略模式是一个非常简单的模式。它在项目中使用得非常多,但它单独使用的地方就比 较少了,因为它有致命缺陷:所有的策略都需要暴露出去,这样才方便客户端决定使用哪一 个策略。
适配器模式
适配器模式的定义
适配器模式(Adapter Pattern)的定义如下:
将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
适配器模式又叫做变压器模式,也叫做包装模式(Wrapper),但是包装模式可不止一 个,还包括了装饰模式。
适配器模式的通用类图:
适配器模式在生活中还是很常见的,比如笔记本上的电源适配器,可以使用在110~ 220V之间变化的电源,而笔记本还能正常工作,这也是适配器一个良好模式的体现,简单地说,适配器模式就是把一个接口或类转换成其他的接口或类,从另一方面来说,适配器模式也就是一个包装模式,为什么呢?它把Adaptee包装成一个Target接口的类,加了一层衣服, 包装成另外一个靓妞了。大家知道,设计模式原是为建筑设计而服务的,软件设计模式只是借用了人家的原理而已,那我们来看看最原始的适配器是如何设计的。
适配器模式的三个角色:
-
Target目标角色
该角色定义把其他类转换为何种接口,也就是我们的期望接口,例子中的IUserInfo接口 就是目标角色。
-
Adaptee源角色
你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对 象,经过适配器角色的包装,它会成为一个崭新、靓丽的角色。
-
Adapter适配器角色
适配器模式的核心角色,其他两个角色都是已经存在的角色,而适配器角色是需要新建 立的,它的职责非常简单:把源角色转换为目标角色,怎么转换?通过继承或是类关联的方 式。
适配器模式的优点
-
适配器模式可以让两个没有任何关系的类在一起运行,只要适配器这个角色能够搞定 他们就成。
-
增加了类的透明性
想想看,我们访问的Target目标角色,但是具体的实现都委托给了源角色,而这些对高 层次模块是透明的,也是它不需要关心的。
-
提高了类的复用度
当然了,源角色在原有的系统中还是可以正常使用,而在目标角色中也可以充当新的演员。
-
灵活性非常好
某一天,突然不想要适配器,没问题,删除掉这个适配器就可以了,其他的代码都不用 修改,基本上就类似一个灵活的构件,想用就用,不想就卸载。
适配器模式的使用场景
适配器应用的场景只要记住一点就足够了:你有动机修改一个已经投产中的接口时,适配器模式可能是最适合你的模式。比如系统扩展了,需要使用一个已有或新建立的类,但这个类又不符合系统的接口,怎么办?使用适配器模式。
适配器模式的注意事项
适配器模式最好在详细设计阶段不要考虑它,它不是为了解决还处在开发阶段的问题, 而是解决正在服役的项目问题,没有一个系统分析师会在做详细设计的时候考虑使用适配器 模式,这个模式使用的主要场景是扩展应用中,就像我们上面的那个例子一样,系统扩展 了,不符合原有设计的时候才考虑通过适配器模式减少代码修改带来的风险。
再次提醒一点,项目一定要遵守依赖倒置原则和里氏替换原则,否则即使在适合使用适 配器的场合下,也会带来非常大的改造。
适配器模式的扩展
适配器的通用代码也比较简单,把原有的继承关系变更为关联关系就可以了。对象适配器和类适配器的区别是:类适配器是类间继承,对象适配器是对象的合成关系,也可以说是类的关联关系,这是两者的根本区别。二者在实际项目中都会经常用到,由于对象适配器是通过类间的关联关系进行耦合的,因此在设计时就可以做到比较灵活,比如修补源角色的隐形缺陷,关联其他对象等,而类适配器就只能通过覆写源角色的方法进行扩展,在实际项目中,对象适配器使用到场景相对较多。
最佳实践
适配器模式是一个补偿模式,或者说是一个“补救”模式,通常用来解决接口不相容的问 题,在百分之百的完美设计中是不可能使用到的,什么是百分之百的完美设计?“千虑”而没 有“一失”的设计,但是,再完美的设计也会遇到“需求”变更这个无法逃避的问题,就以我们 上面的人力资源管理系统为例来说,不管系统设计得多么完美,都无法逃避新业务的发生, 技术只是一个工具而已,是因为它推动了其他行业的进步和发展而具有了价值,通俗地说, 技术是为业务服务的,因此业务在日新月异变化的同时,也对技术提出了同样的要求,在这 种要求下,就需要我们有一种或一些这样的补救模式诞生,使用这些补救模式可以保证我们 的系统在生命周期内能够稳定、可靠、健壮的运行,而适配器模式就是这样的一个“救世 主”,它在需求巨变、业务飞速而导致你极度郁闷、烦躁、崩溃的时候横空出世,它通过把 非本系统接口的对象包装成本系统可以接受的对象,从而简化了系统大规模变更风险的存在。
迭代器模式
迭代器模式的定义
迭代器模式(Iterator Pattern)目前已经是一个没落的模式,基本上没人会单独写一个迭代器,除非是产品性质的开发,其定义为:它提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。
迭代器是为容器服务的,那什么是容器呢? 能容纳对象的所有类型都可以称之为容器,例如Collection集合类型、Set类型等,迭代器模式就是为解决遍历这些容器中的元素而 诞生的。
其通用类图如下:
迭代器模式提供了遍历容器的方便性,容器只要管理增减元素就可以了,需要遍历时交 由迭代器进行。迭代器模式正是由于使用得太频繁,所以大家才会忽略,我们来看看迭代器 模式中的各个角色:
-
Iterator抽象迭代器
抽象迭代器负责定义访问和遍历元素的接口,而且基本上是有固定的3个方法:first()获 得第一个元素,next()访问下一个元素,isDone()是否已经访问到底部(Java叫做hasNext()方法)。
-
ConcreteIterator具体迭代器
具体迭代器角色要实现迭代器接口,完成容器元素的遍历。
-
Concrete Aggregate具体容器
具体容器实现容器接口定义的方法,创建出容纳迭代器的对象。
注意:开发系统时,迭代器的删除方法应该完成两个逻辑:一是删除当前元素,二是当前游标指向下一个元素。简单地说,迭代器就类似于一个数据库中的游标,可以在一个容器内上下翻滚,遍历所有它需要查看的元素。
迭代器模式的应用
迭代器现在应用得越来越广泛了,甚至已经成为一个最基础的工具。一些大师级人物甚 至建议把迭代器模式从23个模式中删除,为什么呢?就是因为现在它太普通了,已经融入到各个语言和工具中了,比如PHP中你能找到它的身影,Perl也有它的存在,甚至是前端的页面技术AJAX也可以有它的出现(如在Struts2中就可以直接使用iterator)。基本上,只要你不是在使用那些古董级(指版本号)的编程语言的话,都不用自己动手写迭代器。
组合模式
组合模式(Composite Pattern)也叫合成模式,有时又叫做部分-整体模式(Part-Whole),主要是用来描述部分与整体的关系,其定义为:将对象组合成树形结构以表 示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。
组合模式的通用类图:
组合模式的几个角色:
-
Component抽象构件角色
定义参加组合对象的共有方法和属性,可以定义一些默认的行为或属性,比如我们例子 中的getInfo就封装到了抽象类中。
-
Leaf叶子构件
叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
-
Composite树枝构件
树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。
组合模式的优点
-
高层模块调用简单
一棵树形机构中的所有节点都是Component,局部和整体对调用者来说没有任何区别, 也就是说,高层模块不必关心自己处理的是单个对象还是整个组合结构,简化了高层模块的代码。
-
节点自由增加
使用了组合模式后,我们可以看看,如果想增加一个树枝节点、树叶节点是不是都很容易,只要找到它的父节点就成,非常容易扩展,符合开闭原则,对以后的维护非常有利。
组合模式的缺点
组合模式有一个非常明显的缺点,看到我们在场景类中的定义,提到树叶和树枝使用时 的定义了吗?直接使用了实现类!这在面向接口编程上是很不恰当的,与依赖倒置原则冲 突,读者在使用的时候要考虑清楚,它限制了你接口的影响范围。
组合模式的使用场景
- 维护和展示部分-整体关系的场景,如树形菜单、文件和文件夹管理。
- 从一个整体中能够独立出部分模块或功能的场景。
组合模式的注意事项
只要是树形结构,就要考虑使用组合模式,这个一定要记住,只要是要体现局部和整体的关系的时候,而且这种关系还可能比较深,考虑一下组合模式吧。
组合模式的扩展
真实的组合模式
什么是真实的组合模式?就是你在实际项目中使用的组合模式,而不是仅仅依照书本上学习到的模式,它是“实践出真知”。在项目中使用关系型数据库来存储这些信息,你可以从数据库中直接提取出哪些人要分配到树枝,哪些人要分配到树叶,树枝与树枝、树叶的关系等,这些都是由相关的业务人员维护到数据库中的,通常这里是把数据存放到一张单独的表中。数据表定义了一个树形结构,我们要做的就是从数据库中把它读取出来,然后展现到前台上,用for循环加上递归就可以完成这个读取。用了数据库后,数据和逻辑已经在表中 定义好了,我们直接读取放到树上就可以了,这个还是比较容易做的,大家不妨自己考虑一 下。
这才是组合模式的真实引用,它依靠了关系数据库的非对象存储性能,非常方便地保存 了一个树形结构。
透明的组合模式
组合模式有两种不同的实现:透明模式和安全模式,上面讲的就是安全模式,那透明模式是什么样子呢?
透明模式的通用类图:
我们与上图所示的安全模式类图对比一下就非常清楚了,透明模式是把用来组合使用 的方法放到抽象类中,比如add()、remove()以及getChildren等方法(顺便说一下,getChildren 一般返回的结果为Iterable的实现类,很多,大家可以看JDK的帮助),不管叶子对象还是树枝对象都有相同的结构,通过判断是getChildren的返回值确认是叶子节点还是树枝节点,如果处理不当,这个会在运行期出现问题,不是很建议的方式;安全模式就不同了,它是把树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全,我们的例子使用了安全模式。
组合模式的遍历
我们在上面也还提到了一个问题,就是树的遍历问题,从上到下遍历没有问题,但是我 要是从下往上遍历呢?比如组织机构这棵树,我从中抽取一个用户,要找到它的上级有哪 些,下级有哪些,怎么处理?甭管是树枝节点还是树叶节点,在每个节点都增加了 一个属性:父节点对象,这样在树枝节点增加子节点或叶子节点是设置父节点,然后你看整棵树除了根节点外每个节点都有一个父节点,剩下的事情还不好处理吗?每个节点上都有父节点了,你要往上找,那就找呗!写个find方法,然后一步一步往上 找,非常简单的方法。
最佳实践
组合模式在项目中到处都有,比如现在的页面结构一般都是上下结构,上面放系统的 Logo,下边分为两部分:左边是导航菜单,右边是展示区,左边的导航菜单一般都是树形的 结构,比较清晰,有非常多的JavaScript源码实现了类似的树形菜单,可以到网上搜索 一下。
还有,大家常用的XML结构也是一个树形结构,根节点、元素节点、值元素这些都与我 们的组合模式相匹配,之所以本章节不以XML为例子讲解,是因为很少有人还直接读写 XML文件,一般都是用JDOM或者DOM4J了。
还有一个非常重要的例子:我们自己本身也是一个树状结构的一个树枝或树叶。根据我 能够找到我的父母,根据父亲又能找到爷爷奶奶,根据母亲能够找到外公外婆等,很典型的树形结构。
观察者模式
观察者模式(Observer Pattern)也叫做发布订阅模式(Publish/subscribe),它是一个在项目中经常使用的模式,其定义是:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。
观察者模式的通用类图:
观察者模式的几个角色名称:
-
Subject被观察者
定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
-
Observer观察者
观察者接收到消息后,即进行update(更新方法)操作,对接收到的信息进行处理。
-
ConcreteSubject具体的被观察者
定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。
-
ConcreteObserver具体的观察者
每个观察在接收到消息后的处理反应是不同,各个观察者有自己的处理逻辑。
观察者模式的优点
-
观察者和被观察者之间是抽象耦合
如此设计,则不管是增加观察者还是被观察者都非常容易扩展,而且在Java中都已经实现的抽象层级的定义,在系统扩展方面更是得心应手。
-
建立一套触发机制
根据单一职责原则,每个类的职责是单一的,那么怎么把各个单一的职责串联成真实世 界的复杂的逻辑关系呢?比如,我们去打猎,打死了一只母鹿,母鹿有三个幼崽,因失去了母鹿而饿死,尸体又被两只秃鹰争抢,因分配不均,秃鹰开始斗殴,然后羸弱的秃鹰死掉, 生存下来的秃鹰,则因此扩大了地盘…这就是一个触发机制,形成了一个触发链。观察者模式可以完美地实现这里的链条形式。
观察者模式的缺点
观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂,而且在Java中消息的通知默认是顺序执行,一个观察者卡壳,会影响整体的执行效率。在这种情况下,一般考虑采用异步的方式。多级触发时的效率更是让人担忧,大家在设计时注意考虑。
观察者模式的使用场景
- 关联行为场景。需要注意的是,关联行为是可拆分的,而不是“组合”关系。
- 事件多级触发场景。
- 跨系统的消息交换场景,如消息队列的处理机制。
观察者模式的注意事项
使用观察者模式也有以下两个重点问题要解决:
-
广播链的问题
如果你做过数据库的触发器,你就应该知道有一个触发器链的问题,比如表A上写了一 个触发器,内容是一个字段更新后更新表B的一条数据,而表B上也有个触发器,要更新表 C,表C也有触发器…完蛋了,这个数据库基本上就毁掉了!我们的观察者模式也是一样 的问题,一个观察者可以有双重身份,既是观察者,也是被观察者,这没什么问题呀,但是 链一旦建立,这个逻辑就比较复杂,可维护性非常差,根据经验建议,在一个观察者模式中 最多出现一个对象既是观察者也是被观察者,也就是说消息最多转发一次(传递两次),这还是比较好控制的。
注意 它和责任链模式的最大区别就是观察者广播链在传播的过程中消息是随时更改 的,它是由相邻的两个节点协商的消息结构;而责任链模式在消息传递过程中基本上保持消 息不可变,如果要改变,也只是在原有的消息上进行修正。
-
异步处理问题
这个EJB是一个非常好的例子,被观察者发生动作了,观察者要做出回应,如果观察者比较多,而且处理时间比较长怎么办?那就用异步呗,异步处理就要考虑线程安全和队列的问题,这个大家有时间看看Message Queue,就会有更深的了解。
项目中真实的观察者模式
在系统设计中会对观察者模式进行改造或改装,主要在以下3个方面:
-
观察者和被观察者之间的消息沟通
被观察者状态改变会触发观察者的一个行为,同时会传递一个消息给观察者,这是正确 的,在实际中一般的做法是:观察者中的update方法接受两个参数,一个是被观察者,一个是DTO(Data Transfer Object,数据传输对象),DTO一般是一个纯洁的JavaBean,由被观察者生成,由观察者消费。当然,如果考虑到远程传输,一般消息是以XML格式传递。
-
观察者响应方式
我们这样来想一个问题,观察者是一个比较复杂的逻辑,它要接受被观察者传递过来的 信息,同时还要对他们进行逻辑处理,在一个观察者多个被观察者的情况下,性能就需要提 到日程上来考虑了,为什么呢?如果观察者来不及响应,被观察者的执行时间是不是也会被 拉长?那现在的问题就是:观察者如何快速响应?有两个办法:一是采用多线程技术,甭管是被观察者启动线程还是观察者启动线程,都可以明显地提高系统性能,这也就是大家通常 所说的异步架构;二是缓存技术,甭管你谁来,我已经准备了足够的资源给你了,我保证快速响应,这当然也是一种比较好方案,代价就是开发难度很大,而且压力测试要做的足够充分,这种方案也就是大家说的同步架构。
-
被观察者尽量自己做主
这是什么意思呢?被观察者的状态改变是否一定要通知观察者呢?不一定吧,在设计的时候要灵活考虑,否则会加重观察者的处理逻辑,一般是这样做的,对被观察者的业务逻辑 doSomething方法实现重载,如增加一个doSomething(boolean isNotifyObs)方法,决定是否通知观察者,而不是在消息到达观察者时才判断是否要消费。
订阅发布模型
观察者模式也叫做发布/订阅模型(Publish/Subscribe), EJB3是个非常优秀的框架,还是算比较轻量级,写个Bean只要加个Annotaion就成了,配置文件减少了,而且 也引入了依赖注入的概念,虽然只是EJB2的翻版,但是毕竟还是前进了一步。在EJB中有3 个类型的Bean: Session Bean、Entity Bean和MessageDriven Bean,我们这里来说一下 MessageDriven Bean(一般简称为MDB),消息驱动Bean,消息的发布者(Provider)发布一 个消息,也就是一个消息驱动Bean,通过EJB容器(一般是Message Queue消息队列)通知订阅者做出回应,从原理上看很简单,就是观察者模式的升级版,或者说是观察则模式的 BOSS版。
最佳实践
观察者模式在实际项目和生活中非常常见,我们举几个经常发生的例子来说明:
-
文件系统
比如,在一个目录下新建立一个文件,这个动作会同时通知目录管理器增加该目录,并 通知磁盘管理器减少1KB的空间,也就说“文件”是一个被观察者,“目录管理器”和“磁盘管理 器”则是观察者。
-
ATM取钱
比如你到ATM机器上取钱,多次输错密码,卡就会被ATM吞掉,吞卡动作发生的时 候,会触发哪些事件呢?第一,摄像头连续快拍,第二,通知监控系统,吞卡发生;第三, 初始化ATM机屏幕,返回最初状态。一般前两个动作都是通过观察者模式来完成的,后一个 动作是异常来完成。
-
广播收音机
电台在广播,你可以打开一个收音机,或者两个收音机来收听,电台就是被观察者,收音机就是观察者。
门面模式
面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式,其定义是:要求一个子系统的外部与其内部的通信必须通 过一个统一的对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许 有任何访问子系统的行为发生,其通用类图:
类图就这么简单,但是它代表的意义可是异常复杂,Subsystem Classes是子系统 所有类的简称,它可能代表一个类,也可能代表几十个对象的集合。甭管多少对象,我们把 这些对象全部圈入子系统的范畴,其结构如下图:
再简单地说,门面对象是外界访问子系统内部的唯一通道,不管子系统内部是多么杂乱 无章,只要有门面对象在,就可以做到“金玉其外,败絮其中”。先明确一下门面模式的角色。
-
Facade门面角色
客户端可以调用这个角色的方法。此角色知晓子系统的所有功能和责任。一般情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去,也就说该角色没有实际的业务 逻辑,只是一个委托类。
-
subsystem子系统角色可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。子系统并不知道门面的存在。对于子系统而言,门面仅仅是另外一个客户端而已。
门面模式的优点
-
减少系统的相互依赖
想想看,如果我们不使用门面模式,外界访问直接深入到子系统内部,相互之间是一种强耦合关系,你死我就死,你活我才能活,这样的强依赖是系统设计所不能接受的,门面模式的出现就很好地解决了该问题,所有的依赖都是对门面对象的依赖,与子系统无关。
-
提高了灵活性
依赖减少了,灵活性自然提高了。不管子系统内部如何变化,只要不影响到门面对象,任你自由活动。
-
提高安全性
想让你访问子系统的哪些业务就开通哪些逻辑,不在门面上开通的方法,你休想访问到。
门面模式的缺点
门面模式最大的缺点就是不符合开闭原则,对修改关闭,对扩展开放,看看我们那个门面对象吧,它可是重中之重,一旦在系统投产后发现有一个小错误,你怎么解决?完全遵从 开闭原则,根本没办法解决。继承?覆写?都顶不上用,唯一能做的一件事就是修改门面角 色的代码,这个风险相当大,这就需要大家在设计的时候慎之又慎,多思考几遍才会有好收获。
门面模式的使用场景
-
为一个复杂的模块或子系统提供一个供外界访问的接口
-
子系统相对独立——外界对子系统的访问只要黑箱操作即可
比如利息的计算问题,没有深厚的业务知识和扎实的技术水平是不可能开发出该子系统的,但是对于使用该系统的开发人员来说,他需要做的就是输入金额以及存期,其他的都不 用关心,返回的结果就是利息,这时候,门面模式是非使用不可了。
-
预防低水平人员带来的风险扩散
比如一个低水平的技术人员参与项目开发,为降低个人代码质量对整体项目的影响风 险,一般的做法是“画地为牢”,只能在指定的子系统中开发,然后再提供门面接口进行访问操作。
门面模式的注意事项
一个子系统可以有多个门面:
一般情况下,一个子系统只要有一个门面足够了,在什么情况下一个子系统有多个门面呢?以下列举了几个。
-
门面已经庞大到不能忍受的程度
比如一个纯洁的门面对象已经超过了200行的代码,虽然都是非常简单的委托操作,也 建议拆分成多个门面,否则会给以后的维护和扩展带来不必要的麻烦。那怎么拆分呢?按照 功能拆分是一个非常好的原则,比如一个数据库操作的门面可以拆分为查询门面、删除门 面、更新门面等。
-
子系统可以提供不同访问路径
我们以门面模式的通用源代码为例。ClassA、ClassB、ClassC是一个子系统的中3个对象,现在有两个不同的高层模块来访问该子系统,模块一可以完整的访问所有业务逻辑,也 就是通用代码中的Facade类,它是子系统的信任模块;而模块二属于受限访问对象,只能访 问methodB方法,那该如何处理呢?在这种情况下,就需要建立两个门面以供不同的高层模 块来访问,在原有的通用源码上增加一个新的门面即可。
门面不参与子系统内的业务逻辑:
门面对象只是提供一个访问子系统的一个路径而已,它不应该也不能参与具体的业务逻辑,否 则就会产生一个倒依赖的问题:子系统必须依赖门面才能被访问,这是设计上一个严重错误,不仅违反了单一职责原则,同时也破坏了系统的封装性。那对于这种情况该怎么处理呢?建立一个封装类,封装完毕后提供给门面对象。
在门面模式中,门面角色应该是稳定,它不应该经常变化,一个系统一旦投入运行它就不应该被改变,它是一个系统对外的接口,你变来变去还怎么保证其他模块的稳定运行呢?但是,业务逻辑是会经常变化的,我们已经把它的变化封装在子系统内部,无论你如何变化,对外界的访问者来说,都还是同一个门面,同样的方法——这才是架构师最希望看到的结构。
最佳实践
门面模式是一个很好的封装方法,一个子系统比较复杂时,比如算法或者业务比较复杂,就可以封装出一个或多个门面出来,项目的结构简单,而且扩展性非常好。还有,对于 一个较大项目,为了避免人员带来的风险,也可以使用门面模式,技术水平比较差的成员, 尽量安排独立的模块,然后把他写的程序封装到一个门面里,尽量让其他项目成员不用看到 这些人的代码,看也看不懂。使用门面模式后,对门面进行单元测试,约束项目成员的代码质量,对项目整体质量的提升也是一个比较好的帮助。
备忘录模式
备忘录模式(Memento Pattern)提供了一种弥补真实世界缺陷的方法,让“后悔药”在程序的世界中真实可行,其定义是:在不破坏封装性的前提下,捕获一个对象的内部状 态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
通俗地说,备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法,其通用类图:
类图中的三个角色:
-
Originator发起人角色
记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
-
Memento备忘录角色
负责存储Originator发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。
-
Caretaker备忘录管理员角色
对备忘录进行管理、保存和提供备忘录。
备忘录模式的应用
由于备忘录模式有太多的变形和处理方式,每种方式都有它自己的优点和缺点,标准的 备忘录模式很难在项目中遇到,基本上都有一些变换处理方式。因此,我们在使用备忘录模 式时主要了解如何应用以及需要注意哪些事项就成了。
备忘录模式的使用场景
- 需要保存和恢复数据的相关状态场景。
- 提供一个可回滚(rollback)的操作;比如Word中的CTRL+Z组合键,IE浏览器中的后 退按钮,文件管理器上的backspace键等。
- 需要监控的副本场景中。例如要监控一个对象的属性,但是监控又不应该作为系统的 主业务来调用,它只是边缘应用,即使出现监控不准、错误报警也影响不大,因此一般的做 法是备份一个主线程中的对象,然后由分析程序来分析。
- 数据库连接的事务管理就是用的备忘录模式,想想看,如果你要实现一个JDBC驱 动,你怎么来实现事务?还不是用备忘录模式嘛!
备忘录模式的注意事项
-
备忘录的生命期
备忘录创建出来就要在“最近”的代码中使用,要主动管理它的生命周期,建立就要使 用,不使用就要立刻删除其引用,等待垃圾回收器对它的回收处理。
-
备忘录的性能
不要在频繁建立备份的场景中使用备忘录模式(比如一个for循环中),原因有二:一是控制不了备忘录建立的对象数量;二是大对象的建立是要消耗资源的,系统的性能需要考虑。因此,如果出现这样的代码,设计师就应该好好想想怎么修改架构了。
备忘录模式的扩展
clone方式的备忘录
大家还记得在原型模式吗?我们可以通过复制的方式产生一个对象的内部 状态,这是一个很好的办法,发起人角色只要实现Cloneable就成,比较简单,我们来看类图:
从类图上看,发起人角色融合了发起人角色和备忘录角色,具有双重功效。
注意 使用Clone方式的备忘录模式,可以使用在比较简单的场景或者比较单一的场景 中,尽量不要与其他的对象产生严重的耦合关系。
多状态的备忘录模式
以上讲解都是单状态的情况,在实际的开发中一个对象不可能只有一 个状态,一个类有多个属性非常常见,这都是它的状态,如果照搬我们以上讲解的备 忘录模式,是不是就要写一堆的状态备份、还原语句?这不是一个好办法,这种类似的非智 力劳动越多,犯错误的几率越大,那我们有什么办法来处理多个状态的备份问题呢?
面我们来讲解一个对象全状态备份方案,它有多种处理方式,比如使用Clone的方式 就可以解决,使用数据技术也可以解决(DTO回写到临时表中)等,我们要讲的方案就对备 忘录模式继续扩展一下,实现一个对象的所有状态的备份和还原,如下图所示:
还是比较简单的类图,增加了一个BeanUtils类,其中backupProp是把发起人的所有属性 值转换到HashMap中,方便备忘录角色存储;restoreProp方法则是把HashMap中的值返回到 发起人角色中。可能各位要说了,为什么要使用HashMap,直接使用Originator对象的拷贝不 是一个很好的方法吗?可以这样做,你就破坏了发起人的通用性,你在做恢复动作的时候需 要对该对象进行多次赋值操作,也容易产生错误。
注意 如果要设计一个在运行期决定备份状态的框架,则建议采用AOP框架来实现,避 免采用动态代理无谓地增加程序逻辑复杂性。
多备份的备忘录
不知道你有没有做过系统级别的维护?比如Backup Administrator(备份管理员),每天 负责查看系统的备份情况,所有的备份都是由自动化脚本产生的。有一天,突然有一个重要 的系统说我数据库有点问题,请把上一个月末的数据拉出来恢复,那怎么办?对备份管理员 来说,这很好办,直接根据时间戳找到这个备份,还原回去就成了,但是对于我们刚刚学习 的备忘录模式却行不通,为什么呢?它对于一个确定的发起人,永远只有一份备份,在这种 情况下,单一的备份就不能满足要求了,我们需要设计一套多备份的架构。
我们先来说一个名词,检查点(Check Point),也就是你在备份的时候做的戳记,系统 级的备份一般是时间戳,那我们程序的检查点该怎么设计呢?一般是一个有意义的字符串。
注意 内存溢出问题,该备份一旦产生就装入内存,没有任何销毁的意向,这是非常危 险的。因此,在系统设计时,要严格限定备忘录的创建,建议增加Map的上限,否则系统很 容易产生内存溢出情况。
封装得更好一点
在系统管理上,一个备份的数据是完全、绝对不能修改的,它保证数据的洁净,避免数 据污染而使备份失去意义。在我们的设计领域中,也存在着同样的问题,备份是不能被篡改 的,也就是说需要缩小备份出的备忘录的阅读权限,保证只能是发起人可读就成了,那怎么 才能做到这一点呢?使用内置类。如下图:
这也是比较简单的,建立一个空接口IMemento——什么方法属性都没有的接口,然后在 发起人Originator类中建立一个内置类(也叫做类中类)Memento实现IMemento接口,同时也实现自己的业务逻辑。
访问者模式
访问者模式(Visitor Pattern)是一个相对简单的模式,其定义是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
访问者模式的通用类图:
-
Visitor——抽象访问者
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
-
ConcreteVisitor——具体访问者
它影响访问者访问到一个类后该怎么干,要做什么事情。
-
Element——抽象元素
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。
-
ConcreteElement——具体元素
实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。
-
ObjectStruture——结构对象
元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目 中,一般很少抽象出这个角色。
大家可以这样理解访问者模式,我作为一个访客(Visitor)到朋友家(Visited Class)去 拜访,朋友之间聊聊天,喝喝酒,再相互吹捧吹捧,炫耀炫耀,这都正常。聊天的时候,朋 友告诉我,他今年加官晋爵了,工资也涨了30%,准备再买套房子,那我就在心里盘算 (Visitor-self-method)“你这么有钱,我去年要借10万你都不借”,我根据朋友的信息,执行了自己的一个方法。
访问者模式的优点
-
符合单一职责原则
具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。
-
优秀的扩展性
由于职责分开,继续增加对数据的操作是非常快捷的,例如,现在要增加一份给大老板的报表,这份报表格式又有所不同,直接在Visitor中增加一个方法,传递数据后进行整理打印。
-
灵活性非常高
例如,数据汇总,如果要统计所有员工的工资之和,怎么计算?把所有人的工资for循环加一遍?是个办法,那我再提个问题,员工工资 ×1.2,部门经理×1.4,总经理×1.8,然后把这些工资加起来,你怎么处理?1.2,1.4,1.8是什 么?不是吧?!你没看到领导不论什么时候都比你拿得多,工资奖金就不说了,就是过节发 个慰问券也比你多,就是这个系数在作祟。我们继续说你想怎么统计?使用for循环,然后使 用instanceof来判断是员工还是经理?这可以解决,但不是个好办法,好办法是通过访问者模 式来实现,把数据扔给访问者,由访问者来进行统计计算。
访问者模式的缺点
-
具体元素对访问者公布细节
访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其 他类的内部细节,这是迪米特法则所不建议的。
-
具体元素变更比较困难
具体元素角色的增加、删除、修改都是比较困难的,就上面那个例子,你想想,你要是想增加一个成员变量,如年龄age,Visitor就需要修改,如果Visitor是一个还好办,多个呢? 业务逻辑再复杂点呢?
-
违背了依赖倒置转原则
访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
访问者模式的使用场景
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
- 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。
总结一下,在这种地方你一定要考虑使用访问者模式:业务规则要求遍历多个不同的对 象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据(当然了,如 果你使用instanceof,那么能访问所有的数据,这没有争论),而访问者模式是对迭代器模式 的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不 同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色,例如在微服务中使用API gateway,api gateway就充当拦截器一样。
访问者模式的扩展
访问者模式是经常用到的模式,虽然你不注意,有可能你起的名字也不是什么Visitor,但是它确实是非常容易使用到的。
统计功能
访问者的统计功能,汇总和报表是金融类企业非常常用的功能,基 本上都是一堆的计算公式,然后出一个报表,很多项目采用了数据库的存储过程来实现,我 不是很推荐这种方式,除非海量数据处理,一个晚上要批处理上亿、几十亿条的数据,除了 存储过程来处理还没有其他办法,你要是用应用服务器来处理,连接数据库的网络就是处于 100%占用状态,一个晚上也未必能处理完这批数据!除了这种海量数据外,我建议数据统 计和报表的批处理通过访问者模式来处理会比较简单。
多个访问者
在实际的项目中,一个对象,多个访问者的情况非常多。
双分派
说到访问者模式就不得不提一下双分派(double dispatch)问题,什么是双分派呢?我 们先来解释一下什么是单分派(single dispatch)和多分派(multiple dispatch),单分派语言 处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定 之说,它的实现是依据重载(overload)和覆写(override)实现的,我们来说一个简单的例子。
例如,演员演电影角色,一个演员可以扮演2个角色,双胞胎兄弟一样。
状态模式
状态模式定义是:当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。
状态模式的核心是封装,状态的变更引起了行为的变更,从外部看起来就好像这个对象 对应的类发生了改变一样。
状态模式的通用类图:
状态模式中的3个角色
-
State——抽象状态角色
接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。
-
ConcreteState——具体状态角色
每一个具体状态必须完成两个职责:本状态的行为管理以及趋向状态处理,通俗地说, 就是本状态下要做的事情,以及本状态如何过渡到其他状态。
-
Context——环境角色
定义客户端需要的接口,并且负责具体状态的切换。
状态模式相对来说比较复杂,它提供了一种对物质运动的另一个观察视角,通过状态变 更促使行为的变化,就类似水的状态变更一样,一碗水的初始状态是液态,通过加热转变为 气态,状态的改变同时也引起体积的扩大,然后就产生了一个新的行为:鸣笛或顶起壶盖, 瓦特就是这么发明蒸汽机的。
环境角色有两个不成文的约束:
- 把状态对象声明为静态常量,有几个状态对象就声明几个静态常量。
- 环境角色具有状态抽象角色定义的所有行为,具体执行使用委托方式。
状态模式的优点
-
结构清晰
避免了过多的switch…case或者if…else语句的使用,避免了程序的复杂性,提高系统的可维护性。
-
遵循设计原则
很好地体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就要增 加子类,你要修改状态,你只修改一个子类就可以了。
-
封装性非常好
这也是状态模式的基本要求,状态变换放置到类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变换。
状态模式的缺点
状态模式既然有优点,那当然有缺点了。但只有一个缺点,子类会太多,也就是类膨胀。如果一个事物有很多个状态也不稀奇,如果完全使用状态模式就会有太多的子类,不好 管理,这个需要大家在项目中自己衡量。其实有很多方式可以解决这个状态问题,如在数据 库中建立一个状态表,然后根据状态执行相应的操作,这个也不复杂,看大家的习惯和嗜好了。
状态模式的使用场景
-
行为随状态改变而改变的场景
这也是状态模式的根本出发点,例如权限设计,人员的状态不同即使执行相同的行为结果也会不同,在这种情况下需要考虑使用状态模式。
-
条件、分支判断语句的替代者
在程序中大量使用switch语句或者if判断语句会导致程序结构不清晰,逻辑混乱,使用 状态模式可以很好地避免这一问题,它通过扩展子类实现了条件的判断处理。
状态模式的注意事项
状态模式适用于当某个对象在它的状态发生改变时,它的行为也随着发生比较大的变化,也就是说在行为受状态约束的情况下可以使用状态模式,而且使用时对象的状态最好不要超过5个。
最佳实践
大多数人的理解状态模式是一个状态到另一个状态的过渡。状态间的过渡是固定的。例如,从状态A到状态B、状态B到状态C,状态C到状态D。状态A只能切换到状态B,状态B再切换到状态C。比如TCP状态,TCP有3个状态:等待状态、连接状态、断开状态,然后这3个状态按照顺序循环切换。按照这个状态变更来讲解状态模式,其实是不太合适的,为什么呢?在项目中很少看到一个状态只能过渡到另一个状态情形,项目中遇到的大多数情况都是一个状态可以转换为几种状态。状态B既可以切换到状态C,又可以切换到状态D,而状态D也可以切换到状态A或状态B,这在项目分析过程中有一个状态图可以完整地展示这种蜘蛛网结构,例如,一些收费网站的用户就有很多状态,如普通用户、普通会员、VIP会员、白金级用户等,这个状态的变更你不允许跳跃?!这当然是不可能的。
再提一个问题,状态间的自由切换,那会有很多种呀,你要挨个去牢记一遍吗?比如电梯的当前的一个状态,我要一个正常的电梯运行逻辑,规则是开门->关门->运行->停止;还要一个紧急状态(如火灾)下的运行逻辑,关门->停止,紧急状态时,电梯当然不能用了;再要一个维修状态下的运行逻辑,这个状态任何情况都可以,开着门电梯运行?可以!门来回开关?可以!永久停止不动?可以!那这怎么实现呢?需要我们把已经有的几种状态按照一定的顺序再重新组装一下,那这个是什么模式?是建造者模式!建造模式+状态模式会起到非常好的封装作用。
解释器模式
解释器模式(Interpreter Pattern)是一种按照规定语法进行解析的方案,在现在项目中使用较少,其定义是:给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
解释器模式的通用类图:
-
AbstractExpression——抽象解释器
-
具体的解释任务由各个实现类完成,具体的解释器分别由TerminalExpression和Non terminalExpression完成。
-
TerminalExpression——终结符表达式
-
实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。具体到我们例子就是VarExpression类,表达式中的每个终结符都在栈中产生了一个VarExpression对象。
-
NonterminalExpression——非终结符表达式
文法中的每条规则对应于一个非终结表达式,具体到我们的例子就是加减法规则分别对应到AddExpression和SubExpression两个类。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。
-
Context——环境角色
解释器模式的优点
解释器是一个简单语法分析工具,它最显著的优点就是扩展性,修改语法规则只要修改应的非终结符表达式就可以了,若扩展语法,则只要增加非终结符类就可以了。
解释器模式的缺点
-
解释器模式会引起类膨胀
每个语法都要产生一个非终结符表达式,语法规则比较复杂时,就可能产生大量的类文件,为维护带来了非常多的麻烦。
-
解释器模式采用递归调用方法
每个非终结符表达式只关心与自己有关的表达式,每个表达式需要知道最终的结果,必须一层一层地剥茧,无论是面向过程的语言还是面向对象的语言,递归都是在必要条件下使用的,它导致调试非常复杂。想想看,如果要排查一个语法错误,我们是不是要一个断点一个断点地调试下去,直到最小的语法单元。
-
效率问题
解释器模式由于使用了大量的循环和递归,效率是一个不容忽视的问题,特别是一用于解析复杂、冗长的语法时,效率是难以忍受的。
解释器模式使用的场景
-
重复发生的问题可以使用解释器模式
例如,多个应用服务器,每天产生大量的日志,需要对日志文件进行分析处理,由于各个服务器的日志格式不同,但是数据要素是相同的,按照解释器的说法就是终结符表达式都是相同的,但是非终结符表达式就需要制定了。在这种情况下,可以通过程序来一劳永逸地解决该问题。
-
一个简单语法需要解释的场景
为什么是简单?看看非终结表达式,文法规则越多,复杂度越高,而且类间还要进行递归调用(看看我们例子中的栈)。想想看,多个类之间的调用你需要什么样的耐心和信心去排查问题。因此,解释器模式一般用来解析比较标准的字符集,例如SQL语法分析,不过该部分逐渐被专用工具所取代。
解释器模式的注意事项
尽量不要在重要的模块中使用解释器模式,否则维护会是一个很大的问题。在项目中可以使用shell、JRuby、Groovy等脚本语言来代替解释器模式,弥补编译型语言的不足。
最佳实践
解释器模式在实际的系统开发中使用得非常少,因为它会引起效率、性能以及维护等问题,一般在大中型的框架型项目能够找到它的身影,如一些数据分析工具、报表设计工具、科学计算工具等,若你确实遇到“一种特定类型的问题发生的频率足够高”的情况,准备使用解释器模式时,可以考虑一下Expression4J、MESP(Math Expression String Parser)、Jep等开源的解析工具包(这三个开源产品都可以通过百度、Google搜索到),功能都异常强大,而且非常容易使用,效率也还不错,实现大多数的数学运算完全没有问题,自己没有必要从头开始编写解释器。有人已经建立了一条康庄大道,何必再走自己的泥泞小路呢?
享元模式
享元模式(Flyweight Pattern)是池技术的重要实现方式,其定义是:使用共享对象可有效地支持大量的细粒度的对象。
享元模式的定义为我们提出了两个要求:细粒度的对象和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免呢?就是享元模式提到的共享技术。我们先来了解一下对象的内部状态和外部状态。
要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。
-
内部状态
内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变,如我们例子中的id、postAddress等,它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。
-
外部状态
外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如我们例子中的考试科目+考试地点复合字符串,它是一批对象的统一标识,是唯一的一个索引值。
有了对象的两个状态,我们就可以来看享元模式的通用类图:
类图也很简单,先来看享元模式角色名称:
-
Flyweight——抽象享元角色
它简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法。在抽象角色中,一般需要把外部状态和内部状态(当然了,可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。
-
ConcreteFlyweight——具体享元角色
具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。
-
unsharedConcreteFlyweight——不可共享的享元角色
不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。
-
FlyweightFactory——享元工厂
职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。
享元模式的目的在于运用共享技术,使得一些细粒度的对象可以共享,我们的设计确实也应该这样,多使用细粒度的对象,便于重用或重构。
享元模式的优点和缺点
享元模式是一个非常简单的模式,它可以大大减少应用程序创建的对象,降低程序内存的占用,增强程序的性能,但它同时也提高了系统复杂性,需要分离出外部状态和内部状态,而且外部状态具有固化特性,不应该随内部状态改变而改变,否则导致系统的逻辑混乱。
享元模式的使用场景
在如下场景中则可以选择使用享元模式:
- 系统中存在大量的相似对象。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
- 需要缓冲池的场景
桥梁模式
桥梁模式(Bridge Pattern)也叫做桥接模式,是一个比较简单的模式,其定义是:将抽象和实现解耦,使得两者可以独立地变化。
桥梁模式的重点是在“解耦”上,如何让它们两者解耦是我们要了解的重点,先来看桥梁模式的通用类:
先来看桥梁模式中的4个角色:
-
Abstraction——抽象化角色
它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。
-
Implementor——实现化角色
它是接口或者抽象类,定义角色必需的行为和属性。
-
RefinedAbstraction——修正抽象化角色
它引用实现化角色对抽象化角色进行修正。
-
ConcreteImplementor——具体实现化角色
它实现接口或抽象类定义的方法和属性。
桥梁模式中的几个名词比较拗口,大家只要记住一句话就成:抽象角色引用实现角色,或者说抽象角色的部分实现是由实现角色完成的。
桥梁模式是一个非常简单的模式,它只是使用了类间的聚合关系、继承、覆写等常用功能,但是它却提供了一个非常清晰、稳定的架构。
桥梁模式的优点
-
抽象和实现分离
这也是桥梁模式的主要特点,它完全是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。
-
优秀的扩充能力
-
实现细节对客户透明
客户不用关心细节的实现,它已经由抽象层通过聚合关系完成了封装
桥梁模式的使用场景
-
不希望或不适用使用继承的场景
例如继承层次过渡、无法更细化设计颗粒等场景,需要考虑使用桥梁模式。
-
接口或抽象类不稳定的场景
明知道接口不稳定还想通过实现或继承来实现业务需求,那是得不偿失的,也是比较失败的做法。
-
重用性要求较高的场景
设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。
桥梁模式的注意事项
桥梁模式是非常简单的,使用该模式时主要考虑如何拆分抽象和实现,并不是一涉及继承就要考虑使用该模式,那还要继承干什么呢?桥梁模式的意图还是对变化的封装,尽量把可能变化的因素封装到最细、最小的逻辑单元中,避免风险扩散。因此读者在进行系统设计时,发现类的继承有N层时,可以考虑使用桥梁模式。
最佳实践
大家对类的继承有什么看法吗?继承的优点有很多,可以把公共的方法或属性抽取,父类封装共性,子类实现特性,这是继承的基本功能。缺点有没有?有!即强侵入,父类有一个方法,子类也必须有这个方法。这是不可选择的,会带来扩展性的问题。我举个简单的例子来说明:Father类有一个方法A,Son继承了这个方法,然后GrandSon也继承了这个方法,问题是突然有一天Son要重写父类的这个方法,他敢做吗?绝对不敢!GrandSon要用从Father继承过来的方法A,如果你修改了,那就要修改Son和GrandSon之间的关系,那这个风险就太大了!
桥梁模式就是这一问题的解决方法,桥梁模式描述了类间弱关联关系,还说上面的那个例子,Father类完全可以把可能会变化的方法放出去,Son子类要拥有这个方法很简单,桥梁搭过去,获得这个方法,GrandSon也一样,即使你Son子类不想使用这个方法也没关系,对GrandSon不产生影响,它不是从Son中继承来的方法!
不能说继承不好,它非常好,但是有缺点,我们可以扬长避短,对于比较明确不发生变化的,则通过继承来完成;若不能确定是否会发生变化的,那就认为是会发生变化,则通过桥梁模式来解决,这才是一个完美的世界。
工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程,旨在通过一步一步地精确构造创建出一个复杂的对象。我们举个简单例子来说明两者的差 异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够 飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然 后再把内裤外穿,于是一个超人就诞生了。
通过工厂方法模式生产出对象,然后由客户端进行对象的其他操作,但是并不代表所有生产出的对象都必须具有相同的状态和行为,它是由产品所决定。
最佳实践
工厂方法模式和建造者模式都属于对象创建类模式,都用来创建类的对象。但它们之间的区别还是比较明显的。
-
意图不同
在工厂方法模式里,我们关注的是一个产品整体,无须关心产品的各部分是如何创建出来的;但在建造者模式中,一个具体产品的产生是依赖各个部件的产生以及装 配顺序,它关注的是“由零件一步一步地组装出产品对象”。简单地说,工厂模式是一个对象创建的粗线条应用,建造者模式则是通过细线条勾勒出一个复杂对象,关注的是产品组成部分的创建过程。
-
产品的复杂度不同
工厂方法模式创建的产品一般都是单一性质产品,如成年超人,都是一个模样,而建造 者模式创建的则是一个复合产品,它由各个部件复合而成,部件不同产品对象当然不同。这 不是说工厂方法模式创建的对象简单,而是指它们的粒度大小不同。一般来说,工厂方法模 式的对象粒度比较粗,建造者模式的产品对象粒度比较细。
两者的区别有了,那在具体的应用中,我们该如何选择呢?是用工厂方法模式来创建对 象,还是用建造者模式来创建对象,这完全取决于我们在做系统设计时的意图,如果需要详 细关注一个产品部件的生产、安装步骤,则选择建造者,否则选择工厂方法模式。
抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零 配件而产生一个新产品,两者的区别还是比较明显的。
在抽象工厂模式中使用“工厂”来描述构建者,而在建造者模式 中使用“车间”来描述构建者,其实我们已经在说它们两者的区别了,抽象工厂模式就好比是 一个一个的工厂,宝马车工厂生产宝马SUV和宝马VAN,奔驰车工厂生产奔驰车SUV和奔驰 VAN,它是从一个更高层次去看对象的构建,具体到工厂内部还有很多的车间,如制造引擎 的车间、装配引擎的车间等,但这些都是隐藏在工厂内部的细节,对外不公布。也就是对领 导者来说,他只要关心一个工厂到底是生产什么产品的,不用关心具体怎么生产。而建造者 模式就不同了,它是由车间组成,不同的车间完成不同的创建和装配任务,一个完整的汽车 生产过程需要引擎制造车间、引擎装配车间的配合才能完成,它们配合的基础就是设计蓝 图,而这个蓝图是掌握在车间主任(导演类)手中,它给建造车间什么蓝图就能生产什么产 品,建造者模式更关心建造过程。虽然从外界看来一个车间还是生产车辆,但是这个车间的 转型是非常快的,只要重新设计一个蓝图,即可产生不同的产品,这有赖于建造者模式的功劳。
相对来说,抽象工厂模式比建造者模式的尺度要大,它关注产品整体,而建造者模式关 注构建过程,因此建造者模式可以很容易地构建出一个崭新的产品,只要导演类能够提供具 体的工艺流程。也正因为如此,两者的应用场景截然不同,如果希望屏蔽对象的创建过程, 只提供一个封装良好的对象,则可以选择抽象工厂方法模式。而建造者模式可以用在构件的 装配方面,如通过装配不同的组件或者相同组件的不同顺序,可以产生出一个新的对象,它可以产生一个非常灵活的架构,方便地扩展和维护系统。
结构类模式大PK
结构类模式包括适配器模式、桥梁模式、组合模式、装饰模式、门面模式、享元模式和 代理模式。为什么叫结构类模式呢?因为它们都是通过组合类或对象产生更大结构以适应更 高层次的逻辑需求。我们来分析以下几个模式的相似点和不同点。
代理模式VS装饰模式
对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点 是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的 功能进行加强或减弱,它着重类的功能变化。来看它们的区别。
代理模式
一个著名的短跑运动员有自己的代理人。如果你很仰慕他,你找运动员说“你跑个我看 看”,运动员肯定不搭理你,不过你找到他的代理人就不一样了,你可能和代理人比较熟, 可以称兄道弟,这个忙代理人还是可以帮的,于是代理人同意让你欣赏运动员的练习赛。
装饰模式
而装饰模式是对类功能的加强,怎么加强呢?比如要增强运动员的跑步速度?在屁股后面安装一个喷气动力装置,类似火箭的喷气装置,那速度变得很快。
最佳实践
代理模式是把当前的行为或功能委托给其他对象执行,代理类负责接口限定:是否可以 调用真实角色,以及是否对发送到真实角色的消息进行变形处理,它不对被主题角色(也就 是被代理类)的功能做任何处理,保证原汁原味的调用。代理模式使用到极致开发就是 AOP。
装饰模式是在要保证接口不变的情况下加强类的功能,它保证的是被修饰的对象功能比 原始对象丰富(当然,也可以减弱),但不做准入条件判断和准入参数过滤,如是否可以执 行类的功能,过滤输入参数是否合规等,这不是装饰模式关心的。
装饰模式VS适配器模式
装饰模式和适配器模式在通用类图上没有太多的相似点,差别比较大,但是它们的功能有相似的地方:都是包装作用,都是通过委托方式实现其功能。不同点是:装饰模式包装的 是自己的兄弟类,隶属于同一个家族(相同接口或父类),适配器模式则修饰非血缘关系 类,把一个非本家族的对象伪装成本家族的对象,注意是伪装,因此它的本质还是非相同接口的对象。
最佳实践
两个模式有较多的不同点:
-
意图不同
装饰模式的意图是加强对象的功能,例子中就是把一个怯弱的小天鹅强化成了一个美 丽、自信的白天鹅,它不改变类的行为和属性,只是增加(当然了,减弱类的功能也是可能 存在的)功能,使美丽更加美丽,强壮更加强壮,安全更加安全;而适配器模式关注的则是 转化,它的主要意图是两个不同对象之间的转化,它可以把一个天鹅转化为一个小鸭子看 待,也可以把一只小鸭子看成是一只天鹅(那估计要在小鸭子的背上装个螺旋桨了),它关注转换。
-
施与对象不同
装饰模式装饰的对象必须是自己的同宗,也就是相同的接口或父类,只要在具有相同的属性和行为的情况下,才能比较行为是增加还是减弱;适配器模式则必须是两个不同的对象,因为它着重于转换,只有两个不同的对象才有转换的必要,如果是相同对象还转换什么?!
-
场景不同
装饰模式在任何时候都可以使用,只要是想增强类的功能,而适配器模式则是一个补救 模式,一般出现在系统成熟或已经构建完毕的项目中,作为一个紧急处理手段采用。
-
扩展性不同
装饰模式很容易扩展!今天不用这个修饰,好,去掉;明天想再使用,好,加上。这都 没有问题。而且装饰类可以继续扩展下去;但是适配器模式就不同了,它在两个不同对象之 间架起了一座沟通的桥梁,建立容易,去掉就比较困难了,需要从系统整体考虑是否能够撤销。
行为类模式大PK
行为类模式包括责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式。该组真可谓是人才济济,高手如云。行为类模式的11个模式基本上都是大家耳熟能详的,而且它们之间还有 很多的相似点,特别是一些扩展部分就更加相似了,我们挑选几个比较重要的模式进行对比说明。
命令模式VS策略模式
命令模式和策略模式的类图确实很相似,只是命令模式多了一个接收者(Receiver)角 色。它们虽然同为行为类模式,但是两者的区别还是很明显的。策略模式的意图是封装算 法,它认为“算法”已经是一个完整的、不可拆分的原子业务(注意这里是原子业务,而不是 原子对象),即其意图是让这些算法独立,并且可以相互替换,让行为的变化独立于拥有行 为的客户;而命令模式则是对动作的解耦,把一个动作的执行分为执行对象(接收者角 色)、执行行为(命令角色),让两者相互独立而不相互影响。
策略模式和命令模式相似,特别是命令模式退化时,比如无接收者(接收者非常简单或 者接收者是一个Java的基础操作,无需专门编写一个接收者),在这种情况下,命令模式和 策略模式的类图完全一样,代码实现也比较类似,但是两者还是有区别的。
-
关注点不同
策略模式关注的是算法替换的问题,一个新的算法投产,旧算法退休,或者提供多种算 法由调用者自己选择使用,算法的自由更替是它实现的要点。换句话说,策略模式关注的是 算法的完整性、封装性,只有具备了这两个条件才能保证其可以自由切换。
命令模式则关注的是解耦问题,如何让请求者和执行者解耦是它需要首先解决的,解耦 的要求就是把请求的内容封装为一个一个的命令,由接收者执行。由于封装成了命令,就同 时可以对命令进行多种处理,例如撤销、记录等。
-
角色功能不同
策略模式中的抽象算法和具体算法与命令模式的接收者非常相似,但是它们的职责不同。策略模式中的具体算法是负责一个完整算法逻辑,它是不可再拆分的原子业务单元,一旦变更就是对算法整体的变更。
而命令模式则不同,它关注命令的实现,也就是功能的实现。例如我们在分支中也提到 接收者的变更问题,它只影响到命令族的变更,对请求者没有任何影响,从这方面来说,接收者对命令负责,而与请求者无关。命令模式中的接收者只要符合六大设计原则,完全不用 关心它是否完成了一个具体逻辑,它的影响范围也仅仅是抽象命令和具体命令,对它的修改 不会扩散到模式外的模块。
当然,如果在命令模式中需要指定接收者,则需要考虑接收者的变化和封装,例如一个 老顾客每次吃饭都点同一个厨师的饭菜,那就必须考虑接收者的抽象化问题。
-
使用场景不同
策略模式适用于算法要求变换的场景,而命令模式适用于解耦两个有紧耦合关系的对象 场合或者多命令多撤销的场景。
策略模式VS状态模式
在行为类设计模式中,状态模式和策略模式是亲兄弟,两者非常相似,先看看两者的通用类图,把两者放在一起比较一下。
两个类图非常相似,都是通过Context类封装一个具体的行为,都提供了一个封装的方法,是高扩展性的设计模式。但根据两者的定义,我们发现两者的区别还是很明显的:策略 模式封装的是不同的算法,算法之间没有交互,以达到算法可以自由切换的目的;而状态模 式封装的是不同的状态,以达到状态切换行为随之发生改变的目的。这两种模式虽然都有变 换的行为,但是两者的目标却是不同的。
策略模式和状态模式确实非常相似,称之为亲兄弟亦不为过,但是这两者还是存在着非常大的差别,而且也是很容易区分的。
-
环境角色的职责不同
两者都有一个叫做Context环境角色的类,但是两者的区别很大,策略模式的环境角色只 是一个委托作用,负责算法的替换;而状态模式的环境角色不仅仅是委托行为,它还具有登 记状态变化的功能,与具体的状态类协作,共同完成状态切换行为随之切换的任务。
-
解决问题的重点不同
策略模式旨在解决内部算法如何改变的问题,也就是将内部算法的改变对外界的影响降 低到最小,它保证的是算法可以自由地切换;而状态模式旨在解决内在状态的改变而引起行 为改变的问题,它的出发点是事物的状态,封装状态而暴露行为,一个对象的状态改变,从外界来看就好像是行为改变。
-
解决问题的方法不同
策略模式只是确保算法可以自由切换,但是什么时候用什么算法它决定不了;而状态模式对外暴露的是行为,状态的变化一般是由环境角色和具体状态共同完成的,也就是说状态模式封装了状态的变化而暴露了不同的行为或行为结果。
-
应用场景不同
两者都能实现前面例子中的场景,但并不表示两者的应用场景相同,这只是为了更好地 展示出两者的不同而设计的一个场景。我们来想一下策略模式和状态模式的使用场景有什么 不同,策略模式只是一个算法的封装,可以是一个有意义的对象,也可以是一个无意义的逻 辑片段,比如MD5加密算法,它是一个有意义的对象吗?不是,它只是我们数学上的一个公 式的相关实现,它是一个算法,同时DES算法、RSA算法等都是具体的算法,也就是说它们 都是一个抽象算法的具体实现类,从这点来看策略模式是一系列平行的、可相互替换的算法 封装后的结果,这就限定了它的应用场景:算法必须是平行的,否则策略模式就封装了一堆 垃圾,产生了“坏味道”。状态模式则要求有一系列状态发生变化的场景,它要求的是有状态 且有行为的场景,也就是一个对象必须具有二维(状态和行为)描述才能采用状态模式,如 果只有状态而没有行为,则状态的变化就失去了意义。
-
复杂度不同
通常策略模式比较简单,这里的简单指的是结构简单,扩展比较容易,而且代码也容易 阅读。当然,一个具体的算法也可以写得很复杂,只有具备很高深的数学、物理等知识的人才可以看懂,这也是允许的,我们只是说从设计模式的角度来分析,它是很容易被看懂的。 而状态模式则通常比较复杂,因为它要从两个角色看到一个对象状态和行为的改变,也就是 说它封装的是变化,要知道变化是无穷尽的,因此相对来说状态模式通常都比较复杂,涉及 面很多,虽然也很容易扩展,但是一般不会进行大规模的扩张和修正。
观察者模式VS责任链模式
为什么要把观察者模式和责任链模式放在一起对比呢?看起来这两个模式没有太多的相 似性,真没有吗?回答是有。我们在观察者模式中也提到了触发链(也叫做观察者链)的问 题,一个具体的角色既可以是观察者,也可以是被观察者,这样就形成了一个观察者链。这 与责任链模式非常类似,它们都实现了事务的链条化处理,比如说在上课的时候你睡着了, 打鼾声音太大,盖过了老师讲课声音,老师火了,捅到了校长这里,校长也处理不了,然后 告状给你父母,于是你的魔鬼日子来临了,这是责任链模式,老师、校长、父母都是链中的 一个具体角色,事件(你睡觉)在链中传递,最终由一个具体的节点来处理,并将结果反馈 给调用者(你挨揍了)。那什么是触发链?你还是在课堂上睡觉,还是打鼾声音太大,老师 火了,但是老师掏出个扩音器来讲课,于是你睡不着了,同时其他同学的耳朵遭殃了,这就 是触发链,其中老师既是观察者(相对你)也是被观察者(相对其他同学),事件从“你睡 觉”到老师这里转化为“扩音器放大声音”,这也是一个链条结构,但是链结构中传递的事件 改变了。
新模式
设计模式已经诞生多年,“23”这个数字也在逐渐变大,这是好事情,表明我们软件界正 在积累、汇编我们的知识和经验。一个模式的提出和成熟需要一段时间,因此本章挑选了5 个大家时常使用,但又经常忽视的新模式进行讲解,即规格模式、对象池模式、雇工模式、 黑板模式、空对象模式。
规格模式
用全局的观点思考一下,基类代表的是所有的规格书,它的目的是描述一个完整 的、可组合的规格书,它代表的是一个整体,其下的And规格书、Or规格书、Not规格书、年 龄大于基准年龄规格书等都是一个真实的实现,也就是一个局部,现在我们又回到了整体和 部分的关系了,那这是什么模式?对,组合模式,它是组合模式的一种特殊应用,我们来看它的通用类图:
为什么在通用类图中把方法名称都定义出来呢?是因为只要使用规格模式,方法名称都是这四个,它是把组合模式更加具体化了,放在一个更狭小的应用空间中。我们再仔细看看,还能不能找到其他模式的身影?对,策略模式,每个规格书都是一个策略,它完成了一系列逻辑的封装,用年龄相等的规格书替换年龄大于指定年龄的规格书上层逻辑有什么改变 吗?不需要任何改变!规格模式非常重要,它巧妙地实现了对象筛选功能。
规格模式已经是一个非常具体的应用框架了(相对于23个设计模式),大家遇到类似多 个对象中筛选查找,或者业务规则不适于放在任何已有实体或值对象中,而且规则的变化和 组合会掩盖那些领域对象的基本含义,或者是想自己编写一个类似LINQ的语言工具的时候 就可以照搬这部分代码,只要实现自己的逻辑规格书即可。
对象池模式
对象池模式,或者称为对象池服务,其意图是:通过循环使用对象,减少资源在初始化和释放时的昂贵损耗[1]。(注意 这里的“昂贵”可能是时间效益(如性能),也可能是空间效益(如并行处理),在大多的情况下,“昂贵”指性能。)
简单地说,在需要时,从池中提取;不用时,放回池中,等待下一个请求。典型例子是连接池和线程池,这是我们开发中经常接触到的。类图如下图:

对象池提供两个公共的方法:checkOut负责从池中提取对象,checkIn负责把回收对象 (当然,很多时候checkIn已经自动化处理,不需要显式声明,如连接池)。
最佳实践
把对象池化的本意是期望一次性初始化所有对象,减少对象在初始化上的昂贵性能开 销,从而提高系统整体性能。然而池化处理本身也要付出代价,因此,并非任何情况下都适 合采用对象池化。
通常情况下,在重复生成对象的操作成为影响性能的关键因素时,才适合进行对象池 化。但是若池化所能带来的性能提高并不显著或重要的话,建议放弃对象池化技术,以保持 代码的简明,转而使用更好的硬件来提高性能为佳。
对象池技术在Java领域已经非常成熟,只要做过企业级开发的人员,基本都用过C3P0、 DBCP、Proxool等连接池,也配置过minPoolSize、maxPoolSize等参数,这是对象池模式的典 型应用。在实际开发中若需要对象池,建议使用common-pool工具包来实现,简单、快捷、 高效。
雇工模式
雇工模式也叫做仆人模式(Servant Design Pattern),其意图是:雇工模式是行为模式的一种,它为一组类提供通用的功能,而不需要类实现这些功能, 它是命令模式的一种扩展[1]。
黑板模式
黑板模式(Blackboard Design Pattern)是观察者模式的一个扩展,知名度并不高,但是 我们使用的范围却非常广。黑板模式的意图如下:
允许消息的读写同时进行,广泛地交互消息[1]。
简单地说,黑板模式允许多个消息读写者同时存在,消息的生产者和消费者完全分开。 这就像一个黑板,任何一个教授(消息的生产者)都可以在其上书写消息,任何一个学生 (消息的消费者)都可以从黑板上读取消息,两者在空间和时间上可以解耦,并且互不干扰。
看到这个图大家可能会说:这不是一个简单的消息广播吗?是的,确实如此,黑板模式 确实是消息的广播,主要解决的问题是消息的生产者和消费者之间的耦合问题,它的核心是 消息存储(黑板),它存储所有消息,并可以随时被读取。当消息生产者把消息写入到消息 仓库后,其他消费者就可以从仓库中读取。当然,此时消息的写入者也可以变身为消息的阅读者,读写者在时间上解耦。对于这些消息,消费者只需要关注特定消息,不处理与自己不 相关的消息,这一点通常通过过滤器来实现。
黑板模式的实现方法
黑板模式一般不会对架构产生什么影响,但它通常会要求有一个清晰的消息结构。黑板 模式一般都会提供一系列的过滤器,以便消息的消费者不再接触到与自己无关的消息。在实际开发中,黑板模式常见的有两种实现方式。
-
数据库作为黑板
利用数据库充当黑板,生产者更新数据信息,不同的消费者共享数据库中信息,这是最 常见的实现方式。该方式在技术上容易实现,开发量较少,熟悉度较高。缺点是在大量消息 和高频率访问的情况下,性能会受到一定影响。
在该模式下,消息的读取是通过消费者主动“拉取”,因此该模式也叫做“拉模式”。
-
消息队列作为黑板
以消息队列作为黑板,通过订阅-发布模型即可实现黑板模式。这也是黑板模式被淡忘 的一个重要原因:消息队列(Message Queue)已经非常普及了,消息队列技术已经非常普遍了,例如kafka、nsq等开源的消息中间件。
在该模式下,消费者接收到的消息是被主动推送过来的,因此该模式也称为“推模式”。
小结
黑板模式的使用已经非常普遍,因为我们现在已经在大量使用消息队列,既可以做到消息的同步处理,也可以实现异步处理,相信大家已经在开发中广泛使用了,它已经成为跨系 统交互的一个事实标准了。
空对象模式
空对象模式(Null Object Pattern)是通过实现一个默认的无意义对象来避免null值出现,简单地说,就是为了避免在程序中出现null值判断而诞生的一种常用设计方法。
最佳实践
空对象模式是通过空代码实现一个接口或抽象类的所有方法,以满足开发需求,简化程序。它如此简单,以至于我们经常在代码中看到和使用,对它已经熟视无睹了,而它无论是 事前规划或事后重构,都不会对我们的代码产生太大冲击,这也是我们“藐视”它的根本原 因。