系统架构设计笔记(53)—— 设计模式
发布日期:2021-06-29 21:04:48 浏览次数:2 分类:技术文章

本文共 15292 字,大约阅读时间需要 50 分钟。

面向对象技术为软件技术带来新的发展。人们运用面向对象的思想分析系统 、 为系统建模并设计系统,最后使用面向对象的程序语言来实现系统。但是面向对象的设计并不是一件很简单的事情,尤其是要设计出架构良好的软件系统更不容易。为了提高系统的复用性,需要进行一些 “ 额外 ” 的设计(这里的额外并不是无用的,而是指业务领域之外),定义类的接口 、 规划类的继承结构 、 建立类与类之间的关系。毋庸置疑,良好的设计可以让系统更容易地被复用 、 被移植和维护,而如何快速进行良好的设计则是设计模式要讨论的问题。设计模式是软件架构设计师的必修课,设计模式中蕴含的思想是架构设计师必须掌握的。

1 设计模式概述

在 20 世纪 70 年代, Christopher Alexander 提出了城市建筑的模式,他认为:模式就是描述一个不断发生的问题和该问题的解决方案。随后, Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 写了一本著名的参考书 《 设计模式:可复用面向对象软件的基础 》。 后人也因为这本书称这四个人为四人组,将这本书中描述的模式称为 GoF ( Gang of Four )设计模式。在这本书中,四人组将设计模式定义为:对被用来在特定场景下解决一般设计问题的类和互相通信的对象的描述。通俗地说,可以把设计模式理解为对某一类问题的通用解决方案。

1.1 设计模式的概念

首先,设计模式解决的是一类问题,例如工厂模式就是为了解决类创建的问题,而适配器模式则是为了解决类接口不匹配的问题。如果把解决 A 问题的设计模式使用在 B 问题上,结果肯定是张冠李戴了。所以在描述设计模式前,首先要描述这个设计模式究竟要解决什么样的问题。

其次,设计模式是一种通用的解决方案,而不是具体的,也不是唯一的。在 GoF 的书中对设计描述主要着重于思想的描述,虽然也给出了 C++ 的实现方法,但同样也可以使用 Java 甚至非面向对象的语言实现。具体应用时可以根据实际情况进行相应的变化,例如,对于工厂模式就有很多种变化。

需要指出的是,虽然在 GoF 的着作中第一次提出了软件的设计模式,但设计模式并非这四人所创,它来源于很多项目中的成功设计,并将这些优雅的设计方式进行抽象 、 总结 、 归纳出来。

在学习设计模式时需要注意以下两点:

(1)学习这些模式是一个方面,另一方面更要了解模式中的思想。设计模式本身是为了提高软件架构的质量,学习设计模式的目的也是为了提高架构设计的水平。虽然设计模式中描述的大多是面向对象的低层设计方案,但其中包含的却是软件设计的思想,同软件架构风格是一致的。例如, MVC 既可以看作一种设计模式也可以看作一种架构风格。掌握这种设计思想是非常有意义的。
(2)设计模式虽然可以使设计变得更精妙,但滥用设计模式会适得其反。在软件设计中使用设计模式,可以优化设计,提高架构质量。但是,首先,设计模式有其应用的场合,不相宜的场合乱用设计模式有害无益;其次,设计模式主要解决对象之间相互通信 、 相互依赖的结构关系,架构设计师需要把握好使用设计模式的力度,过度的使用设计模式不但不会提高软件的复用性,反而会让架构变得混乱而难以维护。

1.2 设计模式的组成

一般的,在描述一个设计模式时,至少需要包含四个方面:模式名称( Pattern name ) 、 问题( Problem ) 、 解决方案( Solution ) 、 效果( Consequence )。这四个方面就是设计模式的四要素。

名不正则言不顺,每种设计模式都有自己的名字,也就是模式名称;设计模式都有其应用的场合,即该设计模式意图解决的问题,超出了这个问题就不应该再应用这种模式, 所以问题是设计模式的第二要素;设计模式的目的就是解决问题,所以在描述设计模式时当然要有解决问题的方法描述,这就是设计模式的另外一个要素 —— 解决方案;虽然架构设计师知道应用设计模式可以提高架构质量,提高软件的复用性,但对于每一种设计模式而言,还有其更具体的效果描述,所以设计模式的最后一个要素就是效果。

这四个要素是描述设计模式时必不可少的部分。

1.3 GoF 设计模式

GoF 的著作第一次总结了设计中的常用模式,还在学术上建立了软件设计模式的地位。因此,人们习惯上将 GoF 提出的 23 个模式统称为 GoF 模式,这 23 个模式分别简述如下。

(1) FactoryMethod 模式。 FactoryMethod 模式提供了一种延迟创建类的方法,使用这个方法可以在运行期由子类决定创建哪一个类的实例。
(2) AbstractFactory 模式。 AbstractFactory 又称为抽象工厂模式,该模式主要为解决复杂系统中对象创建的问题。抽象工厂模式提供了一个一致的对象创建接口来创建一系列具有相似基类或相似接口的对象。抽象工厂模式是一种很有代表性的设计模式。
(3) Builder 模式。 Builder 模式与 AbstractFactory 模式非常类似,但 Builder 模式是逐步地构造出一个复杂对象,并在最后返回对象的实例。 Builder 模式可以把复杂对象的创建与表示分离,使得同样的创建过程可以创建不同的表示。
(4) Prototype 模式。 Prototype 模式可以根据原型实例制定创建的对象的种类,并通过深复制这个原型来创建新的对象。 Prototype 模式有着同 AbstractFactory 模式和 Builder 模式相同的效果,不过当需要实例化的类是在运行期才被指定的而且要避免创建一个与产品曾是平行的工厂类层次时,可以使用 Prototype 模式。使用 Prototype 模式可以在运行时增加或减少原型,比 AbstractFactory 和 Builder 模式更加灵活。
(5) Singleton 模式。 Singleton 模式也是一种很有代表性的模式。使用 Singleton 模式可以保证一个类仅有一个实例,从而可以提供一个单一的全局访问点。
(6) Adapter 模式。 Adapter 模式可以解决系统间接口不相容的问题。通过 Adapter 可以把类的接口转化为客户程序所希望的接口,从而提高复用性。
(7) Bridge 模式。 Bridge 模式把类的抽象部分同实现部分相分离,这样类的抽象和实现都可以独立地变化。
(8) Composite 模式。 Composite 模式提供了一种以树形结构组合对象的方法,使用 Composite 可以使单个对象和组合后的对象具有一致性以提高软件的复用性。
( 9 ) Decorator 模式。 Decorator 模式可以动态地为对象的某一个方法增加更多的功能。在很多时候,使用 Decorator 模式可以不必继承出新的子类从而维护简洁的类继承结构。
( 10 ) Facade 模式。 Facade 模式为一组类提供了一致的访问接口。使用 Facade 可以封装内部具有不同接口的类,使其对外提供统一的访问方式。 Facade 模式在 J2EE 系统开发中发展为Session Facade 模式。
( 11 ) Flyweight 模式。 Flyweight 模式可以共享大量的细粒度对象,从而节省创建对象所需要分配的空间,不过在时间上的开销会变大。
( 12 ) Proxy 模式。顾名思义, Proxy 模式为对象提供了一种访问代理,通过对象 Proxy 可以控制客户程序的访问。例如:访问权限的控制 、 访问地址的控制 、 访问方式的控制等,甚至可以通过 Proxy 将开销较大的访问化整为零,提高访问效率。
( 13 ) Interpreter 模式。定义了一个解释器,来解释遵循给定语言和文法的句子。
( 14 ) TemplateMethod 模式。定义一个操作的模板,其中的一些步骤会在子类中实现,以适应不同的情况。
( 15 ) Chain of Responsibility 模式。 Chain of Responsibility 模式把可以响应请求的对象组织成一条链,并在这条对象链上传递请求,从而保证多个对象都有机会处理请求而且可以避免请求方和相应方的耦合。
( 16 ) Command 模式。将请求封装为对象,从而增强请求的能力,如参数化 、 排队 、 记录日志等。
( 17 ) Iterator 模式。 Iterator 模式提供了顺序访问一个对象集合中的各元素的方法,使用 Iterator 可以避免暴露集合中对象的耦合关系。
( 18 ) Mediator 模式。 Mediator 模式可以减少系统中对象间的耦合性。 Mediator 模式使用中介对象封装其他的对象,从而使这些被封装的对象间的关系就成了松散耦合。
( 19 ) Memento 模式。 Memento 模式提供了一种捕获对象状态的方法,且不会破坏对象的封装。并且可以在对象外部保存对象的状态,并在需要的时候恢复对象状态。
( 20 ) Observer 模式。 Observer 模式提供了将对象的状态广播到一组观察者的方式,从而可以让每个观察者随时可以得到对象更新的通知。
( 21 ) State 模式。 State 模式允许一个对象在其内部状态改变的时候改变它的行为。
( 22 ) Strategy 模式。使用 Strategy 模式可以让对象中算法的变化独立于客户。
( 23 ) Visitor 模式。表示对某对象结构中各元素的操作,使用 Visitor 模式可以在不改变各元素类的前提下定义作用于这些元素的新操作。

1.4 其他设计模式

在 GoF 之后,人们继续对设计模式进行发掘,总结出更多的设计模式。在 J2EE 应用领域,人们也对使用 J2EE 框架开发的应用程序总结出一系列设计模式。比如 Intercepting Filter 模式。在 J2EE 的 BPS ( Basic Programming System ,基本编程系统)应用框架下,在真正响应客户端请求前经常需要进行一些预处理,如客户身份验证 、 客户 Session 的合法性验证 、 字符集转码 、 客户请求记录等。当然可以将这些请求预处理在每一个 Servlet 中,不过很明显,这样的话预处理的代码就 “ 侵入 ” 了真正的处理程序,使得代码变得更加难以维护。 Intercepting Filter 模式提供了解决这个问题的方法。它通过截取客户请求,并将请求发送到 Filter 链中,一步一步地进行预处理,直到这些处理结束,请求才会被转发到真正响应客户请求的 Servlet 中。

1.5 设计模式与软件架构

软件架构描述了软件的组成,例如,经典的 “4+1” 视图,将软件架构通过逻辑视图 、 开发视图 、 进程视图 、 物理视图及场景视图来进行描述。在这些视图中,描述了软件系统中类之间的关系 、 进程之间的关系 、 软件和硬件的结合等问题。

一般来说,软件架构更倾向于从整体和全局上描述软件的组成。而设计模式则更侧重于类与类 、 对象与对象之间的关系。例如在逻辑视图中,可以使用多种设计模式来组织类与类之间的关系。因此,有很多人认为,设计模式和软件架构是面向不同层次问题的解决方案。同设计模式一样,软件架构也有一些固定的模式,通常称为架构风格。

常见的架构风格有分层架构 、 客户端 — 服务器架构 、 消息总线 、 面向服务的架构( Service-Oriented Architecture , SOA )等。软件架构风格同设计模式在某种含义上是一致的。设计模式和软件架构中蕴含的很多思想是一致的。

无论是架构风格还是设计模式,人们在追求良好设计的过程中,将一些常见解决方案总结 、 整理出来,形成固定的风格与模式。例如消息总线的架构风格同 Observer 模式就有神似之处。因此,掌握设计模式对于软件架构设计有非常大的帮助。

1.6 设计模式分类

可以说,设计模式是面向问题的,即每一种设计模式都是为了解决一种特定类型的问题。因此,根据设计模式要解决的问题将设计模式分为三类,分别为创建型 、 结构型和行为型。

事实上,面向对象的设计中,需要解决的就是:如何管理系统中的对象 、 如何组织系统中的类与对象 、 系统中的类与对象如何相互通信。这三类设计模式分别解决了这三个方面的问题。

创建型设计模式主要解决对象创建的问题。在最简单的情况下,在程序中定义类,在使用时创建一个对象实例。但在实际开发中,对象的创建会变得复杂很多,这时就需要使用创建型设计模式解决创建对象的问题。

随着开发系统的不断扩张,系统功能更加丰富,模块之间的复用越来越多,系统中类与对象的结构变得更加复杂。如果缺乏良好的设计,这些类之间的关系将会变得非常混乱。结构型设计模式就是为了解决这些问题的。除了这种分类方法外, GoF 还提出了可以根据设计模式主要应用于类还是对象来对设计模式进行分类,对于这种分类方法就不再赘述了。综合这两种分类方法,可以把 GoF 模式进行分类,如下表所示。

应用范围 创建型 结构型 行为型
应用于类 Factory Method Adapter Interpreter, Template Method
应用于对象 Abstract Factory, Builder, Prototype, Singleton Adapter, Bridge, Composite, Decorator, Façade, Flyweight, Proxy Chain of Responsibility, Command, Iterator, Mediator, Memento, Observer, State, Strategy, Visitor

随着 GoF 设计模式的提出,后人也总结出了更多的良好设计的范本,并根据其他的方法进行分类。例如,在 《Core J2EE Patterns》 一书中,作者将书中列举的 Design Pattern 分为表现层模式 、 业务层模式和综合层模式。根据这种分类方法,可以得到应用于 J2EE 框架的设计模式图谱,如下表所示。

表现层 业务层 综合层
Intercepting Filter, Front Controller, View Helper, Composite View, Service to Worker, Dispatcher View Business Delegate, Value Object, Session Façade, Composite Entity, Value Object Assembler, Value List Handler, Service Locator Data Access Object, Service Activator

2 设计模式及实现

这里就几种常用的设计模式,讨论其实现问题。

2.1 Abstract Factory 模式

(1)模式名称

Abstract Factory,也经常称之为抽象工厂模式。

(2)意图解决的问题

在程序中创建一个对象似乎是不能再简单的事情,其实不然。在大型系统开发中存在以下问题:

(1) object new ClassName 是最常见的创建对象方法,但这种方法造成类名的硬编码,需要根据不同的运行环境动态加载相同接口但实现不同的类实例,这样的创建方法就需要配合上复杂的判断,实例化为不同的对象。
(2)为了适用于不同的运行环境,经常使用抽象类定义接口,并在不同的运行环境中实现这个抽象类的子类。普通的创建方式必然造成代码同运行环境的强绑定,软件产品无法移植到其他的运行环境。

抽象工厂模式就可以解决这样的问题,根据不同的配置或上下文环境加载具有相同接口的不同类实例。

(3)模式描述

Abstract Factory 模式的结构如图 1 所示。

就如同抽象工厂的名字一样, Abstract Factory 类将接受 Client 的 “ 订单 ”—— Client 发送过来的消息,使用不同的 “ 车间 ”—— 不同的 Concrete Factory ,根据已有的 “ 产品模型 ”—— Abstract Product,生产出特定的 “ 产品 ”—— Product。不同的车间生产出不同的产品供客户使用,车间与产品的关系是一一对应的。由于所有的产品都遵循产品模型 ——Abstract Product ,具有相同的接口,所以这些产品都可以直接交付客户使用。在抽象工厂模式中, Abstract Factory 可以有多个类似于Create Product ()的虚方法,就如同一个工厂中有多条产品线一样。Create Product 1() 创建产品线1,Create Product 2() 创建产品线 2。

在 Abstract Factory 中, Create Product () 方法与 Abstract Product 是一一对应的,而 Concrete Factory 的数量同实际的 Product 的数量是一致的。

(4)效果

应用 Abstract Product 模式可以实现对象可配置的 、 动态的创建。灵活运用 Abstract Product 模式可以提高软件产品的移植性,尤其是当软件产品运行于多个平台,或有不同的功能配置版本时,抽象工厂模式可以减轻移植和发布时的压力,提高软件的复用性。

(5)相关讨论

在实际应用中, Abstract Factory 可以有更灵活的变化。事实上,如果仔细观察 Abstract Factory 模式就可以发现,对于 Client 来说,最关注的就是在不同条件下获得接口一致但实现不同的对象,只要避免类名的硬编码,采用其他方式也可以实现。所以也可以采用其他的方式实现。例如在 Java 中就可以采用接口的方式实现,如图 2 所示。

图 2 中所描绘的类就应用了 Abstract Factory 的思想,采用面向接口的方式,简单实现了工厂模式 。 Product Factory 既可以看作抽象工厂 Abstract Factory ,也可以看作具体的工厂(车间) Concrete Factory 。

其中提供了一个获得产品的方法: getProduct(productName:String ),该方法将根据产品的名称创建特定的产品,并返回。所有的产品都实现了 Product 接口,可以在客户程序中加以使用。

同抽象工厂模式一样,工厂中获得对象的方法( getProduct())与实际的产品线数量是一致的,如果要增加新的产品线 —— 例如定义新的产品接口 Product X,需要增加相应的 getProduct() 方法。由于这种方式把 Abstract Factory 和 Concrete Factory 合并为一个 Product Factory,所以增加新的实现 Product 接口的 Product 不需要修改任何代码。

除了这种面向接口的方法外,工厂模式还可以有更多的变化。前面已经说过,学习设计模式最终要的是学习设计思想,学习了工厂模式后,应该知道:

① 可配置的对象创建方法可以提高系统的移植性和复用性 。
② 充分利用面向对象多态的特性可以避免对象创建过程的硬编码。

2.2 Singleton 模式

(1)模式名称

Singleton,也常称之为单件模式或单根模式。

(2)意图解决的问题

在软件开发中,开发人员希望一些服务类有且仅有一个实例供其他程序使用。例如,短消息服务程序或打印机服务程序,甚至对于系统配置环境的控制,为了避免并发访问造成的不一致,也希望仅为其他程序提供一个实例。

对于供整个系统使用的对象可以使用一个全局变量,不过全局变量仅能保证正确编码时使用了唯一的实例。但随着系统不断的扩张,开发队伍的扩大,仍然无法保证这个类在系统中有且仅有一个实例。

(3)模式描述

Singleton 模式可以解决上面的问题,其结构如图 3 所示。

从结构角度而言, Singleton 是最简单的一个模式,不过其用途很广。在 Singleton 中,通过将 Singleton 类的构造函数设为 protected 型(或 private )来防止外部对其直接初始化。需要访问 Singleton 的程序必须通过 getInstance() 方法来获得一个 Singleton 。

在 getInstance() 中仅创建一次 unique Instance 就可以保证系统中的唯一实例。对于 Singleton 中的 unique Instance 有两种不同的初始化策略( 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 类,可以参考有关文献中的方法。

2.3 Decorator 模式

(1)模式名称

Decorator 模式,又称装饰模式或油漆工模式。

(2)意图解决的问题

在开发时,经常会发现为类预先设计的功能并不够强大,需要增强或扩展类的功能。解决这个问题的最简单的办法是继承出一个新的类,并扩展相应的方法。但是这样做会产生大量的子类,让系统中类的层次结构变得复杂且混乱 。Decorator 模式通过在原有类的基础上包装一层来解决功能扩展的问题。

(3)模式描述

Decorator 模式的结构如图 4 所示。

Decorator Component 是 Concrete Component 的装饰类,它们继承自同一个抽象类 Component ,拥有相同的接口。对于 Concrete Component 的装饰可能不止一种,因此又继承出 Concrete Decorator 1 和 Concrete Decorator 2 来作为具体的装饰类。这个结构在类层次上是很清晰的,比静态继承更具有灵活性。图 5 中使用顺序图描述了使用 Decorator 类的方法。

通过图 4 的方法,对于接口相同的 operations() ,在运行期可以根据客户的选择具有不同的行为特征,实现功能动态的扩展。

(4)效果

Decorator 模式可以动态地扩展类的功能,同时又避免继承出大量的子类,比继承的方法更灵活。由于 Decorator 提供了动态的扩展方法,因此可以随时根据具体需求产生新的装饰类,从而可以在一定程度上避免层次较高类的复杂性。但是大量使用 Decorator 模式会造成系统中出现很多接口类似的小装饰对象,这样就造成系统可维护性的下降。

(5)相关讨论

Decorator 模式提供了一种动态为类扩展功能的方法,有着较广的应用。例如,在开源项目 displaytag (一个提供 JSP 标签扩展的开源项目,可以简单地在网页中画出美观的表格,http:// displaytag .sourceforge.net/)中就提供了以 Decorator 方式扩展功能,进行二次开发的接口,在 JDK 的 I/OAPI 中,也大量使用了 Decorator 模式。

同其他模式一样,滥用 Decorator 会降低系统的可维护性。如果开发出来的装饰类仅被单一的地方使用或只进行了相当简单的处理,就需要考虑是否有必要使用 Decorator 模式了。

2.4 Facade 模式

(1)模式名称

Facade 模式,又称外观模式,也有人很形象地把它翻译成“门面模式”。

(2)意图解决的问题

在程序中,经常会用到其他若干个子系统。在不作任何处理的时候,需要了解每一个子系统的接口,并使用这些接口进行调用,于是系统就如图 6 所示一样混乱。

这些调用不但让结构变得混乱,客户程序和各子系统的耦合性也大大增加,扩展与维护都变得相当困难。

(3)模式描述

Facade 模式的结构如图 7 所示。

如图 7 所示, Facade 模式通过在原有系统前增加一层的方法,封装这些子系统的接口,对外提供一致的访问接口,解决了上面的问题。

(4)效果

Facade 模式屏蔽了子系统的细节,降低了客户程序使用这些子系统的复杂度,同时也降低了客户程序和子系统的耦合程度。这样就从需要让所有的人了解所有的子系统接口变成让个别专家抽象子系统的接口。

Facade 模式应用起来非常灵活,也没有特定的实现,其应用的关键就是抽象出合理的 Facade 接口。

(5)相关讨论

Facade 模式很好地体现了封装的思想,它封装的是一个子系统的内部结构。例如,经常对数据访问对象( Data Access Object , DAO )进行封装,使数据访问同具体的数据库相分离。客户程序只要知道最外层的数据库访问构件的接口就可以操纵任何支持的数据库。

2.5 Mediator 模式

(1)模式名称

Mediator 模式,又称中介者模式。

(2)意图解决的问题

在一个复杂系统中会有很多对象,这些对象之间会相互通信,从而造成对象的相互依赖。修改其中一个对象可能会影响到其他若干对象,系统中对象的复杂耦合关系造成系统可维护性的降低,系统显得混乱且难以理解 。Mediator 通过封装一组对象间的通信为系统解耦。

(3)模式描述

Mediator 模式的结构如图 8 所示。

在 Mediator 模式中,一组对象 —— 抽象类 Colleague 的子类,通过中介类 —— Concrete Mediator 进行通信,中介类Concrete Mediator 继承了抽象类 Mediator ,其中需要实现 Mediator 中定义的通信接口,保证中介类和相互通信类之间接口的一致。

Mediator 模式看上去类似于消息机制,其实和消息机制有很大的区别 。Mediaor 不需要负责消息的排队 、 优先级等处理,但它必须根据 Colleague 发送的消息作出响应,并向特定的 Colleague 类发送消息。换句话说,Concrete Mediator 类中封装了 Colleague 之间的通信,就好比人的大脑,接收到眼睛发送的 “ 前面有一个坑 ” 的消息,就会向脚发送 “ 停止前进 ” 的消息。

(4)效果

Mediator 封装了一组对象间的通信,降低了 Colleague 之间的耦合性,单个 Colleague 的变化不会影响到其他的对象。还举前面大脑的例子,在行走的时候,大脑接收到眼睛发送的 “ 前面有状况 ” 的消息会通知脚 “ 停止前进 ” ,而在开车的时候,大脑接收这条消息后则会向脚发送 “ 踩下刹车 ” 的消息。

不过当 Colleague 比较多,且其中的关系复杂时,中介类Concrete Mediator 会变得非常复杂,难以维护。

(5)相关讨论

Mediator 模式在 GUI 方面有较多的使用。当一个窗体或表单或对话框中有多个需要互相通信的对象时 —— 例如显示部门中人员的下拉列表需要根据部门下拉列表不同的选择而变化,使用 Mediator 封装通信可以降低这些对象的耦合性,提高系统的可维护性。

Mediator 经常会变得非常复杂,不但每一个需要通信的对象都需要知道 Mediator 的存在,而且 Mediator 也需要知道每一个通信的对象的实现。所以,一般仅在较小的范围内使用 Mediator 模式,否则过多的 Colleague 类造成中介类的难以维护会抵消掉该模式带来的优点,反而让系统更难以维护。

2.6 Observer 模式

(1)模式名称

Observer 模式,又称观察者模式。

(2)意图解决的问题

由于对象封装的特性,一般的,在系统中对象状态的变化都是独立的,即 A 对象状态的变化不会立即反映到 B 对象。但是,在系统中很多对象是相互关联的,例如对于一个股票行情系统,股票状态的变化需要同时反映到多个视图中 —— 实时行情 、 技术分析图表等。对于这个问题,最简单的解决办法就是硬编码这些关联关系。但是这样会造成系统中对象的紧密耦合,系统难以维护和复用。

(3)模式描述

Observer 模式的结构如图 9 所示。

抽象类 Subject 定义了主题类 —— 也就是被观察者的接口,它的子类Concrete Subject 的任何状态改变将会通知全部的观察者 —— Concrete Observer。

为了保持接口的一致性,这些观察者继承自相同的抽象类 Observer。 抽象类在 Subject 中保存有观察者的列表 —— observers ,并通过 attach 和 detach 方法来动态地添加或移出一个特定的观察者。

这种采用订阅方法来添加的观察者并不知道还有其他的观察者,它仅仅能够接收被观察的主题对象的状态改变。在 notify 方法中,主题类将根据目前加入订阅的观察者列表 observers 来向每一个观察者发出状态改变的消息。

Concrete Subject 中的 setState() 方法表明对象的状态发生了变化,该方法会调用 notify 方法对所有的观察者进行更新,如图 10 所示。

(4)效果

在 Observer 模式中,实现了具体对象和观察者之间的抽象耦合。虽然 Subject 了解目前有哪些观察者需要捕获自己状态的变化,但它并不了解这些观察者要做什么;而每一个观察者仅仅知道自己捕获到对象的变化,但并不清楚目前有多少观察者在观察对象的状态,也不需要通知其他的观察者。

这样,动态的增加和移除观察者是非常简单的。通过 Observer 模式可以实现对象间的消息广播,这在很多处理中非常有用。不过广播的副作用是增加了系统的负载,任何消息都将被自动地发送到每一个观察者中,每一个观察者都将根据这些消息作出响应,这些响应未必都是必要的。

(5)相关讨论

Observer 模式将一对多的对象依赖转化为抽象耦合,提高系统的复用价值,得到了广泛的应用 。Smalltalk 的 MVC 架构中就应用了 Observer 模式,将 Model 的变化传递到 View 中。

但是 Observer 使用不当会造成很多问题。在图 10 中描述的更新方法是在 setState() 后由 Subject 自动执行 nofity() ,向所有的 Observer 发送通知,这种方法可以保证所有的更新都能够通知到观察者。

但是对象状态并不总是孤立的,客户程序很可能在执行了 setState1() 方法之后紧接着执行 setState2() 方法,甚至要连续对对象的状态作若干次的更改。这样,每一次更改都会带来一个 noifty() 事件,这些更改被放大到每一个 Observer 对象中。并发执行观察者的 update() 方法很可能造成程序的错误,而独占的访问 update() 方法又会造成程序阻塞。

因此有时会采用如图 11 所示的方法发送对象状态变化的通知。

这种方法在客户程序执行了一系列改变对象状态的方法后调用 notify() 方法,避免了上面的问题,不过这种方法首先不能保证对象状态的变化一定会被通知到各个观察者,其次还造成观察者对客户程序的不透明。

所以,在使用 Observer 模式时一定要注意精确地定义对象状态变化的依赖关系,以避免这些问题。前面也提到使用 Observer 可以实现消息的广播。广播是一柄双刃剑,虽然可以简化一对多的消息发送但也将带来一系列的问题。

在 Observer 模式中需要尽力避免广播的扩大,在 update() 对象中避免更新 Subject 类的状态,尤其不能更新自己观察的 Subject 类的状态。否则,可能会造成对象间的消息无穷循环下去。

2.7 Intercepting Filter 模式

(1)模式名称

Intercepting Filter 模式,又称筛选器模式。

(2)意图解决的问题

在使用 MVC 架构进行 Web 应用开发时,通常需要对来自于客户的请求进行一些预处理,如验证客户身份 、 验证请求来源 、 对请求解码等,然后再传递给控制器。如果把这些预处理都交由控制器来完成,将增加控制器的复杂度,而且难以维护和扩展。

(3)模式描述

Intercepting Filter 模式的结构如图 12 所示。

FilterManager 负责调度整个 FilterChain ,它将在请求到达 Target 前拦截请求,并传递给 FilterChain ,由 FilterChain 中的过滤器依次进行预处理。直到请求经过最后一个过滤器后才完成了全部的预处理,然后由 FilterManger 把请求转发给实际的目标。使用 InterceptingFilter 模式时的时序关系如图 13 所示。

(4)效果

使用 InterceptingFilter 模式使得预处理的逻辑和真正的处理逻辑分离,进行实际处理的 Target 只需要关心具体的逻辑,而同请求相关的预处理都放在 FilterManager 中进行。同时解除了这两类处理的耦合性,扩展 、 修改预处理过程变得容易,系统具有更好的维护性和扩展性。

(5)相关讨论

虽然 InterceptingFilter 作为一种 J2EE 模式出现在有关文献中,但实际上有很广泛的应用。不仅应用 J2EE 的开发可以使用该模式分离预处理过程,使用 .Net 框架等其他的 Web 应用时也可以使用 InterceptingFilter 。

仔细观察 InterceptingFilter 模式就可以发现,它与 Decorator 模式有些类似,都是在真正的处理对象前增加一层扩展对象的操作。只不过实现起来有很大的差别。

完全可以让 Intercepting Filter 变得更通用,更容易配置。如果对于不同的请求需要使用不同的 Filter Chain —— 例如根据客户不同的编码方式使用不同的解码程序 —— 可以使用工厂模式来动态创建 Filter 对象进行处理。类似的灵活使用设计模式的方法还有很多,也可以用于各种模式,需要架构设计师根据实际情况进行深入的思考。

3 设计模式总结

学习设计模式最重要的是理解,而不是生搬硬套。每种设计模式中都包含着良好的设计架构的思想,如隐藏内部细节 、 降低耦合度等,越是复杂的系统越需要这些思想的支撑。

观察人类社会就可以发现很多与设计模式相同的地方。每一个人都是一个封装良好的对象,通过感官和行动提供了与外界沟通的媒介。人类对外接收消息的方式不过是视觉 、 听觉 、 嗅觉 、 触觉等有限的几种,但这些感觉器官封装了内部复杂的结构;可以使用望远镜 、 电话或其他的工具来扩展能力;对于人类的复杂组织,有间接沟通的渠道,中介所提供了交流的平台;新闻机构则可以让人们随时了解其他人和事的最新状态 …… 类似的例子还可以举出来很多,如经纪人与 Proxy 模式非常相近,而绝大多数的组织看起来都像 Composite。

软件系统的最终目的是辅助人类的活动,以良好的方式抽象现实世界才可以获得良好的设计,单纯的死记硬背这些模式不会有任何价值,与实际情况相联系才能得出易维护、易复用的系统来。在使用中,经常会把几种模式综合起来解决复杂的问题。这需要系统架构设计师具体问题具体分析,综合利用设计模式消除系统中的混乱与耦合,提高系统复用性。

要切记不能滥用设计模式,尤其在一些简单系统中。如果系统中的对象都用工厂模式创建 、 系统中的工具类都设计成Singleton 、 两个对象间的通信还要硬加上一层 Mediator 等都是不可取的,只能毫无价值地提高系统复杂度,反而不利于系统的理解与维护。除最初的设计外,重构也是一个很好的时机,系统架构设计师可以在重构的时候根据需要逐步应用设计模式改良系统,提高系统的维护性和复用性。

转载地址:https://deniro.blog.csdn.net/article/details/107448193 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:说说 Python 内置序列类型
下一篇:系统架构设计笔记(52)—— 软件架构视图

发表评论

最新留言

留言是一种美德,欢迎回访!
[***.207.175.100]2024年04月09日 09时53分01秒