跳转至

设计模式

设计原则

单一职责原则

单一职责原则(Simple Responsibility Principle,SRP)是最简单的面向对象设计原则,它用于控制类的粒度大小。

一个对象应该只包含单一的职责,并且该职责被完整地封装在一个类中。

将类的粒度进行更进一步的划分,会更加清晰。包括我们以后在设计 Mapper、Service、Controller 等等,根据不同的业务进行划分,都可以采用单一职责原则,以它作为我们实现高内聚低耦合的指导方针。实际上我们的微服务也是参考了单一职责原则,每个微服务只应担负一个职责。

开闭原则

开闭原则(Open Close Principle)

软件实体应当对扩展开放,对修改关闭。

如:

Java
public abstract class Coder {
    public abstract void coding();

    class JavaCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("Java");
        }
    }

    class PHPCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("PHP");
        }
    }

    class CppCoder extends Coder {
        @Override
        public void coding() {
            System.out.println("CPlusPlus");
        }
    }
}

通过提供一个 Coder 抽象类,定义出编程的行为,但是不进行实现,而是开放给其他具体类型的程序员来实现,这样就可以根据不同的业务进行灵活扩展了,具有较好的延续性。

里氏替换原则

里氏替换原则(Liskov Substitution Principle)是对子类型的特别定义。

所有引用基类的地方必须能透明地使用其子类的对象。

简单的说就是,子类可以扩展父类的功能,但不能改变父类原有的功能:

  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  2. 子类可以增加自己特有的方法。
  3. 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入/入参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的输出/返回值)要比父类更严格或与父类一样。

依赖倒转原则

依赖倒转原则(Dependence lnversion Principle)也是我们一直在使用的,最明显的就是我们的Spring框架了。

高层模块不应依赖于底层模块,它们都应该依赖抽象。抽象不应依赖于细节,细节应该依赖于抽象。

Java
public class Main {

    public static void main(String[] args) {
        UserController controller = new UserController();
    }

    static class UserMapper {
        //CRUD...
    }

    static class UserServiceNew {   //由于UserServiceNew发生变化,会直接影响到其他高层模块
        UserMapper mapper = new UserMapper();
        //业务代码....
    }

    static class UserController {   //焯,干嘛改底层啊,我这又得重写了
        UserService service = new UserService();   //哦豁,原来的不能用了
        UserServiceNew serviceNew = new UserServiceNew();   //只能修改成新的了
        //业务代码....
    }
}

我们发现,我们的各个模块之间实际上是具有强关联的,一个模块是直接指定依赖于另一个模块,虽然这样结构清晰,但是底层模块的变动,会直接影响到其他依赖于它的高层模块,如果我们的项目变得很庞大,那么这样的修改将是一场灾难。

而有了Spring框架之后,我们的开发模式就发生了变化:

Java
public class Main {

    public static void main(String[] args) {
        UserController controller = new UserController();
    }

    interface UserMapper {
        //接口中只做CRUD方法定义
    }

    static class UserMapperImpl implements UserMapper {
        //实现类完成CRUD具体实现
    }

    interface UserService {
        //业务代码定义....
    }

    static class UserServiceImpl implements UserService {
        @Resource   //现在由Spring来为我们选择一个指定的实现类,然后注入,而不是由我们在类中硬编码进行指定
        UserMapper mapper;

        //业务代码具体实现
    }

    static class UserController {
        @Resource
        UserService service;   //直接使用接口,就算你改实现,我也不需要再修改代码了

        //业务代码....
    }
}

通过使用接口,我们就可以将原有的强关联给弱化,我们只需要知道接口中定义了什么方法然后去使用即可,而具体的操作由接口的实现类来完成,并由Spring来为我们注入,而不是我们通过硬编码的方式去指定。

接口隔离原则

接口隔离原则(Interface Segregation Principle, ISP)实际上是对接口的细化。

客户端不应依赖那些它不需要的接口。

如一个获取设备信息的接口,有获取 cpu 的方法,有获取功率的方法。对于一台电脑,这两个方法都能用到;但是对于一台电风扇,根本用不到获取 cpu 的方法,这就违背了接口隔离原则。应该细化粒度,如分为智能设备信息接口和普通设备信息接口。

合成复用原则

合成复用原则(Composite Reuse Principle)的核心就是委派。

优先使用对象组合,而不是通过继承来达到复用的目的。

如:

Java
class A {
    public void connectDatabase() {
        System.out.println("连接数据库操作");
    }
}

class B extends A {    // 直接通过继承的方式,得到A的数据库连接逻辑
    public void test() {
        System.out.println("B也需要连接数据库");
        connectDatabase();   //直接调用父类方法
    }
}

这样,使用继承的方式实现复用,会增加耦合度。一旦该功能不由 A 来实现,B 也要跟着修改继承的父类。而且通过继承子类会得到一些父类中的实现细节,比如某些字段或是方法,这样直接暴露给子类,并不安全。

因此,可以选择以下方式来实现复用:

Java
class A {
    public void connectDatabase() {
        System.out.println("连接数据库操作");
    }
}

class B {   // 不进行继承,而是在用的时候给我一个A,当然也可以抽象成一个接口,更加灵活
    public void test(A a) {
        System.out.println("B也需要连接数据库");
        a.connectDatabase();   // 在通过传入的对象A去执行
    }
}

或是

Java
class A {
    public void connectDatabase() {
        System.out.println("连接数据库操作");
    }
}

class B {

    A a;
    public B(A a) {   // 在构造时就指定好
        this.a = a;
    }

    public void test() {
        System.out.println("B也需要连接数据库");
        a.connectDatabase();   // 也是通过对象A去执行
    }
}

通过对象之间的组合,降低了类之间的耦合度,并且 A 的实现细节也不会之间被 B 得到,只管用就行。

迪米特法则

迪米特法则(Law of Demeter)又称最少知识原则,是对程序内部数据交互的限制。

每一个软件单位对其他单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。、

一个类/模块对其他的类/模块有越少的交互越好。

创建型设计模式

工厂方法模式

我们想要创建一个类的实现类时,如果用普通写法:

Java
public abstract class Fruit {
    private final String name;

    public Fruit(String name) {
        this.name = name;
    }

    @Override
    String toString() {
        return name + "@" + hashCode();
    }
}

public class Apple extends Fruit {
    public Apple() {
        super("苹果");
    }
}

public class Orange extends Fruit {
    public Orange() {
        super("橘子");
    }
}

直接 new 来得到对象

Java
1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Apple apple = new Apple();
        System.out.println(apple);
    }
}

但是这样写,如果创建对象的时候需要传入一些参数,要修改实现类时就会改动很多代码。根据迪米特法则,应该减少类之间的交互。因此,我们可以创建一个工厂,我们需要什么类,找工厂生成就行,于是有了下面的简单工厂模式。

简单工厂模式

简单工厂模式不是一个正式的设计模式,但它是工厂模式的基础。它使用一个单独的工厂类来创建不同的对象,根据传入的参数决定创建哪种类型的对象。

将对象封装到工厂中:

Java
public class FruitFactory {
    /**
     * 这里就直接来一个静态方法根据指定类型进行创建
     * @param type 水果类型
     * @return 对应的水果对象
     */
    public static Fruit getFruit(String type) {
        switch (type) {
            case "苹果":
                return new Apple();
               case "橘子":
                return new Orange();
            default:
                return null;
        }
    }
}

然后通过工厂来创建对象:

Java
1
2
3
4
5
6
public class Main {
    public static void main(String[] args) {
        Fruit fruit = FruitFactory.getFruit("橘子"); // 直接问工厂要,而不是自己去创建
        System.out.println(fruit);
    }
}

但这样还是有一些问题,根据开闭原则,一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭。如果我们现在需要新增一种水果,比如桃子,那么这时就得去修改工厂提供的工厂方法了,但是这样是不太符合开闭原则的,因为工厂实际上是针对于调用方提供的,所以我们应该尽可能对修改关闭。

工厂方法模式

针对上面的问题,改进出了工厂方法模式。它定义了一个创建对象的接口,但将具体的对象创建延迟到子类中。

核心思想

  • 将对象的创建与使用分离:客户端不需要知道具体创建哪个类的对象,只需要通过工厂接口获取对象。
  • 扩展性:如果需要增加新的产品类型,只需要增加新的工厂子类,而不需要修改现有代码。

优点 + 解耦:将对象的创建与使用分离,客户端不需要关心具体的产品类。 + 扩展性:增加新的产品类型时,只需要增加新的工厂类,符合开闭原则。 + 可维护性:代码结构清晰,易于维护。

缺点 + 类的数量增加:每增加一个产品类型,都需要增加一个具体工厂类,可能会导致类的数量膨胀。 + 复杂性增加:对于简单的对象创建,使用工厂方法模式可能会显得过于复杂。

适用场景 + 需要灵活创建对象:当对象的创建过程比较复杂,或者需要根据条件动态创建对象时。 + 需要解耦:当需要将对象的创建与使用分离,避免客户端依赖具体类时。 + 需要扩展性:当系统需要支持多种产品类型,并且未来可能会增加新的产品类型时。

应用实例 + 汽车制造:你需要一辆汽车,只需从工厂提货,而不需要关心汽车的制造过程及其内部实现。 + Hibernate:更换数据库时,只需更改方言(Dialect)和数据库驱动(Driver),即可实现对不同数据库的切换。

Java
public abstract class FruitFactory<T extends Fruit> { // 将水果工厂抽象为抽象类,添加泛型T由子类指定水果类型
    public abstract T getFruit(); // 不同的水果工厂,通过此方法生产不同的水果
}

public class AppleFactory extends FruitFactory<Apple> { // 苹果工厂,直接返回Apple,一步到位
    @Override
    public Apple getFruit() {
        return new Apple();
    }
}

这样,我们就可以使用不同类型的工厂来生产不同类型的水果了,并且如果新增了水果类型,直接创建一个新的工厂类就行,不需要修改之前已经编写好的内容。

Java
public class Main {
    public static void main(String[] args) {
        test(new AppleFactory()::getFruit); // 比如我们现在要吃一个苹果,那么就直接通过苹果工厂来获取苹果
    }

    // 此方法模拟吃掉一个水果
    private static void test(Supplier<Fruit> supplier){
        System.out.println(supplier.get() + " 被吃掉了,真好吃。");
    }
}

抽象工厂模式

抽象工厂是围绕一个超级工厂创建其他工厂,该超级工厂又称为其他工厂的工厂。

在抽象工厂模式中,接口是负责创建一个相关对象的工厂,不需要显式指定它们的类。每个生成的工厂都能按照工厂模式提供对象。

抽象工厂模式提供了一种创建一系列相关或相互依赖对象的接口,而无需指定具体实现类。通过使用抽象工厂模式,可以将客户端与具体产品的创建过程解耦,使得客户端可以通过工厂接口来创建一族产品。

核心思想 + 提供一个接口:用于创建一组相关或依赖的对象,而不需要指定具体的类。 + 封装对象的创建:客户端只需要知道抽象工厂和抽象产品,不需要关心具体的实现。

结构 + 抽象工厂(Abstract Factory):定义创建一组产品的接口。 + 具体工厂(Concrete Factory):实现抽象工厂接口,创建具体的产品。 + 抽象产品(Abstract Product):定义产品的接口。 + 具体产品(Concrete Product):实现抽象产品接口的具体类。

优点 + 确保同一产品族的对象一起工作。 + 客户端不需要知道每个对象的具体类,简化了代码。

缺点 + 扩展产品族非常困难。增加一个新的产品族需要修改抽象工厂和所有具体工厂的代码,违背了开闭原则。

使用场景 + QQ 换皮肤时,整套皮肤一起更换。 + 创建跨平台应用时,生成不同操作系统的程序。

Java
1
2
3
4
5
6
7
8
public class Phone {
}

public class Computer {
}

public class TV {
}

定义抽象工厂

Java
1
2
3
4
5
public abstract class AbstractFactory {
    public abstract Phone getPhone();
    public abstract Phone getComputer();
    public abstract Phone getTV();
}

定义具体工厂

Java
1
2
3
4
5
6
7
8
class XiaoMiFactory extends AbstractFactory {
}

class AppleFactory extends AbstractFactory {
}

class HuaweiFactory extends AbstractFactory {
}

使用抽象工厂模式

Java
public class Main {
    public static void main(String[] args) {
        AbstractFactory factory;

        // 获取小米的产品
        factory = new XiaoMiFactory();
        Phone = factory.getPhone();
        Computer = factory.getComputer();

        // 获取华为的产品
        factory = new HuaweiFactory();
        ...
    }
}

建造者模式

StringBuilder 等 XXXBuilder 就属于建造者模式的思想,经常使用这样的类来创建需要的对象。

实际上是通过建造者来不断配置参数或内容,最后进行对象的构建。

相比直接去 new 一个新的对象,建造者模式的重心更加关注在如何完成每一步的配置,同时如果一个类的构造方法参数过多,我们通过建造者模式来创建这个对象,会更加优雅。

Java
public class Student {
    private int id;
    private int age;
    private String name;
    private List<String> awards;

    // 一律使用建造者来创建,不对外直接开放
    private Student(int id, int age, String name, List<String> awards) {
        ...
    }

    public static StudentBuilder builder() { // 通过builder方法直接获取建造者
        return new StudentBuilder();
    }

    public static class StudentBuilder { // 创建一个内部类
        // Builder也需要将所有的参数都进行暂时保存,所以Student怎么定义的这里就怎么定义
        int id;
        int age;
        String name;
        List<String> awards;

        public StudentBuilder id(int id) { // 直接调用建造者对应的方法,为对应的属性赋值
            this.id = id;
            return this; // 为了支持链式调用,这里直接返回建造者本身,下同
        }

        ...

        public StudentBuilder awards(String... awards) {
            this.awards = Arrays.asList(awards);
            return this;
        }

        public Student build() { // 最后我们只需要调用建造者提供的build方法即可根据我们的配置返回一个对象
            return new Student(id, age, name, awards);
        }
    }
}

单例模式

有时在整个程序中,同一个类始终只会有一个对象来进行操作,如数据库连接类,只需要创建一个对象或使用静态方法进行操作即可,没必要创建多个对象。

饿汉式单例

Java
1
2
3
4
5
6
7
8
9
public class Singleton {
    private final static Singleton INSTANCE = new instance();

    private Singleton() {}

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

当我们需要获取对象时,通过 getInstance() 方法来获取唯一的对象。

懒汉式单例

并不一开始就创建对象,而是在第一次使用的时候才创建。

Java
public class Singleton {
    private final static Singleton INSTANCE;

    private Singleton() {}

    public static Singleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }
}

这样做虽然能够节省一定资源,但是在多线程环境下可能会出现问题。

多线程环境下的问题 在多线程环境下,上述实现可能会导致以下问题:

  1. 竞态条件(Race Condition) 当多个线程同时调用 getInstance () 方法时,可能会同时进入 if (instance == null) 的判断。

如果此时 instance 为 null,多个线程都会执行 instance = new LazySingleton (),从而导致创建多个实例。

  1. 实例不一致 由于竞态条件的存在,不同线程可能会获取到不同的实例,这违反了单例模式的唯一性要求。

示例场景 假设有两个线程 T 1 和 T 2 同时调用 getInstance () 方法:

T 1 进入 getInstance () 方法,判断 instance == null 为 true,准备执行 instance = new LazySingleton ()。

在 T 1 执行 new LazySingleton () 之前,T 2 也进入 getInstance () 方法,判断 instance == null 也为 true。

T 1 和 T 2 都会执行 new LazySingleton (),从而创建两个不同的实例。

解决方法 为了解决懒汉式单例模式在多线程环境下的问题,可以采用以下方法:

  1. 加锁(Synchronized) 在 getInstance () 方法上加锁,确保同一时间只有一个线程可以执行创建实例的代码:

Java 复制 Public class LazySingleton { Private static LazySingleton instance;

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private LazySingleton() {
    // 私有构造函数
}

public static synchronized LazySingleton getInstance() {
    if (instance == null) {
        instance = new LazySingleton();
    }
    return instance;
}

} 优点:简单直接,确保线程安全。

缺点:每次调用 getInstance () 方法时都会加锁,性能较差。

  1. 双重检查锁(Double-Checked Locking) 在加锁的基础上,通过双重检查来减少加锁的次数:

Java 复制 Public class LazySingleton { Private static volatile LazySingleton instance;

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private LazySingleton() {
    // 私有构造函数
}

public static LazySingleton getInstance() {
    if (instance == null) {
        synchronized (LazySingleton.class) {
            if (instance == null) {
                instance = new LazySingleton();
            }
        }
    }
    return instance;
}

} 优点:只有在第一次创建实例时加锁,后续调用不需要加锁,性能较好。

注意:instance 需要使用 volatile 关键字,禁止指令重排序,确保多线程环境下的可见性。

  1. 静态内部类(Initialization-on-demand Holder Idiom) 利用类加载机制来保证线程安全:

Java 复制 Public class LazySingleton { Private LazySingleton () { // 私有构造函数 }

Text Only
1
2
3
4
5
6
7
private static class SingletonHolder {
    private static final LazySingleton INSTANCE = new LazySingleton();
}

public static LazySingleton getInstance() {
    return SingletonHolder.INSTANCE;
}

} 优点:线程安全,延迟加载,性能好。

原理:静态内部类在第一次被引用时才会加载,从而保证实例的唯一性。

  1. 枚举(Enum) 使用枚举实现单例模式:

Java 复制 Public enum LazySingleton { INSTANCE;

Text Only
1
2
3
public void doSomething() {
    // 业务方法
}

} 优点:线程安全,防止反射攻击,代码简洁。

缺点:不够灵活,无法延迟加载。

总结 懒汉式单例模式在多线程环境下可能会出现问题,主要是因为竞态条件导致创建多个实例。为了解决这个问题,可以采用以下方法:

加锁(synchronized)。

双重检查锁(Double-Checked Locking)。

静态内部类(Initialization-on-demand Holder Idiom)。

枚举(Enum)。

其中,静态内部类和枚举是实现单例模式的推荐方式,它们既能保证线程安全,又能实现延迟加载。

原型模式

原型模式使用原型示例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。也就是说,原型对象作为模板,通过克隆操作,来产生更多的对象。

要注意浅拷贝和深拷贝的区别。对于引用类型,浅拷贝只能复制内层对象的地址,而深拷贝相当于使用其值创建一个新的引用对象。

结构型设计模式

类/对象适配器模式

日常生活中,经常会遇到数据线接口不匹配或充电器功率不合适的问题,需要用拓展坞、转接头、电源适配器等工具解决。在程序开发中也会遇到类似的问题,如:

TestSupplier
1
2
3
4
5
public class TestSupplier { // 手机供应商
    public String doSupply() {
        return "iPhone 16e";
    }
}
Main
public class Main {
    public static void main(String[] args) {
        TestSupplier supplier = new TestSupplier();
          test( ? );   //我们没有Target类型的手机供应商,只有其他的,那这里该填个啥
    }

    public static void test(Target target) { // 现在我们需要调用test方法,但是test方法需要Target类型的手机供应商
        System.out.println("成功得到:"+target.supply());
    }
}
Target
1
2
3
public interface Target { // 现在的手机供应商,并不是test方法所需要的那种类型
    String supply();
}

这是就可以使用适配器模式来解决。

类适配器模式

创建一个适配器类:

TestAdapter
1
2
3
4
5
6
7
// 让适配器继承TestSupplier并实现Target接口
public class TestAdapter extends TestSupplier implements Target {
    @Override 
    public String supply() {
        return super.supply();
    }
}

这样,就得到了一个 Target 类型的实现类,并且采用的是 TestSupplier 提供的实现。

Main
public class Main {
    public static void main(String[] args) {
        TestAdapter adapter = new TestAdapter();
        test(adapter);
    }

    public static void test(Target target) {
        System.out.println("成功得到:"+target.supply());
    }
}

但这种实现方式会占用一个继承坑位,由于 Java 不支持多继承,那么如果 Target 不是接口而是抽象类的话,就不能实现了。同时,根据复用原则,我们应该更多的通过合成的方式去实现功能,于是有了对象适配器。

对象适配器

TestAdapter
public class TestAdapter implements Target {

    TestSupplier supplier;

    public TestAdapter(TestSupplier supplier) {
        this.supplier = supplier;
    }

    @Override
    public String supply() {
        return supplier.doSupply();
    }
}

这样,通过组合的方式把对象存放在了 TestAdapter 中,然后借助存放的对象调用具体实现。

桥接模式

桥接模式的核心思想是通过组合而不是继承来解耦抽象和实现,用于解决多层继承带来的复杂性,避免类爆炸问题。适用于需要将抽象和实现解耦的场景,例如不同平台上的图形绘制、不同数据库的访问等。

在学习桥接模式的时候,我想这不是和枚举类有点相似吗?但是仔细思考,还是有很多区别的:

1 设计目的

  • 桥接模式:
    • 是一种结构型设计模式,用于将抽象部分和实现部分分离,使它们可以独立变化。
    • 核心思想是通过组合而不是继承来解耦抽象和实现。
  • 枚举类:
    • 是一种特殊的类,用于定义一组固定的常量。
    • 核心思想是提供一种类型安全的方式来表示有限的、预定义的值。

2 主要用途

  • 桥接模式:
    • 用于解决多层继承带来的复杂性,避免类爆炸问题。
    • 适用于需要将抽象和实现解耦的场景,例如不同平台上的图形绘制、不同数据库的访问等。
  • 枚举类:
    • 用于定义一组固定的常量,例如状态、类型、模式等。
    • 适用于需要限制变量取值范围的场景,例如星期几、颜色、操作类型等。

3 设计思想

  • 桥接模式:
    • 强调抽象与实现的分离,通过组合将抽象部分和实现部分连接起来。
    • 抽象部分和实现部分可以独立扩展,互不影响。
  • 枚举类:
    • 强调类型安全和代码简洁性,通过枚举常量来限制变量的取值范围。
    • 枚举类本身是一个完整的类型,可以包含方法和属性。

4 扩展性

  • 桥接模式:
    • 抽象部分和实现部分可以独立扩展。例如,可以增加新的形状(抽象部分)或新的绘制方式(实现部分),而不影响现有代码。
  • 枚举类:
    • 枚举常量是固定的,不能动态扩展。如果需要增加新的常量,必须修改枚举类的定义。

5 线程安全性

  • 桥接模式:
    • 线程安全性取决于具体的实现。
    • 如果实现部分是线程安全的,则桥接模式也是线程安全的。
  • 枚举类:
    • 枚举常量是天然线程安全的,因为枚举实例在类加载时就已经创建,并且是不可变的。

6 适用场景

  • 桥接模式:适用于需要将抽象和实现解耦的场景。
    • 不同平台上的图形绘制。
    • 不同数据库的访问。
    • 不同消息格式的处理。
  • 枚举类:适用于需要定义一组固定常量的场景。
    • 状态(如订单状态、任务状态)。
    • 类型(如颜色、文件类型)。
    • 模式(如单例模式、策略模式)。

::: code-tabs @tab 桥接模式

Java
// 实现部分接口
interface DrawAPI {
    void drawCircle(int radius, int x, int y);
}

// 具体实现
class RedCircle implements DrawAPI {
    @Override
    public void drawCircle(int radius, int x, int y) {
        System.out.println("Drawing Red Circle: " + radius + ", " + x + ", " + y);
    }
}

class GreenCircle implements DrawAPI {
    @Override
    public void drawCircle(int radius, int x, int y) {
        System.out.println("Drawing Green Circle: " + radius + ", " + x + ", " + y);
    }
}

// 抽象部分
abstract class Shape {
    protected DrawAPI drawAPI;

    protected Shape(DrawAPI drawAPI) {
        this.drawAPI = drawAPI;
    }

    public abstract void draw();
}

// 具体抽象
class Circle extends Shape {
    private int x, y, radius;

    public Circle(int x, int y, int radius, DrawAPI drawAPI) {
        super(drawAPI);
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    @Override
    public void draw() {
        drawAPI.drawCircle(radius, x, y);
    }
}

// 使用
public class Client {
    public static void main(String[] args) {
        Shape redCircle = new Circle(100, 100, 10, new RedCircle());
        Shape greenCircle = new Circle(200, 200, 20, new GreenCircle());

        redCircle.draw();
        greenCircle.draw();
    }
}

@tab 枚举类

Java
enum Day {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;

    public void printDay() {
        System.out.println("Today is " + this);
    }
}

// 使用
public class Client {
    public static void main(String[] args) {
        Day today = Day.MONDAY;
        today.printDay(); // 输出: Today is MONDAY
    }
}
:::

通过桥接模式,使得抽象和实现可以沿着各自的维度来进行变化,不再是固定的绑定关系。

组合模式

组合模式实际上就是将多个组件进行组合,让用户可以对他们进行一致性处理。

比如系统中的文件系统,一个文件夹中可以有多个文件夹或文件,像一个树形结构。组合模式就是对树上的节点进行递归处理,比如给所有文件的文件名加一个前缀,就可以使用组合模式来实现。

::: code-tabs @tab 组件抽象

Java
1
2
3
4
5
6
7
// 首先创建一个组件抽象,组件可以包含组件,组件有自己的业务方法
public abstract class Component {
    public abstract void addComponent(Component component);    // 添加子组件
    public abstract void removeComponent(Component component);   // 删除子组件
    public abstract Component getChild(int index);   // 获取子组件
    public abstract void test();   // 执行对应的业务方法,比如修改文件名称
}

@tab 文件夹实现类

Java
public class Directory extends Component {   // 目录可以包含多个文件或目录

    List<Component> child = new ArrayList<>();   // 这里我们使用List来存放目录中的子组件

    @Override
    public void addComponent(Component component) {
        child.add(component);
    }

    @Override
    public void removeComponent(Component component) {
        child.remove(component);
    }

    @Override
    public Component getChild(int index) {
        return child.get(index);
    }

    @Override
    public void test() {
        child.forEach(Component::test);   // 将继续调用所有子组件的test方法执行业务
    }
}

@tab 文件实现类

Java
public class File extends Component {   // 文件就相当于是树叶,无法再继续添加子组件了

    @Override
    public void addComponent(Component component) {
        throw new UnsupportedOperationException();   // 不支持这些操作了
    }

    @Override
    public void removeComponent(Component component) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Component getChild(int index) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void test() {
        System.out.println("文件名称修改成功!"+this);   // 具体的名称修改操作
    }
}

@tab Main

Java
1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
    Directory outer = new Directory();   // 新建一个外层目录
    Directory inner = new Directory();   // 新建一个内层目录
    outer.addComponent(inner);
    outer.addComponent(new File());   // 在内层目录和外层目录都添加点文件
    inner.addComponent(new File());
    inner.addComponent(new File());
    outer.test();    // 开始执行文件名称修改操作
}
:::

发现最后会递归处理所有文件。

装饰模式

装饰模式,可以对现有的类进行修饰。核心在于在不改变一个对象本身功能的基础上,给对象添加额外的行为。并且这是通过组合而不是继承完成的。

装饰类将装饰目标组合到类中,重写基类的方法,在调用基类方法的基础上增加装饰。

::: code-tabs @tab 顶层抽象类

Java
1
2
3
public abstract class Base {
    public abstract void test();
}

@tab 实现类

Java
1
2
3
4
5
6
public class BaseImpl extends Base {
    @Override
    public void test() {
        System.out.println("我是业务方法");   // 具体的业务方法
    }
}

@tab 装饰类

Java
public class Decorator extends Base {   // 装饰者需要将装饰目标组合到类中

    protected Base base;

    public Decorator(Base base) {
        this.base = base;
    }

    @Override
    public void test() {
        base.test();    // 这里暂时还是使用目标的原本方法实现
    }
}

@tab 装饰类实现

Java
public class DecoratorImpl extends Decorator {

    public DecoratorImpl(Base base) {
        super(base);
    }

    @Override
    public void test() {    // 对原本的方法进行装饰,可以在原本方法实现前后添加额外操作
        System.out.println("装饰方法:我是操作前逻辑");
        super.test();
        System.out.println("装饰方法:我是操作后逻辑");
    }
}
:::

代理模式

...

行为型设计模式

解释器模式

用于定义语言的语法规则,并提供一个解释器来解释语言中的句子。它通常用于处理一些简单的语言或表达式,例如数学表达式、正则表达式、SQL 查询等。

核心思想

  • 将语言中的每个语法规则表示为一个类。
  • 通过组合这些类来构建语法树。
  • 提供一个解释器来遍历语法树并执行相应的操作。

模板方法模式

在程序中,可能某些操作是固定的,我们就可以直接在类中对应方法进行编写。但是可能某些操作需要视情况而定,由不同的子类实现来决定,这时,我们就需要让这些操作由子类来延迟实现了,就需要用到模板方法模式。

以医院挂号看病为例:

Java
/**
 * 抽象诊断方法,因为现在只知道挂号和看医生是固定模式,剩下的开处方和拿药都是不确定的
 */
public abstract class AbstractDiagnosis {

    public void test(){
        System.out.println("今天头好晕,不想起床,开摆,先跟公司请个假");
        System.out.println("去医院看病了~");
        System.out.println("1 >> 先挂号");
        System.out.println("2 >> 等待叫号");
        //由于现在不知道该开什么处方,所以只能先定义一下行为,然后具体由子类实现
          //大致的流程先定义好就行
        this.prescribe();
        this.medicine();  //开药同理
    }

    public abstract void prescribe();   //开处方操作根据具体病症决定了

    public abstract void medicine();   //拿药也是根据具体的处方去拿
}

现在我们定义好了抽象方法,只是将具体的流程先定义出来了,但是部分方法需要根据实现决定:

Java
/**
 * 感冒相关的具体实现子类
 */
public class ColdDiagnosis extends AbstractDiagnosis{
    @Override
    public void prescribe() {
        System.out.println("3 >> 一眼丁真,鉴定为假,你这不是感冒,纯粹是想摆烂");
    }

    @Override
    public void medicine() {
        System.out.println("4 >> 开点头孢回去吃吧");
    }
}

这样,我们就有了一个具体的实现类,并且由于看病的逻辑已经由父类定义好了,所以子类只需要实现需要实现的部分即可,这样我们就实现了简单的模板方法模式:

Java
1
2
3
4
public static void main(String[] args) {
    AbstractDiagnosis diagnosis = new ColdDiagnosis();
    diagnosis.test();
}

责任链模式

责任链模式也非常好理解,比如我们的钉钉审批,实际上就是一条流水线一样的操作,由你发起申请,然后经过多个部门主管审批,最后才能通过,所以你的申请表相当于是在一条责任链上传递。当然除了这样的直线型责任链之外,还有环形、树形等。

JavaWeb 中学习的 Filter 过滤器,正是采用的责任链模式,通过将请求一级一级不断向下传递,来对我们所需要的请求进行过滤和处理。

Java
public abstract class Handler {

    protected Handler successor;    // 设计责任链以单链表形式存在,这里存放后继节点

    public Handler connect(Handler successor){     // 拼接后续节点
        this.successor = successor;
        return successor;  // 这里返回后继节点,方便我们一会链式调用
    }

    public void handle(){
        this.doHandle();   // 由不同的子类实现具体处理过程
        Optional
                .ofNullable(successor)
                .ifPresent(Handler::handle);    // 责任链上如果还有后继节点,就继续向下传递
    }

    public abstract void doHandle();   // 结合上节课学习的模板方法,交给子类实现
}
Java
1
2
3
4
5
6
7
8
9
public class FirstHandler extends Handler{   // 用于一面的处理器
    @Override
    public void doHandle() {
        System.out.println("============= 一面 ==========");
        System.out.println("1. 谈谈你对static关键字的理解?");
        System.out.println("2. 内部类可以调用外部的数据吗?如果是静态的呢?");
        System.out.println("3. hashCode()方法是所有的类都有吗?默认返回的是什么呢?");
    }
}
Java
public class SecondHandler extends Handler{  // 二面
    @Override
    public void doHandle() {
        System.out.println("============= 二面 ==========");
        System.out.println("1. 如果我们自己创建一个java.lang包并且编写一个String类,能否实现覆盖JDK默认的?");
        System.out.println("2. HashMap的负载因子有什么作用?变化规律是什么?");
        System.out.println("3. 线程池的运作机制是什么?");
        System.out.println("4. ReentrantLock公平锁和非公平锁的区别是什么?");
    }
}
Java
public class ThirdHandler extends Handler{ // 三面
    @Override
    public void doHandle() {
        System.out.println("============= 三面 ==========");
        System.out.println("1. synchronized关键字了解吗?如何使用?底层是如何实现的?");
        System.out.println("2. IO和NIO的区别在哪里?NIO三大核心组件?");
        System.out.println("3. TCP握手和挥手流程?少一次握手可以吗?为什么?");
        System.out.println("4. 操作系统中PCB是做什么的?运行机制是什么?");
    }
}

编写好了每一轮的面试流程,现在我们就可以构建一个责任链了:

Java
1
2
3
4
5
6
7
public static void main(String[] args) {
    Handler handler = new FirstHandler();  // 一面首当其冲
    handler
            .connect(new SecondHandler())   // 继续连接二面和三面
            .connect(new ThirdHandler());
    handler.handle();   // 开始面试
} 

命令模式

以智能家居为例,我们只需要在一个终端上进行操作,就可以随便控制家里的电器。

比如现在我们有很多的类,彩电、冰箱、空调、洗衣机、热水器等,既然现在我们要通过一个遥控器去控制他们,那么我们就需要将控制这些电器的指令都给设计好才行,并且还不能有太强的关联性。

所有的电器肯定需要通过蓝牙或是红外线接受遥控器发送的请求,所以所有的电器都是接收者:

接收者
1
2
3
public interface Receiver {
    void action();   // 具体行为
}
指令
public abstract class Command {   // 指令抽象,不同的电器有指令

    private final Receiver receiver;

    protected Command(Receiver receiver){   // 指定此命令对应的电器(接受者)
        this.receiver = receiver;
    }

    public void execute() {
        receiver.action();   // 执行命令,实际上就是让接收者开始干活
    }
}
遥控器
1
2
3
4
5
public class Controller {   // 遥控器只需要把我们的指令发出去就行了
    public static void call(Command command){
        command.execute();
    }
}

创建一个空调,作为命令的接收者:

空调
1
2
3
4
5
6
public class AirConditioner implements Receiver{
    @Override
    public void action() {
        System.out.println("空调已开启");
    }
}

创建一个开启空调的命令:

开机命令
1
2
3
4
5
public class OpenCommand extends Command {
    public OpenCommand(AirConditioner airConditioner) {
        super(airConditioner);
    }
}

最后通过遥控器发送出去:

Java
1
2
3
4
public static void main(String[] args) {
    AirConditioner airConditioner = new AirConditioner();   // 先创建一个空调
    Controller.call(new OpenCommand(airConditioner));   // 直接通过遥控器来发送空调开启命令
}

通过这种方式,遥控器这个角色并不需要知道具体会执行什么,只需要发送命令即可,遥控器和电器的关联性就不再那么强了。

迭代器模式

迭代器可以说是我们学习 Java 语言的基础,没有迭代器,集合类的遍历就成了问题,正是因为有迭代器的存在,我们才能更加优雅的使用 foreach 语法。

依照 JDK 提供的迭代器接口(JDK 已经为我们定义好了一个迭代器的具体相关操作),也来设计一个迭代器:

Java
public class ArrayCollection<T> implements Iterable<T> {    // 设计一个简单的数组集合

    private final T[] array;   // 底层使用一个数组来存放数据

    private ArrayCollection(T[] array){   // private掉,自己用
        this.array = array;
    }

    public static <T> ArrayCollection<T> of(T[] array){   // 开个静态方法直接吧数组转换成ArrayCollection,其实和直接new一样,但是这样写好看一点
        return new ArrayCollection<>(array);
    }

    @Override
    public Iterator<T> iterator() {    // 需要实现iterator方法,此方法会返回一个迭代器,用于迭代我们集合中的元素
        return new ArrayIterator();
    }

    public class ArrayIterator implements Iterator<T> {   // 这里实现一个,注意别用静态,需要使用对象中存放的数组
        private int cur = 0;   // 这里我们通过一个指针表示当前的迭代位置

        @Override
        public boolean hasNext() {     // 判断是否还有下一个元素
            return cur < array.length;   // 如果指针大于或等于数组最大长度,就不能再继续了
        }

        @Override
        public T next() {   // 返回当前指针位置的元素并向后移动一位
            return array[cur++];   // 正常返回对应位置的元素,并将指针自增
        }
    }
}

接着,就可以对这个简单的集合类进行迭代了:

Java
1
2
3
4
5
6
7
public static void main(String[] args) {
    String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
    ArrayCollection<String> collection = ArrayCollection.of(arr);
    for (String s : collection) {    // 可以直接使用foreach语法糖,当然最后还是会变成迭代器调用
        System.out.println(s);
    }
}

编译后的代码:

Java
public static void main(String[] args) {
    String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
    ArrayCollection<String> collection = ArrayCollection.of(arr);
    Iterator var3 = collection.iterator();   // 首先获取迭代器,实际上就是调用我们实现的iterator方法

    while(var3.hasNext()) {
        String s = (String)var3.next();   // 直接使用next()方法不断向下获取
        System.out.println(s);
    }
}

中介者模式

借助 Facetime、微信等,可以同时让多个人参与到群通话中进行群聊,这样我们就不需要一个一个单独进行通话或是转达了。实际上正是依靠了一个中间商给我们提供了进行群体通话的平台,我们才能实现此功能,而这个平台实际上就是一个中间人。

在我们的程序中,可能也会出现很多的对象,但是这些对象之间的相互调用关系错综复杂,可能一个对象要做什么事情就得联系好几个对象。以房产中介为例:

Java
public class Mediator {   // 房产中介
    private final Map<String, User> userMap = new HashMap<>();   // 在出售的房子需要存储一下

    public void register(String address, User user){   // 出售房屋的人,需要告诉中介他的房屋在哪里
        userMap.put(address, user);
    }

    public User find(String address){   // 通过此方法来看看有没有对应的房源
        return userMap.get(address);
    }
}

两种用户,一种是租房,一种是房主:

Java
public class User {
    String name;
    String tel;

    public User(String name, String tel) {
        this.name = name;
        this.tel = tel;
    }

    public User find(String address, Mediator mediator){   // 找房子的话,需要一个中介和你具体想找的地方
        return mediator.find(address);
    }

    @Override
    public String toString() {
        return name+" (电话:"+tel+")";
    }
}

测试:

Java
public static void main(String[] args) {
    User user0 = new User("刘女士", "10086");   // 出租人
    User user1 = new User("李先生", "10010");   // 找房人
    Mediator mediator = new Mediator();        // 中介

    mediator.register("成都市武侯区天府五街白马程序员", user0);   // 先把房子给中介挂上去

    User user = user1.find("成都市武侯区天府五街下硅谷", mediator);  // 开始找房子
    if(user == null) System.out.println("没有找到对应的房源");

    user = user1.find("成都市武侯区天府五街白马程序员", mediator);  // 开始找房子
    System.out.println(user);   // 成功找到对应房源
}

中介者模式优化了原有的复杂多对多关系,而是将其简化为一对多的关系,更容易理解。

备忘录模式

备忘录模式,就为我们的软件提供了一个可回溯的时间节点,可能我们程序在运行过程中某一步出现了错误,这时我们就可以回到之前的某个被保存的节点上重新来过,我们平时编辑文本的时候,当我们编辑出现错误时,就需要撤回,而我们只需要按下 Ctrl+Z 就可以回到上一步,这样就大大方便了我们的文本编辑。

这里我们就模拟一下对象的状态保存:

Java
public class Student {
    private String currentWork;   // 当前正在做的事情
    private int percentage;   // 当前的工作完成百分比

    public void work(String currentWork) {
        this.currentWork = currentWork;
        this.percentage = new Random().nextInt(100);
    }

    @Override
    public String toString() {
        return "我现在正在做:"+currentWork+" (进度:"+percentage+"%)";
    }
}

接着编写一个状态保存类,来保存它在某一时刻的状态:

Java
1
2
3
4
5
6
7
8
9
public class State {
    final String currentWork;
    final int percentage;

    State(String currentWork, int percentage) {   //仅开放给同一个包下的Student类使用
        this.currentWork = currentWork;
        this.percentage = percentage;
    }
}

将状态的保存和恢复操作都实现一下:

Java
public class Student {
    ...

    public State save(){
        return new State(currentWork, percentage);
    }

    public void restore(State state){
        this.currentWork = state.currentWork;
        this.percentage = state.percentage;
    }

    ...
}

测试:

Java
public static void main(String[] args) {
    Student student = new Student();
    student.work("学Java");   //开始学Java
    System.out.println(student);

    State savedState = student.save();   //保存一下当前的状态

    student.work("打电动");   //刚打开B站播放视频,学一半开始摆烂了
    System.out.println(student);

    student.restore(savedState);   //两级反转!回到上一个保存的状态
    System.out.println(student);   //回到学Java的状态
}

观察者模式

观察者模式(Observer Pattern) 是一种行为型设计模式,用于在对象之间建立一种一对多的依赖关系。当一个对象(称为主题或被观察者)的状态发生变化时,所有依赖于它的对象(称为观察者)都会自动收到通知并更新。

核心作用 + 解耦:观察者模式将主题和观察者解耦,使得它们可以独立变化,而不会相互影响。 + 实时更新:当主题的状态发生变化时,所有观察者能够实时收到通知并更新自身状态。 + 动态订阅:观察者可以动态地订阅或取消订阅主题,灵活性高。

适用场景 + 当一个对象的改变需要同时改变其他对象,且不知道具体有多少对象需要改变时。 + 当一个对象需要通知其他对象,但不希望与这些对象紧密耦合时。 + 例如:事件处理系统、消息通知系统、GUI 框架中的事件监听等。

在 Java 中,一个对象的状态发生改变,可能就会影响到其他的对象,与之相关的对象可能也会联动的进行改变。还有我们之前遇到过的监听器机制,当具体的事件触发时,我们在一开始创建的监听器就可以执行相关的逻辑。

我们可以使用观察者模式来实现这样的功能,当对象发生改变时,观察者能够立即观察到并进行一些联动操作,我们先定义一个观察者接口:

Java
1
2
3
public interface Observer {   // 观察者接口
    void update();   // 当对象有更新时,会回调此方法
}

写一个支持观察者的实体类:

Java
public class Subject {
    private final Set<Observer> observerSet = new HashSet<>();

    public void observe(Observer observer) {   // 添加观察者
        observerSet.add(observer);
    }

    public void modify() {   // 模拟对象进行修改
        observerSet.forEach(Observer::update);   // 当对象发生修改时,会通知所有的观察者,并进行方法回调
    }
}

测试:

Java
1
2
3
4
5
6
public static void main(String[] args) {
    Subject subject = new Subject();
    subject.observe(() -> System.out.println("我是一号观察者!"));
    subject.observe(() -> System.out.println("我是二号观察者!"));
    subject.modify();
}

此外,JDK 也为我们提供了观察者模式接口:

Java
import java.util.Observable;    // java.util包下提供的观察者抽象类

public class Subject extends Observable {   // 继承此抽象类表示支持观察者

    public void modify(){
        System.out.println("对对象进行修改!");
        this.setChanged();    // 当对对象修改后,需要setChanged来设定为已修改状态
        this.notifyObservers(new Date());   // 使用notifyObservers方法来通知所有的观察者
          // 注意只有已修改状态下通知观察者才会有效,并且可以给观察者传递参数,这里传递了一个时间对象
    }
}

测试:

Java
1
2
3
4
5
6
public static void main(String[] args) {
    Subject subject = new Subject();
    subject.addObserver((o, arg) -> System.out.println("监听到变化,并得到参数:"+arg));  
      //注意这里的Observer是java.util包下提供的
    subject.modify();   //进行修改操作
}

状态模式

在标准大气压下,水在 0 度时会结冰变成固态,在 0-100 度之间时,会呈现液态,100 度以上会变成气态,水这种物质在不同的温度下呈现出不同的状态,而我们的对象,可能也会像这样存在很多种状态,甚至在不同的状态下会有不同的行为,我们就可以通过状态模式来实现。

我们来设计一个学生类,然后学生的学习方法会根据状态不同而发生改变,我们先设计一个状态枚举:

Java
1
2
3
public enum State {   // 状态直接使用枚举定义
    NORMAL, LAZY
}

编写学生类:

Java
public class Student {

    public class Student {

    private State state;   // 使用一个成员来存储状态

    public void setState(State state) {
        this.state = state;
    }

    public void study(){  
        switch (state) {   // 根据不同的状态,学习方法会有不同的结果
            case LAZY:
                System.out.println("只要我不努力,老板就别想过上想要的生活,开摆!");
                break;
            case NORMAL:
                System.out.println("拼搏百天,我要上清华大学!");
                break;
        }
    }
}

在不同的状态下,学习会出现不同的效果:

Java
1
2
3
4
5
6
7
8
public static void main(String[] args) {
    Student student = new Student();
    student.setState(State.NORMAL);   // 先正常模式
    student.study();

    student.setState(State.LAZY);   // 开启摆烂模式
    student.study();
}

策略模式

我们可以为对象设定一种策略,这样对象之后的行为就会按照我们在一开始指定的策略而决定了,看起来和前面的状态模式很像,但是,它与状态模式的区别在于,这种转换是“主动”的,是由我们去指定,而状态模式,可能是在运行过程中自动切换的。

以这个排序类为例,根据不同的策略,会选择不同的排序方案:

Java
1
2
3
4
5
6
7
public interface Strategy {   //策略接口,不同的策略实现也不同

    Strategy SINGLE = Arrays::sort;   //单线程排序方案
    Strategy PARALLEL = Arrays::parallelSort;   //并行排序方案

    void sort(int[] array);
}

编写一个排序类:

Java
public class Sorter {

    private Strategy strategy;   //策略

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }

    public void sort(int[] array){
        strategy.sort(array);
    }
}

指定策略进行排序:

Java
1
2
3
4
5
6
public static void main(String[] args) {
    Sorter sorter = new Sorter();
    sorter.setStrategy(Strategy.PARALLEL);    //指定为并行排序方案

    sorter.sort(new int[]{9, 2, 4, 5, 1, 0, 3, 7});
}

访问者模式

医院医生开的处方单中包含多种药元素,査看它的划价员和药房工作人员对它的处理方式也不同,划价员根据处方单上面的药品名和数量进行划价,药房工作人员根据处方单的内容进行抓药,相对于处方单来说,划价员和药房工作人员就是它的访问者,不过访问者的访问方式可能会不同。

在我们的 Java 程序中,也可能会出现这种情况,我们就可以通过访问者模式来进行设计。

比如一个人在某比赛获奖,而不同的人对于这分荣誉,有着不同的反应:

Java
public class Prize {   //奖
    String name;   //比赛名称
    String level;    //等级

    public Prize(String name, String level) {
        this.name = name;
        this.level = level;
    }

    public String getName() {
        return name;
    }

    public String getLevel() {
        return level;
    }
}

定义访问者接口:

Java
1
2
3
public interface Visitor {
    void visit(Prize prize);   //visit方法来访问我们的奖项
}

访问者的实现:

::: code-tabs @tab Teacher. java

Java
1
2
3
4
5
6
7
public class Teacher implements Visitor {   //指导老师作为一个访问者
    @Override
    public void visit(Prize prize) {   //它只关心你得了什么奖以及是几等奖,这也关乎老师的荣誉
        System.out.println("你得奖是什么奖?"+prize.name);
        System.out.println("你得了几等奖?"+prize.level);
    }
}

@tab Boss. java

Java
1
2
3
4
5
6
7
public class Boss implements Visitor{    //你的公司老板作为一个访问者
    @Override
    public void visit(Prize prize) {   //你的老板只关心这些能不能为公司带来什么效益,奖本身并不重要
        System.out.println("你的奖项大么,能够为公司带来什么效益么?");
        System.out.println("还不如老老实实加班给我多干干,别去搞这些没用的");
    }
}

@tab Classmate. java

Python
1
2
3
4
5
6
7
public class Classmate implements Visitor{   //你的同学也可以作为一个访问者
    @Override
    public void visit(Prize prize) {   //你的同学也关心你得了什么奖不过是因为你是他的奖学金竞争对手他其实并不希望你得奖
        System.out.println("你得了"+prize.name+"奖啊,还可以");
        System.out.println("不过这个奖没什么含金量,下次别去了");
    }
}

@tab Family. java

Java
1
2
3
4
5
6
7
public class Family implements Visitor{    //你的家人也可以是一个访问者
    @Override
    public void visit(Prize prize) {   //你的家人并不是最关心你得了什么奖,而是先关心你自己然后才是奖项,他们才是真正希望你好的人。这个世界很残酷,可能你会被欺负得遍体鳞伤,可能你会觉得活着如此艰难,但是你的背后至少还有爱你的人,为了他们,怎能就此驻足。
        System.out.println("孩子,辛苦了,有没有好好照顾自己啊");
        System.out.println("你得了什么奖啊?"+prize.name+",很不错,要继续加油啊!");
    }
}
:::

这里我们就设计了四种访问者,但是不同的访问者对于某一件事务的处理可能会不同。访问者模式把数据结构和作用于结构上的操作解耦,使得操作集合可相对自由地演化,我们上面就是将奖项本身的属性和对于奖项的不同操作进行了分离。