1、设计模式 Faade, Adapter, Strategy, Bridge, Abstract Factory,王亚沙 北京大学软件研究所,内容提要,一、Faade模式 二、Adapter模式 三、Strategy模式 四、Bridge模式 五、Abstract Factory模式 六、对面向对象相关概念的反思,一、Faade模式,什么是Faade?,法文单词,Faade是建筑的正面的意思 欧洲古典建筑通常在正面重点装饰,一些做得十分华丽 幕墙,Faade解决的问题,先说一个生活中的例子照相 话说春节同学聚会上,我被指派为大家拍张合影 当时的情况是这样的 时间:晚上8点 地点:餐厅包厢内 要求
2、:能把每个同学正常的照出来(如此而已) 本人摄影水平:相当初级,从没有用过这款相机 问题 在夜间的包厢里,光线严重不足,如果想找出一张还算正常的照片,需要 调整焦距 调整光圈 打开闪光灯 没有时间对照说明书学习了 如何在不浪费大家表情的情况下迅速把这些都做好呢? 结果:我比较圆满的完成了拍照任务 我是如何做到的呢?,我的解决方案 将照相机设定到夜间肖像模式 按下快门,一切OK 神奇的夜间肖像模式 由照相机自动的为用户配置好夜间肖像拍摄所需的各种模块 为用户提供了一个极为简便的使用接口 现有主流相机大都提供了多种内建的拍摄模式(运动模式、静物模式、连拍模式),上述问题用图形来表示:,照相机的拍摄
3、控制模式为使用者提供了简单的接口,大大简化了使用者所可能面对的复杂交互关系 软件设计中是否也存在相同的问题,相同的解? Faade模式,抽象一下,看看软件中经常存在的问题,现代的软件系统越来越复杂,设计师常常“分而治之”来处理复杂性,一个软件系统中存在多个子系统, 一个子系统中又存在多个模块,当某客户只关注系统的特定功能时,他往往需要同时与系统内部的诸多模块交互后才能达到目的,问题: 用户使用系统难度增加(必须对系统内部结构有足够了解) 系统逻辑变得复杂(客户端代码中充满了对于各种底层模块的访问) 维护成本增加(客户端代码紧耦合于系统内部模块。一旦系统内部结构变化,就必须修改相应客户端代码),
4、Faade模式,Faade模式的图示,Client2,Client1,Client3,Faade类,子系统1,子系统2,子系统3,子系统4,示例:照相机的例子,UML,Client,Faade,FlashLight,Aperture,Focus,Shutter,代码,代码,Class Facade public:void setNightMode()m_flashlight.open();m_focus.setFocusValue(5.0);m_aperture.setApertureValue(3.0);void action()m_shutter.action(); private:Fla
5、shLight m_flashlight;Focus m_focus;Aperture m_aperture;Shutter m_shutter; ;,Faade模式的关键特征,二、Adapter模式,Adapter模式解决的问题,就如前面的课程中说到的那个例子 总是存在一些类,我们希望复用他,但是他却没有我们期望的接口 这个期望的接口,通常是不能修改的“将一个类的接口转换成客户希望的另一个接口。Adapter模式使原来由于接口不兼容而不能一起工作的类可以一起工作。,Adapter模式,图示: 1. 对象Adapter,图示: 2. 类Adapter,示例:画图的例子,客户类通过Shape接口
6、操作所有的图形,各种Shape的继承关系如下:,现在需要增加一种新的形状Circle 当然可以选择重新编写这个Circle类,并且使其实现Shape接口 因为需要重新开发Circle的display、undisplay、fill函数,工作量比较大 我们发现有一个别人编写的类可以满足我们的功能,但是这个类的接口不符合Shape类的要求,应用Adapter模式将其轻松搞定,注意到Point、Line和Square类并未重新实现这三个接口,代码:,两个问题:,1,应用类Adapter模式如何设计? 2,SetLocation、GetLocation和SetColor方法如果不是虚函数,如何处理?,A
7、dapter模式的关键特征,Adapter模式的关键特征,思考:Faade和Adapter是一个模式吗?,Faade和Adapter模式很相似: 他们都是包装利用对象将已有系统的一部分包装起来,以便于在新系统中使用。 甚至类结构都是相似的:被包装的对象作为一个成员出现在新对象中,当新对象接受到消息时,他将其交付给被包装对象处理。那么我们是否可以认为Faade和Adapter是不同名字的同一个模式呢?,教材中对这两个模式的比较,结论Faade模式简化了接口,而Adapter模式则将一个已有的接口转换成了另一个,进一步的思考模型与模式的区别 先从定义上看: 设计模型是对设计方案的抽象,他抽象的是问
8、题的解 而设计模式是对在某一特定语境下反复出现的一类问题的解决方案 再思考: 设计模式至少包括:context/problem/solution三个部分 设计模式是一个三元组,这三个部分是一个整体,他告诉我们一种解决问题的方法设计模型是鱼,设计模式是渔 授人一鱼不如授人以渔,三、Strategy模式,Strategy模式解决的问题,在实际中我们经常会发现一个问题有多种可选的策略,这些策略在概念上具有相同的功能,但是适用于不同的环境 如果我们简单的使用继承关系来对这些策略上的差异进行建模,可能会导致很多问题: 类的个数迅速失控 代码大量重复、冗余 复用无法进行,用一个例子来说:这是一个电子商务系
9、统,其中有一个控制器对象(TaskController),用于处理销售请求。他能够确认何时有人在请求销售订单,并将请求转给SalesOrder对象处理,SalesOrder对象的功能包括: 允许客户通过GUI填写订单 处理税额的计算 处理订单,打印销售收据,新的需求 要处理多种税额计算的方法。例如要处理美国、加拿大、中国三个国家的税收方法(税法不同计税方法就不同) 应对策略: 复制粘贴修改 使用Switch语句 使用继承前两种方法的问题不言而喻,我们肯定不会采用(你不会菜到用前两种方法吧?)看起来,继承是一种不错的方案,我们看看。,看起来不错,但是当变化继续发生的时候。,现在又出现了新的变化,
10、希望在打印收据时有大单(A4纸)、小单(B5纸)和固定票据三种格式(在一个固定格式的空票据上打印) 为了应对上述需求,我们修改上述设计,问题进一步恶化新的需求:要求能够支持两种不同的GUI输入订单信息,一种是Browser输入,一种是Client输入,现在已经有33218个类了,回顾前面提到的问题 类的个数迅速失控 代码大量重复、冗余 复用无法进行,Strategy模式,图示:,示例:订单处理的例子,代码:,class calcTax public:double taxAmount (long itemSold, double price)=0; ;class canTax: public c
11、alcTax public:double taxAmount (long itemSold, double price)return 0.0; ;class usTax : public calcTax public:double taxAmount (long itemSold, double price)return 1.0; ;,Strategy模式的关键特征,练习,一个贩卖各类书籍的电子商务网站的购物车(Shopping Cart)系统 计算本次购物金额的方法。比如: 对所有的教材类图书实行每本1元的折扣 对连环画类图书提供每本7的促销折扣 对非教材类的计算机图书有3的折扣 对其余的图
12、书没有折扣请同学们分组讨论,并请一个小组派代表到黑板上给出你们讨论的解决方案,四、Bridge模式,Bridge模式解决的问题,蜡笔和毛笔的故事 故事背景 我们需要用蜡笔或者毛笔绘制图画,画里有山水、花鸟、人物等; 我们需要 不同粗细的笔(填充颜色用细笔不方便,勾勒细节用粗笔无法完成) 不同的色彩(彩色图画) 36只蜡笔 购置了粗、中、细3套蜡笔,一套12色,一共36只蜡笔,蜡笔和毛笔的故事(续) 我们也需要36只毛笔吗?,3只毛笔,12种颜料,不同的毛笔使用不同的颜料,毛笔和蜡笔什么区别? 毛笔的方案显然更加优雅 需要处理的对象数目减少(乘法变成了加法) 蜡笔:31236 毛笔:31215
13、复用 12种颜料被3种毛笔复用 导致蜡笔模型与毛笔模型出现差异的原因是什么? 笔和颜色是否能够分离 更抽象些:抽象(abstraction)与实现(implementation)是否能够分离 我们可以理解为:毛笔用颜料作画。颜料是直接使得画纸上某些局部出现色彩的东西,它是底层的实现。画笔是人们画画时操作的对象,它并不能直接在画纸上留下痕迹,它必须调用颜料的功能(染色功能)才能绘画;但是client(画家)确不必知道颜料是如何染色的这样的细节,他只需要关注如何操作画笔。画笔是高层的抽象。,Bridge模式,意图: 将抽象与其实现解耦,使它们可以独立变化 图示:,示例(log工具): 现在我们要开
14、发一个通用的日志记录工具 支持数据库记录DatabaseLog和文本文件记录FileLog两种方式 同时它既可以运行在.NET平台,也可以运行在Java平台上 如果不考虑Bridge模式,传统的OO设计将得到下面的类结构,上述设计的问题: 违背了类的单一职责原则 即一个类只有一个引起它变化的原因,而这里引起Log类变化的原因却有两个,即日志记录方式的变化和日志记录平台的变化 重复代码会很多 不同的日志记录方式在不同的平台上也会有一部分的代码是相同的 类的结构过于复杂,继承关系太多 难于维护 扩展性太差 上面我们分析的变化只是沿着某一个方向,如果变化沿着日志记录方式和不同的运行平台两个方向变化,
15、我们会看到这个类的结构会迅速的变庞大,两种可能出现的修改:,增加XML方式的Log,在Boland系统上实现Log,应用Bridge模式 UML,代码:,class ImpLog public: void WriteInDataBase(string msg)=0; void WriteInFile(string msg)=0; class NImpLog : public ImpLog public:void WriteInDataBase(string msg) / .NET平台 void WriteInFile(string msg) / .NET平台 ;class JImpLog :
16、public ImpLog public: void WriteInDataBase(string msg) / .java平台 void WriteInFile(string msg) / .java平台 ;,class Log protected:ImpLog,Bridge模式的关键特征,练习,设计一组容器: 这些容器可能是数组,也可能是链表 容器包括堆栈、队列请同学们分组讨论,并请一个小组派代表到黑板上给出你们讨论的解决方案,Strategy模式与Bridge模式的异同,相似之处 都存在一个对象使用聚合的方式引用另一个对象的抽象接口的情况,而且该抽象接口的实现可以有多种并且可以替换 两者
17、在表象上都是调用者与被调用者之间的解耦,以及抽象接口与实现的分离都是对变化的封装,有人说:Strategy模式是一种缩减版的Bridge模式,不同之处 形式上的区别在Bridge模式中不仅Implementor具有变化(ConcreateImplementior),而且Abstraction也可以发生变化(RefinedAbstraction),而且两者的变化是完全独立的,RefinedAbstraction与ConcreateImplementior之间松散耦合,它们仅仅通过Abstraction与Implementor之间的关系联系起来 而在Strategy模式中,并不考虑Context的
18、变化,只有算法的可替代性,不同之处(续) 2. 语意上的区别 Bridge模式强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作 Strategy模式强调Strategy抽象接口的提供的是一种算法,而Context则简单调用这些算法完成其操作3. 粒度上的区别 Bridge模式中不仅定义Implementor的接口而且定义Abstraction的接口,Abstraction的接口不仅仅是为了与Implementor通信而存在的,这也反映了结构型模式的特点:通过继承、聚合的方式组合类和对象以形成更大的结构 在Strategy模式中,Star
19、tegy和Context的接口都是两者之间的协作接口,并不涉及到其它的功能接口,总结 相对Strategy模式,Bridge模式要表达的内容要更多,结构也更加复杂。 Bridge模式表达的主要意义其实是接口隔离的原则,即把本质上并不内聚的两种体系区别开来,使得它们可以松散的组合,而Strategy在解耦上还仅仅是某一个算法的层次,没有到体系这一层次。 从结构图中可以看到,策略的结构是包容在Bridge结构中的,Bridge中必然存在着Strategy模式,Abstraction与Implementor之间就可以认为是Strategy模式,但是Bridge模式一般Implementor将提供一系
20、列的成体系的操作,而且Implementor是具有状态和数据的静态结构。而且Bridge模式Abstraction也可以独立变化。,五、Abstract Factory模式,Abstract Factory模式解决的问题,关注点分离 将对象的创建与对象的使用分离 看一个例子(图形显示的例子): 设计一个系统来显示和打印数据库中读出的图形,并满足: 根据当前所使用硬件的配置来选择驱动速度快的机器选择高分辨率的显示、打印驱动,速度慢的选择低分辨率的驱动驱动的选择依据如下表所示,一般的解决方案:,代码:,class ApControl DisplayDriver* dp;PrintDriver* p
21、p; public: ApControl(RESOLUTION RES)switch (RES)case LOW: dp=new LRDD;pp=new LRPD;case HIGH: dp=new HRDD;pp=new HRPD;void doDraw()dp-line();dp-point();void doPrint() dp-line();dp-point(); ;,对象的创建,对象的使用,对象的创建与使用这两个关注点没有分离,如何分离? 自然我们会想到用另外一个对象来专门负责对象的生成产生了工厂类的概念,为什么要分离?,代码:,class ApControl DisplayDriv
22、er* dp;PrintDriver* pp; public: ApControl(RESOLUTION RES)switch (RES)case LOW: dp=CreateHRD:CreateDisplayDriver();pp=CreateHRD:CreatePrintDriver();case HIGH: dp=CreateLRD:CreateDisplayDriver();pp=CreateLRD:CreatePrintDriver();void doDraw()dp-line();dp-point();void doPrint() dp-line();dp-point(); ;,对
23、象的创建,对象的使用,对象创建与对象使用的关注点已经分离,但是Client还是依赖于使用的具体类型: CreateHRD CreateLRD 被应编码到了client中,也就是说Client依赖于具体的工厂类,信息隐藏 将实现的信息隐藏起来,使得用户不必关注实现的细节隐藏创建具体类的信息,使其对Client不可见,Abstract Factory模式,意图: 为特定类型的应用创建一个对象族 图示:,示例2 (动物世界): 在一个电脑游戏中,存在着美洲和非洲两块大陆。美洲大陆上有食肉动物(美洲虎)和食草动物(美洲羊);非洲大陆上有食肉动物(非洲虎)和食草动物(非洲羊)。 电脑游戏的应用逻辑中需要
24、实现这样的场景 根据当前主人公角色所处大陆(美洲或非洲)来初始化当前游戏中的食肉动物和食草动物 食肉动物开始追捕食草动物 一个最直观的OO解决方案可能是这样的:,此时,游戏的示意性代码可能是这样的: 食肉动物* a; 食草动物* b;if(mainRole.currentPosition = “美洲”)a = new 美洲虎;b = new 美洲羊; else if(mainRole.currentPosition = “非洲”)a = new 非洲虎;b = new 非洲羊; a-捕猎(b); ,上述设计的问题: 游戏系统依赖于食肉动物和食草动物实例如何被创建、组合和表达的细节(出现了“ne
25、w 美洲虎”这样的代码),这会使得 当游戏设定发生改变,美洲的食肉动物由美洲虎变为美洲狮时,游戏代码需要改动(想象一下当百万行规模的游戏中存在着100处”new 美洲虎”这样的代码,且分布于30个不同文件中) 当游戏设定发生改变,在创建动物后需要再调用一下系统提供的init函数将其初始化后才能使用,则游戏代码需要改动(在所有的new后面加init函数的调用,orz) 无法从设计上对产品族进行约束,保证游戏在任意时刻只能使用美洲大陆的两种动物或非洲大陆的两种动物 游戏开发者完全可以写出这样的代码:a = new 美洲虎;b = new 非洲羊;a-捕猎(b);从而引发“关公战秦琼”这样的千古悬案
26、,应用Abstract Factory模式 UML,代码,class AbstractFactory public: Herbivore CreateHerbivore()=0;/创建食草动物 Carnivore CreateCarnivore()=0;/创建食肉动物 ; class AmericanFactory : AbstractFactory public:Herbivore CreateHerbivore() return new AmericanTiger(); Carnivore CreateCarnivore() return new AmericanSheep(); ;/美洲
27、工厂类class AfricanFactory : AbstractFactory public:Herbivore CreateHerbivore() return new AfricanTiger(); Carnivore CreateCarnivore() return new AfricanSheep(); ; /非洲工厂类,class Herbivore /食草动物的抽象类 ; class Carnivore /食肉动物的抽象类 public:void chase(Herbivore /非洲虎类,Abstract Factory模式的关键特征,练习,创建在不同操作系统的视窗环境下都能
28、够运行的系统 两种操作系统:Windows和Unix Windows操作系统下,使用具有Windows风格的视窗构件(这里设为WindowsButton对象和WindowsText对象) Unix操作系统下,使用具有Unix风格的视窗构件UnixButton对象和UnixText对象 如何进行设计,使得 当需要增加对新操作系统的支持时(如系统还需要支持Solaris),现有代码不必修改(符合“开-闭原则”) 在系统的设计中约束用户使用的各种构件一定属于同一操作系统(不会出现将WindowsButton和UnixText一起使用这种情况)请同学们分组讨论,并请一个小组派代表到黑板上给出你们讨论的
29、解决方案,从产品族的角度来看,UML示意,六、对面向对象相关概念 的反思,不是概念的新解,而是回归,下面将对OO中一些常用概念的“通常”的理解进行反思,并给出不同的解释 这些不同的解释并非对OO概念的创新性的新的理解,正好相反,而是这些概念的“王者归来” 就像我们的自然语言一样,一些原本深刻的见解、概念,在传播的过程中总是趋向于表面化,例如: 真相 自然 面向对象方法中原本深刻的见解也难逃传播过程中的失真,这里我们通过体会前面5个模式中的实践,来还原一些我们自以为理解的概念的本来面目 约定: 通常的理解:在传播中被表面化的理解 正解:OO方法中原本的理解,对象,通常的理解: 对象是“数据函数”
30、“智能数据” 这种理解直接进入了编码的细节中,而面向对象的开发范型是一种“返璞归真”的方法用我们日常的思考的方式来思考软件开发 在我们面临一个问题的时候,我们通常先理解全局的概念,然后在逐步进入细节 所以,对象只有到了最后实现阶段才需要真正阐明其构成的数据和函数,而在早期(不仅仅是需求规约和设计阶段,也包括实现的早期)我们都不必,也不应该过早的陷入细节 正解: 对象是具有责任的实体 我们在开发中识别的对象可以仅仅是一个概念,仅仅需要指出他的责任,而责任通常表现为他能接收什么消息,具有什么样的功能,最终表现为其功能接口 这样的方式,便于我们从接口开始构思程序,便于我们建立倒置的依赖,便于程序具有灵活的结构,封装,通常的理解: 将数据(属性)和对数据的操作(方法)放到一个程序单元(类)中,从而使得概念上相关的数据和操作在编程语言上也相关 通过private/protected/public这些访问控制符对数据和方法的访问性进行控制,使得一些信息对外部不可见 正解: 信息隐藏隐藏细节 对对象内部细节的隐藏隐藏private的数据和函数 对抽象概念的具体实现的隐藏通过接口隐藏具体的派生类对象 对设计/实现细节的隐藏例如,Strategy模式中对实现的具体算法的隐藏,