CPT304 SoftwareEngineeringII 软件工程 2 Pt.3 设计模式(上)

文章目录

  • [1. 什么是设计模式?](#1. 什么是设计模式?)
    • [1.1 如何使用设计模式](#1.1 如何使用设计模式)
    • [1.2 设计模式的好处](#1.2 设计模式的好处)
    • [1.3 设计模式的缺点](#1.3 设计模式的缺点)
    • [1.4 设计模式的分类](#1.4 设计模式的分类)
      • [1.4.1 创建型模式(Creational Patterns)](#1.4.1 创建型模式(Creational Patterns))
        • [1.4.1.1 工厂模式(Factory Pattern)](#1.4.1.1 工厂模式(Factory Pattern))
          • [1.4.1.1.1 工厂模式示例1](#1.4.1.1.1 工厂模式示例1)
          • [1.4.1.1.2 工厂模式示例2](#1.4.1.1.2 工厂模式示例2)
          • [1.4.1.1.3 依赖倒置原则依赖倒置原则(Dependency Inversion Principle,DIP)](#1.4.1.1.3 依赖倒置原则依赖倒置原则(Dependency Inversion Principle,DIP))
          • [1.4.1.1.4 工厂模式最好配合依赖注入 DI 使用](#1.4.1.1.4 工厂模式最好配合依赖注入 DI 使用)
        • [1.4.1.2 建造者模式 / 生成器模式(Builder Pattern)](#1.4.1.2 建造者模式 / 生成器模式(Builder Pattern))
          • [1.4.1.2.1 具体做法](#1.4.1.2.1 具体做法)
          • [1.4.1.2.2 应用](#1.4.1.2.2 应用)
          • [1.4.1.2.3 具体场景](#1.4.1.2.3 具体场景)
      • [1.4.2 结构型模式(Structural Patterns)](#1.4.2 结构型模式(Structural Patterns))
        • [1.4.2.1 装饰器模式(Decorator Pattern)](#1.4.2.1 装饰器模式(Decorator Pattern))
          • [1.4.2.1.1 装饰器模式示例1](#1.4.2.1.1 装饰器模式示例1)
          • [1.4.2.1.2 装饰器模式示例2](#1.4.2.1.2 装饰器模式示例2)
          • [1.4.2.1.3 应用](#1.4.2.1.3 应用)
          • [1.4.2.1.4 Java 或 C# 的输入输出流](# 的输入输出流)
          • [1.4.2.1.5 装饰器模式练习1](#1.4.2.1.5 装饰器模式练习1)
          • [1.4.2.1.6 装饰器模式练习2](#1.4.2.1.6 装饰器模式练习2)
        • [1.4.2.2 适配器模式(Adapter Pattern)](#1.4.2.2 适配器模式(Adapter Pattern))
          • [1.4.2.2.1 关键组成部分](#1.4.2.2.1 关键组成部分)
          • [1.4.2.2.2 适配器模式示例1](#1.4.2.2.2 适配器模式示例1)
          • [1.4.2.2.3 应用](#1.4.2.2.3 应用)
          • [1.4.2.2.4 适配器模式练习1](#1.4.2.2.4 适配器模式练习1)

1. 什么是设计模式?

设计模式是软件设计中针对常见问题的典型解决方案。

它们就像预先制作好的蓝图,你可以根据需要对其进行定制,以解决代码中重复出现的设计问题。

设计模式不是可以直接复制粘贴的具体代码,而是一种用于解决特定问题的通用概念。

我们可以按照模式的细节来实现适合自己程序实际情况的解决方案。

大多数模式的描述都非常正式,这样人们可以在很多不同的场景中复用它们。

一个模式描述通常包含以下几个部分:

Intent(意图):简要描述模式要解决的问题以及给出的解决方案。

Motivation(动机):进一步解释问题以及模式所提供的解决方案如何解决问题。

Structure(结构):展示模式中各个部分以及它们之间的关系。

Code example(代码示例):使用流行编程语言的示例,让理解模式背后的思想更容易。

Consequences(后果):应用模式后的结果以及权衡。

etc(其他):可能还有其他辅助说明。

1.1 如何使用设计模式

主要把它用作沟通工具,而不是直接当作设计工具。

有时候,它也可以成为警示故事------告诉你哪些该做,哪些不该做。

我们不会直接跳去使用某个设计模式,而是在设计过程中逐渐发现并使用模式。

  • 当你的设计接近某个模式时,可以考虑采用该模式,而不是一开始就去寻找模式。
  • 不要强迫自己一定要使用某个模式。

1.2 设计模式的好处

  1. 可复用性(Reusability):设计模式提供可以在多个项目中复用的解决方案,从而减少整体开发时间。
  2. 最佳实践(Best Practices):它们反映了软件开发专业人士的经验和见解,因此代表了该领域的最佳实践。
  3. 易理解性与可维护性(Understandability and Maintainability):设计模式提供了一种一致、易理解且有良好文档记录的方法,使代码更容易理解和维护。
  4. 沟通(Communication):使用知名的设计模式可以为开发团队提供共同的术语,从而改善团队内部沟通。

1.3 设计模式的缺点

  1. 过度使用(Overuse):
    有时候,简单的解决方案更高效,也更容易理解。
    如果在不需要的地方使用设计模式,可能会导致不必要的复杂性。
  2. 前期成本高(Initial Overhead):
    理解和实现设计模式在一开始可能比较耗时,甚至会减慢开发过程。
  3. 适用性有限(Specificity):
    设计模式并不是放之四海而皆准的方案。
    它们通常只适用于特定场景,未必适合所有情况。
  4. 依赖性(Dependence):
    过度依赖设计模式,可能会限制开发者跳出固有思维、提出创新解决方案的能力。
  5. 学习困难(Difficulty in Learning):
    有些设计模式学习和实现起来比较复杂,尤其对经验较少的开发者来说更明显。

1.4 设计模式的分类

所有设计模式都可以根据它们的意图(intent)或目的来分类。

主要有三大类:

  1. 创建型模式(Creational Patterns)
    提供对象创建机制,从而提高灵活性,并增强现有代码的复用性。
  2. 结构型模式(Structural Patterns)
    说明如何将对象和类组装成更大的结构,同时保持这些结构的灵活和高效。
  3. 行为型模式(Behavioral Patterns)
    关注对象之间的高效沟通,以及职责的合理分配。

1.4.1 创建型模式(Creational Patterns)

创建型设计模式用于处理对象的创建,目的是根据不同情况,用合适的方式创建对象。

  • 灵活性(Flexibility):
    提供一种方式,让系统不依赖于对象是如何被创建、组合和表示的。
  • 可复用性(Reusability):
    通过为对象创建定义独立的工厂类或方法,同样的代码可以被复用来获取类的实例,而不是一次次重复创建,从而减少冗余代码。
  • 抽象性(Abstraction):
    创建型设计模式会把实例化过程抽象出来,隐藏被创建对象的实现细节。
  • 对对象创建的控制(Control Over Object Creation):
    创建型模式让我们可以更好地控制对象创建过程,并把这个过程封装在单独的函数或类中,这样开发者就能修改或优化创建逻辑,而不会影响代码的其他部分。

下面介绍5种创建型模式:

  • 工厂模式(Factory Pattern)
    提供一个简单的决策类,根据提供的数据,从某个抽象基类的多个可能子类中返回其中一个对象。
  • 抽象工厂模式(Abstract Factory Pattern)
    提供一个接口,用来创建并返回若干组相关对象中的某一组。
  • 建造者模式(Builder Pattern)
    顾名思义,它是构建复杂对象的一种替代方式。
    当你想用同样的对象构建过程来创建不同的不可变对象时,才适合使用这种模式。
  • 原型模式(Prototype Pattern)
    先从一个已经初始化并实例化好的对象开始,通过复制或克隆它来创建新实例,而不是重新创建新的实例。
  • 单例模式(Singleton Pattern)
    指一个类最多只能有一个实例,并且它提供一个全局唯一的访问点来访问这个实例。
1.4.1.1 工厂模式(Factory Pattern)
  • 四人组(Gang of Four)定义:
    定义一个用于创建对象的接口,但让子类决定实例化哪一个类;
    工厂方法让一个类把它所使用对象的实例化过程延迟到子类中完成。
  • 现实应用中最常用的设计模式之一。

换句话说工厂模式的核心思想,不要直接在代码里到处 new 对象,而是通过一个"工厂"来决定创建哪种对象。

工厂模式在下面这种情况下特别有用:

  • 当一个类无法预先确定自己需要创建哪一种对象时;
  • 或者当一个类希望由它的子类来指定所创建的对象时。

上图是在说明工厂模式里的几个角色:

  1. Product(产品接口)
    定义产品对象的统一接口,比如 doStuff()。
  2. ConcreteProductA / ConcreteProductB(具体产品)
    具体实现这个接口的不同产品。
  3. Creator(创建者)
    声明工厂方法 createProduct(): Product,以及其他业务方法。
    它自己不直接写死创建哪个具体产品,而是依赖工厂方法。
  4. ConcreteCreatorA / ConcreteCreatorB(具体创建者)
    分别重写 createProduct(),返回不同的具体产品,比如:
    ConcreteCreatorA 返回 ConcreteProductA
    ConcreteCreatorB 返回 ConcreteProductB
1.4.1.1.1 工厂模式示例1

汉堡店点餐。

  • orderBurger
    表示"点一个汉堡"的请求入口。
  • Restaurant <<Abstract Factory>>
    Restaurant 是抽象工厂,定义"创建汉堡"的统一方式,但不指定具体做哪种汉堡。
  • VeggieBurgerRestaurant <<Concrete Factory>>
    具体工厂之一,负责创建 VeggieBurger(素汉堡)。
  • BeefBurgerRestaurant <<Concrete Factory>>
    具体工厂之一,负责创建 BeefBurger(牛肉汉堡)。
  • Burger <<Interface>>
    Burger 是产品接口,表示"汉堡"这个抽象概念。
  • VeggieBurger / BeefBurger <<Implementation>>
    具体产品,实现 Burger 接口。

用户调用 orderBurger 时,不需要自己关心:

  • 是怎么创建汉堡的
  • 创建的是哪一个具体类
  • 具体逻辑写在哪里

而是把"创建哪种汉堡"交给不同的餐厅工厂来处理。最后的汉堡由具体的餐厅决定,但这并不代表用户无法控制得到什么汉堡,可以用户传入一个类型参数,比如 veggie / beef,然后工厂根据这个参数返回对应的 Burger 对象。

下面的代码就说明了这一点。

java 复制代码
@GetMapping("/burger/{request}")
public Burger orderBurger(@PathVariable("request") String request){
    if("BEEF".equals(request)){
        Restaurant beefBurgerRestaurant = new BeefBurgerRestaurant();
        return beefBurgerRestaurant.orderBurger();
    } else{
        Restaurant veggieBurgerRestaurant = new VeggieBurgerRestaurant();
        return veggieBurgerRestaurant.orderBurger();
    }
}

用户决定要哪类汉堡,具体对象由对应工厂创建。

java 复制代码
public abstract class Restaurant {
    public Burger orderBurger(){
        Burger burger = createBurger();
        burger.prepare();

        return burger;
    }

    public abstract Burger createBurger();
}

父类规定流程,子类决定创建哪种具体产品。

java 复制代码
public class BeefBurgerRestaurant extends Restaurant{
    @Override
    public Burger createBurger() {
        return new BeefBurger();
    }
}
java 复制代码
public class VeggieBurgerRestaurant extends Restaurant{
    @Override
    public Burger createBurger() {
        return new VeggieBurger();
    }
}

具体工厂专门负责生产牛肉汉堡。

1.4.1.1.2 工厂模式示例2

我们现在设计一个文档管理系统,系统里有不同类型的文档,比如 Word、PDF、Excel。

这些文档都需要支持 打开(open)、保存(save)、关闭(close)。

所以我们应该:

  • 先定义一个统一的文档接口
  • 再为不同文档实现具体类
  • 最后通过工厂来创建对应文档对象

示例代码如下。

java 复制代码
public interface Document {
    void open();
    void save();
    void close();
}
java 复制代码
public class WordDocument implements Document {
    @Override
    public void open() {
        System.out.println("Opening Word document");
    }

    @Override
    public void save() {
        System.out.println("Saving Word document");
    }

    @Override
    public void close() {
        System.out.println("Closing Word document");
    }
}
java 复制代码
public class PdfDocument implements Document {
    @Override
    public void open() {
        System.out.println("Opening PDF document");
    }

    @Override
    public void save() {
        System.out.println("Saving PDF document");
    }

    @Override
    public void close() {
        System.out.println("Closing PDF document");
    }
}
java 复制代码
public class ExcelDocument implements Document {
    @Override
    public void open() {
        System.out.println("Opening Excel document");
    }

    @Override
    public void save() {
        System.out.println("Saving Excel document");
    }

    @Override
    public void close() {
        System.out.println("Closing Excel document");
    }
}
java 复制代码
public abstract class DocumentFactory {
    public abstract Document createDocument();
}
java 复制代码
public class WordDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new WordDocument();
    }
}
java 复制代码
public class PdfDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new PdfDocument();
    }
}}
java 复制代码
public class ExcelDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new ExcelDocument();
    }
}
java 复制代码
@GetMapping("/document/{type}")
public Document handleDocument(@PathVariable("type") String type) {
    DocumentFactory factory;

    if ("WORD".equalsIgnoreCase(type)) {
        factory = new WordDocumentFactory();
    } else if ("PDF".equalsIgnoreCase(type)) {
        factory = new PdfDocumentFactory();
    } else if ("EXCEL".equalsIgnoreCase(type)) {
        factory = new ExcelDocumentFactory();
    } else {
        throw new IllegalArgumentException("Unsupported document type: " + type);
    }

    Document document = factory.createDocument();
    document.open();
    return document;
}
1.4.1.1.3 依赖倒置原则依赖倒置原则(Dependency Inversion Principle,DIP)

DIP 的核心是:高层模块不应该依赖低层模块,二者都应该依赖抽象(abstractions)。

如我们前面的示例2中:

java 复制代码
public interface Document {
    void open();
    void save();
    void close();
}

这个 Document 就是抽象。

我们后续的具体类都是依赖并实现 Document 接口。

java 复制代码
public class WordDocument implements Document
java 复制代码
public class PdfDocument implements Document
java 复制代码
public class ExcelDocument implements Document

如果高层类直接依赖具体低层类,会导致代码耦合太死。

例如:

java 复制代码
public class DocumentManager {
    private PdfDocument document = new PdfDocument();

    public void handle() {
        document.open();
    }
}

这个时候 DocumentManager 只能处理 PdfDocument。

如果之后想改成处理 Excel:

java 复制代码
private ExcelDocument document = new ExcelDocument();

因此应该是让高层类依赖 Document 接口,而不是依赖具体的 PdfDocument、WordDocument 或 ExcelDocument。

所以抽象不应该依赖具体细节,具体细节应该依赖抽象。(Abstractions should not depend on details. Details should depend on abstractions.)

那我们现在有了一个新的问题:为什么只使用接口还不够,为什么还需要工厂模式?

虽然我们已经让高层类依赖接口了,但对象总得在某个地方被 new 出来。

如果这个 new 还是写在高层类里,那么高层类又重新依赖了具体类。

所以我们用工厂来负责创建对象,把创建逻辑从高层类中拿出去。

假设我们的系统里有 50 个不同的类都需要处理文档。

如果没有工厂模式,这 50 个类里面可能都写了:

java 复制代码
Document document = new PdfDocument();

这样虽然可以运行,但是问题很大。比如我们现在不需要 PdfDocument 了,而是希望用 ExcelDocument,那么我们就要去这 50 个类里面,把 new PdfDocument() 全部改成 new ExcelDocument()。

如果用了工厂模式,这 50 个类都不直接写 new PdfDocument(),而是统一调用工厂方法:

java 复制代码
Document document = factory.createDocument();

真正的 new PdfDocument() 只出现在工厂类里面:

java 复制代码
public class PdfDocumentFactory extends DocumentFactory {
    @Override
    public Document createDocument() {
        return new PdfDocument();
    }
}

这样以后如果要从 PDF 换成 Excel,就不需要改 50 个类,只需要改工厂里的创建逻辑。

1.4.1.1.4 工厂模式最好配合依赖注入 DI 使用

前面说了高层类不要直接 new PdfDocument(),应该通过 DocumentFactory 创建文档对象。

我们在这里进一步说明高层类最好也不要自己 new DocumentFactory(),而应该让外部把工厂传进来。

也就是说,高层类不应该这样写:

java 复制代码
public class UserDashboard {
    private DocumentFactory documentFactory = new PdfDocumentFactory();
}

因为这样 UserDashboard 又依赖了具体工厂PdfDocumentFactory,这还是有耦合。

所以不应该这样:

java 复制代码
DocumentFactory factory = new PdfDocumentFactory();

而应该是:

java 复制代码
private DocumentFactory documentFactory;

public UserDashboard(DocumentFactory documentFactory) {
    this.documentFactory = documentFactory;
}

也就是工厂从外面传进来。这就叫依赖注入。

因此前面的例子的正确写法应该如下:

java 复制代码
class UserDashboard {
    private DocumentFactory documentFactory;

    public UserDashboard(DocumentFactory documentFactory) {
        this.documentFactory = documentFactory;
    }

    public void onButtonClick() {
        documentFactory.openDocument().save();
    }
}

高层类不但不应该直接创建具体文档对象,也不应该直接创建具体工厂对象。而是应该通过依赖注入的方式接收一个工厂接口,然后通过这个工厂创建文档对象。

1.4.1.2 建造者模式 / 生成器模式(Builder Pattern)

它也是一种 创建型设计模式,和前面讲的 Factory Pattern 一样,都属于"用来创建对象"的模式。

建造者模式可以把复杂对象的初始化过程拆成一步一步来完成。

当一个类有很多属性时,如果只靠构造方法 new Car(...) 来创建对象,会变得很复杂、难读、容易出错。

建造者模式就是为了解决这种"复杂对象创建"的问题。

例如我们有一个 Car 类,它有很多属性:

java 复制代码
class Car {
    int id;
    String brand;
    String model;
    String color;
    int nbrDoors;
    String screenType;
    double weight;
    double height;
}

如果我们直接用构造方法接受所有参数,那么构建对象时就要传一大堆参数。

java 复制代码
Car car = new Car(id, brand, model, color, nbrDoors, screenType, weight, height);

这会让代码可读性很差。

如果参数顺序写错了,可能编译不报错,但逻辑是错的。

有时候你并不想设置所有属性,只想设置一部分属性。

但是构造方法要求你传很多参数,所以你只能给不需要的参数传 null。

java 复制代码
new Car(1, "BMW", null, "Black", 4, null, 2000.5, null);

这样代码很难看,也不安全。

因为后面别人看到这些 null,不一定知道它们分别代表哪个属性。

为了应对不同的创建需求,我们可能会写很多个构造方法。

java 复制代码
new Car(id, brand, model);
new Car(id, screenType, weight, height);
new Car(id, brand, model, color, nbrDoors);
new Car(id, brand, screenType, weight, height);

这会导致类里面构造方法越来越多,代码越来越乱。

1.4.1.2.1 具体做法

因此建造者模式的具体做法是把复杂对象的创建过程,从对象自己的类里抽出来,放到一个单独的 Builder 类中。

回到刚刚的例子中就是:原本 Car 类自己要负责处理很多构造参数,现在把这些"创建和配置对象"的代码交给 CarBuilder 来做。

也就是从原来的:

java 复制代码
public class Car {
    private final int id;
    private final String brand;
    private final String model;
    private final String color;

    public Car(int id, String brand, String model, String color) {
        this.id = id;
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public int getId() {
        return id;
    }

    public String getBrand() {
        return brand;
    }

    public String getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }
}

用 Builder 变为:

java 复制代码
public class CarBuilder {
    private int id;
    private String brand;
    private String model;
    private String color;

    public CarBuilder id(int id) {
        this.id = id;
        return this;
    }

    public CarBuilder brand(String brand) {
        this.brand = brand;
        return this;
    }

    public CarBuilder model(String model) {
        this.model = model;
        return this;
    }

    public CarBuilder color(String color) {
        this.color = color;
        return this;
    }

    public Car build() {
        return new Car(id, brand, model, color);
    }
}

对应的使用方式是:

java 复制代码
Car car = new CarBuilder()
        .id(1)
        .brand("BMW")
        .model("X5")
        .color("Black")
        .build();

虽然现在我们可以使用 Builder 进行构造,这样构造清楚很多,但客户端还是要自己一步一步写创建流程。

我们可以引入一个 Director 类,让对象创建过程更简单,它负责规定 Builder 方法的调用顺序。

这么做同时还可以重复使用一些对象的配置,因为我们可能经常需要创建标准 BMW 车、标准 Tesla 车、标准 Audi 车等。如果每次都手动写 Builder 链式调用,会重复很多代码,用 Director 后,可以把这些固定配置封装起来。

例如:

java 复制代码
public class Director {

    public void buildBugatti(CarBuilder builder) {
        builder.brand("Bugatti")
               .color("Blue")
               .nbrDoors(2)
               .engine("8L")
               .height(115);
    }

    public void buildLambo(CarBuilder builder) {
        builder.brand("Lamborghini")
               .model("Aventador")
               .color("Yellow")
               .nbrDoors(2)
               .height(115);
    }
}

所以现在使用方式如下:

java 复制代码
Director director = new Director();
CarBuilder builder = new CarBuilder();
director.buildBugatti(builder);
Car car = builder.build();

创建完 Director 和 Builder 后,让 Director 指挥 Builder 创建 Bugatti 的配置,最后通过 Builder 真正生成 Car 对象。

当然,Director 不是必须的。Builder Pattern 可以没有 Director。

Director 可以隐藏对象创建细节,没有 Director 时,客户端要知道创建过程。

1.4.1.2.2 应用

当一个类的构造方法里有很多可选参数时,可以用建造者模式来简化代码。

例如普通写法可能是:

java 复制代码
new User("张三", 18, "男", "哈尔滨", "123456", true);

参数很多时,很容易搞混每个参数代表什么。

用 Builder 模式后可以写成:

java 复制代码
User user = new User.Builder()
    .name("张三")
    .age(18)
    .gender("男")
    .city("哈尔滨")
    .phone("123456")
    .build();

这样可读性更强,也更不容易传错参数。

当我们想用同一套创建流程,生成同一种产品的不同形式时,可以使用建造者模式,避免让使用者理解复杂的创建细节。

比如都是创建一份"套餐",但可以有不同组合:

套餐A:汉堡 + 可乐

套餐B:汉堡 + 薯条 + 可乐

套餐C:鸡肉卷 + 咖啡

客户端只需要告诉 Builder 要什么类型,具体怎么一步步创建由 Builder 处理。

这些都与我们前面提到的原来写代码时候的痛点相对应。

1.4.1.2.3 具体场景

我们现在给出一些可以使用建造者模式的具体场景:

  1. 一个机器人可能有很多组成部分,比如头、身体、手臂、腿等。每个部分又可能有很多不同选择。
    身体材料可以是金属、塑料、高级合金。
    而头部可能包含不同类型的传感器、不同类型的处理器。
    所以机器人对象的结构比较复杂,适合用建造者模式一步一步创建。
  2. 当我们处理一个配置很复杂的对象时,可以使用建造者模式。例如网上点披萨。
    披萨有很多可选配置,比如饼底类型、尺寸、配料、芝士、酱料。
    如果直接用构造函数创建披萨,参数会很多,很容易混乱。用 Builder 模式可以更清楚地设置每一项。
java 复制代码
Pizza pizza = new Pizza.Builder()
    .size("Large")
    .crust("Thin")
    .cheese("Mozzarella")
    .sauce("Tomato")
    .addTopping("Beef")
    .build();
  1. 如果我们在设计一个复杂的 3D 游戏,游戏里面有不同类型的角色,每个角色都有很多属性,比如力量、速度、生命值、武器、技能。
    不同角色的能力、武器、属性都可能不同。如果直接创建,会非常复杂。用建造者模式可以把角色创建过程拆开,让代码更清晰。
java 复制代码
Character warrior = new Character.Builder()
    .strength(90)
    .speed(60)
    .health(100)
    .weapon("Sword")
    .ability("Shield")
    .build();

总结来说:当一个对象结构复杂、参数很多、可选配置很多时,就适合使用 Builder Pattern。

1.4.2 结构型模式(Structural Patterns)

结构型设计模式很重要,因为它们可以帮助我们简化和管理不同软件实体之间的关系。

也就是说,在程序中会有很多类、对象、接口、模块。结构型模式可以指导我们如何把这些对象组合起来,形成更大的系统结构,同时保持系统灵活、高效。

因此其主要解决:类和对象之间如何组合,如何组织成更大的结构,同时让系统保持灵活、清晰、容易维护。

它的优点如下:

  1. 代码复用性(Code Reusability)
    结构型模式提供了一些软件设计中经常出现的问题的解决方案。使用这些模式时,我们可以复用已经被证明有效的方案。
  2. 提高模块化程度(Improved Modularity)
    结构型模式可以通过解耦系统和创建接口,提高程序的模块化程度。
  3. 代码维护性(Code Maintenance)
    结构型模式可以让类和对象组织得更清楚,结构更合理,从而让代码更容易理解和维护。
  4. 灵活性(Flexibility)
    结构型模式允许我们独立地改变系统中的组件,从而提高系统灵活性。

其的常见模式如下:

  1. 适配器模式(Adapter Pattern)
    适配器模式像一个连接器,用来连接两个原本不兼容的接口。适配器会把已有的类包装成一个新的接口,让它能够和客户端需要的接口兼容。
  2. 桥接模式(Bridge Pattern)
    桥接模式用于把抽象部分和实现部分解耦,使它们可以独立变化。
    例如:
    不同形状------圆形、方形、三角形。
    不同颜色------红色、蓝色、绿色。
    如果不适应桥接模式就会出现------红色圆形、蓝色圆形、绿色圆形、红色方形、蓝色方形、绿色方形。
    桥接模式可以把"形状"和"颜色"分开------形状负责形状
    颜色负责颜色,这样新增颜色时,不需要修改所有形状类。
  3. 组合模式(Composite Pattern)
    组合模式允许我们用相同的方式处理单个对象和对象组合。
  4. 装饰器模式(Decorator Pattern)
    装饰器模式可以给一个对象添加额外的职责或功能,可以是静态添加,也可以是动态添加。
    它适用于当我们想给对象增加功能,但又不想直接修改原来的类时,可以用装饰器模式。
  5. 外观模式(Facade Pattern)
    外观模式把一个复杂的子系统封装在一个简单接口后面。它隐藏了很多复杂细节,让这个子系统更容易使用。
  6. 享元模式(Flyweight Pattern)
    享元模式可以让程序支持大量对象,同时降低内存消耗。它通过让多个对象共享部分状态来实现这一点。
    比如游戏中有很多树,如果每一棵树都单独存一份完整数据,比如树的模型、纹理、颜色、形状,会非常占内存。
    但很多树其实可以共享相同数据:树的模型、纹理、基础颜色。
    独立不同的是:每棵树的位置、高度、旋转角度。
    这样就不用为每棵树重复保存相同内容。
  7. 代理模式(Proxy Pattern)
    代理模式会放置一个替代对象或占位对象,用它来控制对真实目标对象的访问。客户端通过访问代理对象,间接操作真实对象。

我们接下来对一些模式进行详细介绍。

1.4.2.1 装饰器模式(Decorator Pattern)

装饰器模式允许我们给某一个对象动态添加新的功能和行为,同时不会影响同一个类中的其他对象。

继承也可以扩展一个类的行为,但继承是在编译阶段确定的,而且这个类的所有对象都会拥有扩展后的行为。

我们可以根据需求和选择,把装饰器应用到某一个具体对象上,而非整个类上。

因此其也被叫做包装器(Wrapper)。

装饰器模式通常使用抽象类或接口,再结合组合关系来实现包装。

装饰器模式会创建装饰器类,这些装饰器类会包装原始类,并且在保持方法签名不变的情况下,提供额外功能。

如上图所示,第一张图:人没有穿衣服,表示基础对象。

第二张图:人穿了毛衣,表示加了一层装饰,增加了保暖功能。

第三张图:人又穿了雨衣,表示继续加一层装饰,增加了防雨功能。

如下图所示。

我们本来有一个统一接口。所有原始对象和装饰器都要遵守这个接口。

java 复制代码
interface Component {
    void execute();
}

因此其的具体组件可以如下。这是被包装对象所属的类。它定义基础行为,这些行为可以被装饰器改变或增强。

java 复制代码
class ConcreteComponent implements Component {
    public void execute() {
        System.out.println("基础功能");
    }
}

此外我们还有基础装饰器(Base Decorator)。这里是装饰器模式的核心。

基础装饰器类中有一个字段,用来引用被包装对象。这个字段的类型应该声明为 Component 接口,这样它既可以保存具体组件,也可以保存其他装饰器。基础装饰器会把所有操作委托给被包装对象。

这里的 wrappee 意思是:被包装的对象。

它的 execute() 通常会调用:

java 复制代码
wrappee.execute();

这样就可以保留原来的功能。

接下来就是具体装饰器(Concrete Decorator),具体装饰器定义可以动态添加到组件上的额外行为。具体装饰器会重写基础装饰器的方法,并在调用父类方法之前或之后执行自己的行为。

例如:

java 复制代码
class ConcreteDecorator extends BaseDecorator {
    public void execute() {
        super.execute();
        extra();
    }

    public void extra() {
        System.out.println("额外功能");
    }
}

因此现在我们可以创建一个基础对象:

java 复制代码
a = new ConcreteComponent();

用第一个装饰器包装它:

java 复制代码
b = new ConcreteDecorator1(a);

再用第二个装饰器包装已经装饰过的对象:

java 复制代码
c = new ConcreteDecorator2(b);

最后调用:

java 复制代码
c.execute();

执行顺序大概是:

复制代码
ConcreteDecorator2 的功能
ConcreteDecorator1 的功能
ConcreteComponent 的基础功能
1.4.2.1.1 装饰器模式示例1

我们拿数据源为例,我们可以本来有读取和写入功能,然后用装饰器给数据源增加额外功能,比如加密和压缩。

所有数据源都提供两个方法:

复制代码
writeData(data):写入数据
readData():读取数据

具体数据源如下所示:

复制代码
FileDataSource
- filename
+ FileDataSource(filename)
+ writeData(data)
+ readData()

它是真正负责读写文件的类。

我们现在写一个基础装饰器。

复制代码
DataSourceDecorator
- wrappee: DataSource
+ DataSourceDecorator(s: DataSource)
+ writeData(data)
+ readData()

注意这里的 wrappee 的类型是 DataSource。

对于加密功能我们可以单独做一个加密装饰器。

复制代码
EncryptionDecorator
+ writeData(data)
+ readData()

它的作用是:在写入数据之前进行加密,在读取数据之后进行解密。

再做一个压缩装饰器:

复制代码
CompressionDecorator
+ writeData(data)
+ readData()

示例代码如下:

java 复制代码
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

// 1. Component 接口
interface DataSource {
    void writeData(String data);

    String readData();
}

// 2. Concrete Component 具体组件
class FileDataSource implements DataSource {
    private final String filename;

    public FileDataSource(String filename) {
        this.filename = filename;
    }

    @Override
    public void writeData(String data) {
        try {
            Files.writeString(Path.of(filename), data, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException("写入文件失败", e);
        }
    }

    @Override
    public String readData() {
        try {
            return Files.readString(Path.of(filename), StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException("读取文件失败", e);
        }
    }
}

// 3. Base Decorator 基础装饰器
class DataSourceDecorator implements DataSource {
    protected DataSource wrappee;

    public DataSourceDecorator(DataSource source) {
        this.wrappee = source;
    }

    @Override
    public void writeData(String data) {
        wrappee.writeData(data);
    }

    @Override
    public String readData() {
        return wrappee.readData();
    }
}

// 4. Concrete Decorator 具体装饰器:加密装饰器
class EncryptionDecorator extends DataSourceDecorator {
    private static final byte KEY = 0x5A;

    public EncryptionDecorator(DataSource source) {
        super(source);
    }

    @Override
    public void writeData(String data) {
        String encryptedData = encrypt(data);
        super.writeData(encryptedData);
    }

    @Override
    public String readData() {
        String encryptedData = super.readData();
        return decrypt(encryptedData);
    }

    private String encrypt(String data) {
        byte[] bytes = data.getBytes(StandardCharsets.UTF_8);

        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) (bytes[i] ^ KEY);
        }

        return Base64.getEncoder().encodeToString(bytes);
    }

    private String decrypt(String data) {
        byte[] bytes = Base64.getDecoder().decode(data);

        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) (bytes[i] ^ KEY);
        }

        return new String(bytes, StandardCharsets.UTF_8);
    }
}

// 5. Concrete Decorator 具体装饰器:压缩装饰器
class CompressionDecorator extends DataSourceDecorator {

    public CompressionDecorator(DataSource source) {
        super(source);
    }

    @Override
    public void writeData(String data) {
        String compressedData = compress(data);
        super.writeData(compressedData);
    }

    @Override
    public String readData() {
        String compressedData = super.readData();
        return decompress(compressedData);
    }

    private String compress(String data) {
        try {
            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream);

            gzipStream.write(data.getBytes(StandardCharsets.UTF_8));
            gzipStream.close();

            byte[] compressedBytes = byteStream.toByteArray();
            return Base64.getEncoder().encodeToString(compressedBytes);

        } catch (IOException e) {
            throw new RuntimeException("压缩失败", e);
        }
    }

    private String decompress(String data) {
        try {
            byte[] compressedBytes = Base64.getDecoder().decode(data);

            ByteArrayInputStream byteStream = new ByteArrayInputStream(compressedBytes);
            GZIPInputStream gzipStream = new GZIPInputStream(byteStream);

            byte[] result = gzipStream.readAllBytes();
            return new String(result, StandardCharsets.UTF_8);

        } catch (IOException e) {
            throw new RuntimeException("解压失败", e);
        }
    }
}

// 6. Client 客户端
public class Main {
    public static void main(String[] args) {
        String filename = "data.txt";

        DataSource source = new FileDataSource(filename);

        source = new EncryptionDecorator(source);

        source = new CompressionDecorator(source);

        String originalData = "Hello, Decorator Pattern! This is a data source example.";

        source.writeData(originalData);

        System.out.println("原始数据:");
        System.out.println(originalData);

        System.out.println();

        System.out.println("文件中实际保存的数据:");
        System.out.println(new FileDataSource(filename).readData());

        System.out.println();

        System.out.println("经过装饰器读取后的数据:");
        System.out.println(source.readData());
    }
}
1.4.2.1.2 装饰器模式示例2

假设我们有一个 Robot 接口,里面有两个功能:

java 复制代码
public interface Robot {
    public void Move(int x, int y, int speed);

    public void Cook();
}

这是一个统一接口。

我们现在做一个对应的具体组件。

java 复制代码
class JapaneseRobot implements Robot {

    @Override
    public void Move(int x, int y, int speed) {
        System.out.println("Japanese Robot moved to " + x + "...");
    }

    @Override
    public void Cook() {
        System.out.println("Cooking Japanese food");
    }
}

我们现在实现了 Robot 接口,所以必须写出 Move() 和 Cook() 的具体内容。这是具体组件,也就是基础对象。

我们还可以做别的具体组件。

java 复制代码
class ChineseRobot implements Robot {

    @Override
    public void Move(int x, int y, int speed) {
        System.out.println("ChineseRobot moved to " + x + "...");
    }

    @Override
    public void Cook() {
        System.out.println("Cooking Chinesefood");
    }
}

我们可以有一个类控制这些机器人都去做饭。

java 复制代码
class PlayRobot {
    private List<Robot> robots = new ArrayList<Robot>();

    public void AddRobot(Robot r) {
        robots.add(r);
    }

    public void AllRobotsCook() {
        robots.stream().forEach(r -> r.Cook());
    }
}

现在我们希望第三个机器人能够根据顾客的不同性别提供不同的饭量,那么现在 PlayRobot 类就无法完成这个需求。

所以我们可以使用装饰器去解决这个问题,因为这样可以不修改别的机器人的代码,保留了 Robot 接口的统一调用方式。

java 复制代码
public class RobotDecorator implements Robot {
    private Robot r;

    protected RobotDecorator(Robot r) {
        this.r = r;
    }

    @Override
    public void Move(int x, int y, int speed) {
        r.Move(x, y, speed);
    }

    @Override
    public void Cook() {
        r.Cook();
    }
}

这里首先要实现 Robot 接口,这样它才能被当作普通机器人使用,才可以被加入 PlayRobot 的列表。

我们这里先包着一个机器人对象。这就是要被装饰的机器人。

所以现在这里会用一个构造方法负责接受被包装的机器人,从而把外部传进来的机器人对象保存到当前装饰器内部。

所以现在当别人调用装饰器的 Move() 方法时,装饰器自己不重新写移动逻辑,而是让内部的机器人 r 去执行移动。这叫 delegating(委托)。

调用装饰器的 Cook() 方法时,实际执行的是内部机器人 r 的 Cook() 方法。这也是委托。

现在我们再做一个具体装饰器。

java 复制代码
public class RationalRobotDecorator extends RobotDecorator {
    private boolean gender;

    public RationalRobotDecorator(Robot r, boolean gender) {
        super(r);
        this.gender = gender;
    }

    @Override
    public void Cook() {
        if (gender) {
            System.out.println("I will cook you a small portion");
        } else {
            System.out.println("I will cook you a big portion");
        }

        super.Cook();
    }
}

它继承了 RobotDecorator,并且重写了 Cook() 方法,在原来的做饭功能基础上增加了根据性别提供不同份量的新功能。

1.4.2.1.3 应用

当我们需要在程序运行时,给某些对象临时增加额外功能,并且不影响原来使用这些对象的代码时,可以使用装饰器模式。

例如刚刚例子中我们只需要第三个机器人有额外功能。

当使用继承来扩展对象功能很麻烦,或者无法使用继承时,也可以使用装饰器模式。

比如一个类被 final 修饰,而不能被继承,这个时候就可以用装饰器。

1.4.2.1.4 Java 或 C# 的输入输出流

Java 或 C# 的输入输出流(Stream I/O),其实大量使用了装饰器模式。

如果你写过 Java 或 C# 的代码,你可能已经在不知不觉中用过装饰器模式了。

因为 Java 和 C# 标准库里的输入输出系统,很多就是用装饰器模式设计的。

比如我们最开始创建一个基础的 FileInputStream,它负责从文件中读取原始字节。

java 复制代码
InputStream file = new FileInputStream("data.txt");

它只能从文件中读取原始字节数据。

我们然后用 BufferedInputStream 把 FileInputStream 包起来,给它增加缓冲功能,提高读取速度。

java 复制代码
InputStream buffered = new BufferedInputStream(file);

BufferedInputStream 的作用是一次多读取一些数据放进缓冲区,后面读取时先从缓冲区拿数据,减少直接访问文件的次数,提高效率。

这就是一个装饰器。

再用 DataInputStream 把已经缓冲过的流继续包起来,增加读取特定数据类型的能力。

java 复制代码
DataInputStream data = new DataInputStream(buffered);

DataInputStream 可以读取更具体的数据类型,比如int、double、boolean、String。

因此 Java 的输入输出流就是装饰器模式的经典例子。通过一层一层包装最终得到一个功能更强的输入流。

1.4.2.1.5 装饰器模式练习1

在前面示例2的基础上,我们再增加一个新的具体装饰器,从而有一种新的机器人功能。这个机器人更聪明,因为它可以使用深度学习来把饭做得更好。

而且一个机器人可以同时是理性机器人,可以根据顾客性别决定饭量。

智能机器人,可以使用深度学习把饭做得更好。

下面给出参考代码:

java 复制代码
class SmartRobotDecorator extends RobotDecorator {

    public SmartRobotDecorator(Robot r) {
        super(r);
    }

    @Override
    public void Move(int x, int y, int speed) {
        super.Move(x, y, speed);
    }

    @Override
    public void Cook() {
        System.out.println("I can apply deep learning to cook better");

        super.Cook();
    }
public class Main {
    public static void main(String[] args) {
        PlayRobot play = new PlayRobot();

        Robot r1 = new ChineseRobot();
        play.AddRobot(r1);

        Robot r2 = new JapaneseRobot();
        play.AddRobot(r2);

        Robot r3 = new ChineseRobot();

        r3 = new RationalRobotDecorator(r3, true);

        r3 = new SmartRobotDecorator(r3);

        play.AddRobot(r3);

        play.AllRobotsCook();
    }
}
1.4.2.1.6 装饰器模式练习2

我们现在设计一个咖啡点单系统。

首先我们先想象一下场景:

顾客去咖啡店时,一般先选择一杯基础咖啡,然后根据需求添加额外配料,每加一种配料,咖啡的最终价格都会变化。

因此如果配料越多,组合越多,类的数量会快速增加。

所以这个场景很适合装饰器模式。

如下所示。

复制代码
Coffee 接口
对应 Component,组件接口

PlainCoffee 普通咖啡
对应 Concrete Component,具体组件

CoffeeDecorator 咖啡装饰器
对应 Base Decorator,基础装饰器

MilkDecorator 牛奶装饰器
SugarDecorator 糖装饰器
WhippedCreamDecorator 奶油装饰器
对应 Concrete Decorator,具体装饰器

这些装饰器可以一层一层包起来,形成一条对象链。

java 复制代码
Coffee coffee = new PlainCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
coffee = new WhippedCreamDecorator(coffee);

这就表示:

复制代码
普通咖啡
加牛奶
加糖
加奶油

最后这杯咖啡的功能和价格都会叠加。

参考代码如下:

java 复制代码
interface Coffee {
    String getDescription();

    double getCost();
}

class PlainCoffee implements Coffee {

    @Override
    public String getDescription() {
        return "Plain Coffee";
    }

    @Override
    public double getCost() {
        return 10.0;
    }
}

abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

class MilkDecorator extends CoffeeDecorator {

    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " + Milk";
    }

    @Override
    public double getCost() {
        return super.getCost() + 2.0;
    }
}

class SugarDecorator extends CoffeeDecorator {

    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " + Sugar";
    }

    @Override
    public double getCost() {
        return super.getCost() + 1.0;
    }
}

class WhippedCreamDecorator extends CoffeeDecorator {

    public WhippedCreamDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " + Whipped Cream";
    }

    @Override
    public double getCost() {
        return super.getCost() + 3.0;
    }
}

class FlavorDecorator extends CoffeeDecorator {
    private String flavorName;
    private double flavorCost;

    public FlavorDecorator(Coffee coffee, String flavorName, double flavorCost) {
        super(coffee);
        this.flavorName = flavorName;
        this.flavorCost = flavorCost;
    }

    @Override
    public String getDescription() {
        return super.getDescription() + " + " + flavorName + " Flavor";
    }

    @Override
    public double getCost() {
        return super.getCost() + flavorCost;
    }
}

public class Main {

    public static void printCoffee(Coffee coffee) {
        System.out.println("Coffee: " + coffee.getDescription());
        System.out.println("Cost: $" + coffee.getCost());
        System.out.println();
    }

    public static void main(String[] args) {
        Coffee coffee1 = new PlainCoffee();
        printCoffee(coffee1);

        Coffee coffee2 = new PlainCoffee();
        coffee2 = new MilkDecorator(coffee2);
        coffee2 = new SugarDecorator(coffee2);
        printCoffee(coffee2);

        Coffee coffee3 = new PlainCoffee();
        coffee3 = new MilkDecorator(coffee3);
        coffee3 = new FlavorDecorator(coffee3, "Vanilla", 2.5);
        coffee3 = new WhippedCreamDecorator(coffee3);
        printCoffee(coffee3);

        Coffee coffee4 = new WhippedCreamDecorator(
                new SugarDecorator(
                        new MilkDecorator(
                                new PlainCoffee()
                        )
                )
        );
        printCoffee(coffee4);
    }
}

输出如下:

复制代码
Coffee: Plain Coffee
Cost: $10.0

Coffee: Plain Coffee + Milk + Sugar
Cost: $13.0

Coffee: Plain Coffee + Milk + Vanilla Flavor + Whipped Cream
Cost: $17.5

Coffee: Plain Coffee + Milk + Sugar + Whipped Cream
Cost: $16.0
1.4.2.2 适配器模式(Adapter Pattern)

适配器模式可以让两个不兼容的接口变得兼容,并且不需要修改它们原来的代码。

可以把它理解成生活中的转接头。

比如:

电脑接口:Type-C

显示器接口:HDMI

它们不能直接连接,所以需要一个转换器------Type-C 转 HDMI 转接头。

在代码里,适配器模式也是这个意思。

适配器模式使用一个单独的适配器类,把独立的或不兼容的接口、类连接起来。

适配器模式也可以叫 Wrapper,包装器。这个名字和装饰器模式有相似之处。

适配器会实现客户端需要的接口,同时在内部包装另一个已有对象。

例如客户端需要这个接口:

java 复制代码
interface Printer {
    void print();
}

已有旧类是:

java 复制代码
class OldPrinter {
    void printOld() {
        System.out.println("Old printer is printing");
    }
}

旧类的方法名和新接口不一致,不能直接用。

可以写适配器:

java 复制代码
class PrinterAdapter implements Printer {
    private OldPrinter oldPrinter;

    public PrinterAdapter(OldPrinter oldPrinter) {
        this.oldPrinter = oldPrinter;
    }

    @Override
    public void print() {
        oldPrinter.printOld();
    }
}

这样客户端就可以这样用:

java 复制代码
Printer printer = new PrinterAdapter(new OldPrinter());
printer.print();

我们再解释一遍适配器是如何工作的:

  1. 适配器会提供一个接口,这个接口和某个已有对象是兼容的。
  2. 已有对象可以通过这个兼容接口,安全地调用适配器的方法。
  3. 当适配器收到调用后,会把请求转换成另一个对象需要的格式和调用顺序,再传给另一个对象。

    如上图所示。
    Stock Data Provider 股票数据提供者输出 XML 数据,Analytics Library 分析库需要 JSON 数据,因此这里中间加了一个 XML 转 JSON 适配器把 XML 数据接收进来转换成 JSON 数据再交给 Analytics Library。

Client(客户端)是使用功能的一方。它只认识自己需要的接口,也就是图中的 Client Interface(客户端接口)。

Service 是已经存在的类,它有自己的方法,现在的问题在于客户端调用的和客户端提供的方法名和参数格式都不一样,所以无法直接使用,因此就需要 Adapter(适配器)。

适配器做了两件事:

第一,它内部保存一个 Service 对象:

java 复制代码
private Service adaptee;

第二,它实现客户端需要的接口:

java 复制代码
method(data)

因此整个调用过程是:

复制代码
Client
调用 Client Interface 的 method(data)

Adapter
接收 method(data)

Adapter
把 data 转换成 specialData

Service
执行 serviceMethod(specialData)

用代码简单表示如下:

java 复制代码
interface ClientInterface {
    void method(String data);
}

class Service {
    public void serviceMethod(int specialData) {
        System.out.println("Service receives: " + specialData);
    }
}

class Adapter implements ClientInterface {
    private Service adaptee;

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

    @Override
    public void method(String data) {
        int specialData = Integer.parseInt(data);
        adaptee.serviceMethod(specialData);
    }
}

客户端使用:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Service service = new Service();

        ClientInterface adapter = new Adapter(service);

        adapter.method("100");
    }
}
1.4.2.2.1 关键组成部分

主要讲三个部分:

  1. Client Interface(客户端接口)
    客户端接口定义了客户端代码要求遵守的规则。其他类想和客户端配合使用,就必须符合这个接口。
  2. Service (服务类)
    服务类是一个有用的类,通常来自第三方库或旧系统。但是客户端不能直接使用它,因为它的接口和客户端需要的接口不兼容。
  3. Adapter(适配器)
    适配器是一个可以同时和客户端、服务类配合工作的类。它实现客户端需要的接口,同时内部包装 服务类对象。客户端调用适配器的方法后,适配器会把这个调用转换成 服务类能理解的形式,再交给服务类执行。
1.4.2.2.2 适配器模式示例1

假设我们有一个控制机器人的项目,在这个项目中,我们需要开发不同类型的机器人。这些机器人都会通过一个叫 Robot 的公共接口,被 PlayRobot 类统一使用。

java 复制代码
interface Robot {
    public void Move(int x, int y, int speed);

    public void Cook();
}
class PlayRobot {
    private List<Robot> robots = new ArrayList<Robot>();

    public void AddRobot(Robot r) {
        robots.add(r);
    }

    public void AllRobotsCook() {
        robots.stream().forEach(r -> r.Cook());
    }
}

随着项目继续开发,我们发现系统里已经有一些 extra robots,额外机器人。这些机器人可能是公司其他团队已经写好的,也可能来自第三方 API。

这些已有机器人虽然也有类似"移动、攻击"等功能,但是它们的接口和我们当前项目规定的 Robot 接口不一样。

java 复制代码
public abstract class Machine {
    private int speed;

    public void SetSpeed(int speed) {
        this.speed = speed;
    }

    public int GetSpeed() {
        return speed;
    }

    public abstract void Kill();

    public abstract void Run(int x, int y);
}
class MilitaryMachine extends Machine {

    @Override
    public void Kill() {
        System.out.println("Aim for the heart");
    }

    @Override
    public void Run(int x, int y) {
        System.out.println(
            "Soldier runs to " + x + ", " + y + " at the speed of " + GetSpeed()
        );
    }
}
class ZombieMachine extends Machine {

    @Override
    public void Kill() {
        System.out.println("Aim for the brain");
    }

    @Override
    public void Run(int x, int y) {
        System.out.println(
            "Zombie runs to " + x + ", " + y + " at the speed of " + GetSpeed()
        );
    }
}

这些 Machine 类已经存在,而且有用。

但是我们的系统 PlayRobot 只能使用 Robot 接口。

所以会出现接口不兼容,这时就需要 Adapter(适配器)。把 Machine 包装成 Robot。

java 复制代码
class MachineAdapter implements Robot {
    private Machine m;

    public MachineAdapter(Machine m) {
        this.m = m;
    }

    @Override
    public void Move(int x, int y, int speed) {
        m.SetSpeed(speed);
        m.Run(x, y);
    }

    @Override
    public void Cook() {
        if (m instanceof ZombieMachine) {
            System.out.println("Cooking Zombie food");
        } else if (m instanceof MilitaryMachine) {
            System.out.println("Cooking Soldier food");
        } else {
            System.out.println("Cooking cannot be completed");
        }
    }
}

这样 MachineAdapter 把 Machine 系列对象转换成 Robot 接口对象,现在 PlayRobot 可以统一管理它们。

1.4.2.2.3 应用

当我们想使用某个已经存在的类,但是它的接口和你当前代码系统不兼容时,可以使用适配器类。

例如前面的 Machine 和 Robot。

当我们想复用多个已有子类,但是这些子类缺少某些共同功能,并且这些共同功能又不能直接加到它们的父类中时,也可以使用适配器模式。

比如前面的 MilitaryMachine、ZombieMachine类,的父类我们不能随便修改,当前系统又要求对象必须符合 Robot 接口。这时可以用一个适配器统一处理:

java 复制代码
Robot r1 = new MachineAdapter(new MilitaryMachine());
Robot r2 = new MachineAdapter(new ZombieMachine());

play.AddRobot(r1);
play.AddRobot(r2);
1.4.2.2.4 适配器模式练习1

用 Adapter Pattern 适配器模式 设计一个电商支付系统。

在一个电商系统中,用户可以选择不同的支付方式,比如:PayPal、信用卡、加密货币。

每一种支付方式都有自己的 API 和接入方式。

比如:PayPal 可能用 paypal.pay()

信用卡可能用 card.charge()

加密货币可能用 crypto.transfer()

电商系统本身希望有一个统一的支付接口,比如:pay(amount)

因此现在适配器模式可以给每一种支付方式写一个适配器,让它们都符合统一接口。

示例代码如下:

java 复制代码
interface PaymentProcessor {
    void pay(double amount);
}

class PayPalPayment {
    public void sendPayment(double amount) {
        System.out.println("PayPal payment completed: $" + amount);
    }
}

class CreditCardPayment {
    public void chargeCard(String cardNumber, double amount) {
        System.out.println("Credit card payment completed");
        System.out.println("Card number: " + cardNumber);
        System.out.println("Amount: $" + amount);
    }
}

class CryptoPayment {
    public void transferCrypto(String walletAddress, double amount) {
        System.out.println("Cryptocurrency payment completed");
        System.out.println("Wallet address: " + walletAddress);
        System.out.println("Amount: $" + amount);
    }
}

class PayPalAdapter implements PaymentProcessor {
    private PayPalPayment payPalPayment;

    public PayPalAdapter(PayPalPayment payPalPayment) {
        this.payPalPayment = payPalPayment;
    }

    @Override
    public void pay(double amount) {
        payPalPayment.sendPayment(amount);
    }
}

class CreditCardAdapter implements PaymentProcessor {
    private CreditCardPayment creditCardPayment;
    private String cardNumber;

    public CreditCardAdapter(CreditCardPayment creditCardPayment, String cardNumber) {
        this.creditCardPayment = creditCardPayment;
        this.cardNumber = cardNumber;
    }

    @Override
    public void pay(double amount) {
        creditCardPayment.chargeCard(cardNumber, amount);
    }
}

class CryptoAdapter implements PaymentProcessor {
    private CryptoPayment cryptoPayment;
    private String walletAddress;

    public CryptoAdapter(CryptoPayment cryptoPayment, String walletAddress) {
        this.cryptoPayment = cryptoPayment;
        this.walletAddress = walletAddress;
    }

    @Override
    public void pay(double amount) {
        cryptoPayment.transferCrypto(walletAddress, amount);
    }
}

class ECommerceSystem {
    public void processPayment(PaymentProcessor paymentProcessor, double amount) {
        paymentProcessor.pay(amount);
    }
}

public class Main {
    public static void main(String[] args) {
        ECommerceSystem shop = new ECommerceSystem();

        PaymentProcessor paypal = new PayPalAdapter(new PayPalPayment());

        PaymentProcessor creditCard = new CreditCardAdapter(
                new CreditCardPayment(),
                "1234 5678 9999 0000"
        );

        PaymentProcessor crypto = new CryptoAdapter(
                new CryptoPayment(),
                "0xABC123456789"
        );

        System.out.println("User chooses PayPal:");
        shop.processPayment(paypal, 100.0);

        System.out.println();

        System.out.println("User chooses Credit Card:");
        shop.processPayment(creditCard, 250.0);

        System.out.println();

        System.out.println("User chooses Cryptocurrency:");
        shop.processPayment(crypto, 500.0);
    }
}

这里的对应关系如下:

复制代码
PaymentProcessor
客户端需要的统一支付接口

PayPalPayment
已有的 PayPal 支付系统

CreditCardPayment
已有的信用卡支付系统

CryptoPayment
已有的加密货币支付系统

PayPalAdapter
把 PayPalPayment 适配成 PaymentProcessor

CreditCardAdapter
把 CreditCardPayment 适配成 PaymentProcessor

CryptoAdapter
把 CryptoPayment 适配成 PaymentProcessor

ECommerceSystem
电商系统客户端,只调用统一的 pay 方法

这样电商系统只需要调用:

java 复制代码
shop.processPayment(paymentProcessor, amount);

不同支付方式内部具体怎么支付,由各自的 Adapter 负责转换。

相关推荐
mit6.8241 小时前
20种Agent 设计模式
人工智能·设计模式
workflower1 小时前
企业酝酿数智化内驱力
大数据·人工智能·设计模式·机器人·动态规划
威尔逊·柏斯科·希伯理1 小时前
软考-软件工程(1-软件工程基础与开发方法)
软件工程
likerhood2 小时前
java设计模式 · 适配器模式 (Adapter Pattern)
java·设计模式·适配器模式
蜡笔小马3 小时前
04.C++设计模式-桥接模式
c++·设计模式·桥接模式
geovindu4 小时前
go:Condition Variable Pattern
开发语言·后端·设计模式·golang·条件变量模式
geovindu5 小时前
Python: Condition Variable Pattern
开发语言·python·设计模式·条件变量模式
身如柳絮随风扬19 小时前
MyBatis 与 Spring 中的设计模式
spring·设计模式·mybatis
挨踢ren1 天前
单例模式:C++实现与多线程安全
c++·设计模式