软件构造第四章总结—Part Ⅱ
前言:第四章学习笔记: Design Patterns for Reuse
文章目录
Part Ⅱ Design Patterns for Reuse
1.结构型设计模式
①Adapter:适配器模式
解决的问题:类B已存在,类A想对其进行复用,但二者交互的接口并不兼容
解决方法:增加一个接口进行实现类的封装,将A所需要的功能全部封装在里面,创建一个实现子类Adapter,在Adapter中作各种转换,转换后对类B进行复用,如继承或委托。
下面的图很好地演示了这个过程:
LegacyRectangle中的方法是我们想要复用的,但client调用时的参数与其接收参数不一致,于是新建接口Shape,提供client需要的功能,client只需对接口进行编程,然后建立适配器:Rectangle作为接口的实现类完成功能,在其内部可以对参数进行各种转换,然后利用委托将转换后的参数传给LegacyRectangle的对象完成复用。而client由于是对接口编程,并不知道具体实现如何。
优点:
<1>客户端通过适配器可以透明地调用目标接口。
<2>复用了现存的类,不需要修改原有代码而重用现有的适配者类。
<3>将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
缺点:对类适配器来说,更换适配器的实现过程比较复杂
②Decorator:装饰者模式
解决的问题:Part Ⅰ 中所描述的多特性时继承带来的组合爆炸问题
解决方法:使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能
细节设计:
<1>装饰者和被装饰对象有相同的超类型。
<2>你可以用一个或多个装饰者包装一个对象。
<3>既然装饰者和被装饰对象有相同的超类型,所以在任何需要原始对象(被包装的)的场合,可以用装饰过的对象代替它。
<4>装饰者可以在所委托被装饰者的行为之前与 / 或之后,加上自己的行为,以达到特定的目的。
<5>对象可以在任何时候被装饰,所以可以在运行时动态地、不限量地用装饰者来装饰对象。
<6>采用递归方式实现
可以用一个结构图来简单表示一下:
<1>Component是一个抽象接口,将所有的公共操作都在里面定义出来
<2>ConcreteComponent:起始对象,实现Component接口,将操作中最基础的部分实现
<3>Decorator:抽象类,是所有装饰类的父类,里面包含Component成员变量,对于公共操作部分利用委托部分加以实现,里面可以定义各种特性功能,实现装饰的目的
<4>Decorator的各个子类:继承Decorator,对<3>中所述的特性功能予以实现,并可以定义新的操作。此外,对于功能中公共方法也可以进行装饰,比如为Component中公共方法增添一个print操作,那么可以在通过委托完成基础操作之后,增添新的操作,如下图例子所示:
先通过继承父类方法(父类中的实现是靠委托)实现公共操作,然后在后面增添特有操作即可。
无论哪一个方法的实现,公共部分均可以直接通过继承到父类+父类中委托Component成员变量完成复用。
优点:扩展功能时十分灵活,可以创造出特性的各种组合
缺点:由于装饰带来了很多子类,有可能会使程序很复杂
③Facade:外观模式
解决的问题:对于同一个/相似的功能,复杂系统内提供了多个接口,client想要实现一个功能需要自己选择要使用哪一个具体接口中的功能进行调用
解决方法:提供一个统一接口来提供功能与client交互,在该接口内部通过client传入参数的不同来自己识别、调用对应接口的功能
以下面图中示例作说明:
client这里是要通过数据库产生报告,而数据库有两种( MYSQL,ORACLE),产生报告的形式也有两种( HTML,PDF).如果我们什么都不做,那么用户的行为应该是选择数据库创建对象,然后去查找其中对应的方法并进行选择,这个选择与查找的过程对于client来说就并不是很友好。
而如果我们使用Facade模式,将其封装在上图所示的类中,用户只需要根据自己需要的类型输入参数即可,非常易于理解。而我们需要在其内部进行具体的区分。
优点:
<1>降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
<2>对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:增加新的子系统可能需要修改外观类或客户端的源代码,比如需要增加新的switch-case分支,违背了“开闭原则”
2.行为类模式
①Strategy:策略模式
解决的问题:有多种不同的算法来实现同一个任务,程序具体实现选择的算法是固定的,但client根据需要随时有可能需要切换算法.
解决方法:
为不同的实现算法构造抽象接口,利用delegation,运行时client根据需要动态传入倾向的算法类实例
以下面的例图简要说明:
Context是我们需要实现的程序代码,其中有方法需要实现动态切换算法。
<1>我们定义一个抽象接口Strategy,并在Context中维护一个Strategy类型的成员变量。同时维护一个接收函数可对Strategy进行赋值
<2>在Strategy中同样定义该方法,将Context中需要动态切换算法的方法委托给Strategy类型的成员变量。
<3>不同的算法策略作为Strategy的各个子类进行实现
<4>client需要执行该方法时,选定算法实现类,作为参数传进Context的接收函数,于是Context在委托时就按照对应选择来自动执行
优点:
<1>策略模式可以提供相同行为的不同实现,client可以根据不同时间或空间要求选择不同的策略
<2>策略模式可以在不修改原代码的情况下,灵活增加新算法
缺点:
<1>client必须理解所有策略算法的区别,以便适时选择恰当的算法类。
<2>可能会带来很多Strategy的子类
② Template Method:模板模式
解决的问题:程序实现固定,而client对于程序要求按固定顺序执行,但顺序中的某一(某些)步骤可以采用不同的方法
解决方法:共性的步骤在抽象类内公共实现,差 异化的步骤在各个子类中实现 ;同时在抽象类中定义程序通用的执行逻辑
如图所示:
client对于建造车(CarBuilder)的方法指定了建造顺序:框架(BuildSkeleton)→引擎(InstallEngine)→车门(InstallDoor)
但是每个步骤的实现方法又要求有多种。
于是我们将每一个需要多种实现方法的步骤都定义为一个抽象函数,并在抽象类中先指定每个函数的执行顺序(BuildCar),这时无论各个步骤如何实现,执行的顺序都是固定的。
然后,设计子类,对于每一个步骤的抽象函数,实现不同的方法,满足要求
优点:
它封装了不变部分,扩展可变部分。把认为是不变部分的算法封装到父类中实现,实现了复用,而把可变部分算法由子类继承实现,便于子类继续扩展。同时算法的骨架是确定了的,封装在父类中
缺点:对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大、复杂。
③ Iterator:迭代器模式
解决的问题:一组实现的ADT无法进行遍历或遍历方式不统一
解决方法:让自己的集合类实现Iterable接口,并实现自己的独特Iterator迭代器(hasNext, next, remove),允许客户端利用这个迭代器进行显式或隐式的迭代遍历
该模式的具体结构如上所示
<1>在Iterator接口中定义了统一的遍历方法
<2>在Aggregate接口(一般为Iterable接口)中定义方法,提供返回迭代器、加入新对象、删除新对象等功能接口
<3>自己设计的ADT实现Aggregate接口(如果ADT为接口时是继承),完成需要提供的功能
<4>需要完成的功能中有提供迭代器,这种迭代器需要实现Iterator接口,设计自己的遍历顺序与方法,完成Iterator中定义的方法即可
一个简单的实例如下:
Pair类实现了Iterable接口,需要返回一个迭代器
返回的迭代器需要定义适于ADT特有的遍历顺序,因此定义了一个private类实现自己定义的遍历顺序
返回的迭代器需要在实现Iterator规定的统一遍历方式中实现特有遍历顺序:三个方法:next、hasNext、remove
优点:
<1>遍历任务交由迭代器完成,这简化了存储一组ADT的聚合类.
<2>它支持以不同方式遍历一个聚合,可以自定义迭代器的子类以支持新的遍历。
<3>增加新的聚合类和迭代器类都很方便,无须修改原有代码
缺点:
对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐