软件构造-11 面向可复用性和可维护性的设计模式

本章内容十分重要,也比较难,主要涉及到了创建型模式(工厂方法模式),结构型模式(适配器模式/装饰器模式),行为型模式(策略模式/模板模式/迭代器模式/访问者模式)

 

设计模式:针对软件设计中给定上下文中经常发生的问题的一般可重用解决方案。

面向对象设计模式(OO design pattern):除了类本身,设计模式更强调多个类/对象之间的关系和交互过程---比接口/类复用的粒度更大

 

设计模式分类:创建型模式(关心对象创建过程)

结构型模式(处理类或对象的组合)

行为类模式(描述类或对象交互和分配责任的方式)

 

(本章很多概念比较抽象,需要结合实例理解,故本章截图较多。文内带下划线的内容为非官方定义的个人理解,可能存在不准确之处,仅供参考)

1.创建模式

工厂方法模式:

工厂方法(虚拟构造器):

  • 满足OCP(开闭原则):对扩展开放,对修改已有代码封闭
  • 当client不知道/不确定要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
  • 定义一个用于创建对象的接口(不是原来的接口),让该接口的子类型来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。相当于是用户只需要知道产生产品的工厂的名称即可得到产品,不需要得知生产产品的细节。
  • 如下图例子,接口Product(抽象产品)有两种实现类(具体产品),之后再写一个用于创建实现Product的类的对象的接口Creator(抽象工厂),里面有工厂方法factoryMethod(这个方法返回类型是Product,方法是没有实现的,在实现类中才实现),在接口Creator的两个实现类(具体工厂)中实现工厂方法,返回一个new得到的接口Product实现类,用户可以自行选择调用,在实际使用的时候,接口对象=new 实现接口Creator的实现类中的创建方法(只知道工厂名,用工厂创建产品)
  •  Eg1(上面解释的):

          

         (不用工厂方法的话可以直接new实现接口的类的对象,需要new哪种就new哪种,但是这种方法不推荐)

          Eg2:这里的TraceFactory相当于上面的Creator,是实现接口。有新的产品加入时,直接增加新的工厂类或修改已有工厂类。

           

         Eg3:也可以根据类型决定创建的产品,但这种方式不推荐

         

  • 静态工厂方法:工厂方法用static修饰,不必在每次调用的时候都创建新的工厂对象,可以返回原返回类型的任意子类型

           

 

2.结构化模式

(1)适配器模式

  • 将某个类/接口转换为client期望的其他形式(原设计参数与用户期望的输入参数不相同),解决类之间接口不兼容的问题。通过增加接口,将已存在的子类封装起来,隐藏具体子类
  • 两种适配器设计模式(继承/委托)

         

  • 举例:LegacyRectagle的函数display()期望输入是“x,y,w,h”(左上角坐标+宽+高),但用户希望传送“x1,y1,x2,y2”(左上角坐标+右上角坐标),这里通过委派(Rectangle作为中介,实际上执行的是用户看不到的LegacyRectangle)来完成适配器模式:

       

  • 代码:当用户输入与写好的方法不匹配时,编写适配器,在adapter类这个适配器接口的具体实现类中完成适配(参数的形式改变),然后再委派LegacyRectangle(原本写好的方法)完成具体实现。(客户端调用可以直接接口对象=new 实现接口的类)

          

  • Eg图:

         

 

(2)装饰器模式

  • 假设你需要一个数据结构的多种扩展,一般是选择用子类实现不同特性。但是如果在需要特性的任意组合还使用编写子类的方式,在每个子类中重复编写一些特性容易造成大量的代码重复。因此选择使用装饰器模式
  • 问题:为对象增加不同侧面的特性
  • 解决:对每个特性构造子类,通过委派机制增加到对象上,以递归的方式实现。
  • (装饰器同时使用子类型和委派机制)
  • 下图中,Component为接口,定义装饰物执行的公共操作,ConcreteComponent作为起始对象(实现接口基础功能的类),在其基础上增加装饰,将通用方法放入此对象中。Decorator抽象类是所有装饰器的基类(也是实现Component接口的类),里面包含的成员变量component指向被装饰的对象。ConcreteDecorator类是真正添加特征的装饰器类,可以有很多个(比如下图中的xxA,xxB)

       

  • (上图Decorator为装饰器基类的构造器,传入的参数为被装饰的对象)
  • 代码实例:UML图:

     

  • 下图中,Stack作为接口,ArrayStack类为接口的实现类,实现了最基本的功能。

        

  • 下面是用于装饰的基础类(也实现了Stack接口,是一个抽象类),构造器接受等待被装饰的接口对象作为参数:

         

  • 由下图,真正的装饰类是装饰基类的子类,基础功能通过基类的方法实现(用super()调用父类方法,之后直接添加新的特性),它的构造器接受的也是待装饰的Stack接口对象。

       

  • 实现基础功能(接口对象=new 实现接口的类):

       

  • 装饰一个undo功能(UndoStack的构造参数是待装饰接口对象(实现接口的类的对象))

           

  • 装饰secure,synchronized,undo功能:递归使用

            

  • 代码实例2:冰淇淋制作,装饰器为在冰淇凌顶部加上各种辅料。(UML图如下):

        

            

  • (由上图,可以看到装饰器基类为抽象类(里面有没有实现的抽象方法),变量input为protected final类型(子孙类内部可见,不可变),指代这个被装饰的冰淇凌。具体的装饰方法为抽象方法,留给子类实现)

           

  • (由上图,在具体装饰类中,构造器直接继承父类的,实现了装饰方法AddTopping,同时在装饰方法中,先调用父类中的变量input(待装饰冰淇凌)的AddTopping方法,这样实际嵌套调用时就会先把之前已经有的装饰打印出来。之后再进行这一步的装饰)
  • 下面为客户端代码与运行结果:

         

  • 装饰器和继承的区别:1.装饰器在运行时组合各种特征,继承在编译时进行这部分内容。

                                              2.装饰器包含多个合作的对象,继承产生单个的,类型明确的对象。

                                              3.装饰器可以混合匹配多种装饰,而多重继承在概念上是难以实现的

  • Java.uitl.colledctions中的装饰器:

         1.将可变list转换为不可变的list:使用时(collections.unmodifiableList(lst)即可)

       

         2.线程安全List:(使用方法和上面类似)

       

3. 行为型模式

(1)策略模式

  • 有多种方法实现同一个任务,但需要client根据需要动态切换算法。(例如,需要实现排序算法,现在有多种实现方式冒泡排序,快速排序,归并排序等,要求能够根据用户的要求在它们之中进行选择和切换,而不是规定只能用哪种方法)
  • 解决方案:为不同的实现算法构造抽象接口,利用delegation,运行时动态传入client倾向的算法类实例
  • UML图:可以看出策略要求被包含于上下文中,策略是一个接口,有多种接口实现类(具体策略)。

        

  • 代码实例:用购物车购物时需要选择支付策略,有两种选择,分别是信用卡支付和paypal支付(一种第三方在线支付手段,类似支付宝),UML图如下:

         

  • 支付策略接口代码与信用卡支付策略代码如下,可以看出,支付策略接口中有一个名为pay的待实现方法,传入的参数为支付金额。在信用卡支付策略中,在实现接口时增加了构造器方法,需要传入的参数有姓名,卡号,安全码cvv,支付有效期。重写pay方法即为加入用信用卡支付的说明。

           

  • Paypal支付代码和购物车代码如下:可以看到paypal支付策略的代码实现与信用卡大致相同,只是需要传入的参数改变为email和密码。
  • 比较重要的是在购物车(上下文角色类)中如何实现策略的选择。在下图代码中可以看出在购物车中的pay方法里,传入的参数是购物策略接口对象,并调用这个对象的pay方法。(这里使用delegation,具体购物策略方法对用户是不可见的)。
  • 当我们确定要使用哪种购物策略时,传入这个购物策略对象作为参数即可。比如要paypal支付,把购物车里的PyamentStrategy修改为PaypalStrategy即可,(在context类中根据传入的输出动态选择策略可以看作是结合了简单工厂模式,由context角色类作为工厂,决定生产什么产品))

            

(2)模板模式

  • 做事情的步骤一样,但具体方法不同时使用模板模式。
  • 解决方案:共性的步骤在抽象类内公共实现,差异化的步骤在各个子类中实现。模板方法定义算法步骤,允许子类为一个或多个步骤提供实现。
  • 实现方式:使用继承和重写实现
  • UML图:

       

  • 举例:现在需要造车,流程是造骨架,装引擎,装门,有两个子类(造的两种车),UML图和抽象类代码如下:

       

         

  • (可以看到抽象类中有代表各个流程的抽象方法,都还没有实现,将在子类中进行实现。还有一个实现了的方法,在里面有整个流程的实现,这个方法将直接被子类继承,不必再重新实现)
  • 下面的代码是子类型中对这些方法的具体实现(相同的流程但是实现不一样,比如说都是装车门,生产A品牌的车装A车门,生产B品牌装B车门)
  • 子类代码:

        

  • (最后客户端代码实现时直接new子类对象,调用子类继承的BuildCar方法即可)

(3)迭代器模式

  • 客户端希望对放入容器/集合类的一组ADT对象进行遍历访问,而无需关心容器的具体类型,对象无论被放在什么类型中,都提供相同的遍历方式。
  • 组成部分:

          1>抽象迭代器:定义遍历协议的类(实现本接口的类要实现三种方法)

          2>具体迭代器:每个聚合类的子类(针对某个具体类型编写的迭代器实现类)

          3>聚合:定义创建迭代器对象的方法的接口(形式类似下面的Iterable接口)

          4>具体聚合:实现创建相应迭代器的接口。(要返回实现迭代器接口的具体实现类)

         

  • Iterable接口:实现该接口的容器对象是可迭代遍历的(里面需要有一个迭代器方法,会返回一个迭代器):   

        

  • Iterator接口:迭代器,实现接口的类必须要实现接口的三种方法:

        

  • 代码样例:可迭代的类(具体聚合)继承Iterable接口(抽象聚合),里面有返回迭代器的方法(返回的是实现迭代器接口的具体类型的实现类),在迭代器实现类(具体迭代器)中,要实现Iterator接口(抽象迭代器)的三种方法。

        

 

(4)访问者模式

  • 对特定类型object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类,本质上就是将数据和作用于数据上的某种特定操作分离。
  • 将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。它将对数据的操作与数据结构进行分离,是行为类模式中最复杂的一种模式。
  • 目的:为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码 可以在不改变ADT本身的情况下在需要时通过delegation接入ADT。
  • 包含元素:
  1.  抽象访问者(Visitor):定义访问具体元素的接口,为每个元素类对应一个访问操作visit(),该操作中的参数类型标识被访问的具体元素。
  2.  具体访问者(ConcreteVisitor):实现抽象访问者角色中声明的各个访问操作。确定访问者访问一个元素是该做什么。
  3. 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  4. 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  5. 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
  • UML图:

        

  • 代码实现:
  • 下图中为抽象元素接口(包含接收visitor为参数的accept方法)
  • 以及实现抽象元素接口的具体元素,在具体元素中,实现accept方法,主要是通过委派外部传入的visitor调用它的visit方法来实现。

         

  • 下图中为抽象访问者的接口,里面的方法为接受不同类作为被访问对象的visit方法。(接受的类就是上面实现的具体元素),以及具体访问者,里面实现visit方法。

        

  • 具体使用代码:在main函数中创建了ItemElements元素集合,传入Book等类使其成为可被访问的元素。在calculatePrice方法中计算所有对象的价格总和,在方法中首先创建访问购物车的visitor接口实现类对象,在这个对象中,由上图,它定义了对各种元素的访问方法。之后只需要遍历元素集合,对其中的每个元素都调用accept方法(accept方法接收visitor接口实现类对象作为参数),accept方法实际上就是委派外部传入的visitor调用它的visit方法来实现的。
  • 这样,元素和操作元素的方法就实现了分离,程序员可以直接在visitor接口实现类中扩展或改变对visitor的操作,不会影响到ADT。

         

  • 迭代器模式和访问者模式比较:
  1. 迭代器:以遍历的方式访问集合数据而无需暴露其内部表 示,将“遍历”这项功能delegate到外部的iterator对象。
  2. 访问者:在特定ADT上执行某种特定操作,但该操作不 在ADT内部实现,而是delegate到独立的visitor对象,客户端可灵活 扩展/改变visitor的操作算法,而不影响ADT
  • 策略模式和访问者模式比较:
  1. 二者都是通过delegation建立两个对象的动态联系。
  2. 但是Visitor强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client 通过它设定visitor操作并在外部调用。
  3. 而Strategy则强调是对ADT内部某些要实现的功能的相应算法的灵活替换。 这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。

4. 设计模式的异同点

1. 共性样式1:

  •  只使用“继承”,不使用delegation。
  •  核心思路:OCP/DIP。
  •  依赖反转,客户端只依赖“抽象”,不能依赖于“具体”,发生变化时最好是“扩展”而不是“修改”
  •  Adapter:

        

  •  Template:

        

2.共性样式2:

  • 两棵继承树,两个层次的delegation
  • Strategy

       

  • Iterator

       

  • Factory method

        

  • Visitor

       

posted @ 2022-06-07 21:59  redTide  阅读(84)  评论(0)    收藏  举报