设计模式系列文章导航

  1. 设计模式概述
  2. 面向对象设计原则
  3. 设计模式 - 创建型模式
  4. 设计模式 - 结构型模式 📍当前位置
  5. 设计模式 - 行为型模式

结构型模式

  • 结构型模式关注如何将现有类或对象组织在一起形成更加强大的结构。
  • 不同的结构型模式从不同的角度组合类或对象,它们在尽可能满足各种面向对象设计原则的同时为类或对象的组合提供一系列巧妙的解决方案。

适配器模式(Adapter)

定义

将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。

模式结构

结构图

  • 类适配器

    类适配器

  • 对象适配器

    对象适配器

实现代码

  • 目标抽象类(Target)

    目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是具体类。在类适配器中,由于Java语言不支持多重继承,它只能是接口。

  • 适配器类(Adapter)

    它可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器Adapter是适配器模式的核心,在类适配器中,它通过实现Target接口并继承Adaptee类来使二者产生联系,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。

    • 类适配器

      1
      2
      3
      4
      5
      public class Adapter extends Adaptee implements Target {
      public void request() {
      super.specificRequest();
      }
      }
    • 对象适配器

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public class Adapter extends Target {
      //维持一个对适配者对象的引用
      private Adaptee adaptee;

      public Adapter(Adaptee adaptee) {
      this.adaptee=adaptee;
      }

      public void request() {
      //转发调用
      adaptee.specificRequest();
      }
      }
  • 适配者类(Adaptee)

    适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下甚至没有适配者类的源代码。

应用实例

某公司欲开发一款儿童玩具汽车,为了更好地吸引小朋友的注意力,该玩具汽车在移动过程中伴随着灯光闪烁和声音提示。在该公司以往的产品中已经实现了控制灯光闪烁(例如警灯闪烁)和声音提示(例如警笛音效)的程序,为了重用先前的代码并且使得汽车控制软件具有更好的灵活性和扩展性,现使用适配器模式设计该玩具汽车控制软件。

image-20231229133559453

  • 抽象目标:CarController
  • 适配者:PoliceSoundPoliceLamp
  • 适配器:PoliceCarAdapter

特点及使用环境

优点

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构
  2. 增加了类的透明性和复用性,提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
  3. 灵活性和扩展性非常好
  4. 类适配器模式:置换一些适配者的方法很方便
  5. 对象适配器模式:可以把多个不同的适配者适配到同一个目标,还可以适配一个适配者的子类

缺点

  1. 类适配器模式:
    1. 一次最多只能适配一个适配者类,不能同时适配多个适配者
    2. 适配者类不能为最终类
    3. 目标抽象类只能为接口,不能为类
  2. 对象适配器模式:在适配器中置换适配者类的某些方法比较麻烦

适用环境

  1. 系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码
  2. 创建一个可以重复使用的类,用于和一些彼此之间没有太大关联的类,包括一些可能在将来引进的类一起工作

桥接模式☒

组合模式★(Composite)

定义

组合多个对象形成树形结构以表示具有部分-整体关系的层次结构。组合模式让客户端可以统一对待单个对象和组合对象。

模式结构

结构图

image-20231229125805145

实现代码

  • 抽象构件(Component)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public abstract class Component {
    //增加成员
    public abstract void add(Component c);
    //删除成员
    public abstract void remove(Component c);
    //获取成员
    public abstract Component getChild(int i);
    // 业务方法
    public abstract void operation();
    }
  • 叶子构件(Leaf)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Leaf extends Component {
    public void add(Component c) {
    //异常处理或错误提示
    }
    public void remove(Component c) {
    //异常处理或错误提示
    }
    public Component getChild( int i) {
    //异常处理或错误提示
    return null;
    }
    public void operation() {
    //叶子构件具体业务方法的实现
    }
    }
  • 容器构件(Composite)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import java.util.* ;

    public class Composite extends Component {
    private ArrayList <Component> list = new ArrayList <Component>();
    public void add(Component c) {
    list.add(c);
    }
    public void remove(Component c) {
    list.remove(c);
    }
    public Component getChild(int i) {
    return (Component)list.get(i);
    }
    public void operation() {
    //容器构件具体业务方法的实现,将递归调用成员构件的业务方法
    for(Object obj:list) {
    ((Component)obj).operation();
    }
    }
    }

应用实例

某软件公司欲开发一个杀毒(Antivirus)软件,该软件既可以对某个文件夹(Folder)杀毒,也可以对某个指定的文件(File)进行杀毒。该杀毒软件还可以根据各类文件的特点,为不同类型的文件提供不同的杀毒方式,例如图像文件(ImageFile)和文本文件(TextFile)的杀毒方式就有所差异。现使用组合模式来设计该杀毒软件的整体框架。

image-20231229130502078

  • 抽象构件:AbstractFile
  • 容器构件:Folder
  • 叶子构件:ImageFileTextFileVideoFile

模式变化

透明组合模式

  • 抽象构件Component中声明了所有用于管理成员对象的方法,包括add()、remove(),以及getChild()等方法

  • 在客户端看来,叶子对象与容器对象所提供的方法是一致的,客户端可以一致地对待所有的对象

  • 缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的

    image-20231229130843722

安全组合模式

  • 抽象构件Component中没有声明任何用于管理成员对象的方法,而是在Composite类中声明并实现这些方法

  • 对于叶子对象,客户端不可能调用到这些方法

  • 缺点是不够透明,客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件

    image-20231229130915270

特点及使用环境

优点

  1. 可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,让客户端忽略了层次的差异,方便对整个层次结构进行控制
  2. 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
  3. 增加新的容器构件和叶子构件都很方便,符合开闭原则
  4. 为树形结构的面向对象实现提供了一种灵活的解决方案

缺点

  1. 在增加新构件时很难对容器中的构件类型进行限制

适用环境

  1. 在具有整体和部分的层次结构中,希望通过一种方式忽略整体与部分的差异,客户端可以一致地对待它们
  2. 在一个使用面向对象语言开发的系统中需要处理一个树形结构
  3. 在一个系统中能够分离出叶子对象和容器对象,而且它们的类型不固定,需要增加一些新的类型

装饰模式✲

外观模式★(Facade)

定义

为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

image-20231229131346810

模式结构

结构图

image-20231229131357167

实现代码

  • 外观角色(Facade)

    在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统,传递给相应的子系统对象处理。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Facade {
    private SubSystemA objl = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();
    private SubSystemC obj3 = new SubSystemC();
    public void method() {
    objl.methodA();
    obj2.methodB();
    obj3.methodC();
    }
    }
  • 子系统角色(SubSystem)

    在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class SubSystemA {
    public void methodA() {
    //业务实现代码
    }
    }
    public class SubSystemB {
    public void methodB() {
    //业务实现代码
    }
    }
    public class SubSystemC {
    public void methodC() {
    //业务实现代码
    }
    }

应用实例

某软件公司要开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括3个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这3个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原则,这3个操作的业务代码封装在3个不同的类中。

image-20231229131857710

  • 外观类:EncryptFacade
  • 子系统类:FileReaderCipherMachineFileWriter

特点及使用环境

优点

  1. 它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易
  2. 它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
  3. 一个子系统的修改对其他子系统没有任何影响,而且子系统的内部变化也不会影响到外观对象

缺点

  1. 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
  2. 如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则

适用环境

  1. 要为访问一系列复杂的子系统提供一个简单入口
  2. 客户端程序与多个子系统之间存在很大的依赖性
  3. 在层次化结构中,可以使用外观模式的定义系统中每一层的入口,层与层之间不直接产生联系,而是通过外观类建立联系,降低层之间的耦合度

享元模式☒

代理模式(Proxy)

定义

给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

模式结构

结构图

image-20231229144858539

实现代码

  • 抽象主题角色(Subject)

    它声明了真实主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。

    1
    2
    3
    public abstract class Subject {
    public abstract void request();
    }
  • 代理主题角色(Proxy)

    它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象;在代理主题角色中提供了一个与真实主题角色相同的接口,以便在任何时候都可以替代真实主题;代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。通常,在代理主题角色中客户端在调用所引用的真实主题操作之前或之后还需要执行其他操作,而不仅仅是单纯调用真实主题对象中的操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class Proxy extends Subject {
    //维持一个对真实主题对象的引用
    private RealSubject realSubject = new RealSubject();
    public void preRequest() {
    ……
    }

    public void request() {
    preRequest();
    //调用真实主题对象的方法
    realSubject.request();
    postRequest();
    }

    public void postRequest() {
    ……
    }
    }
  • 真实主题角色(RealSubject)

    它定义了代理角色所代表的真实对象,在真实主题角色中实现了真实的业务操作,客户端可以通过代理主题角色间接调用真实主题角色中定义的操作。

    1
    2
    3
    4
    5
    public class RealSubject extends Subject{
    public void request() {
    //业务方法具体实现代码
    }
    }

应用实例

某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:

(1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;

(2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。

该软件公司开发人员已完成了商务信息查询模块的开发任务,现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。

image-20231229145241223

  • 抽象主题角色:Searcher
  • 真实主题角色:RealSearcher
  • 代理主题角色:ProxySearcher
  • 其他业务类:
    • AccessValidator:验证用户身份
    • Logger:记录用户查询日志

模式变化

远程代理

  • 客户端程序可以访问在远程主机上的对象,远程主机可能具有更好的计算性能与处理速度,可以快速地响应并处理客户端的请求

  • 可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在

  • 客户端完全可以认为被代理的远程业务对象是在本地而不是在远程,而远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用

    image-20231229145646331

虚拟代理

  • 对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理
  • 在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象
  • 使用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能

Java动态代理

  • 动态代理可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法
  • Java语言提供了对动态代理的支持,Java语言实现动态代理时需要用到位于java.lang.reflect包中的一些类
    • Proxy类
      • public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces):该方法用于返回一个Class类型的代理类,在参数中需要提供类加载器并需要指定代理的接口数组(与真实主题类的接口列表一致)
      • public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):该方法用于返回一个动态创建的代理类的实例,方法中第一个参数loader表示代理类的类加载器,第二个参数interfaces表示代理类所实现的接口列表(与真实主题类的接口列表一致),第三个参数h表示所指派的调用处理程序类
    • InvocationHandler接口
      • InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用处理者(InvocationHandler接口的子类)
      • public Object invoke(Object proxy, Method method, Object[] args):该方法用于处理对代理类实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时将自动调用该方法。invoke()方法包含三个参数,其中第一个参数proxy表示代理类的实例,第二个参数method表示需要代理的方法,第三个参数args表示代理方法的参数数组
  • 动态代理类需要在运行时指定所代理真实主题类的接口,客户端在调用动态代理对象的方法时,调用请求会将请求自动转发给InvocationHandler对象的invoke()方法,由invoke()方法来实现对请求的统一处理。

特点及使用环境

优点

  1. 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
  2. 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性
  3. 各代理方式:
    1. 远程代理:可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高了系统的整体运行效率
    2. 虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
    3. 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
    4. 保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限

缺点

  1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢(例如保护代理)
  2. 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂(例如远程代理)

适用环境

  1. 当客户端对象需要访问远程主机中的对象时可以使用远程代理
  2. 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理
  3. 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理
  4. 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
  5. 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理