1、软件设计模式第3章 设计模式入门 l 3.1 模式概念l 3.2 通用责任分配模式(GRASP)l 3.3 简单工厂模式(Simple Factory Pattern)提纲3.1.1 模式的定义 模式存在于生活的各个角落,不仅仅是工程技术领域。比如,“看云识天气”是透过云的特征判断物理 气象的一个生活技能,由人们经过长期实践训练与生活总结习得。“看云识天气”将具体物理场景中云的特征和气象关联起来,不同云的特征代表不同的气象,这些经验可以反复地应用至实际的生活中,该案例可以称为“云模式”。对于“云模式”,人们用以下特征进行描述:1)名称。为了方便这些模式的记忆与传播,人们会对每个云模式起个符合其
2、特征的名称;如炮台云指堡状高积云或堡状层积云。2)目标气象。每个云模式都会用于目标气象的判断,如“炮台云,雨淋淋”,即透过“炮台云”判断目标气象为“雨淋淋”;而且,这种经验可以被反复使用。3)内部关联。以“炮台云,雨淋淋”为例,该经验蕴含的气象与云之间的关联关系是炮台云多出现在低压槽前,表示空气不稳定,一般隔数个小时即有雷雨降临。软件设计模式是基于设计目标上下文的、针对常见代码问题的、可行和可重用的代码设计方案。理论设计模式一般只提供抽象的设计建议,并不能作为完整的代码设计框架。有很多软件工程领域的专家或学者从不同经验视角提出了各种设计模式 如GoF设计模式、GRASP(General Res
3、ponsibility Assignment Software Patterns,通用责任分配软件模式)设计模式、并发设计模式(Concurrency Patterns)、软件架构设计模式(Architectural Patterns)等。3.1.2 使用设计模式 由于软件设计模式是实践经验的抽象与概括,学习者往往无法准确把握不同模式的使用方法。因此,如何恰当地使用设计模式是软件工程师需要直接面对的问题。GoF建议工程师按照如下步骤使用软件设计模式:1)了解设计模式(原文:Read the pattern once through for an overview)。工程师需要对目标设计模式的应
4、用场景、优缺点等有基本了解,并确定该模式是否适用目标设计问题 不能恰当地使用设计模式,不仅无法获得优化的代码质量,还会导致更大的负作用,甚至比没有设计更糟糕!2)熟悉设计模式(原文:Go back and study the Structure,Participants,and Collaborations sections)。这要求工程师能够理解模式类或对象的角色,以及它们之间的协作关系。模式类或对象的角色一般指业务角色,即更加强调其业务职责。在面向对象的软件系统中,所有的系统表象特征都是系统内部类或对象的行为表示,对象之间协作共同产生向外部环境提供的软件服务。准确地定位类或对象的角色以及它
5、们之间的协作关系是正确使用设计模式的前提。3)参照设计模式样例代码(原文:Look at the Sample Code section to see a concrete example of the pattern in code)。GoF在其出版的书中,用C+语言提供了模式实现样例代码。虽然不同编程语言在语法或规范上不同,但都以面向对象作为语言特征。因此,编程语言对模式的学习和使用并无影响。参照模式样例代码可以达到以下目的:A.通过实现代码加深对模式的理解;B.学会如何将抽象模式类图用代码实现;C.帮助实现自己的模式代码。4)根据应用上下文定义模式角色类或对象的名称(原文:Choose
6、names for pattern participants that are meaningful in the application context)。同样地,模式角色类或对象的命名一般与其业务职责保持一致,用角色名称作为类或对象的名称;这样做可以带来以下好处:A.增加代码可读性。因为,模式本身就是是对业务问题通用解决方案的概括或抽象,与目标系统的领域业务联系较弱;如果模式类或对象命名与业务职责不一致,会导致代码理解更加困难。B.使模式代码更加容易实现(或维护);模式类或对象命名与业务上下文之间的关联使代码开发者容易理解或实现目标业务逻辑,达到顺利编写(或维护)代码的目的。5)定义类(原
7、文:Define the classes)。定义类的活动包括:定义接口、关联(如继承、依赖等)、域;识别引入新的模式类或对象后,对已有设计类的影响,并修改已有设计方案;定义类业务行为等。6)定义模式中面向具体应用的操作名称(原文:Define application-specific names for operations in the pattern)。如,创建型模式类操作(或方法)一般加“create”前缀,访问者模式类操作(或方法)用“visit”前缀等。7)实现6)中定义的操作(原文:Implement the operations to carry out the responsi
8、bilities and collaborations in the pattern)。即,参考设计模式样例,实现自己的模式代码。对于恰当使用设计模式解决问题,补充如下:8)以模式作为语言帮助团队进行技术沟通。首先,技术团队需要建立沟通语言,即每个技术人员都能理解指代词语内涵。9)正视模式的缺陷。没有任何一个技术或方法是万能的,模式也一样。针对目标问题,软件设计模式提供了一种可行、且可复用的设计方案;但,不一定是最优设计方案。模式的引入一定会携带自身缺陷;比如,增加设计类数量,或使设计方案变得更复杂等。工程师需要明白的是:设计是以较小代价换取目标问题的解决,而不是没代价;所有模式都有缺陷,大家
9、要恰当地使用它们,而不是滥用!10)多个模式可以复合使用。在初学者中,很多人认为一个类或对象承担了某个模式职责后,就不能再引入其他模式职责了;这是错误的思维!使用模式是以问题解决为目标的,而不限于形式。在行业案例章节的内容中,读者可以看到,很多经典案例都将多个设计模式复合在一起,以解决目标代码问题。模式之间的联系或相关性是因业务建立的,而不是模式理论自身。11)不必完全按照形式化模式理论定义代码结构。代码设计是以解决问题为目标的。GoF或其他作者提出的设计模式理论是泛化且抽象的,同时也带有一定的技术局限。计算机或软件工程技术的发展使得30年前的领域经验和软件认知发生了巨大的变化,现在所面临的影
10、响代码设计的因素变得更加多样和复杂,比如移动互联网、云计算、大数据等是30年前的技术专家在当时不曾面对的领域环境。经典的理论具有指导实践的作用,但在实际运用时,工程师却不能僵硬地受限于经典理论的约束。表3.1 创建者、信息专家、控制器和简单工厂模式使用总结模式名称模式名称模式定义模式定义使用场景使用场景解决方案解决方案创建者(Creator)创建目标类实例的对象谁负责创建指定类的实例?类实例的创建职责可以按如下规则分配:1)类B对象是类A对象的聚合体,则B创建A的实例;2)类B对象包含类A对象,则B创建A的实例;3)类B对象保存A对象实例,则B创建A的实例;4)类B对象使用A对象,则B创建A的
11、实例;5)类B对象持有A对象初始化所需数据,则B创建A的实例。信息专家(Information Expert)拥有处理实现目标职责所需信息的对象将行为职责分配给哪个类或对象?将行为分配给拥有实现该职责所需信息的专家类。控制器(Controller)负责接收或处理系统事件的非用户接口(User Interface,UI)对象谁应该处理目标系统输入事件?判断目标系统输入事件接收或处理的对象,满足以下条件之一即可:1)该对象代表整个系统的服务入口;2)该对象代表了一个系统事件生成的用例场景。简单工厂(Simple Factory)负责创建目标产品实例的对象如何实现产品对象创建行为的一致或复用?将目标
12、产品创建行为分配给工厂类,由工厂类向客户端提供产品对象创建服务。表3.2 GoF模式使用总结模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景创建型模式单例(Singleton)目标类(Class)只有一个实例对象(Object),并且向使用该对象的客户端提供全局访问方法。1)当一个类只能有一个实例,并且客户端需要访问该实例;2)当一个类的实例化代价很大,且向所有客户端提供的服务都无状态或不因客户端的变化而改变状态。原型(Prototype)通过复制自己达到构造目标对象新实例的目的。1)当一个类的实例状态只能是不同组合中的一种时,而不想通过平行类或子类的方式区分不同的状态组合;2
13、)当业务代码中不能静态引用目标类的构造器来创建新的目标类的实例时;3)当目标类实例化代价昂贵,不同的客户端需要单独使用一个目标类的对象时。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景创建型模式 构造器(Builder)为构造一个复杂产品对象,进行产品组成元素构建和产品组装,并将产品构造过程(或算法)进行独立封装。1)需要将复杂产品对象的构造过程(或算法)封装在独立的代码中;2)对不同的产品表示复用同一个构造过程(或算法)。抽象工厂(Abstract Factory)在不指定具体产品类的情况下,为相互关联的产品簇或产品集(Families of Products)提供创建接口
14、,并向客户端隐藏具体产品创建的细节或表示。1)需要实现产品簇样式的可扩展性,并向客户端隐藏具体样式产品簇的创建细节或表示;2)向客户端保证产品簇对象的一致性,但只提供产品对象创建接口;3)使用产品簇实现软件的可配置性。工厂方法(Factory Method)定义产品对象创建接口,但由子类实现具体产品对象的创建。1)当业务类处理产品对象业务时,无法知道产品对象的具体类型或不需要知道产品对象的具体类型(产品具有不同的子类型);2)当业务类处理不同产品子类型对象业务时,希望由自己的子类实现产品子类型对象的创建。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景结构型模式 适配器(Ada
15、pter)将某种接口或数据结构转换为客户端期望的类型,使得与客户端不兼容的类或对象,能够一起协作。1)想要使用已存在的目标类(或对象),但它没有提供客户端所需要的接口,而更改目标类(或对象)或客户端已有代码的代价都很大;2)想要复用某个类,但与该类协作的客户类信息是预先无法知道的。桥(Bridge)使用组合关系将代码的实现层和抽象层分离,让实现层与抽象层代码可以分别自由变化。1)抽象层代码和实现层代码分别需要自由扩展时;2)减弱或消除抽象层与实现层之间的静态绑定约束;3)向客户端完全隐藏实现层代码时;4)需要复用实现层代码,将其独立封装。组合(Composite)使用组合和继承关系将聚合体及其
16、组成元素分解成树状结构,以便客户端在不需要区分聚合体或组成元素类型的情况下,使用统一的接口操作它们。1)将聚合体及组成元素用树状结构表达,并且能够很容易添加新的组成元素类型;2)为了简化客户端代码,需要以统一的方式操作聚合体及其组成元素。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景结构型模式 装饰器(Decorator)通过包装(不是继承)的方式向目标对象动态地添加功能。1)动态地向目标对象添加功能,而不影响到其他同类的对象;2)对目标对象进行功能扩展,且,能在需要时删除扩展的功能;3)需要扩展目标类的功能,但不知道目标类的具体定义,无法完成子类定义;4)目标类的子类只在扩
17、展行为上有区别,但数量巨大,需要减少设计类。门面(Facade)向客户端提供使用子系统的统一接口,简化客户端使用子系统。1)想要简化客户端使用子系统的接口;2)需要将客户端与子系统进行独立分层;3)向客户端隐藏子系统的内部实现,用于隔离或保护子系统。享元(Flyweight)采用共享方式向客户端提供服务的数量庞大的细粒度对象。1)运行时产生大量的相似对象,这些对象可被不同的客户端共享,需要要减少它们实例的数量;2)向客户端提供对象的共享实例,以提高程序运行的效率。代理(Proxy)用于控制客户端对目标对象访问的占位对象。1)向客户端提供远程对象的本地表示;2)向客户端按需提供昂贵对象的实例;如
18、,写时拷贝(由于目标对象是昂贵的,只在其状态被改变时进行副本的复制,这是一种按需提供服务的做法);3)控制客户端对目标对象的访问;4)客户端访问目标对象服务时,需要执行额外的操作。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景行为型模式 责任链(Chain of Responsibility Pattern)处理同一客户端请求的不同职责对象组成的链1)动态设定请求处理的对象集合;2)处理请求对象的类型有多个,且请求需要被所有对象处理,但客户端不能显式指定具体处理对象的类型;3)请求处理行为封装在不同类型的对象中,这些对象之间的优先级由业务决定。命令(Command)将类的业务
19、行为以对象的方式封装,以便实现行为的参数化、撤销、重做等操作1)需要对目标类的行为实现撤销或重做的操作;2)将目标类的行为作为参数在不同的对象间传递;3)需要对目标业务行为及状态进行存储,以便在需要时调用;4)在原子操作组成的高级接口上构建系统。解释器(Interpreter)用于表达语言语法树,和封装语句的解释(或运算)行为1)目标语言的语法规则简单;2)目标语言程序效率不是设计的主要目标。迭代器(Iterator)在不暴露聚合体内部表示的情况下,向客户端提供遍历其聚合元素的方法1)需要提供目标聚合对象内部元素的遍历接口,但不暴露其内部表示(通常指聚合元素的管理方式或数据结构);2)目标聚合
20、对象向不同的客户端提供不同的遍历内部元素的方法;3)为不同聚合对象提供统一的内部元素遍历接口。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景行为型模式 仲裁者(Mediator)用来封装和协调多个对象之间耦合交互行为,以降低这些对象之间的紧耦合关系1)多个对象之间进行有规律交互,因交互关系复杂导致难以理解和维护;2)想要复用多个相互交互对象中的某一个或多个,但,复杂的交互关系使得复用难以实现;3)分布在多个协作类中的行为需要定制实现,但不想以协作类子类的方式设计。备忘录(Memento)在不破坏封装特性的基础上,将目标对象内部的状态存储在外部对象中,以备之后状态恢复时使用保持
21、对象的封装特性,实现其状态的备份和恢复功能观察者(Observer)当目标对象状态发生变化后,对状态变化事件进行及时响应或处理的对象1)当目标对象状态发生变化时,需要将状态变化事件通知到其他依赖对象,但并不知道依赖对象的具体数量或类型;2)抽象层中具有依赖关系的两个对象需要独立封装,以便复用或扩展。状态(State)指状态对象,用于封装上下文对象特定状态的相关行为,使得上下文对象在内部状态改变时,改变其自身的行为1)上下文对象的行为依赖于内部的状态,状态在运行时变化;2)需要消除上下文对象中状态逻辑的分支语句。模式类型模式类型模式名称模式名称模式定义模式定义使用场景使用场景行为型模式 策略(S
22、trategy)用于封装一组算法中单个算法,使得单个算法的变化不影响使用它的客户端1)算法需要实现不同的可替换变体;2)向使用算法的客户类(或上下文类)屏蔽算法内部的数据结构;3)客户类(或上下文类)定义了一组相互替换的行为,需要消除调用这组行为的分支语句;4)一组类仅在行为上不同,而不想通过子类方式实现行为多态。模板方法(Template Method)用来定义算法的框架,将算法中可变步骤定义为抽象方法,指定子类实现或重定义1)当算法中含有可变步骤和不可变步骤的时候,让子类决定可变步骤的具体实现;2)当多个类中含有公共业务行为,想要避免定义重复代码;3)想要控制子类的扩展行为,只允许子类实现
23、特定的扩展点。访问者(Visitor)用于封装施加在聚合体中聚合元素的操作(或算法),从而使该操作(或算法)从聚合对象分离出来,在不对聚合对象产生影响的前提下,实现自由扩展1)目标聚合对象包含不同的聚合元素类型,需要针对不同的聚合元素类型,施加不同的业务操作或算法行为;2)目标聚合对象结构稳定,但针对聚合元素的操作需要实现不同的扩展;3)有多个单一且不相关的操作施加在聚合元素上,但不想“污染”聚合元素类的代码。3.2通用责任分配模式(GRASP)GRASP包含:信息专家、创建者、高内聚、低耦合、控制器、多态(Polymorphism)、纯净虚构(Pure Fabrication)、间接耦合(I
24、ndirection)、受保护变化(Protected Variations)等模式 每一种设计模式都提供了面向具体代码设计问题的解决建议。3.2.1 创建者模式(Creator Pattern)面向对象程序开发中,目标软件服务由若干种类型对象之间的协作行为向外部环境提供;类定义了对象类型,对象是类具体的实例。那么,对象是怎么创建出来的,谁负责创建这些对象呢?工程师往往会把目标类型对象按需地创建;即,需要使用指定类型对象的时候,则进行创建。然而,这种做法会给程序引入大量耦合。图3.1 客户、订单、配餐员与餐厅员工领域模型图3.1中的Patron、MealDeliverer和CafeteriaS
25、taff都使用(或关联)MealOrder类型的对象完成自己的业务行为。如果按照“所需即创建”的想法,则Patron、MealDeliverer和CafeteriaStaff都会创建MealOrder类的对象。也即,Patron、MealDeliverer和CafeteriaStaff都与MealOrder类对象的创建代码产生耦合关联。那么,当MealOrder类对象创建方式发生变化时,则必须修改Patron、MealDeliverer和CafeteriaStaff,这就降低了代码的可扩展性、稳定性等质量。根据领域业务逻辑重新审查图3.1发现,MealDeliverer和CafeteriaSt
26、aff是使用已被创建的MealOrder对象(即,餐厅员工和配餐员是对已有订单对象进行操作,而不是创建新订单对象后再操作)。因此,MealDeliverer和CafeteriaStaff不需要耦合MealOrder类对象的创建行为。那么,谁来创建MealOrder对象呢?GRASP的创建者模式给出了对象创建行为的职责分配原则,如下:1)类B对象是类A对象的聚合体,则B创建A的实例;2)类B对象包含类A对象,则B创建A的实例;3)类B对象保存A对象实例,则B创建A的实例;4)类B对象使用A对象,则B创建A的实例;5)类B对象持有A对象初始化所需数据,则B创建A的实例。对于GRASP创建者模式原则
27、,笔者补充一个前提条件:类逻辑与业务逻辑一致。再看图3.1,Patron与MealOrder的业务逻辑为“Patron对象生成、保存和支付MealOrder对象”;即,Patron对象持有MealOrder对象初始化所需要的数据。因此,Patron是MealOrder对象的创建者之一。MealDeliverer和CafeteriaStaff分别是配送和修改已有的MealOrder对象,虽然符合规则4),但业务逻辑不正确。所以,MealDeliverer和CafeteriaStaff不能作为MealOrder对象的创建者。MealDeliverer和CafeteriaStaff使用的MealOr
28、der对象来自于哪里呢?如果只从业务逻辑角度判断,应该来自于Patron所创建的MealOrder对象。即,当Patron生成MealOrder对象后,MealDeliverer和CafeteriaStaff才可以配送或修改MealOrder对象。如果从对象协作逻辑看,MealDeliverer和CafeteriaStaff配送或修改的MealOrder对象在配送或修改行为执行前,就已经被创建好。因此,MealOrder对象可能是来自于触发配送或修改行为的客户端,也可能是其他业务逻辑对象。最终,图3.1中MealOrder对象的创建行为耦合到Patron中,而不是同时耦合到MealDelive
29、rer和CafeteriaStaff中,这在一定程度上减弱了CafeteriaStaff、MealDeliverer与MealOrder之间的代码耦合度。对于GRASP创建者模式的使用,你可能已经感觉到,并非一定会带来程序质量的优化。因为,单纯依据上述5个原则分配目标对象创建者行为时,会导致创建行为分散到系统的各个模块中,并不利于代码的维护和复用。此外,对象创建行为职责分配的5个原则并无法保证业务逻辑的正确性。如,图3.1中的Patron、MealDeliverer和CafeteriaStaff与MealOrder之间的关系分别符合5个原则中的某个,如果按照创建者模式建议,则会导致MealDe
30、liverer、CafeteriaStaff与MealOrder对象的协作逻辑错误。对GRASP创建者模式总结如下:模式名称:创建者(Creator);应用场景:谁负责创建目标类型对象?解决方案:5个创建行为分配原则,见前文;使用前提:保证业务逻辑一致或正确;模式优点:有可能会降低代码耦合;模式缺陷:有可能会导致对象创建行为分散或不一致。3.2.2 信息专家模式(Information Expert Pattern)用设计类对程序逻辑进行静态模型构建时,不仅要抽取类名称、域及关联关系,还需要抽取设计类的行为。软件系统中的设计类数量十分庞大,而类行为的数量则更多。如何将一个行为职责正确地分配至某
31、个类是设计的关键。不恰当的行为职责分配不仅会引入业务逻辑错误,还可能降低代码质量。图3.2 客户支付订单的设计类模型例如,COS系统的设计类模型中的客户(Patron)选择具体支付方式(PayOrder)支付菜品订单(MealOrder),见图3.2。PayOrder实现支付业务时,需要获得订单总金额。那么,订单总金额的计算行为getOrderAmount()应该分配给哪个类呢?假设1:将getOrderAmount()行为分配给Patron;则,Patron在实现该行为时,需要遍历目标订单的所有订单项(FoodItem),将订单项金额进行求和计算;PayOrder向Patron请求获取订单总
32、金额。订单项信息又封装在哪个类中呢?MealOrder负责封装和管理订单项信息。于是,MealOrder需要向Patron提供遍历订单项接口。不仅如此,Patron还需要调用订单项FoodItem的接口获取订单项信息。所以,getOrderAmount()行为分配给Patron后,Patron会在行为上依赖MealOrder和FoodItem。为什么会使Patron在实现getOrderAmount()行为时,与MealOrder、FoodItem产生新的耦合依赖呢?因为,行为getOrderAmount()所需要的业务信息来自于MealOrder和FoodItem。假设2:将getOrder
33、Amount()行为分配给MealOrder;则,MealOrder需要遍历所有的订单项,将订单项金额求和;PayOrder向MealOrder请求获取订单总金额。FoodItem作为订单项信息封装类,同时也是MealOrder的聚合元素类。MealOrder与FoodItem之间的业务聚合关系是一种强耦合,为了保证业务逻辑一致,不能在设计时消除或减弱。但是,将getOrderAmount()行为分配给MealOrder后,Patron就不需要依赖订单项FoodItem的接口了。不仅如此,PayOrder也不需要向Patron请求获取订单总金额了。最终,Patron与MealOrder、Foo
34、dItem、PayOrder之间的耦合度都会降低。两个假设中getOrderAmount()行为的分配方式不同,代码的耦合度也不同;产生这种现象的原因是:Patron并不具备getOrderAmount()行为实现所需要的业务信息,但MealOrder具备。GRASP将具有行为实现所需业务信息(或数据)的类称为信息专家,也简称专家。信息专家模式建议:将目标行为分配给信息专家类。图3.2的案例中,MealOrder(实际上FoodItem也是信息专家,但它作为MealOrder的聚合元素向getOrderAmount()行为提供业务信息。)就是getOrderAmount()行为的信息专家。因此
35、,将getOrderAmount()行为错误地分配给Patron后,仍然要依赖信息专家MealOrder提供业务信息,直接形成新的代码耦合。而将getOrderAmount()行为分配给信息专家MealOrder后,Patron对外的部分依赖就可以消除。按照GRASP信息专家模式的建议,在设计类时应尽可能地将行为职责分配给专家类实现。但,单纯执行该建议,意味着MealOrder类需要承担所有和订单信息相关的职责实现,这又会导致另一个后果浮肿类(Bloated Class,也翻译成“胖类”)。设计模型中的浮肿类一般会违反“单一职责”,导致代码不稳定。针对GRASP信息专家模式,总结如下:模式名称
36、:信息专家(Information Expert,也称专家);应用场景:目标行为职责应该分配给哪个类或对象?解决方案:目标行为职责应分配给信息专家类或对象;使用前提:无;模式优点:降低代码耦合,保持专家类的封装特性;模式缺陷:有可能生成浮肿类。3.2.3 控制器模式(Controller Pattern)对于业务系统程序设计,工程师需要解决的问题有:1)哪些(或哪个)对象负责系统服务的可视化?2)哪些(或哪个)对象负责系统事件的处理?3)哪些(或哪个)对象负责服务行为的实现?4)其他问题。用户使用目标软件提供的服务时,需要与系统进行交互。因此,工程师必须能够在代码中准确地捕捉和表达交互行为。在
37、事件驱动的程序开发中,将外部用户与系统的交互行为定义为事件。当特定事件发生时,系统需要执行一些列动作,完成用户交互请求的响应。那么,在系统内部对象中,应该由哪些(或哪个)对象负责用户输入事件的处理呢?假设:工程师不将对象行为职责分离;即,所有的业务职责均委托同一类对象实现;则,无论是用户接口可视化、输入事件处理,还是数据持久化、数据运算等,都由同一类对象封装上述行为职责。图3.3 职责不分离的系统对象工作时序示意由于图3.3中的系统对象没有实现职责分离,某个(或类型)对象就需要承担多种行为职责的实现,这明显违反了“单一职责”代码设计原则。当任何一类职责行为的需求发生变更时,系统对象的源码都需要
38、进行修改,系统的代码极不稳定!不仅如此,由于系统各种职责行为都封装在同一个(或同一类型)对象中,使得这些代码逻辑混乱,不同逻辑业务源码耦合关联很大,难以实施代码复用,代码可维护性、可读性也大大降低。要解决上述代码弊端,对象职责分离势在必行!图3.4视图分离后的系统对象工作时序示意如何分离对象职责呢?如何分离对象职责呢?大多业务系统的视图代码与业务服务代码的运行环境在不同的物理设备上,如,B/S(Browser/Server,浏览器/服务器)结构软件,其视图代码执行在用户设备的浏览器端,服务代码部署在另一个物理网络节点。因此,容易将视图代码单独从系统源码中分离出来,负责实现用户接口的可视化和用户
39、事件的生成。图3.3的协作时序则会变为图3.4。视图对象与系统其他对象分离后,使得系统代码有了分层结构。视图对象负责用户可视化及事件生成;系统其他对象负责事件处理与业务实现。分离后的视图代码与其他业务代码耦合性减弱,可以实现很好地复用性和可维护性。由于业务系统需要处理复杂的事件逻辑与业务流程,图3.4所示的系统分层仍然无法解决需求变更给系统稳定性带来的影响,需要继续将代码职责分离。而图3.4中的输入事件处理与业务实现相对独立,二者分离能够减少代码耦合。那么,将处理输入事件的行为职责单独封装在一个(或一类)对象中,GRASP模式将该类对象称为控制器。引入控制器对象后,图3.4的协作时序变为图3.
40、5。图3.5中的控制器对象将业务逻辑控制与事件处理独立封装,减少了与业务实现代码的耦合。同时,控制器对象也将视图层与业务实现层隔离,使得二者的变化不会相互影响,降低了视图层与业务实现层之间的耦合。图3.5视图和控制器分离后的系统对象工作时序示意GRASP控制器对象在实现时有两种选择:1)一个控制器对象实现业务系统的所有输入事件处理和业务逻辑分发;这一类控制器对象被称为前端控制器(Front Controller,FC);2)不同的业务用例或GUI(Graphical User Interface,图形化用户接口)页面分别由不同的控制器对象实现输入事件处理和业务逻辑分发;这种类型控制器被称为页面
41、控制器(Page Controller,PC)。前端控制器能够实现事件的集中处理,易于代码复用,但会导致浮肿控制器(Bloated Controller)对象的出现。页面控制器可以避免浮肿控制器对象的出现,但不利于代码复用和事件控制。对GRASP控制器模式总结如下:模式名称:控制器(Controller);应用场景:谁负责接收、处理和分发系统的输入事件?解决方案:系统输入事件处理的职责分给控制器对象(前端控制器或页面控制器);使用前提:代码职责分离;模式优点:降低耦合,提高复用;模式缺陷:使用前端控制器会生成浮肿的控制器。3.3简单工厂模式(Simple Factory Pattern)在3.
42、2.1节中,我们学习了GRASP创建者模式;该模式建议将被引用对象的创建行为分配给引用对象或信息专家对象。这种做法常常导致对象的创建行为分散在不同的客户端,造成代码复用及维护困难。如何避免创建者模式的代码缺陷呢?举个例子,在COS系统的设计类中,客户(Patron)对订单(MealOrder)对象具有增(Create)、删(Delete)、改(Update)、查(Retrieve)操作简称CRUD操作;餐厅管理员(CafeteriaStaff)同样具有对订单对象的CRUD操作;订单对象的CRUD操作行为由设计类MealOrderDAO实现。因此,Patron需要使用MealOrderDAO,C
43、afeteriaStaff也需要使用MealOrderDAO。按照GRASP创建者模式,MealOrderDAO对象的创建行为会分配给Patron和CafeteriaStaff;设计类图如3.6所示。图3.6 Patron和CafeteriaStaff创建MealOrderDAO对象图3.6中的MealOrderDAO对象的创建行为分散在客户端Patron和CafeteriaStaff中。由于客户端使用MealOrderDAO对象的CRUD行为和状态是相同的,即客户端对订单的操作都是面向同一个数据库。因此,客户端创建MealOrderDAO对象的行为createMealOrderDAO()也是
44、一致的。图3.6的设计方案将createMealOrderDAO()行为的实现分散在不同的客户端,将会造成以下代码缺陷:1)createMealOrderDAO()代码的重复编写。因为在不同的客户端均单独实现了一次,图3.6设计方案没有考虑代码复用,造成重复编写相同的代码块。2)createMealOrderDAO()代码维护困难。同样是因为该行为分散在不同客户端造成的。当MealOrderDAO对象的创建方式发生变化时,假如需要增加初始化域,则势必导致所有客户端创建MealOrderDAO对象的行为修改,极大地增加了代码维护成本。解决如上代码缺陷,可以将图3.6中可复用的行为createMe
45、alOrderDAO()单独封装在一个类MealOrderDAOFactory中,并且该类只负责MealOrderDAO对象的创建与初始化;工程师们将MealOrderDAOFactory称为MealOrderDAO对象的工厂类。MealOrderDAOFactory通过createMealOrderDAO()方法向使用MealOrderDAO对象的客户端提供已创建好的产品(Product)对象(这里指MealOrderDAO对象),如图3.7。图3.7 MealOrderDAOFactory创建MealOrderDAO对象图3.7中的客户类Patron、CafeteriaStaff使用Mea
46、lOrderDAO时,向MealOrderDAOFactory请求创建MealOrderDAO对象实例。不仅如此,其他所有使用MealOrderDAO对象的客户类都可以将MealOrderDAO对象创建请求委托给MealOrderDAOFactory。由于MealOrderDAO的创建行为进行了单独封装,在保证了创建行为一致性的同时,也向所有客户端复用该行为。当MealOrderDAO创建行为需求变更时,只需要修改工厂类MealOrderDAOFactory的代码即可,而使用MealOrderDAO对象的客户端不会受到任何影响。图3.7中的设计方案即为简单工厂模式(Simple Factory
47、 Pattern),角色类有:工厂(图3.7中的MealOrderDAOFactory)负责目标产品对象的创建与初始化,并向使用产品对象的客户端提供获取已创建产品对象的接口(图3.7中的createMealOrderDAO()方法);产品(图3.7中的MealOrderDAO)向客户端提供产品服务(图3.7中的CRUD服务);客户端(图3.7中的Patron和CafeteriaStaff)使用产品服务的对象或模块。图3.8 简单工厂模式类对象协作时序对简单工厂模式总结如下:模式名称:简单工厂(Simple Factory);应用场景:如何实现产品对象创建行为的一致或复用?解决方案:将目标产品创建行为分配给工厂类,由工厂类向客户端提供产品对象创建服务;使用前提:无;模式优点:提高代码复用或可维护性;模式缺陷:引入了新的工厂类。总结 通用责任分配软件模式和SOLID面向对象设计原则类似,给工程师提供了不同角度的代码设计方式 在使用在使用GRASP设计模式时,尤其需要注意:设计模式时,尤其需要注意:每个模式都有不同的缺陷每个模式都有不同的缺陷 简单工厂模式同样是解决对象创建行为的分配问题,能够给设计方案带来代码复用或提高代码可维护性的好处,是一种简单且容易使用的设计模式