设计模式 速查与复习(第1版)

参考教材:刘伟《设计模式》第2版 清华大学出版社

链接:https://pan.baidu.com/s/1ZQgwSacGUJ0BWh5XnRm7lw
提取码:0000

面向对象设计原则
可维护性与复用性
Robert Cecil Martin认为,可维护性较低的软件表现在:
(1)僵硬(Rigidity)。添加一个新功能时,需要改动大量模块,代码的灵活性差。
(2)脆弱(Fragility)。只是修改某处的代码,却使得另一处似乎与本次修改没什么关系的地方发生故障。
(3)低复用率(Immobility)。代码的复用,或称重用,指的是同一段代码在本软件不同的地方甚至在其它软件中重复使用——开发人员无需反复编写高度相似甚至相同的代码。对代码进行简单的复制粘贴,广义上也算作重用,但很多时候这样的重用并没有什么实用价值。
(4)高黏度(Viscosity)。改动系统时,有时可能得以保存原有的设计意图与框架,有时也可能破坏原始意图和框架。显然,前者对于扩展更有利。如果在改动软件时,发现后者比前者更容易,则称该软件系统的黏度过高,这将导致程序员采用错误的代码维护方案,使得维护困难。
Peter Coad认为,一个好的系统,应当具备如下的性质:
(1)可扩展性(Extensibility)。容易将新的功能添加到现有系统中,与“过于僵硬”相对。
(2)灵活性(Flexibility)。代码修改时不会波及很多其它模块,与“过于脆弱”相对应。
(3)可插入性(Pluggability)。可以很方便地将一个类抽取出去,同时将另一个有相同接口的类添加进来,与“黏度过高”相对应。
如何使得系统满足上述的三个性质,其关键在于恰当提高系统的可维护性和可复用性。
这两者一般是冲突的。例如,A和B两个模块都需要使用另一个模块C。如果A需要C增加一个新的行为,但B不需要,甚至不允许C增加该行为。假如坚持复用,就不得不以系统的可维护性为代价,如:修改B的代码,这将破坏系统的灵活性;而如果从保持系统的可维护性出发,就只好放弃复用。
传统的软件复用技术,包括代码的复用、算法的复用和数据结构的复用等,但这些复用有时候就会破坏系统的可维护性;而面向对象设计复用,在一定程度上可以解决这两个质量属性之间发生冲突的问题。
面向对象设计复用的目标,在于实现支持可维护性的复用。面向对象技术中的抽象、继承、封装和多态等特性,可以用来实现更高层次的复用性。通过抽象和继承,使得类的定义可以复用;通过多态,使得类的实现可以复用;通过抽象和封装,可以保持和促进系统的可维护性。在面向对象的设计里,可维护性复用都是以面向对象设计原则为基础的。这些设计原则,首先都是复用的原则。遵循这些设计原则,可以有效地提高系统的复用性,同时提高可维护性。
面向对象设计原则和设计模式,也是对系统进行合理重构的指南针。重构(Refactoring),是在不改变软件现有功能的基础上,通过调整程序代码,改善软件的质量、性能,使程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。设计模式和重构,也是普通程序员过渡到优秀程序员必学的两项技能。在这里,只介绍面向对象设计原则和设计模式。关于重构的详细学习超出本书的范围,在此不予以扩展,感兴趣的读者可以自行学习相关重构知识。
单一职责原则
一个对象应该只包含一个(或一类)职责,并且,该职责被完整地封装在一个类中。
另一种定义:就一个类而言,应该仅有一个(或一类)引起它变化的原因。

一个类(或者大到模块,小到方法),承担的职责越多,被复用的可能性越小。如果一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其它职责的运作。
单一职责原则,是最简单但又最难运用的原则,需要设计人员发现类的不同职责,并将其分离。而发现类的多重职责,需要设计人员具有较强的分析设计能力和相关的重构经验。
开闭原则
一个软件实体应当对扩展开放,对修改封闭。也就是说,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即:在不修改源代码的情况下,改变这个模块的行为。
软件实体可以指:一个软件模块、一个由多个类组成的局部结构、或一个独立的类。

开闭原则(Open-Closed Principle,OCP)还可以通过一更加具体的“可变性封装原则”来描述,对可变性封装原则(Principle of Encapsulation of Variation,EVP)要求找到系统的可变因素,并将其封装起来。如:将抽象层的不同实现封装到不同的具体类中。EVP要求,尽量不要将一种可变性和另一种可变性混合在一起,这将导致系统中类的个数急剧增长,增加系统的复杂度。
百分之百的开闭原则很难达到,但是要尽可能使系统设计符合开闭原则。后面所学的里氏代换原则、依赖倒转原则等,都是开闭原则的实现方法。在即将学习的设计模式中,绝大部分的模式都符合开闭原则。
Liskov替换原则
形式定义:
如果对每个具有类型S的对象o_1,存在一个T类型的对象o_2,使得对于所有使用T定义的程序P中,当o_1被o_2替换时,程序P的行为不发生变化,则S是T的子类。
等效表述:
(1)任何基类可以出现的地方,子类一定可以出现。
(2)派生类(子类)对象,可以在程序中代替其基类(超类)对象。

里氏代换原则是实现开闭原则的重要方式之一。由于使用基类对象的地方都可以使用子类对象,因此在程序中,尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
使用Liskov替换原则时,通常需要注意:
(1)子类的所有方法必须在父类中声明。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义。如果一个方法只存在于子类中,父类不提供相应的声明,则无法在父类对象中直接使用该方法。此时无法直接使用父类来定义,只能使用子类,则说明该设计违背了里氏代换原则。
(2)在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法。运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。
(3)Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则。这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
依赖反转原则
高层模块不应该依赖低层模块,它们都应该依赖抽象。抽象不应该依赖于细节,细节应该依赖于抽象。
另一种表述:要针对接口编程,不要针对实现编程。

简单来说,依赖倒转原则就是指:代码要依赖于抽象的类,而不要依赖于具体的类;要针对接口或抽象类编程,而不是针对具体类编程。也就是说,在程序代码中传递参数时或在组合聚合关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。
依赖倒转原则是COM、CORBA、EJB、Spring等常用的技术和框架背后的基本原则之一。
接口隔离原则
客户端不应该依赖那些它不需要的接口。
另一种定义:一旦一个接口太大,则需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。
实质上,接口隔离原则(Interface Segregation Principle,ISP)是指使用多个专门的接口,而不使用单一的总接口。每一个接口应该承担一种相对独立的角色,不多不少,不干不该干的事,该干的事都要干。这里的“接口”往往有两种不同的含义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是指某种语言具体的“接口”定义,有严格的定义和结构,如Java和C#里的interface。对于这两种不同的含义,ISP的表达方式以及含义都有所不同。
合成复用原则(组合 / 聚合复用原则)
尽量使用对象组合,而不是继承来达到复用的目的。
通俗地说,合成复用原则,就是指在一个新的对象里,通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法,达到复用其已有功能的目的。简言之,要尽量使用组合或聚合关系,少用继承。
通过继承来实现复用很简单,而且子类可以覆盖父类的方法,易于扩展。但其主要问题在于:继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类。由于基类的某些内部细节对子类来说是可见的,所以这种复用又称为“白箱”复用。如果基类发生改变,那么子类的实现有时也不得不发生改变;从基类继承而来的实现是静态的,不能在运行时发生改变,没有足够的灵活性;而且继承只能在有限的环境中使用(例如,类不能被final修饰)。
通过组合 / 聚合来复用,是将一个类的对象作为另一个类的对象的一部分,或者说,一个对象是由另一个或几个对象组合而成。由于组合或聚合关系可以将已有的对象纳入到新对象中作为成员变量,使之成为新对象的一部分,因此新对象可以调用已有对象的功能。这样做可以使得成员对象的内部实现细节对于新对象是不可见的,所以这种复用又称为“黑箱”复用。相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作;合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其它对象。
组合 / 聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其它类造成的影响相对较少,因此一般首选使用组合 / 聚合来实现复用,其次才考虑继承。在使用继承时,需要尽量遵循里氏代换原则。有效使用继承会有助于对问题的理解,降低复杂度;滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
Demeter定律
迪米特法则(Law of Demeter,LoD),又称为最少知识原则(Least Knowledge Principle,LKP)。它有多种定义方法,其中几种典型定义如下:
(1)不要和“陌生人”说话。英文定义为:“Don’t talk to strangers.”
(2)只与你的直接朋友通信。英文定义为:“Talk only to your immediate friends.”
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。
英文定义:“Each unit should have only limited knowledge about other units:only units ‘closely’ related to the current unit.”

简单地说,Demeter定律,就是指一个软件实体应当尽可能少地与其它实体相互作用。这样,当一个模块修改时,就会尽量少地影响其它模块,扩展会相对容易。这是对软件实体之间通信的限制,它要求限制软件实体之间通信的宽度和深度。
在Demeter定律中,对于一个对象,其朋友包括以下几类:
(1)当前对象本身(this);
(2)以参数形式传入到当前对象方法中的对象;
(3)当前对象的成员对象;
(4)如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5)当前对象所创建的对象。
任何一个对象如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。
Demeter定律可分为狭义和广义两种变体。在狭义的Demeter定律中,如果两个类之间不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中一个类需要调用另一个类的某个方法,可以通过第三者转发这个调用。
广义的Demeter定律,就是指对对象之间的信息流量、流向以及信息的影响的控制,主要是对信息隐藏的控制。信息的隐藏,可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用和修改,同时可以促进软件的复用。模块不依赖于其它模块而存在时,它也可以独立地在其它地方使用。一个系统的规模越大,信息的隐藏就越重要,而信息隐藏的重要性也就越明显。
Demeter定律的主要用途,在于控制信息的过载。在将它运用到系统设计中时,要注意下面的几点:
(1)在类的划分上,应当尽量创建松耦合的类,类之间的耦合度越低,就越有利于复用。一个处在松耦合中的类被修改,便不会对关联的类造成太大波及。
(2)在类的结构设计上,每一个类都应当尽量降低其成员变量和成员函数的访问权限。
(3)在类的设计上,只要有可能,一个类型应当设计成不变类。
(4)在对其它类的引用上,一个对象对其它对象的引用次数应当降到最低。
简单工厂模式(静态工厂方法)
有一个专门的类(工厂类),它根据其工厂方法被传入的参数决定创建哪个子类的实例。

如图,在工厂方法factoryMethod的参数中传入特定的名称A或B,将创建对应的子类ConcreteProductA或ConcreteProductB的实例,而无需知道子类的具体类名:ConcreteProductA或ConcreteProductB。
注意:工厂方法应当返回抽象的产品类型。
优点
(1)工厂类含有必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例(相比之下,如果没有工厂类,则需要用户自行判断合适的时机与场合,在适当的条件下,才调用构造方法,立即将需要的类实例化),客户端(用户)可以免除直接创建产品对象的许多责任,而主要“消费”产品;简单工厂模式通过这种做法实现了对责任的分割,它提供了专门的工厂类,统一用于创建对象。
(2)客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可。对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量,或者减少击键数。
(3)通过引入配置文件,将需要创建的新类对应的参数写入到配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
缺点
(1)由于工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响。
(2)使用简单工厂模式将会增加系统中类的个数,在一定程度上增加了系统的复杂度和理解难度。
(3)系统扩展困难:假如要添加新产品,就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
适用场景
(1)工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
(2)客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端不需要考虑创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数即可。
扩展
视具体场景,有时可将工厂类与抽象产品类合并;甚至可以将工厂类、抽象产品类和具体产品类都合并为同一个类。
工厂方法模式(工厂模式)
Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
工厂接口派生出具体的工厂,这些具体的工厂负责产生相应的产品类的实例。

工厂模式与简单工厂的主要区别在于:
简单工厂模式只有一个工厂类,它要负责全部的产品类的实例创建;
而工厂模式可以具有多个工厂类,它们都派生于同一个工厂接口,分别负责相应的产品类的实例创建。
优点
(1)在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了“哪种具体产品类将被实例化”这一细节,用户只需关心所需产品对应的工厂,无需关心创建的种种细节,甚至无须知道具体产品类的类名。
(2)基于工厂角色和产品角色的多态性设计,是工厂方法模式的关键。它能够使工厂可以自主确定创建何种产品对象;而如何创建这个对象涉及到的细节,也完全封装在具体工厂内部。
工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
(3)使用工厂方法模式的另一个优点是:在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改已有的的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。
缺点
(1)在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
(2)由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到文档对象模型(DOM)、反射(reflection)等技术,增加了系统的实现难度。
适用场景
(1)一个类不知道它所需要的对象的类名:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类类名。
(2)一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏替换原则(Liskov Substitution Principle),在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
(3)将创建对象的任务委托给多个工厂子类中的某一个,客户端在使用时可以无须关心是哪一个工厂子类创建产品子类,需要时再动态指定。在实现时,可将具体工厂类的类名存储在配置文件或数据库中。
扩展
使用多个工厂方法
在抽象工厂角色中,可以定义多个工厂方法,让具体工厂角色实现这些不同的工厂方法,这些方法可以包含不同的业务逻辑,以满足对不同的产品对象的需求。
产品对象的重复使用
工厂方法总是调用产品类的构造函数,创建一个新实例,将它提供给客户端。而在实际情形中,工厂方法所做的事情可以相当复杂,一个常见的复杂逻辑就是:重复使用产品对象。工厂对象可将已经创建过的产品保存到一个集合(如数组、List等)中,根据客户对产品的请求,对集合进行查询。如果有满足要求的产品对象,就直接将该产品返回客户端;否则,创建一个新的满足要求的产品对象,将其增加到集合中,再返回给客户端。这就是后面将要学习的享元模式(Flyweight Pattern)的设计思想。
多态性的丧失和模式的退化
一个工厂方法模式的实现,依赖于工厂角色和产品角色的多态性,在某些情况下,这个模式可以出现退化。工厂方法返回的类型应当是抽象类型,而不是具体类型。调用工厂方法的客户端应当依赖抽象产品编程,而不是具体产品。如果工厂仅仅返回一个具体产品对象,便违背了工厂方法的用意,发生退化,此时就不再是工厂方法模式了。一般来说,工厂对象应当有一个抽象的父类型,如果工厂等级结构中只有一个具体的工厂类,抽象工厂就可以省略,即发生退化。当只有一个具体工厂,在具体工厂中可以创建所有的产品对象,并且工厂方法设计为静态方法时,工厂方法模式就退化成简单工厂模式。
抽象工厂模式(Kit模式)
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

抽象工厂模式与工厂模式的主要区别在于:
在工厂方法模式中,每一个具体工厂只能生产一种具体产品;
而在抽象工厂方法模式中,每一个具体工厂可以生产多种具体产品。
优点
(1)抽象工厂模式隔离了具体类的生成,使得客户并不需要知道到底是什么类被创建。由于这种隔离,更换一个具体工厂就变得相对容易。所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。另外,应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
(2)当同一工厂生产的多种产品的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个工厂产生的对象。这对一些需要根据当前环境来决定其行为的软件系统来说,是一种非常实用的设计模式。
(3)增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
缺点
在添加新的产品对象时,难以扩展抽象工厂来生产新种类的产品,这是因为:在抽象工厂角色中,规定了所有可能被创建的产品集合。要支持新种类的产品,就意味着要对该接口进行扩展,而这将涉及对抽象工厂及其所有子类的修改,显然会带来较大的不便。
适用场景
(1)要求一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节时。这一点,对于所有类型的工厂模式,也都是重要的。用户无须关心对象的创建过程,将对象的创建和使用解耦。
(2)系统中有多家工厂生产各自的多种产品(合称产品族,product family),而每次只使用其中某一产品族。可以通过修改配置文件等方式,来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
(3)属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品,可以是完全没有任何关系的对象,但是它们都具有一些共同的约束(换言之,某种相同的性质),如:同一操作系统下的按钮和文本框。按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的。此时具有一个共同的约束条件:操作系统的类型。
(4)系统提供一个产品类的库,所有的产品都以同样的接口出现,从而使客户端能够不依赖于具体实现。对于这些产品,用户只需要知道它们提供了哪些具体的业务方法,而不需要知道这些对象的创建过程,乃至对象的类名及其相当一部分细节。在客户端代码中,针对抽象编程,而将具体类写入配置文件中(用能够使用户可以不修改源代码的其它方式等效实现亦可)。
扩展
“开闭原则”的倾斜性
“开闭原则”要求:系统对扩展开放,对修改封闭(通过扩展来增强功能)。对于涉及多个产品族与多个产品等级结构(有继承关系的结构,即抽象产品和它派生出来的若干类具体产品)的系统,其功能增强包括两方面:
(1)增加产品族:对于增加新的产品族,抽象工厂模式很好地支持了“开闭原则”,只需要对应增加一个新的具体工厂即可,对已有代码无须做任何修改。
(2)扩充产品等级结构:对于产品等级结构的扩充,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新种类产品的方法,不能很好地支持“开闭原则”。
抽象工厂模式的这种性质称为“开闭原则”的倾斜性。抽象工厂模式以一种倾斜的方式支持增加新的产品,它为新产品族的增加提供方便,但不能为新的产品等级结构的增加提供这样的方便。
工厂模式的退化
当抽象工厂模式中每一个具体工厂类只创建一个产品对象,也就是只存在一个产品等级结构时,抽象工厂模式退化成工厂方法模式;
当工厂方法模式中的抽象工厂与具体工厂合并,提供一个统一的工厂来创建产品对象,并将创建对象的工厂方法设计为静态方法时,工厂方法模式退化成简单工厂模式。
建造者模式(生成器模式)
Separate the construction of a complex object from its representation so that the same construction process can create different representations.
它允许用户只通过指定复杂对象的类型和内容就可以构建它们,而不需要知道内部的具体构建细节。

与之前的几种工厂模式不同,用户直接操纵的是指挥者(Director),而不是建造者(相当于工厂)。建造者(工厂)由作为中介的指挥者去操控。
优点
(1)在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
(2)每一个具体建造者都相对独立,与其它的具体建造者无关,因此可以很方便地替换或增加具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
(3)可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰而有条理,也更方便使用程序来控制创建过程。
(4)增加新的具体建造者,无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
缺点
(1)建造者模式所创建的产品一般具有较多的共同点,其组成部分相似。如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
(2)如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
适用场景
(1)需要生成的产品对象有复杂的内部结构,这些产品对象通常包含很多个成员属性。
(2)需要生成的产品对象的属性相互依赖,要求控制其生成顺序。
(3)对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
(4)隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
原型模式
Specify the kind of objects to create using a prototypical instance, and create new objects by copying this prototype.
使用原型模式来复制一个对象自身,从而克隆出多个与原型对象一模一样的对象。

优点
(1)当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程:通过一个已有实例,提高新实例的创建效率。
(2)可以动态增加或减少产品类。由于创建产品类实例的方法是产品类(具体原型类)内部具有的,因此增加新产品对整个结构没有影响。在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中。增加或减少产品类对原有系统都没有任何影响。
(3)原型模式提供了简化的创建结构。工厂方法模式常常需要有一个与产品类等级结构相同的等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的clone()方法实现的,无须专门的工厂类来创建产品。
(4)可以使用深克隆的方式保存对象的状态。使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如:恢复到某一历史状态)。
缺点
(1)需要为每一个类配备一个克隆方法,而且这个克隆方法需要对类的功能进行通盘考虑,这对全新的类来说不是很难,但对已有的类进行改造时,不一定是件容易的事,必须修改其源代码,违背了“开闭原则”。
(2)在实现深克隆时,可能需要编写较为复杂的代码。
适用场景
(1)创建新对象成本较大(如:初始化需要较长时间,占用太多的CPU或网络资源),而该类的每个对象一般高度相似,可以通过原型模式对已有对象进行复制(并对新对象的属性稍作修改)来获得。
(2)如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占内存不大的时候,也可以使用原型模式配合备忘录模式来应用。相反,如果对象的状态变化或占用的内存很大,那么采用状态模式会比原型模式更好。
(3)需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。
扩展
带原型管理器的原型模式

本例中,原型管理器通过哈希表来索引已经创建的不同原型。
单例模式
Ensure a class has only 1 instance and provide a global point of access to it.
单例模式确保:某一个类总是只有一个实例,它自行实例化,并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式的要点有三个:
1、某个类只能有一个实例;
2、它必须自行创建这个实例;
3、它必须自行向整个系统提供这个实例。

单例类的构造函数必须是私有的,以避免外界利用构造函数直接创建出任意多的实例。
优点
(1)提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样访问、何时访问它,并为设计及开发团队提供了共享的概念。
(2)由于在系统内存中只存在一个对象,因此可以节约系统资源。对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
(3)允许可变数目的实例。可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。
缺点
(1)由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2)单例类的职责可能会过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法;同时又充当了产品角色,包含一些业务方法,将产品的创建和产品本身的功能融合到一起。
(3)滥用单例将带来一些负面问题,如:为了节省资源,将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多,而出现连接池溢出;现在很多面向对象语言(Java、C#等)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,将自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。
适用场景
(1)系统只需1个实例对象。如:要求提供唯一的序列号生成器,或者资源消耗太大而只允许创建1个对象。
(2)客户调用类的单个实例只允许使用1个公共访问点,不能通过其它途径访问该实例。
注意
使用单例模式有一个必要条件:在一个系统中,要求某个类始终只有1个实例时,才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。
不要使用单例模式存取全局变量,因为这违背了单例模式的用意,最好将全局变量放到对应类的静态成员中。
不要将数据库连接做成单例。因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。单例模式使用静态成员存储类的实例,可能会造成资源无法及时释放,带来一些问题。
单例模式与静态成员是不同的:单例模式要求的是某个类在整个软件中只能具有1个实例,而不是类的某个(些)成员在整个软件中只能具有1个实例。
扩展
饿汉式单例类

在类加载时,立刻创建单例对象。
懒汉式单例类

懒汉式单例类在第一次被引用时将自己实例化,在懒汉式单例类被加载时不会将自己实例化。
两者的比较
饿汉式单例类在自己被加载时就将自己实例化。单从资源利用效率的角度来讲,这个比懒汉式单例类稍差些(实例化以后可能有相当长的时间不被引用);从速度和反应时间的角度来讲,则比懒汉式单例类稍好些。然而,懒汉式单例类在实例化时,必须处理好在多个线程同时首次引用此类时的访问限制问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着多个线程同时首次引用此类的几率变得较大,需要做好线程同步。
适配器模式
通过一个新的设备使原本不兼容的事物可以一起工作,这个新的设备称为适配器(adapter)。
Convert the interface of a class into another interface clients expect, Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.
将一个接口转换成客户希望的另一个接口,适配器使接口不兼容的类可以一起工作,其别名为包装器(Wrapper)。
适配目标Target,是用户需要使用的类或接口,它可以是抽象类或接口,也可以是具体类。用户通过直接使用适配目标Target,间接使用被适配者Adaptee。Java、C#等语言不支持多继承,则适配目标只能是接口。适配器模式主要有两种实现:
类适配器模式

适配器与被适配者是泛化(继承)关系。
对象适配器模式

适配器包含被适配者的一个实例。适配器与被适配者是关联关系(或称委派关系)。
可见,判定适配器模式的一个实现是类适配器模式还是对象适配器模式,要看适配器与被适配器之间是继承关系还是关联关系。而适配器与适配目标的关系总是继承(适配目标为类)或实现(适配目标为接口)。
优点
(1)将目标类和适配者类解耦。通过引入一个适配器类,来重用现有的被适配类,而无须修改原有代码。
(2)增加了类的透明性和复用性,将具体的实现封装在被适配类中,对于客户端类来说是透明的,而且提高了被适配者的复用性。
(3)灵活性和扩展性都非常好。例如,通过使用配置文件,可以很方便地更换适配器,也可以在不修改原有的源代码的基础上增加新的适配器类,完全符合“开闭原则”。
具体来说,类适配器模式的优点还有:
由于适配器类是适配者类的子类,因此可以在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。
对象适配器模式的优点还有:
对象适配器可以把多个不同的适配者适配到同一个目标。也就是说,同一个适配器可以把适配者类和它的子类都适配到目标接口。
缺点
类适配器模式的缺点有:
对于Java、C#等不支持多重继承的语言,一次最多只能适配一个被适配者类,而且目标抽象类只能为接口,不能为类,其使用有一定的局限性,不能将一个被适配者类和它的子类都适配到目标接口。
对象适配器模式的缺点有:
与类适配器模式相比,要想置换被适配者类的方法就不容易。如果一定要置换掉被适配者类的一个或多个方法,就只好先做一个被适配者类的子类,将适配者类的方法覆盖掉,然后再把适配者类的子类当作真正的适配者进行适配,实现过程较为复杂。
适用场景
(1)系统需要使用现有的类,而这些类的接口不符合系统的需要。
(2)想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作。
(3)不允许修改原有的被适配类或抽象目标类。例如,没有这些类的源代码。
注意
在使用适配器模式的系统中,客户端一定要针对抽象目标类进行编程,否则,适配器模式的使用将可能导致系统发生一定规模的改动。
扩展
默认适配器(单接口适配器)
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现该接口,并为接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求。它适用于一个接口不想使用其所有的方法的情况,因此也称为单接口适配器模式。

双向适配器
在对象适配器的使用过程中,如果在适配器中同时包含对目标类和被适配者类的实例引用,被适配者可以通过它调用目标类中的方法,目标类也可以通过它调用被适配者类中的方法,那么该适配器就是一个双向适配器。

桥接模式
Decouple an abstraction from its implementation so that the 2 can vary independently.
将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(interface)模式。

桥接模式,将一些类中涉及的不同实体提取出相同特征,将它们用一个抽象类去代表与管理;这将使得涉及该类实体的有关方法的总代码长度因高度相似部分的减少而减少。
桥接模式一般至少涉及2类实体。经桥接模式改造后的代码,可以使得这2类实体的抽象与实现都进行分离。
实例1:画笔

有3种不同粗细的画笔,每个画笔都需要支持5种不同的颜色。
若不采用桥接模式,则需要15个具体类:SmallRedPen、SmallGreenPen、……、BigBlackPen。每个类都有draw方法。使用这些类在屏幕上绘制不同颜色、不同粗细的轨迹。
若采用桥接模式,则只需要8个具体类,如上图。原来的类中都涉及到不同的颜色实体,它们分别对应颜色类Red、……、Black,将它们用抽象类Color统一代表与管理;而涉及这些颜色实体的方法bepaint也在实体对应的各个类Red、……、Black中实现了。
该实例涉及2类具体的实体:颜色和画笔。桥接模式使得颜色与画笔的抽象与实现(这里指绘制颜色或粗细不同的轨迹)分离。
相比之下,在原有的15个具体类中,绘制相同颜色或相同粗细的15种draw方法虽然不完全相同,但会有相当多的部分高度相似甚至相同;在使用桥接模式重构后,15个具体类变为8个具体类:3个具体类对应3种粗细的画笔;剩余5个具体类对应5种不同的绘制颜色。在本例中,Color类就是颜色实体抽象出来的类,而bepaint方法就是直接涉及该抽象部分的实现。很明显,重构以后,代码的相似或相同部分将会更少。
实例2:跨平台视频播放器

现要求为Windows、Linux和其它Unix平台设计一个跨平台的视频播放器。它们至少要支持如下几种格式的播放:MPEG类、RMVB、AVI和WMV。
若不采用桥接模式,则至少需要12个具体类:WindowsVersionMPEG、……、UnixVersionWMV。每个类都有play方法。使用这些类在不同的操作系统上播放不同格式的视频。
若采用桥接模式,则只需要7个具体类,如上图。原来的类中都涉及到不同的视频文件实体,它们分别对应视频文件类MPEGFile、……、WMVFile,将它们用抽象类VideoFile统一代表与管理;而涉及这些视频文件实体的方法decode也在实体对应的各个类MPEGFile、……、WMVFile中实现了。
该实例涉及2类具体的实体:操作系统和视频文件。桥接模式使得操作系统和视频文件的抽象与实现(这里指解码与播放)分离。
相比之下,在原有的12个具体类中,在不同平台播放不同视频文件的12种play方法虽然不完全相同,但会有相当多的部分高度相似甚至相同;在使用桥接模式重构后,12个具体类变为7个具体类:3个具体类对应3种不同的操作系统;剩余4个具体类对应4种不同的视频文件。在本例中,VideoFiles类就是视频文件实体抽象出来的类,而decode方法就是直接涉及该抽象部分的实现。很明显,重构以后,代码的相似或相同部分将会更少。
优点
(1)分离了抽象与实现两个部分。桥接模式通过巧妙利用不同类的关联关系,使得对一些类的抽象和涉及该类的方法可以分离,使得可以自由组合出更多的子类,这些多样化的子类可以更灵活地被应用。
(2)桥接模式有时类似于多继承,但多继承得到的类由于继承了多个父类,很可能将令其违背单一职责原则,使得复用性有降低的风险。此外,多继承容易导致类爆炸。桥接模式往往比多继承的方案更好。
(3)桥接模式使得系统的扩展性提高:将代码经桥接模式重构后,在为更多的抽象类增加子类时,通常都不需要修改原有的代码。
(4)更易实现细节对用户透明,使得用户在使用时往往无需关心内部的实现。
缺点
(1)增加了软件的理解与设计难度。
(2)只有正确分析不同类间的关联关系,识别出变化相对独立的实体,才能充分利用桥接模式的优势,分离出能够自由组合的不同类别的实体,减少总体的代码量。这也使得桥接模式的使用范围具有一定的局限性。
适用场景
(1)要求抽象和具体的角色之间有很高的灵活性。
(2)要求在程序运行时,可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合。
(3)有两类实体的变化是相对独立的,且这两类实体都需要扩展性。
(4)要求独立管理抽象化角色和具体化角色。
(5)其它不适合使用继承的场景,或可能因为使用多继承而导致子类爆炸的场景。
桥接模式与适配器模式的结合
例如,桥接模式用于系统的初步设计:对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候,在设计初期也需要考虑适配器模式,特别是那些涉及大量第三方应用接口的情形。
组合模式(部分-整体模式)
令多个对象形成树形结构,以表示部分与整体的结构层次。组合模式使得对单个对象(非容器对象)和组合对象(容器对象)的使用具有一致性。
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

组合模式适用于同时存在可作为容器(container)与不可作为容器的对象的情况。容器对象可以存储容器对象,也可以存储非容器对象。组合模式将容器对象与非容器对象抽象出一个父类,父类包含那些既适用于容器也适用于非容器的操作:户在对该抽象类的子类进行这些操作时,无需得知其是否为容器。而另一些方法只对容器有效:例如,向容器中增加、修改、删除对象。
例如,在文件系统中,文件夹(目录)属于容器:它既可以包含文件夹,也可以包含文件。而文件不是容器:它既不能包含目录,也不能包含其它文件。许多操作既可以对文件夹进行也可以对文件进行,比如创建、删除、获取大小,等等。于是,可以考虑应用组合模式来实现文件系统。
优点
(1)更清楚地定义了分层次的复杂对象,令新部件的增加与各个部件的统一控制更容易:许多时候,用户能忽略层次的不同而直接添加新的对象并予以统一控制。
(2)客户端调用简单:无论调用的对象是否为容器,有时都可以使用相同的方法,无需关心处理的是单个对象还是组合结构,简化客户端的代码。
(3)易于形成有条理的树形结构。
(4)一般无需因为加入新的对象而改动已有代码。
.缺点
(1)使设计更抽象。对象的业务规则如果很复杂,则组合模式难以实现。并且不是所有方法都适用于非容器对象。
(2)增加新部件时,难以添加约束,例如,限制新部件的类型。在组合模式中,不同的对象可以来自相同的抽象层。举例来说,不同类型的文件,如图片、音乐、视频等,都来自“文件”这个抽象层。如果客户端针对抽象编程(非常多的场景都应这样做),则需要RTTI(运行时类型识别)来对类型进行限制,带来额外开销。
适用场景
(1)要求表示对象的层次结构,且又希望一些操作忽略层次结构或整体与部分的差异(组合体与个体的差异)。
(2)对象的结构是动态变化的,复杂程度不一样,但客户需要在一些条件下一致地处理它们。
扩展
透明组合模式
在透明组合模式中,容器类与非容器类的抽象父类提供了全部用于操作其子类的方法。在客户端看来,所有这些方法既可以对容器使用,也可以对非容器使用。
但是非容器与容器总是有区别的,虽然透明组合模式允许客户端在所有情况下都通过抽象父类来对容器和非容器进行相同的操作,但若对非容器调用了只有容器才支持的方法,则会导致出错。我们说,透明组合模式不够安全。
安全组合模式
在安全组合模式中,容器类与非容器类的抽象父类仅提供容器和非容器都支持的方法。虽然这种做法不会导致出错,但客户端不能在所有情况下都对容器和非容器一视同仁地操作。我们说,安全组合模式不够透明。

装饰模式
装饰模式动态地给一个对象增加一些额外的职责(Responsibility)。就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译 的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

抽象部件类Component是具体部件类的父类,它的子类可以包含未装饰的对象,也可以包含装饰的对象,客户端可以通过它统一操作这些对象;而Decorator是抽象的装饰类,它的子类将对具体部件类进行装饰——添加额外的功能或职责。
一般而言,抽象装饰类Decorator包含一个对抽象部件类Component的引用,通过构造函数或专门的函数来令该引用指向具体部件。抽象部件类component带有某种特定的方法operation,抽象装饰类Decorator需要调用其继承的抽象部件类Component中的该operation方法,实现未装饰时的功能。其子类实现父类专门定义的抽象装饰方法operation:addedBehavior方法指代的代码部分,为被引用的部件添加新的功能。
如果具体的装饰类不将实现装饰的addedBehavior方法提供给客户端,而是将其在覆盖的operation方法中调用,则客户端统一调用具体装饰类的operation来为特定的部件添加新行为。这时,客户端可以使用抽象部件类Component创建并使用未被装饰的类和装饰后的类,并且具体装饰类的对象也可以继续被装饰(多重装饰),这称为透明装饰模式。如果具体的装饰类将实现装饰的addedBehavior方法单独提供给客户端,则客户端使用抽象部件类Component时,无法访问到addedBehavior方法(父类不能访问仅子类包含的成员),也无法通过抽象部件类Component进行多重装饰,这称为半透明装饰模式。大多数装饰模式都是半透明的装饰模式,虽然其无法实现多重装饰,但设计相对简单,使用非常方便。
在使用Swing GUI时,常使用javax.swing.JScrollPane来为javax.swing.JTable、javax.swing.JList等类添加滚动条,这就是装饰模式的典例。此外,java.io.BufferedReader、java.io.BufferedWriter分别也可以视作java.io.FileReader、java.io.FileWriter的装饰类。
优点
(1)比继承更灵活。
(2)可以动态决定具体如何扩展一个对象拥有的功能。例如,通过配置文件等方法,可以选择不同的装饰器,实现不同行为的添加。
(3)通过使用不同的具体装饰类及其组合,可以创造出多种不同行为的组合;使用多个具体装饰类装饰同一对象时,可以进一步得到功能更为强大的对象。
(4)具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,在使用时再对其进行组合,原有代码无须改变,符合“开闭原则”。
缺点
(1)使用装饰模式进行系统设计时,将产生很多小对象。这些对象的区别在于:它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,同时还将产生很多具体装饰类。这些装饰类和小对象的产生将增加系统的复杂度,加大学习与理解的难度。
(2)装饰模式比继承更加灵活机动,意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。
适用场景
(1)要求在不影响其它对象的情况下,以动态、透明的方式给单个对象添加职责。
(2)需要动态地给一个对象增加功能,这些功能也可以动态地被撤销。
(3)无法采用继承的方式扩充功能时。不能继承的情况主要有两类:一是类本身被定义为不允许继承(使用final关键字修饰);二是系统中存在大量独立的扩展,若要支持每一种可能的组合,则势必将产生大量的子类,引起子类爆炸。
装饰模式的简化
大多数情况下,装饰模式的实现比标准的结构图要简单,可以对装饰模式进行简化。在简化过程中,需要注意如下几个问题:
(1)一个装饰类的接口,必须与被装饰类的接口保持相同,使得对于客户端来说,在许多时候,无论是装饰之前的对象还是装饰之后的对象,都可以同等对待。
(2)尽量保持具体构件类ConcreteComponent作为一个“轻”类,也就是说:不要把太多的逻辑和状态放在具体构件类中,而可以通过装饰类对其进行扩展。
(3)如果只有一个具体构件类而没有抽象构件类,那么抽象装饰类可以作为具体构件类的直接子类,如下图。更进一步地,如果只有一个具体装饰类,那么也可以不单独设计抽象装饰类,而是把抽象装饰类和具体装饰类的职责合并在一个类中。

外观模式
为子系统中的一组接口提供一个统一的入口。
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
在外观模式中,外部与一个子系统的通信可以通过一个统一的外观对象进行。外观模式又称为门面模式。

如图,用户通过操纵外观类Façade,来间接访问各个子系统,即:一个子系统的外部与其内部的通信通过一个统。的外观对象进行,这就使得用户不必掌握很多与子系统有关的细节。
外观模式是一个使用频率非常高,但理解较为简单的模式。在几乎所有的软件中,都能够找到外观模式的应用。如:绝大多数B/S(浏览器/服务器)系统,都有一个首页或者导航页面;大部分C/S系统都提供了菜单或工具栏。在这里,首页和导航页面就是B/S系统的外观角色,而菜单和工具栏就是C/S系统的外观角色,通过它们用户可以快速访问子系统,降低了系统的复杂程度。
优点
(1)对客户屏蔽子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户需要掌握的代码将变得很简单,与之关联的对象也很少。
(2)使得客户端与子系统之间变得松耦合:子系统的组件变化通常不会影响到调用它的客户类,这时只需要调整外观类即可。
(3)使用外观模式并将整个系统划分为相对独立的子系统,有利于降低大型软件系统中的编译依赖性,并简化了系统在不同平台之间的移植过程。因为编译一个子系统一般不需要编译所有其它子系统。一个子系统的修改对其他子系统基本没有任何影响,而且子系统内部变化也一般不会影响到外观对象。
(4)只是提供了一个访问子系统的统一入口,多数情况下,并不影响用户直接使用子系统类。
缺点
(1)可能对客户使用子系统的限制过多。因为客户被限定通过外观类访问各个子系统,而子系统中允许客户访问的每个部分都需要被外观类包装(或者说,封装)起来,降低灵活性。
(2)若增加或移除了子系统,则外观类可能需要做少量修改,这在一定程度上不符合开闭原则,但影响不大。
适用场景
(1)要求为若干个复杂的子系统提供一个尽量简单的接口(界面)时。通常,这个接口可以满足大多数用户的需求。对于小部分用户,有时也需要允许他们越过外观类,直接访问子系统。
(2)客户程序与多个子系统之间存在很大的依赖性。引入外观类,将子系统与客户及其它子系统解耦,可以提高子系统的独立性和可移植性。
(3)若软件系统是层次结构的,则可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
扩展
多个外观类
在外观模式中,通常只需要一个外观类,并且它只有一个实例。换言之,它是一个单例类。在很多情况下,为了节约系统资源,一般将外观类设计为单例类。当然,这并不意味着在整个系统里只能有一个外观类:可以设计多个外观类,每个外观类都负责和一些特定的子系统交互,向用户提供相应的业务功能。
不要试图通过外观类为子系统增加新行为
外观模式的用意是:为子系统提供一个集中化和简化的沟通渠道,而不是向子系统加入新的行为。新行为的增加,应通过修改原有子系统类或增加新的子系统类来实现,不能通过外观类来实现。否则,将破坏软件系统的结构,未能令系统的各个部分或层次各司其职,致使日后扩展与维护因为大概率需要牵涉许多个模块的变动而变得麻烦。
外观模式与Demeter定律
Demeter定律告诉我们:只与你直接的朋友通信。Demeter定律要求每一个对象与其它对象的相互作用均是短程的,而不是长程的。换句话说,一个对象只应当知道它的直接交互者的接口。外观模式创造出一个外观对象,将客户端所涉及的属于一个子系统的协作伙伴的数量减到最少,使得客户端与子系统内部的对象的相互作用被外观对象所取代。外观类充当了客户类与子系统类之间的“第三者”,降低了客户类与子系统类之间的耦合度,外观模式就是实现代码重构以便达到Demeter定律要求的一个强有力的武器。
抽象外观类
在有多个外观类的软件中,若时常因为子系统的变动而需要修改外观类,为了尽量减少客户端代码的修改,可以引入抽象外观类,令客户端对抽象外观类编程。

职责链模式(责任链模式)
避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.

抽象处理者类Handler中包含了一个与自身类型相同的引用。于是,所有的具体处理者子类都具有该成员,使得每个处理者实例都能通过该成员将请求交给下一个处理者。
每个具体处理者对象,都可以选择处理或不处理,并在这之后选择是否将请求继续交给责任链上的下一个处理者。
发出请求的客户端,并不知道链上的哪些对象处理了这个请求。这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。
优点
(1)降低耦合度:职责链模式使得一个对象无须知道是其它哪一个对象处理其请求。对象仅需知道该请求会被处理即可,接收者和发送者都没有对方的明确信息,且链中的对象也不需要知道链的结构,由客户端负责链的创建。
(2)可简化对象的相互连接:请求处理对象仅需维持一个指向其后继者的引用,而不需维持它对所有的候选处理者的引用。
(3)增强给对象指派职责的灵活性:在给对象分派职责时,职责链可以带来更多的灵活性。可以通过在运行时对该链进行动态的增删改,来改变每个处理者处理一个请求的具体职责。
(4)增加新的请求处理类很方便:在系统中增加一个新的具体请求处理者,无须修改系统原有的代码,只需要在客户端重新建链即可。从这一点来看,是符合“开闭原则”的。
缺点
(1)不能保证请求一定被接收:不保证请求一定会被处理:该请求可能一直到链的末端都得不到处理;也可能因职责链没有被正确配置而得不到处理。
(2)对于比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响,而且在进行代码调试时不太方便;如果建链不当,可能会造成循环调用,将导致系统陷入死循环。
适用场景
(1)有多个对象可以处理同一个请求,具体哪个对象处理在运行时确定。客户端只将请求提交到链上,而不请求的处理对象是谁,以及它是如何处理的。
(2)在不明确指定接收者的情况下提交请求。请求的发送者与处理者解耦,请求沿链传递,寻求相应的处理者。
(3)可动态指定一组对象处理请求。客户端可以动态创建职责链来处理请求,还可以动态改变处理者的先后次序。
不纯的责任链模式
一个纯的职责链模式,要求一个具体处理者对象只能在两个行为中选择一个:一个是承担责任,另一个是把责任推给下家。不允许出现某一个处理者在承担了一部分责任后又将责任向下传的情况。
在一个纯的职责链模式里,一个请求必须被某一个处理者对象所接收;在一个不纯的职责链模式里,一个请求可以最终不被任何接收端对象所接收。纯的职责链模式的例子是不容易找到的,一般看到的例子均是不纯的职责链模式的实现,如:Java AWT 1.0的事件处理模型,每一级的组件在接收到事件时,都可以处理此事件;而不论此事件是否在这一级得到处理,事件都可以停止向上传播或者继续向上传播,可以随时中断对事件的处理。这是典型的不纯的责任链模式。

Guess you like

Origin blog.csdn.net/COFACTOR/article/details/118530026