Java设计模式大全:23种常见的设计模式详解(二)

本系列文章简介:

设计模式是在软件开发过程中,经过实践和总结得到的一套解决特定问题的可复用的模板。它是一种在特定情境中经过验证的经验和技巧的集合,可以帮助开发人员设计出高效、可维护、可扩展和可复用的软件系统。设计模式提供了一种在设计和编码过程中的指导,它用于解决常见的设计问题,并提供了一种标准化的方案。设计模式能够帮助开发人员降低系统的复杂性,提高代码的可读性和可维护性。本系列文章将详细讲解Java中的23中设计模式 ,并配有图文解析及相应的代码示例,欢迎大家订阅《Java技术栈高级攻略》专栏,一起学习,一起涨分!

目录

1、引言

2、设计模式详解

[2.1 结构型模式(Structural)](#2.1 结构型模式(Structural))

[2.1.1 适配器模式(Adapter Pattern)](#2.1.1 适配器模式(Adapter Pattern))

[2.1.1.1 介绍](#2.1.1.1 介绍)

[2.1.1.2 优缺点](#2.1.1.2 优缺点)

[2.1.1.3 使用场景](#2.1.1.3 使用场景)

[2.1.1.4 使用案例](#2.1.1.4 使用案例)

[2.1.2 桥接模式(Bridge Pattern)](#2.1.2 桥接模式(Bridge Pattern))

[2.1.2.1 介绍](#2.1.2.1 介绍)

[2.1.2.2 优缺点](#2.1.2.2 优缺点)

[2.1.2.3 使用场景](#2.1.2.3 使用场景)

[2.1.2.4 使用案例](#2.1.2.4 使用案例)

[2.1.3 组合模式(Composite Pattern)](#2.1.3 组合模式(Composite Pattern))

[2.1.3.1 介绍](#2.1.3.1 介绍)

[2.1.3.2 优缺点](#2.1.3.2 优缺点)

[2.1.3.3 使用场景](#2.1.3.3 使用场景)

[2.1.3.4 使用案例](#2.1.3.4 使用案例)

[2.1.4 装饰器模式(Decorator Pattern)](#2.1.4 装饰器模式(Decorator Pattern))

[2.1.4.1 介绍](#2.1.4.1 介绍)

[2.1.4.2 优缺点](#2.1.4.2 优缺点)

[2.1.4.3 使用场景](#2.1.4.3 使用场景)

[2.1.4.4 使用案例](#2.1.4.4 使用案例)

[2.1.5 外观模式(Facade Pattern)](#2.1.5 外观模式(Facade Pattern))

[2.1.5.1 介绍](#2.1.5.1 介绍)

[2.1.5.2 优缺点](#2.1.5.2 优缺点)

[2.1.5.3 使用场景](#2.1.5.3 使用场景)

[2.1.5.4 使用案例](#2.1.5.4 使用案例)

[2.1.6 享元模式(Flyweight Pattern)](#2.1.6 享元模式(Flyweight Pattern))

[2.1.6.1 介绍](#2.1.6.1 介绍)

[2.1.6.2 优缺点](#2.1.6.2 优缺点)

[2.1.6.3 使用场景](#2.1.6.3 使用场景)

[2.1.6.4 使用案例](#2.1.6.4 使用案例)

[2.1.7 代理模式(Proxy Pattern)](#2.1.7 代理模式(Proxy Pattern))

[2.1.7.1 介绍](#2.1.7.1 介绍)

[2.1.7.2 优缺点](#2.1.7.2 优缺点)

[2.1.7.3 使用场景](#2.1.7.3 使用场景)

[2.1.7.4 使用案例](#2.1.7.4 使用案例)

[2.2 行为型模式(Behavioral)](#2.2 行为型模式(Behavioral))

3、结语


1、引言

设计模式是一种解决常见软件设计问题的经验总结,它提供了一套可重用的设计思想和方法,帮助开发人员更好地组织和设计他们的代码。在软件开发中,我们经常会遇到一些常见的问题,比如如何实现代码的灵活性、可扩展性、可维护性和可复用性,以及如何减少代码的耦合性等。设计模式通过定义一些通用的解决方案来解决这些问题,从而提高代码的质量和可维护性。

设计模式的概念最早由四位名为Gang of Four(GoF)的作者提出,他们在《设计模式:可复用面向对象软件的基础》一书中总结了23种常见的设计模式。这些设计模式包括创建型模式、结构型模式和行为型模式,它们分别用于解决对象创建、对象组合和对象交互等问题。每个设计模式都有其固定的结构和用法,开发人员可以根据具体的问题选择合适的设计模式来解决。

本文将跟随《Java设计模式大全:23种常见的设计模式详解(一)》的进度,继续介绍Java中常见的设计模式,详细解释它们的原理、应用场景和使用方法。通过学习这些设计模式,开发人员可以更好地理解软件设计的原则和思想,提高自己的设计能力和编码水平。设计模式不仅是一种编码技巧,更是一种思维方式和设计原则的体现。希望通过本文的介绍,读者能够更好地掌握和应用设计模式,写出更优雅、可扩展和可维护的代码。

2、设计模式详解

2.1 结构型模式(Structural)

2.1.1 适配器模式(Adapter Pattern)

2.1.1.1 介绍

适配器模式是一种结构型设计模式,它允许现有的类与其他接口进行协同工作,而不需要修改原始类的代码。 适配器模式主要解决的问题是两个接口不兼容的情况下的协同工作。它包含一个适配器类,该适配器类实现了一个目标接口,并持有一个需要适配的类的实例。适配器类通过调用适配对象的方法,并将其结果转换为目标接口的形式,从而实现了接口的适配。 适配器模式有以下几个角色:

  • 目标接口:定义客户端使用的接口,客户端通过该接口与适配器交互。
  • 适配器类:实现目标接口,同时持有需要适配的类的实例,并将调用转发给适配对象。
  • 适配对象:需要适配的类,在适配器中被调用。 适配器模式的优点包括:
  • 可以让现有类与其他类协同工作,提高代码的复用性和灵活性。
  • 可以将类的实现细节与客户端代码分离,提高代码的可维护性。 适配器模式的缺点包括:
  • 引入了一个额外的适配器类,增加了代码的复杂性。
  • 在适配过程中可能会引入一些性能损耗。 适配器模式可以在各种场景下使用,特别适用于以下情况:
  • 当需要使用一个已经存在的类,但其接口与需要的接口不兼容时。
  • 当需要扩展一个类的功能,但不修改其原始代码时。 总而言之,适配器模式提供了一种通过适配器类将不兼容的接口转换为兼容接口的方法,从而实现类的协同工作,提高代码的复用性和灵活性。
2.1.1.2 优缺点

适配器模式的优点如下:

  1. 可以让不兼容的类能够协同工作,提高代码的复用性。
  2. 可以将修改现有代码的工作降至最小,避免对现有代码的大规模修改。
  3. 可以通过适配器模式,将多个类的功能进行统一,提供统一的接口给客户端使用,降低了客户端的复杂度。
  4. 可以扩展现有的系统,而不影响现有系统的稳定性。

适配器模式的缺点如下:

  1. 增加了系统的复杂性,引入了新的代码,增加了代码的维护成本。
  2. 适配器模式会增加代码的执行时间和内存消耗,可能会影响系统的性能。
  3. 使用适配器模式可能会导致系统的设计变得过于复杂,不易理解和维护。

总的来说,适配器模式在解决不同类之间接口不兼容的问题上具有明显的优势,但在实际使用时需要权衡其使用带来的复杂性和性能损耗。

2.1.1.3 使用场景

适配器模式的使用场景包括以下情况:

  1. 将一个类的接口转换成客户端所期望的另一个接口。当客户端代码依赖于一个接口,而实际提供的类的接口不匹配时,可以通过适配器模式来适配这两个接口。

  2. 在不修改已有代码的情况下,为已有类新增功能。通过在适配器中组合已有类的实例,可以为该实例新增或修改方法,以满足客户端的需求。

  3. 与第三方库或组件进行集成。当我们在使用一个第三方库或组件时,有时候需要将其接口转换成我们系统内部的接口,这时可以使用适配器模式来实现。

  4. 对象的结构和行为需要动态变化。适配器模式可以通过不同的适配器,让对象根据需求来动态适配不同的接口。

  5. 需要复用一些现有类,但是该类的接口与需要的接口不一致。适配器模式可以将现有类的接口进行适配,以满足新的需求。

总之,适配器模式主要针对接口不匹配的情况,通过适配器来统一接口,使得客户端代码能够正常工作。

2.1.1.4 使用案例

适配器模式是一种结构型设计模式,它允许将一个类的接口转换为客户端所期望的另一个接口。适配器模式的目标是使不兼容的类可以一起工作。在Java中,适配器模式通常用于将旧的类或第三方库与新的代码集成在一起,而无需对原有代码进行修改。

下面我们来看一个具体的Java使用案例,以说明适配器模式的应用。

假设我们正在开发一个电子商务网站,我们使用的支付接口是第三方支付服务提供商提供的。该支付服务提供商有一个名为"PaymentService"的类,它具有以下方法:

java 复制代码
public class PaymentService {
    public void makePayment(String paymentType, double amount) {
        // 这里是实际调用第三方支付服务的代码
        System.out.println("Use third-party payment service to make payment");
    }
}

我们现在需要将这个第三方支付服务与我们自己的代码集成在一起。我们的代码中已经定义了一个名为"PaymentProcessor"的接口,它具有以下方法:

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

我们可以创建一个适配器类,将第三方支付服务的"PaymentService"类适配到我们的"PaymentProcessor"接口上。适配器类实现"PaymentProcessor"接口,并在实现方法中调用第三方支付服务的方法。

java 复制代码
public class PaymentServiceAdapter implements PaymentProcessor {

    private PaymentService paymentService;

    public PaymentServiceAdapter(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    @Override
    public void pay(String paymentType, double amount) {
        // 在适配器中调用第三方支付服务的makePayment方法
        paymentService.makePayment(paymentType, amount);
    }
}

现在,我们可以将适配器类作为"PaymentProcessor"接口的实现类,将第三方支付服务与我们自己的代码集成起来。

java 复制代码
public class Main {

    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();
        PaymentProcessor paymentProcessor = new PaymentServiceAdapter(paymentService);

        paymentProcessor.pay("creditCard", 100.00);
    }
}

在这个案例中,我们使用适配器模式将第三方支付服务的类适配到我们自己的代码中。通过适配器模式,我们可以在不修改原有代码的情况下实现与第三方支付服务的集成。

2.1.2 桥接模式(Bridge Pattern)

2.1.2.1 介绍

桥接模式是一种结构型设计模式,它将抽象部分和实现部分分离,使它们可以独立变化。桥接模式的关键是通过抽象和实现之间的桥梁来连接它们。

在桥接模式中,有两个关键角色:抽象化角色和实现化角色。抽象化角色负责定义抽象部分的接口,实现化角色负责实现抽象部分的具体实现。

通过桥接模式,抽象部分和实现部分可以独立变化,使得系统更加灵活。它可以在不修改现有的抽象部分和实现部分的前提下,增加新的抽象部分或实现部分。

2.1.2.2 优缺点

桥接模式的优点包括:

  1. 分离抽象和实现:桥接模式将抽象和实现分离,使得它们可以独立变化。这样可以增加系统的灵活性,可以在运行时动态地选择不同的实现。
  2. 扩展性:桥接模式可以很方便地扩展新的抽象部分和实现部分,而不需要修改已有的代码。只需要新增一个具体的抽象类和具体的实现类即可。
  3. 可维护性:由于桥接模式将抽象和实现分离,使得系统的每个部分独立变化,易于维护和理解。

桥接模式的缺点包括:

  1. 增加了系统的复杂度:桥接模式需要额外的抽象类和实现类,增加了系统的复杂度。
  2. 增加了代码量:由于桥接模式需要定义额外的抽象类和实现类,因此会增加代码量。
  3. 对客户端的使用要求较高:使用桥接模式时,客户端需要了解抽象和实现的细节,对客户端使用要求较高。
2.1.2.3 使用场景

桥接模式的使用场景包括:

  1. 当一个类存在多个变化维度时,可以使用桥接模式将这些变化维度分离,使得它们能够独立地变化。
  2. 当一个类需要在运行时动态地选择一个实现时,可以使用桥接模式。
  3. 当一个类的实现有多个不相关的维度时,可以使用桥接模式将它们分离开来,从而避免类的爆炸性增长。
  4. 当一个类需要通过继承来扩展功能时,可以使用桥接模式代替继承,避免类的继承层次过深而导致的复杂性增加。
  5. 当需要在抽象和实现之间引入更多的灵活性时,可以使用桥接模式。
2.1.2.4 使用案例

桥接模式是一种结构型设计模式,它将抽象部分与实现部分分离,以便它们可以独立地变化。在桥接模式中,抽象部分包含一个指向具体实现的引用,从而将两者解耦。

下面是一个使用桥接模式的简单示例,假设我们正在构建一个咖啡机,其中有几种不同类型的咖啡和不同类型的添加物(例如牛奶、糖等)。我们希望能够轻松地增加新的咖啡和添加物类型,而不需要修改现有的代码。

首先,我们定义咖啡接口Coffee和添加物接口Additive:

java 复制代码
// 咖啡接口
public interface Coffee {
    String getCoffeeType();
}

// 添加物接口
public interface Additive {
    String getDescription();
}

然后,我们创建几个具体的咖啡类和添加物类,实现相应的接口:

java 复制代码
// 普通咖啡类
public class RegularCoffee implements Coffee {
    @Override
    public String getCoffeeType() {
        return "Regular Coffee";
    }
}

// 浓缩咖啡类
public class EspressoCoffee implements Coffee {
    @Override
    public String getCoffeeType() {
        return "Espresso Coffee";
    }
}

// 牛奶类
public class Milk implements Additive {
    @Override
    public String getDescription() {
        return "Milk";
    }
}

// 糖类
public class Sugar implements Additive {
    @Override
    public String getDescription() {
        return "Sugar";
    }
}

接下来,我们定义一个咖啡机类,该类接受咖啡和添加物作为参数,并使用桥接模式将它们组合在一起:

java 复制代码
// 咖啡机类
public class CoffeeMachine {
    private Coffee coffee;
    private Additive additive;

    public CoffeeMachine(Coffee coffee, Additive additive) {
        this.coffee = coffee;
        this.additive = additive;
    }

    public void makeCoffee() {
        System.out.println("Making " + coffee.getCoffeeType() + " with " + additive.getDescription());
    }
}

最后,我们可以在客户端代码中创建不同类型的咖啡并添加不同类型的添加物,并使用咖啡机来制作咖啡:

java 复制代码
public class Main {
    public static void main(String[] args) {
        CoffeeMachine machine1 = new CoffeeMachine(new RegularCoffee(), new Milk());
        machine1.makeCoffee();

        CoffeeMachine machine2 = new CoffeeMachine(new EspressoCoffee(), new Sugar());
        machine2.makeCoffee();
    }
}

输出结果为:

Making Regular Coffee with Milk
Making Espresso Coffee with Sugar

这个例子展示了桥接模式的用法,通过将咖啡和添加物两个不同的抽象部分桥接在一起,我们可以轻松地组合它们来制作不同类型的咖啡。如果我们想要增加新的咖啡或添加物类型,只需要创建新的具体类并实现相应的接口,而无需修改现有的代码。这种解耦的设计使得系统更加灵活和可扩展。

2.1.3 组合模式(Composite Pattern)

2.1.3.1 介绍

组合模式是一种结构型设计模式,它允许你将对象组合成树形结构来表示"整体-部分"的层次结构。组合能让客户端以一致的方式处理个别对象以及对象组合。

使用组合模式,你可以使用简单对象(叶节点)和组合对象(容器节点)来表示复杂的层次结构。这样一来,客户端代码就能够统一对待所有对象,即不论是不是容器,都可以像对待单个对象一样对待它们。

组合模式有以下几个角色:

  • 抽象组件(Component):定义了对象的共同接口,可以用于管理所有子对象。
  • 叶节点(Leaf):是组件的基本实现,不能有子节点。
  • 容器节点(Composite):是组件的容器,可以包含其他子节点。
  • 客户端(Client):使用组合模式的对象。
2.1.3.2 优缺点

组合模式的优点:

  1. 简化客户端代码:组合模式能够使客户端代码更加简洁,因为客户端只需要面对一个统一的接口,无需关心对象是单个对象还是对象的组合。

  2. 扩展性好:通过组合模式,可以很容易地增加新的组合对象或叶子对象,而且对现有的结构不会产生任何影响。

  3. 更好地表示树形结构:组合模式能够更好地表示树形结构,使得代码更加直观、易于理解。

  4. 降低复杂性:组合模式将多个对象组合起来,通过统一的接口对待,从而降低了系统的复杂性。

组合模式的缺点:

  1. 可能导致系统过于庞大:当组合对象过多时,可能会导致整个系统过于庞大,影响系统的性能。

  2. 不够灵活:组合模式中的组合对象和叶子对象有相同的接口,但是却不能完全互换使用,这可能会导致一些不够灵活的问题。

  3. 可能增加系统的复杂性:虽然组合模式可以降低系统的复杂性,但是在某些情况下,可能会增加系统的复杂性,特别是在处理叶子对象和组合对象的逻辑时。

总的来说,组合模式适用于表示树形结构的场景,可以简化客户端代码,提高系统的扩展性,但是也可能导致系统过于庞大,不够灵活,并增加系统的复杂性。

2.1.3.3 使用场景

组合模式适用于以下场景:

  1. 当存在一组对象,且这些对象可以被统一对待时,可以使用组合模式。例如,一个树形结构中的节点,无论是叶子节点还是非叶子节点,都可以被视为一个节点对象。

  2. 当需要对对象的结构进行递归组合,并能够同时对单个对象和组合对象进行操作时,可以使用组合模式。例如,在一个文件系统中,文件夹可以包含文件和其他文件夹,用户可以对文件夹进行操作,也可以对文件进行操作。

  3. 当希望忽略对象与组合对象之间的差异,统一对待时,可以使用组合模式。例如,一个菜单系统中的菜单项,无论是顶级菜单项还是子菜单项,都可以调用同样的方法进行操作。

  4. 当希望动态地组合和调整对象的结构时,可以使用组合模式。例如,在图形绘制软件中,用户可以通过拖拽和复制粘贴操作,动态地组合和调整图形对象的结构。

总之,组合模式适用于任何涉及对象的层次结构和树状结构的场景,以及希望对对象进行统一处理的场景。

2.1.3.4 使用案例

组合模式是一种结构型设计模式,它允许我们将对象组合成树形结构来表示"整体-部分"关系。组合模式使得客户端可以统一对待单个对象和组合对象,从而简化了客户端的代码。

下面是一个使用组合模式的简单 Java 示例:

java 复制代码
// 组件接口
public interface Component {
    void show();
}

// 叶子组件
public class Leaf implements Component {
    private String name;
    
    public Leaf(String name) {
        this.name = name;
    }
    
    public void show() {
        System.out.println(name);
    }
}

// 容器组件
public class Composite implements Component {
    private List<Component> components = new ArrayList<>();
    
    public void addComponent(Component component) {
        components.add(component);
    }
    
    public void removeComponent(Component component) {
        components.remove(component);
    }
    
    public void show() {
        for (Component component : components) {
            component.show();
        }
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 创建一个容器组件
        Composite composite = new Composite();
        
        // 创建两个叶子组件
        Leaf leaf1 = new Leaf("Leaf 1");
        Leaf leaf2 = new Leaf("Leaf 2");
        
        // 将叶子组件添加到容器组件中
        composite.addComponent(leaf1);
        composite.addComponent(leaf2);
        
        // 调用容器组件的 show 方法,会递归调用所有叶子组件的 show 方法
        composite.show();
    }
}

运行以上代码,输出结果为:

Leaf 1
Leaf 2

在上述示例中,Component 接口是组件类的基础接口,定义了 show 方法。Leaf 类是叶子组件,它实现了 Component 接口,并在 show 方法中打印了自己的名字。Composite 类是容器组件,它也实现了 Component 接口,并包含一个组件列表。Composite 类的 addComponent 方法用于向容器组件中添加组件,removeComponent 方法用于移除组件。Composite 类的 show 方法会递归调用所有组件的 show 方法,从而展示整个树形结构。

以上是一个简单的使用组合模式的 Java 示例,通过组合模式,我们可以轻松地处理具有层次结构的对象,并对整体和部分进行一致的处理。这种模式在处理树状结构的问题时非常有用,例如处理文件系统、图形界面等。

2.1.4 装饰器模式(Decorator Pattern)

2.1.4.1 介绍

装饰器模式是一种结构型设计模式,它允许向现有对象动态地添加新功能,同时不改变其结构。该模式通过创建一个包装对象来实现,这个包装对象包含了被装饰对象的引用,并且可以在其方法调用前后执行额外的操作。

在装饰器模式中,装饰器类和被装饰对象都继承于同一个接口或者抽象类,这样可以保证装饰器和被装饰对象具有相同的类型。装饰器类在实现接口的同时,还包含一个被装饰对象的引用。

当需要对被装饰对象进行额外的功能扩展时,可以通过创建对应的装饰器类来实现。装饰器类可以在调用被装饰对象的方法之前、之后或者替代其原有方法来执行额外的操作。装饰器类可以嵌套使用,以实现多层次功能扩展。

2.1.4.2 优缺点

装饰器模式是一种结构型设计模式,它允许通过将对象作为参数传递给装饰器函数或方法来动态地添加功能或行为,而无需修改已有的代码。

优点:

  1. 装饰器模式提供了一种灵活的方式来扩展或修改对象的行为,而无需修改原始对象的代码。这样可以遵循开放封闭原则,即对扩展是开放的,对修改是封闭的。
  2. 可以通过组合多个装饰器来实现复杂的功能组合,从而提供更灵活、可定制的行为。
  3. 装饰器模式遵循单一职责原则,每个装饰器只关注一个具体的功能或行为的实现。
  4. 可以通过装饰器来在运行时动态地添加或删除功能,而无需修改已有的代码。

缺点:

  1. 过多的装饰器会导致类的层级结构变得复杂,增加了系统的复杂性和理解难度。
  2. 装饰器模式增加了很多小对象的数量,可能会导致性能下降。
  3. 如果使用不当,装饰器模式可能会造成代码的混乱和不易维护。

总之, 装饰器模式是一种非常常用的设计模式,它可以灵活地扩展对象的功能,提供了一种切实可行的解决方案。但是在使用时需要权衡好灵活性和复杂性之间的平衡,并遵循设计原则和最佳实践,以确保代码的可维护性和可扩展性。

2.1.4.3 使用场景

装饰器模式适用于以下情况:

  1. 在不修改原有对象的结构和功能的情况下,动态地给对象添加新的功能。
  2. 需要在运行时动态地给对象添加或删除功能。
  3. 需要通过多个不同的方式组合和使用对象的功能。
  4. 需要扩展一个类的功能,但是使用子类进行扩展不切实际或者不可行。
  5. 需要对一个对象的功能进行动态地开闭。

实际应用中,装饰器模式常用于以下场景:

  1. 日志记录:可以通过装饰器模式在不修改原有代码的情况下,给方法添加日志记录的功能。
  2. 缓存:可以通过装饰器模式在不修改原有代码的情况下,给方法添加缓存功能。
  3. 权限控制:可以通过装饰器模式在不修改原有代码的情况下,给方法添加权限控制的功能。
  4. 计时统计:可以通过装饰器模式在不修改原有代码的情况下,给方法添加计时统计的功能。
  5. 输入验证:可以通过装饰器模式在不修改原有代码的情况下,给方法添加输入验证的功能。

总之,装饰器模式可以在不修改原有代码的情况下,给对象添加新的功能,使得代码更加灵活和可扩展。

2.1.4.4 使用案例

装饰器模式是一种结构型设计模式,它允许在运行时通过将对象放入包装器的方式来扩展对象的功能。装饰器模式可以动态地给一个对象添加额外的功能。

下面是一个使用装饰器模式的Java案例:

假设我们有一个简单的汽车类Car,它有一个run()方法用于启动汽车。

java 复制代码
public interface Car {
    void run();
}

然后我们可以有一个基本实现的CarImpl类:

java 复制代码
public class CarImpl implements Car {
    @Override
    public void run() {
        System.out.println("汽车启动了!");
    }
}

现在我们想要给Car类添加额外的功能,例如,我们想要在启动汽车之前打印一条日志。

首先,我们创建一个装饰器接口CarDecorator,它也实现Car接口:

java 复制代码
public interface CarDecorator extends Car {
}

然后,我们创建一个具体的装饰器类LoggingDecorator,它实现了CarDecorator接口并重写了run()方法,在启动汽车之前打印一条日志:

java 复制代码
public class LoggingDecorator implements CarDecorator {
    private Car car;

    public LoggingDecorator(Car car) {
        this.car = car;
    }

    @Override
    public void run() {
        System.out.println("开始记录日志...");
        car.run();
    }
}

最后,我们可以在主程序中使用装饰器来扩展汽车的功能:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Car car = new CarImpl();
        CarDecorator decoratedCar = new LoggingDecorator(car);
        
        decoratedCar.run();
    }
}

输出结果为:

开始记录日志...
汽车启动了!

通过装饰器模式,我们可以在不修改原始对象的情况下添加新的功能。在这个例子中,我们使用LoggingDecorator装饰器来给Car类添加了记录日志的功能。

2.1.5 外观模式(Facade Pattern)

2.1.5.1 介绍

外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简单的接口,隐藏了系统复杂的子系统接口,使得客户端更容易使用子系统。

外观模式通过封装一组复杂的子系统接口,提供一个高层次的接口,以简化客户端的使用。客户端只需要调用外观类的方法,而不需要直接与子系统进行交互。外观类负责将客户端的请求委派给相应的子系统进行处理。

2.1.5.2 优缺点

以下是外观模式的优点和缺点:

优点:

  1. 简化接口:外观模式旨在提供一个简化的接口,使得子系统更加易于使用。客户端只需通过外观类与系统交互,而不需要了解系统内部的复杂逻辑和细节。
  2. 解耦系统:外观模式将客户端与子系统之间的依赖解耦,使得系统的各个模块更加独立。这样一来,系统的各个模块可以更加灵活地变化和演化,而不会对其他模块产生影响。
  3. 提高可维护性:由于外观模式将复杂的子系统封装起来,使得系统的代码更加简洁和易于理解。这样一来,系统的可维护性和可读性都会得到提高。

缺点:

  1. 不符合开闭原则:外观模式在设计时需要事先考虑好可能需要加入新的子系统或者删除现有子系统的情况。如果需要添加一个新的子系统,就需要修改外观类的代码,这违背了开闭原则。
  2. 可能会产生性能问题:由于外观模式是通过增加一层额外的抽象来简化系统接口,可能会增加系统的复杂度,并且引入一定的额外开销。在一些对性能要求比较高的场景中,可能需要考虑避免使用外观模式。

总的来说,外观模式是一种简化复杂系统的有效手段,可以提高系统的易用性和可维护性,但需要注意在设计时遵循开闭原则,以及在性能敏感的场景中谨慎使用。

2.1.5.3 使用场景

外观模式在以下场景中适用:

  1. 当一个复杂系统中的子系统很多,并且它们之间存在复杂的依赖关系时,可以使用外观模式来简化客户端与子系统之间的交互。

  2. 当需要为一个复杂的子系统提供一个简单的接口时,可以使用外观模式来封装子系统的复杂性,使得客户端只需要与外观对象交互。

  3. 当需要将子系统的相关接口进行分层时,可以使用外观模式来定义一个高层接口,让子系统只需要依赖外观接口即可。

  4. 当想要对客户端隐藏复杂的子系统实现细节时,可以使用外观模式来封装子系统,使得客户端不需要了解子系统的具体实现。

总之,外观模式适用于需要简化客户端与复杂子系统之间的交互、隐藏子系统实现细节、分层子系统接口等情况下。

2.1.5.4 使用案例

外观模式(Facade Pattern)是一种结构型设计模式,它提供了一个简化接口,隐藏了子系统的复杂性,并将客户端与子系统之间的依赖关系解耦。

在Java中,外观模式经常被用于简化复杂的类库或框架的使用方法,使得客户端可以更方便地使用这些功能。下面是一个简单的外观模式的使用案例。

假设我们有一个电子商务网站,客户端需要完成以下购买流程:选择商品、添加到购物车、填写配送地址、选择支付方式、确认订单、完成支付。每个步骤都涉及到多个子系统的交互。

首先,我们需要创建一个外观类(Facade)来封装整个购买流程。该类提供了一个简化的接口,隐藏了购买流程中底层子系统的复杂性。

java 复制代码
public class PurchaseFacade {
    private ProductSelectionService productSelectionService;
    private ShoppingCartService shoppingCartService;
    private ShippingService shippingService;
    private PaymentService paymentService;
    private OrderConfirmationService orderConfirmationService;

    public PurchaseFacade() {
        this.productSelectionService = new ProductSelectionService();
        this.shoppingCartService = new ShoppingCartService();
        this.shippingService = new ShippingService();
        this.paymentService = new PaymentService();
        this.orderConfirmationService = new OrderConfirmationService();
    }

    public void purchaseProduct(String productId, String address, String paymentMethod) {
        Product product = productSelectionService.selectProduct(productId);
        shoppingCartService.addToCart(product);
        shoppingCartService.checkout();
        shippingService.setShippingAddress(address);
        paymentService.setPaymentMethod(paymentMethod);
        paymentService.makePayment();
        orderConfirmationService.confirmOrder();
    }
}

接下来,我们来实现购买流程中的各个子系统。

java 复制代码
public class ProductSelectionService {
    public Product selectProduct(String productId) {
        // 选择商品
        return new Product();
    }
}

public class ShoppingCartService {
    public void addToCart(Product product) {
        // 添加到购物车
    }

    public void checkout() {
        // 结算购物车
    }
}

public class ShippingService {
    public void setShippingAddress(String address) {
        // 设置配送地址
    }
}

public class PaymentService {
    public void setPaymentMethod(String paymentMethod) {
        // 设置支付方式
    }

    public void makePayment() {
        // 支付
    }
}

public class OrderConfirmationService {
    public void confirmOrder() {
        // 确认订单
    }
}

最后,在客户端代码中使用外观类来完成购买流程。

java 复制代码
public class Client {
    public static void main(String[] args) {
        PurchaseFacade purchaseFacade = new PurchaseFacade();
        purchaseFacade.purchaseProduct("12345", "123 Street, City", "Credit Card");
    }
}

通过使用外观模式,客户端只需要与外观类进行交互,而无需了解底层子系统的复杂逻辑。这样就实现了将复杂性封装起来,提供了一个简化的接口来完成购买流程。

2.1.6 享元模式(Flyweight Pattern)

2.1.6.1 介绍

享元模式是一种结构型设计模式,用于通过共享对象来有效地支持大量细粒度的对象。

在享元模式中,共享对象被细分为可共享和不可共享两种状态。可共享的对象可以在多个上下文中被共享和重复利用,而不可共享的对象是上下文特定的,无法被共享。

享元模式的核心思想是通过共享对象来减少系统中的对象数量,从而降低内存消耗和提高系统性能。它将对象的特征分为内部状态和外部状态,其中内部状态可以共享,而外部状态是对象在各个上下文中不同的部分,必须在运行时通过参数传递给享元对象。

使用享元模式时,需要创建一个享元工厂类来管理共享对象的创建和管理。当需要创建一个对象时,工厂类首先检查是否已经存在一个可共享的对象,如果存在则返回该对象,否则创建一个新对象并将其放入共享池中。

2.1.6.2 优缺点

享元模式的优点:

  1. 减少内存使用:享元模式通过共享对象来减少内存使用,将对象的状态分为内部状态和外部状态,内部状态可以被多个对象共享,外部状态可以在运行时设置。

  2. 提高性能:由于共享对象的使用,减少了对象的创建和销毁的次数,提高了系统的性能。

  3. 简化系统:使用享元模式可以简化系统,将对象的状态分离出来,使系统的结构更加清晰。

享元模式的缺点:

  1. 增加了系统的复杂度:为了实现对象的共享,在享元模式中需要将对象的状态进行分离,这增加了系统的复杂度。

  2. 对象的共享可能导致线程安全问题:如果多个线程同时访问共享的对象,并对对象的状态进行修改,可能会导致线程安全问题,需要采取相应的措施来解决。

2.1.6.3 使用场景

享元模式的使用场景包括以下几种:

  1. 对象的数量庞大:如果一个系统中需要创建大量相似的对象,且这些对象可以共享一些相同的状态,使用享元模式可以减少对象的创建数量,节省系统资源。

  2. 对象的创建和销毁代价大:如果创建和销毁对象的代价很大,而且系统中需要频繁使用这些对象,使用享元模式可以复用已经创建的对象,避免频繁地创建和销毁对象,提高系统性能。

  3. 对象的状态可以被共享:如果对象的状态可以分为内部状态和外部状态,其中内部状态是不变的,而外部状态是可变的,使用享元模式可以复用内部状态,而只需要在外部状态发生变化时重新创建对象。

  4. 对象需要进行共享和协作:如果系统中的多个对象需要共享同一个对象,而且这些对象之间需要协作完成一些任务,使用享元模式可以提供一个共享对象,让这些对象从中获取需要的状态和行为。

  5. 对象的身份可以忽略:如果系统中的对象可以被视为相同的,而不需要区分它们的具体身份,使用享元模式可以将相同的对象视为一种类型,简化系统结构。

2.1.6.4 使用案例

享元模式是一种结构型设计模式,它通过共享对象来减少内存使用和提高性能。在Java中,享元模式通常用于减少创建大量相似对象的开销。

下面是一个使用享元模式的案例,假设我们有一个图书馆系统,需要管理各种不同的书籍。每本书都有一个唯一的书号、书名和作者。为了节省内存,我们将使用享元模式来共享相同的作者对象。

首先,我们创建一个Author类,表示书籍的作者。它包含作者的姓名和国籍。

java 复制代码
public class Author {
    private String name;
    private String nationality;

    public Author(String name, String nationality) {
        this.name = name;
        this.nationality = nationality;
    }

    // getters and setters
}

然后,我们创建一个Book类,表示一本书。它包含书号、书名和作者。

java 复制代码
public class Book {
    private String bookNumber;
    private String title;
    private Author author;

    public Book(String bookNumber, String title, Author author) {
        this.bookNumber = bookNumber;
        this.title = title;
        this.author = author;
    }

    // getters and setters
}

接下来,我们创建一个BookFactory类,用于创建和管理书籍对象。它使用一个Map来存储已经创建的作者对象,以便在需要时共享使用。

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class BookFactory {
    private Map<String, Author> authors;

    public BookFactory() {
        authors = new HashMap<>();
    }

    public Book createBook(String bookNumber, String title, String authorName, String authorNationality) {
        Author author = authors.get(authorName);
        if (author == null) {
            author = new Author(authorName, authorNationality);
            authors.put(authorName, author);
        }

        return new Book(bookNumber, title, author);
    }
}

最后,我们可以使用BookFactory来创建书籍对象。如果两本书的作者是相同的,那么它们将共享同一个作者对象,以节省内存。

java 复制代码
public class Library {
    public static void main(String[] args) {
        BookFactory bookFactory = new BookFactory();

        Book book1 = bookFactory.createBook("B001", "Book 1", "Author 1", "Nationality 1");
        Book book2 = bookFactory.createBook("B002", "Book 2", "Author 1", "Nationality 1");
        Book book3 = bookFactory.createBook("B003", "Book 3", "Author 2", "Nationality 2");

        System.out.println(book1.getAuthor() == book2.getAuthor()); // true
        System.out.println(book1.getAuthor() == book3.getAuthor()); // false
    }
}

在这个例子中,BookFactory类充当享元工厂,Author类充当享元对象。通过使用享元模式,我们可以避免为每本书都创建新的作者对象。同时,我们可以通过检查作者对象的引用是否相同来判断两本书的作者是否相同。

2.1.7 代理模式(Proxy Pattern)

2.1.7.1 介绍

代理模式是一种结构型设计模式,它允许一个对象(代理对象)代理另一个对象(被代理对象),并控制对被代理对象的访问。

代理模式的主要目的是控制对被代理对象的访问,并且可以在不改变原有代码的情况下,增加一些额外的功能。

代理模式使用抽象接口来定义代理对象和被代理对象的公共方法,以确保代理对象和被代理对象之间的一致性。通过代理对象来间接访问被代理对象,可以在不改变原有代码的情况下,增加一些额外的功能,提高代码的灵活性和可复用性。

2.1.7.2 优缺点

代理模式的优点包括:

  1. 拓展功能:代理模式可以在不修改原始对象的情况下,为原始对象增加额外的功能。这使得代理模式具有非常好的拓展性,可以方便地增加新的功能。
  2. 保护原始对象:代理模式可以通过代理对象来控制对原始对象的访问权限。这样可以保护原始对象,防止不合法的访问或者修改。
  3. 控制资源消耗:代理模式可以在需要时延迟创建原始对象,从而节省资源消耗。例如,在访问远程服务器或者创建昂贵对象时,可以使用代理模式延迟创建对象,从而避免资源的浪费。

代理模式的缺点包括:

  1. 增加复杂性:引入代理对象会增加系统的复杂性,增加了代码的数量和复杂度。
  2. 增加间接性:使用代理模式会增加代码的间接性,降低了代码的直观性和可读性。
  3. 减慢速度:由于代理模式需要额外的处理过程,因此可能会降低系统的性能和速度。
2.1.7.3 使用场景

以下是代理模式的一些使用场景:

  1. 远程代理:当客户端和真实对象位于不同的地址空间时,可以使用代理模式通过网络进行通信。代理对象负责将客户端请求传递给真实对象,并返回结果给客户端。

  2. 虚拟代理:当真实对象的创建和初始化过程较为复杂时,可以使用代理模式延迟加载真实对象。代理对象可以在真正需要使用真实对象时再进行创建和初始化,从而提高系统性能。

  3. 安全代理:当需要对真实对象的访问进行控制时,可以使用代理模式进行权限验证。代理对象可以在对真实对象的访问之前进行身份验证,从而确保只有合法用户才能访问真实对象。

  4. 缓存代理:当需要缓存真实对象的结果时,可以使用代理模式。代理对象可以在接收到客户端请求后,先从缓存中查找结果,如果缓存中不存在,则将请求转发给真实对象,并将结果存入缓存中,从而提高系统性能。

  5. 日志记录代理:当需要记录对真实对象的访问日志时,可以使用代理模式。代理对象可以在对真实对象的方法调用前后进行日志记录,从而方便系统的调试和运行时的分析。

总而言之,代理模式适用于需要对真实对象进行控制或扩展的场景,可以提供额外的功能或限制对真实对象的访问。

2.1.7.4 使用案例

代理模式是一种结构型设计模式,它允许通过创建一个代理对象来控制对另一个对象的访问。

在Java中,代理模式有很多实际应用案例。以下是一个详细的案例。

假设有一个接口Image,用于加载和显示图像:

java 复制代码
public interface Image {
    void display();
}

同时,有一个实现了Image接口的具体类RealImage,用于加载和显示真实的图像文件:

java 复制代码
public class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("Loading image: " + filename);
    }

    public void display() {
        System.out.println("Displaying image: " + filename);
    }
}

现在,我们希望在加载和显示图像之前添加一些额外的逻辑,比如权限检查或者缓存。这时候可以使用代理模式。

我们创建一个代理类ImageProxy,实现Image接口,同时持有一个RealImage对象的引用,并在加载和显示图像之前添加额外的逻辑:

java 复制代码
public class ImageProxy implements Image {
    private String filename;
    private Image realImage;

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

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

现在,我们可以使用代理对象来加载和显示图像,同时在加载和显示之前进行一些额外的操作:

java 复制代码
public class Main {
    public static void main(String[] args) {
        Image image = new ImageProxy("image.jpg");
        image.display();
    }
}

输出:

Loading image: image.jpg
Displaying image: image.jpg

在上面的例子中,ImageProxy类充当了一个代理,在调用display()方法时,先进行了权限检查(如果需要),然后实例化了RealImage对象,并调用它的display()方法来加载和显示图像。

这样,我们就实现了一个简单的代理模式,通过代理对象来控制对RealImage对象的访问,同时可以在访问对象之前或之后添加额外的逻辑。

2.2 行为型模式(Behavioral)

详见《Java设计模式大全:23种常见的设计模式详解(三)

3、结语

文章至此,已接近尾声!希望此文能够对大家有所启发和帮助。同时,感谢大家的耐心阅读和对本文档的信任。在未来的技术学习和工作中,期待与各位大佬共同进步,共同探索新的技术前沿。最后,再次感谢各位的支持和关注。您的支持是作者创作的最大动力,如果您觉得这篇文章对您有所帮助,请考虑给予一点打赏。

相关推荐
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭4 分钟前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫21 分钟前
泛型(2)
java
南宫生30 分钟前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石38 分钟前
12/21java基础
java
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
李小白661 小时前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp1 小时前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
信号处理学渣1 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客1 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s1 小时前
Pandas
开发语言·python