1 Abstract Factory 模式
1.模式名称
Abstract Factory,也经常称之为抽象工厂模式。
2.意图解决的问题
在程序中创建一个对象似乎是不能再简单的事情,其实不然。在大型系统开发中存在以下问题:
(1)object new ClassName 是最常见的创建对象方法,但这种方法造成类名的硬编码,需要根据不同的运行环境动态加载相同接口但实现不同的类实例,这样的创建方法就需要配合上复杂的判断,实例化为不同的对象。
(2)为了适用于不同的运行环境,经常使用抽象类定义接口,并在不同的运行环境中实现这个抽象类的子类。普通的创建方式必然造成代码同运行环境的强绑定,软件产品无法移植到其他的运行环境。
抽象工厂模式就可以解决这样的问题,根据不同的配置或上下文环境加载具有相同接口的不同类实例。
3.模式描述
Abstract Factory 模式的结构如图 10-1 所示。
就如同抽象工厂的名字一样,Abstract Factory 类将接受 Client 的“订单”——Client 发送过来的消息,使用不同的“车间”——不同的 Concrete Factory,根据已有的“产品模型”——Abstract Product,生产出特定的“产品”——Product。不同的车间生产出不同的产品供客户使用,车间与产品的关系是一一对应的。由于所有的产品都遵循产品模型——Abstract Product,具有相同的接口,所以这些产品都可以直接交付客户使用。
在抽象工厂模式中,Abstract Factory 可以有多个类似于 Create Product()的虚方法,就如同一个工厂中有多条产品线一样。Create Product1()创建产品线 1,Create Product2()创建产品线2。
在 Abstract Factory 中,Create Product()方法与 Abstract Product 是一一对应的,而 Concrete Factory 的数量同实际的 Product 的数量是一致的。
4.效果
应用 Abstract Factory 模式可以实现对象可配置的、动态的创建。灵活运用 Abstract Factory 模式可以提高软件产品的移植性,尤其是当软件产品运行于多个平台,或有不同的功能配置版本时,抽象工厂模式可以减轻移植和发布时的压力,提高软件的复用性。
5.相关讨论
在实际应用中,Abstract Factory 可以有更灵活的变化。事实上,如果仔细观察AbstractFactory 模式就可以发现,对于 Client 来说,最关注的就是在不同条件下获得接口一致但实现不同的对象,只要避免类名的硬编码,采用其他方式也可以实现。所以也可以采用其他的方式实现。例如在 Java 中就可以采用接口的方式实现,如图 10-2 所示。
图 10-2 中所描绘的类就应用了 Abstract Factory 的思想,采用面向接口的方式,简单实现了工厂模式。Product Factory 既可以看作抽象工厂 Abstract Factory,也可以看作具体的工厂(车间) Concrete Factory 。其中提供了一个获得产品的方法:getProduct(productName:String),该方法将根据产品的名称创建特定的产品,并返回。所有的产品都实现了 Product 接口,可以在客户程序中加以使用。同抽象工厂模式一样,工厂中获得对象的方法(getProduct())与实际的产品线数量是一致的,如果要增加新的产品线——例如定义新的产品接口 ProductX,需要增加相应的 getProduct()方法。由于这种方式把 Abstract Factory和Concrete Factory 合并为一个 Product Factory,所以增加新的实现 Product 接口的 Productn 不需要修改任何代码。
除了这种面向接口的方法外,工厂模式还可以有更多的变化。前面已经说过,学习设计模式最终要的是学习设计思想,学习了工厂模式后,应该知道:
① 可配置的对象创建方法可以提高系统的移植性和复用性。
② 充分利用面向对象多态的特性可以避免对象创建过程的硬编码。
2 Singleton 模式
1.模式名称
Singleton,也常称之为单件模式或单根模式。
2.意图解决的问题
在软件开发中,开发人员希望一些服务类有且仅有一个实例供其他程序使用。例如,短消息服务程序或打印机服务程序,甚至对于系统配置环境的控制,为了避免并发访问造成的不一致,也希望仅为其他程序提供一个实例。
对于供整个系统使用的对象可以使用一个全局变量,不过全局变量仅能保证正确编码时使用了唯一的实例。但随着系统不断的扩张,开发队伍的扩大,仍然无法保证这个类在系统中有且仅有一个实例。
3.模式描述
Singleton 模式可以解决上面的问题,其结构如图 10-3 所示。
从结构角度而言,Singleton 是最简单的一个模式,不过其用途很广。在 Singleton 中,通过将 Singleton 类的构造函数设为 protected 型(或 private)来防止外部对其直接初始化。需要访问 Singleton 的程序必须通过 getInstance()方法来获得一个 Singleton。在 getInstance() 中仅创建一次 uniqueInstance就可以保证系统中的唯一实例。
对于 Singleton 中的 uniqueInstance 有两种不同的初始化策略(Lazy Initialization和 Early Initialization),在实现中将分别给出这两种初始化策略的代码。
4.效果
使用 Singleton 模式可以保证系统中有且仅有一个实例,这对于很多服务类或者环境配置类来说非常重要。
5.相关讨论
在使用 Singleton 模式时,需要注意:
(1)Singleton 仅能保证在单一系统内的单一实例。如果使用分布式构件,如运行在多个JVM 下的 EJB,上面的实现方法是不能保持整个系统中的单一实例的。
(2)Singleton 模式仅适用于系统中至多有一个实例的情况,应避免滥用。很多过度设计的Singleton 同使用了静态方法的工具类一样,没有任何必要,反而可能降低效率。
(3)从 Singleton 的实现就可以看出,Singleton 不支持继承。对于 C++等支持 Template 技术的开发语言,可以定义 Singleton 模板来构造 Singleton,进一步提高该模式的复用性。但在Java、C#等语言中,则只有采用其他的方法实现。不过 Singleton 不等同于 static 类,所以一般来说,系统中不会也不应该出现很多 Singleton 类,在不支持 Template 的语言中逐一实现也未尝不可。
如果一定要在不支持 Template 的语言中实现批量的 Singleton 类,可以参考有关文献中的方法。
3 Decorator 模式
1.模式名称
Decorator 模式,又称装饰模式或油漆工模式。
2.意图解决的问题
在开发时,经常会发现为类预先设计的功能并不够强大,需要增强或扩展类的功能。解决这个问题的最简单的办法是继承出一个新的类,并扩展相应的方法。但是这样做会产生大量的子类,让系统中类的层次结构变得复杂且混乱。Decorator 模式通过在原有类的基础上包装一层来解决功能扩展的问题。
3.模式描述
Decorator 模式的结构如图 10-4 所示。
DecoratorComponent是 ConcreteComponent 的装饰类,它们继承自同一个抽象类 Component,拥有相同的接口。对于 ConcreteComponent 的装饰可能不止一种,因此又继承出 ConcreteDecorator1 和ConcreteDecorator2 来作为具体的装饰类。这个结构在类层次上是很清晰的,比静态继承更具有灵活性。图10-5 中使用顺序图描述了使用 Decorator 类的方法。
通过图 10-4 的方法,对于接口相同的 operation(),在运行期可以根据客户的选择具有不同的行为特征,实现功能动态的扩展。
4.效果
Decorator 模式可以动态地扩展类的功能,同时又避免继承出大量的子类,比继承的方法更灵活。由于 Decorator 提供了动态的扩展方法,因此可以随时根据具体需求产生新的装饰类,从而可以在一定程度上避免层次较高类的复杂性。但是大量使用 Decorator 模式会造成系统中出现很多接口类似的小装饰对象,这样就造成系统可维护性的下降。
5.相关讨论
Decorator 模式提供了一种动态为类扩展功能的方法,有着较广的应用。例如,在开源项目displaytag(一个提供 JSP 标签扩展的开源项目,可以简单地在网页中画出美观的表格,http://displaytag.sourceforge.net/)中就提供了以 Decorator 方式扩展功能,进行二次开发的接口,在 JDK 的 I/O API 中,也大量使用了 Decorator 模式。
同其他模式一样,滥用 Decorator 会降低系统的可维护性。如果开发出来的装饰类仅被单一的地方使用或只进行了相当简单的处理,就需要考虑是否有必要使用 Decorator 模式了
4 Facade/Session Facade 模式
1.模式名称
Facade 模式,又称外观模式,也有人很形象地把它翻译成“门面模式”。
2.意图解决的问题
在程序中,经常会用到其他若干个子系统。在不作任何处理的时候,需要了解每一个子系统的接口,并使用这些接口进行调用,于是系统就如图 10-6 所示一样混乱。
这些调用不但让结构变得混乱,客户程序和各子系统的耦合性也大大增加,扩展与维护都变得相当困难。
3.模式描述
Facade 模式的结构如图 10-7 所示。
如图 10-7 所示,Facade 模式通过在原有系统前增加一层的方法,封装这些子系统的接口,对外提供一致的访问接口,解决了上面的问题。
4.效果
Facade 模式屏蔽了子系统的细节,降低了客户程序使用这些子系统的复杂度,同时也降低了客户程序和子系统的耦合程度。这样就从需要让所有的人了解所有的子系统接口变成让个别专家抽象子系统的接口。
Facade 模式应用起来非常灵活,也没有特定的实现,其应用的关键就是抽象出合理的Facade 接口。
5.相关讨论
Facade 模式很好地体现了封装的思想,它封装的是一个子系统的内部结构。例如,经常对数据访问对象(Data Access Object,DAO)进行封装,使数据访问同具体的数据库相分离。客户程序只要知道最外层的数据库访问构件的接口就可以操纵任何支持的数据库。
在 EJB 开发领域,又根据 Facade 模式引申出 Session Facade 模式。
EJB 是一种分布式构件,J2EE将对 EJB 的访问定义为远程访问。例如,使用 Servlet 访问 EJB 时,即使 Servlet 和 EJB 部置于同一台主机,但由于 Servlet 和 EJB 分置于不同的容器(Servlet 在Web 容器中,EJB 在 EJB 容器中),Servlet 的默认访问方式也是通过本机的回播地址(127.0.0.1)访问 EJB,从而造成 EJB 访问的效率问题。而 EJB 中封装了复杂的业务对象,把这些业务对象的全部暴露给外部也不利于系统的维护。所以通常使用 Session Bean 封装这些业务逻辑,提高访问效率,减少内部业务逻辑和外部访问者的耦合,封装服务器中的对象模型,如图 10-8 所示。
在 Session Facade 模式中,一般使用 Session Bean作为业务逻辑的接口,在 Session Bean 中进行事务处理,确保客户对 SessionBean 的每一次访问即完成一次业务操作。使用Session Facade 模式除了获得 Facade 模式的优点外,还可以减少远程调用 EJB 的次数,降低网络接口的负载,提高性能。
5 Mediator 模式
1.模式名称
Mediator 模式,又称中介者模式。
2.意图解决的问题
在一个复杂系统中会有很多对象,这些对象之间会相互通信,从而造成对象的相互依赖。修改其中一个对象可能会影响到其他若干对象,系统中对象的复杂耦合关系造成系统可维护性的降低,系统显得混乱且难以理解。Mediator 通过封装一组对象间的通信为系统解耦。
3.模式描述
Mediator 模式的结构如图 10-9 所示。
在 Mediator 模式中,一组对象 ——抽象类 Colleague 的子类,通过中介类 —— ConcreteMediator 进行通信,中介类 ConcreteMediator继承了抽象类 Mediator,其中需要实现Mediator 中定义的通信接口,保证中介类和相互通信类之间接口的一致。
Mediator 模式看上去类似于消息机制,其实和消息机制有很大的区别。Mediaor 不需要负责消息的排队、优先级等处理,但它必须根据 Colleague 发送的消息作出响应,并向特定的 Colleague 类发送消息。换句话说,ConcreteMediator 类中封装了 Colleague 之间的通信,就好比人的大脑,接收到眼睛发送的“前面有一个坑”的消息,就会向脚发送“停止前进”的消息。
4.效果
Mediator 封装了一组对象间的通信,降低了 Colleague 之间的耦合性,单个 Colleague 的变化不会影响到其他的对象。还举前面大脑的例子,在行走的时候,大脑接收到眼睛发送的“前面有状况”的消息会通知脚“停止前进”,而在开车的时候,大脑接收这条消息后则会向脚发送“踩下刹车”的消息。
不过当 Colleague 比较多,且其中的关系复杂时,中介类 ConcreteMediator 会变得非常复杂,难以维护。
5.相关讨论
Mediator 模式在 GUI 方面有较多的使用。当一个窗体或表单或对话框中有多个需要互相通信的对象时——例如显示部门中人员的下拉列表需要根据部门下拉列表不同的选择而变化,使用Mediator 封装通信可以降低这些对象的耦合性,提高系统的可维护性。
Mediator 经常会变得非常复杂,不但每一个需要通信的对象都需要知道Mediator 的存在,而且 Mediator 也需要知道每一个通信的对象的实现。所以,一般仅在较小的范围内使用 Mediator 模式,否则过多的 Colleague 类造成中介类的难以维护会抵消掉该模式带来的优点,反而让系统更难以维护。
6 Observer 模式
1.模式名称
Observer 模式,又称观察者模式。
2.意图解决的问题
由于对象封装的特性,一般的,在系统中对象状态的变化都是独立的,即 A 对象状态的变化不会立即反映到 B 对象。但是,在系统中很多对象是相互关联的,例如对于一个股票行情系统,股票状态的变化需要同时反映到多个视图中——实时行情、技术分析图表等。对于这个问题,最简单的解决办法就是硬编码这些关联关系。但是这样会造成系统中对象的紧密耦合,系统难以维护和复用。
3.模式描述
Observer 模式的结构如图 10-10所示。
抽象类 Subject 定义了主题类——也就是被观察者的接口,它的子类 ConcreteSubject 的任何状态改变将会通知全部的观察者——ConcreteObserver。为了保持接口的一致性,这些观察者继承自相同的抽象类 Observer。
抽象类在 Subject 中保存有观察者的列表——observers,并通过 attach 和 detach 方法来动态地添加或移出一个特定的观察者。这种采用订阅方法来添加的观察者并不知道还有其他的观察者,它仅仅能够接收被观察的主题对象的状态改变。在 notify 方法中,主题类将根据目前加入订阅的观察者列表 observers 来向每一个观察者发出状态改变的消息。
ConcreteSubject 中的 setState()方法表明对象的状态发生了变化,该方法会调用 notify方法对所有的观察者进行更新,如图 10-11 所示。
4.效果
在 Observer 模式中,实现了具体对象和观察者之间的抽象耦合。虽然 Subject 了解目前有哪些观察者需要捕获自己状态的变化,但它并不了解这些观察者要做什么;而每一个观察者仅仅知道自己捕获到对象的变化,但并不清楚目前有多少观察者在观察对象的状态,也不需要通知其他的观察者。这样,动态的增加和移除观察者是非常简单的。
通过 Observer 模式可以实现对象间的消息广播,这在很多处理中非常有用。不过广播的副作用是增加了系统的负载,任何消息都将被自动地发送到每一个观察者中,每一个观察者都将根据这些消息作出响应,这些响应未必都是必要的。
5.相关讨论
Observer 模式将一对多的对象依赖转化为抽象耦合,提高系统的复用价值,得到了广泛的应用。Smalltalk 的 MVC 架构中就应用了 Observer 模式,将 Model 的变化传递到 View 中。
但是 Observer 使用不当会造成很多问题。在图 9-9 中描述的更新方法是在 setState()后由Subject 自动执行 nofity(),向所有的 Observer 发送通知,这种方法可以保证所有的更新都能够通知到观察者。但是对象状态并不总是孤立的,客户程序很可能在执行了 setState1()方法之后紧接着执行 setState2()方法,甚至要连续对对象的状态作若干次的更改。这样,每一次更改都会带来一个noifty()事件,这些更改被放大到每一个 Observer 对象中。并发执行观察者的 update()方法很可能造成程序的错误,而独占的访问 update()方法又会造成程序阻塞。
因此有时会采用如图 10-12 所示的方法发送对象状态变化的通知。
这种方法在客户程序执行了一系列改变对象状态的方法后调用 notify()方法,避免了上面的问题,不过这种方法首先不能保证对象状态的变化一定会被通知到各个观察者,其次还造成观察者对客户程序的不透明。
所以,在使用 Observer 模式时一定要注意精确地定义对象状态变化的依赖关系,以避免这些问题。
前面也提到使用 Observer 可以实现消息的广播。广播是一柄双刃剑,虽然可以简化一对多的消息发送但也将带来一系列的问题。了解计算机网络的读者知道以太网中的广播风暴问题,在Observer 模式中需要尽力避免广播的扩大,在 update()对象中避免更新 Subject 类的状态,尤其不能更新自己观察的 Subject 类的状态。否则,可能会造成对象间的消息无穷循环下去。
7 Intercepting Filter 模式
1.模式名称
Intercepting Filter 模式,又称筛选器模式。
2.意图解决的问题
在使用 MVC 架构进行 Web 应用开发时,通常需要对来自于客户的请求进行一些预处理,如验证客户身份、验证请求来源、对请求解码等,然后再传递给控制器。如果把这些预处理都交由控制器来完成,将增加控制器的复杂度,而且难以维护和扩展。
3.模式描述
Intercepting Filter 模式的结构如图 10-13 所示。
FilterManager 负责调度整个 FilterChain,它将在请求到达 Target 前拦截请求,并传递给FilterChain,由 FilterChain 中的过滤器依次进行预处理。直到请求经过最后一个过滤器后才完成了全部的预处理,然后由 FilterManger 把请求转发给实际的目标。使用 InterceptingFilter 模式时的时序关系如图 10-14 所示。
4.效果
使用 Intercepting Filter 模式使得预处理的逻辑和真正的处理逻辑分离,进行实际处理的Target 只需要关心具体的逻辑,而同请求相关的预处理都放在 FilterManager 中进行。同时解除了这两类处理的耦合性,扩展、修改预处理过程变得容易,系统具有更好的维护性和扩展性。
5.相关讨论
虽然 Intercepting Filter 作为一种 J2EE 模式出现在有关文献中,但实际上有很广泛的应用。不仅应用 J2EE 的开发可以使用该模式分离预处理过程,使用.Net 框架等其他的 Web 应用时也可以使用 Intercepting Filter。
仔细观察 Intercepting Filter 模式就可以发现,它与 Decorator 模式有些类似,都是在真正的处理对象前增加一层扩展对象的操作。只不过实现起来有很大的差别。
完全可以让 Intercepting Filter 变得更通用,更容易配置。如果对于不同的请求需要使用不同的 FilterChain——例如根据客户不同的编码方式使用不同的解码程序——可以使用工厂模式来动态创建 Filter 对象进行处理。类似的灵活使用设计模式的方法还有很多,也可以用于各种模式,需要架构设计师根据实际情况进行深入的思考。