Skip to content

Latest commit

 

History

History
738 lines (550 loc) · 33.7 KB

elegant_design_patterns.md

File metadata and controls

738 lines (550 loc) · 33.7 KB
title description published date tags editor dateCreated
入门优雅的设计模式
true
2023-03-25 10:31:18 UTC
markdown
2023-03-24 15:41:12 UTC

优雅的设计模式

什么是设计模式?

根据参考资料的描述,设计模式是软件设计中常见问题的典型解决方案。 它们就像能根据需求进行调整的预制蓝图, 可用于解决代码中反复出现的设计问题。

设计模式与方法或库的使用方式不同,你很难直接在自己的程序中套用某个设计模式。模式并不是一段特定的代码,而是解决特定问题的一般性概念。人们常常会混淆模式和算法,因为两者在概念上都是已知特定问题的典型解决方案。但算法总是明确定义达成特定目标所需的一系列步骤,而模式则是对解决方案的更高层次描述。同一模式在两个不同程序中的实现代码可能会不一样。

设计模式只是一种设计代码的经验总结,它们并不是某种语言的特性,理论上任何语言终你都可以实现某个设计模式,但是在实际中,某些模式在某些语言中更容易实现,而在其他语言中则更难实现。在某些语言中,某些设计模式其实并不存在,因为语言本身就提供了更好的解决方案。

举个例子,对于Python来讲,单例模式就是一种不必要的模式,因为Python本身就提供了更好的解决方案,即模块级别的单例。而对于Java来讲,单例模式是一种常见的模式。

GoF总共总结了23种设计模式,分为三大类:

  • 创建型模式:用于创建对象,包括工厂方法、抽象工厂、单例、建造者和原型。
  • 结构型模式:用于处理类或对象的组合,包括适配器、桥接、组合、装饰、外观、享元和代理。
  • 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,包括模板方法、命令、迭代器、观察者、中介者、备忘录、解释器、状态、策略和访问者。

这23种设计模式是基于Java语言总结出来的。其中比较常用的有单例模式、工厂模式、适配器模式、观察者模式、Builder模式和策略模式。

需要注意的是,所有设计模式都是基于面向对象的编程语言总结出来的,因此在面向对象的编程语言中,设计模式是一种常见的编程模式,而在面向过程的编程语言中,设计模式则是一种不常见的编程模式。也是因为基于面向对象,设计模式会受到面向对象开销的影响,这也是为什么在某些语言中,某些设计模式其实并不存在,因为语言本身就提供了更好的解决方案。

单例模式

单例模式的”单例“也就是”单个实例“的意思,它的作用是保证在整个应用程序中,某个类只有一个实例存在。单例模式的实现方式有很多种,比如懒汉式、饿汉式、双重检查锁、静态内部类、枚举等。通常,我们可以通过以下几个步骤来实现单例模式:

  1. 将构造函数私有化,防止外部实例化。
  2. 创建一个静态的私有实例。
  3. 提供一个静态的公有方法,用于获取实例。

为什么会出现单例模式?

单例模式的出现源于面向对象设计中的一个重要概念——“全局变量”,即在整个程序中都可以访问的变量。全局变量具有很大的便利性,但同时也带来了很多问题,如程序的可维护性、可扩展性、灵活性、安全性等问题。因此,在设计面向对象程序时,需要对全局变量进行限制,保证其不会对系统造成负面影响。

单例模式就是一种对全局变量的限制方式,它可以保证在整个应用程序中只有一个实例存在,避免了多个实例之间的冲突和资源浪费。单例模式最早是由 GoF(Gang of Four)在《设计模式:可复用面向对象软件的基础》一书中提出的,该书是面向对象设计模式的经典著作之一,对面向对象程序设计有着深远的影响。

随着计算机技术的不断发展,单例模式被广泛应用于各种编程语言和开发框架中,成为面向对象设计中的一个重要概念。在实际开发中,合理地运用单例模式可以提高程序的性能和可维护性,减少系统资源的消耗,对于开发高质量的软件有着重要的作用。

示例程序

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        // 私有构造函数,防止外部实例化
    }

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

下面是Dart语言的单例模式实现代码:

class Singleton {
  // 将构造函数设为私有,禁止通过构造函数创建实例
  Singleton._privateConstructor();

  // 声明一个静态变量_instance,用于存储唯一实例
  static final Singleton _instance = Singleton._privateConstructor();

  // 声明一个静态方法getInstance,用于获取实例
  static Singleton getInstance() {
    return _instance;
  }

  // 其他方法和属性
  void someMethod() {
    // ...
  }
}

// 测试代码
void main() {
  // 获取实例
  Singleton singleton1 = Singleton.getInstance();
  Singleton singleton2 = Singleton.getInstance();

  // 比较两个实例是否相同
  print(identical(singleton1, singleton2)); // 输出 true
}

上面分别给出了Java和Dart语言的单例模式实现代码,可以看到,两种语言的实现方式非常类似,都是通过私有化构造函数、静态变量和静态方法来实现的。

当然,这种实现是有许多缺点的,比如存在线程安全问题,如果在多线程环境下,可能会创建多个实例,这样就违背了单例模式的初衷。

什么时候使用单例模式?

单例模式适用于以下情况:

  • 需要控制某个类的实例数量,保证在整个应用程序中只有一个实例。
  • 需要全局访问一个共享的资源,例如日志文件、数据库连接池等。
  • 需要创建重量级的对象,例如配置信息、网络连接等,如果每次都创建新对象,则会消耗大量的系统资源,降低程序的性能。
  • 需要确保线程安全,保证在多线程环境下只有一个实例,避免线程间竞争导致的问题。

也就是说,单例模式由于其特殊的实例化方式,使得它在某些情况下可以替代全局变量。具体的使用场景有:

  1. 日志记录器:应用程序中只需要一个日志记录器来记录日志信息,并且所有的类都可以使用它来记录日志。
  2. 配置管理器:应用程序中需要一个配置管理器来读取和保存配置信息,所有的类都可以使用它来访问配置信息。
  3. 数据库连接池:应用程序需要一个数据库连接池来管理数据库连接,保证在整个应用程序中只有一个连接池。
  4. 线程池:应用程序需要一个线程池来管理线程,保证在整个应用程序中只有一个线程池。
  5. 缓存管理器:应用程序需要一个缓存管理器来管理缓存数据,保证在整个应用程序中只有一个缓存管理器。

总之,单例模式适用于需要全局唯一实例的场景,可以有效地减少系统资源的消耗,提高程序的性能和可维护性。

单例模式的优缺点

单例模式是一个十分刚需的设计模式,很多情况下我们其实必须使用它来实现某些功能。但是,单例模式有一个需要注意的问题,就是在多线程环境下,如果没有采取措施来保证线程安全,很可能会出现多个实例的情况,这样就会破坏单例模式的设计初衷,因此在使用单例模式时,需要特别注意。

Builder 模式

Builder模式是一种创建型设计模式,旨在通过将对象的构建过程分解为一系列简单的步骤来构建复杂对象。它允许您按照您选择的顺序逐步构建对象,同时隐藏构建的细节。

考虑这样一个场景,我们需要创建一个复杂的对象,该对象包含多个属性,其中一些属性是必须的,另一些属性是可选的。如果我们使用构造函数来创建对象,那么就需要为每个属性都提供一个构造函数参数,这样会导致构造函数的参数数量非常多,而且参数的顺序也很重要,如果参数顺序不正确,就会导致创建对象失败。如果我们使用setter方法来设置对象的属性,那么就需要为每个属性都提供一个setter方法,这样会导致代码的可读性和可维护性都很差。

Builder模式的目的就是为了解决这个问题,它可以将对象的构建过程分解为一系列简单的步骤,每个步骤都可以通过一个方法来实现,这样就可以很方便地控制对象的构建过程,从而避免了构造函数参数过多的问题。

为什么要使用Builder模式?

在早期的软件开发中,对象的构建通常是通过调用构造函数或一组设置方法来完成的。但是,随着对象的复杂性增加,构建过程变得更加复杂,需要在多个步骤中完成。这时,Builder模式就应运而生了。

Builder模式的主要目的是将复杂对象的构建过程分解为多个简单的步骤,并且可以在不同的步骤中插入其他的处理逻辑,从而提高灵活性和可维护性。Builder模式的灵活性和可扩展性使得它成为一个非常流行的设计模式,并且在许多领域得到了广泛应用,如图形用户界面、游戏开发、数据库连接等。

随着软件开发的不断演化,Builder模式也在不断地发展和演变。例如,一些变体模式(如Fluent Builder模式和Step Builder模式)已经出现,它们提供了更加简洁和优雅的方式来构建对象。此外,一些新的编程语言(如Kotlin)已经开始集成Builder模式,使得在构建对象时更加方便和快速。

使用Builder模式的示例程序

public class Product {
  private String partA;
  private String partB;
  private String partC;

  public void setPartA(String partA) {
    this.partA = partA;
  }

  public void setPartB(String partB) {
    this.partB = partB;
  }

  public void setPartC(String partC) {
    this.partC = partC;
  }

  public void show() {
    System.out.println("partA = " + partA);
    System.out.println("partB = " + partB);
    System.out.println("partC = " + partC);
  }
}
public class Builder {
  private Product product = new Product();

  public Builder buildPartA(String partA) {
    product.setPartA(partA);
    return this;
  }

  public Builder buildPartB(String partB) {
    product.setPartB(partB);
    return this;
  }

  public Builder buildPartC(String partC) {
    product.setPartC(partC);
    return this;
  }

  public Product build() {
    return product;
  }
}
public class Main {
  public static void main(String[] args) {
    Product product = new Builder()
        .buildPartA("A")
        .buildPartB("B")
        .buildPartC("C")
        .build();
    product.show();
  }
}

在上面的程序中,我们定义了一个Product类,它是一个复杂的对象,包含了三个属性partApartBpartC。然后,我们定义了一个Builder类,它是一个Builder类,用于构建Product对象。在Builder类中,我们提供了三个buildPartX()方法,用于设置Product对象的三个属性。最后,我们在Main类中创建了一个Product对象,并且通过Builder类来构建该对象。

对于多数Java项目,你也可以使用Lombok来简化代码,如下所示:

@Data
@Builder
public class Product {
  private String partA;
  private String partB;
  private String partC;
}
public class Main {
  public static void main(String[] args) {
    Product product = Product.builder()
        .partA("A")
        .partB("B")
        .partC("C")
        .build();
    product.show();
  }
}

你会发现在这里其实出现了链式调用,如果你以前没有注意过链式调用的实现的话,这里应该就很清晰了。我们只需要在每个方法的最后返回this即可。

什么时候使用Builder模式?

对于一些语言,例如Dart,由于支持可选参数,因此可以使用构造函数来创建对象,因此不需要或者很少需要使用Builder模式。但是,对于Java这样的语言,由于不支持可选参数,因此必须使用Builder模式来创建对象。

我们需要知道,Builder模式并不是为了解决所有的构建对象的问题,它只是为了解决构造函数参数过多的问题。如果构造函数的参数数量不多,那么就没有必要使用Builder模式来创建对象,因为这样会增加代码的复杂度。

除此以外,Builder模式也可以沿伸到创建Map对象的场景中,例如:

import java.util.HashMap;
import java.util.Map;

public class MapBuilder {

    private Map<String, Object> map;

    public MapBuilder() {
        this.map = new HashMap<>();
    }

    public MapBuilder put(String key, Object value) {
        map.put(key, value);
        return this;
    }

    public Map<String, Object> build() {
        return map;
    }

    public static void main(String[] args) {
        Map<String, Object> map = new MapBuilder()
                .put("name", "John")
                .put("age", 25)
                .build();
        System.out.println(map);
    }
}

这个示例使用了Builder模式来创建一个包含键值对的Map对象。在上面的示例中,MapBuilder类作为Builder类,通过使用put()方法向Map中添加键值对,并且返回当前对象本身。最后,build()方法返回所需的Map对象。

Builder模式的优缺点

使用Builder模式的优点包括:

  1. 代码可读性好:使用Builder模式可以提高代码的可读性和可维护性。通过链式编程风格和流式接口,代码更加清晰和易于理解。
  2. 避免构造器参数过多:当一个类有很多构造器参数时,Builder模式可以避免构造器参数过多的问题。使用Builder模式,只需要将需要的参数传递给Builder,而不是在构造器中传递很多参数。
  3. 使代码更加灵活:Builder模式可以使代码更加灵活。可以轻松地添加或删除所需的属性或方法,而不需要更改其他部分的代码。
  4. 可以进行参数校验:Builder模式可以在build()方法中进行参数校验,确保构造器参数的有效性。

然而,Builder模式也有一些缺点:

  1. 增加代码量:使用Builder模式需要编写额外的类和方法,从而增加代码量。
  2. 增加复杂度:虽然使用Builder模式可以使代码更加清晰和易于理解,但是它也会增加代码的复杂度,尤其是在需要处理大量参数的情况下。
  3. 需要更多的开发时间:使用Builder模式需要更多的开发时间和工作量,因为需要编写额外的类和方法。

不过,通常情况下我们只需要使用Lombok@Builder注解来简化代码,因此不需要编写额外的类和方法。

工厂模式

工厂模式(Factory Pattern)是一种常用的创建型设计模式,用于创建对象而不暴露其创建逻辑,同时可以根据需求动态地生成不同类型的对象。

在工厂模式中,我们将创建对象的代码封装在一个工厂类中,客户端只需要调用工厂类的方法即可创建对象,无需知道对象的具体创建细节。工厂类根据客户端的需求,返回相应的对象实例,从而实现了对象的创建与使用的解耦。

工厂模式和Builder模式

工厂模式和Builder模式都是用于创建对象的设计模式,它们都可以将对象的创建与使用进行解耦。

工厂模式是一种常见的设计模式,它主要是用来解决对象创建时的问题。在实际开发中,经常需要创建大量的对象,但是对象的创建过程可能比较复杂,包括对象的初始化、依赖关系的处理等等。如果在代码中直接使用new关键字来创建对象,会导致代码的耦合度比较高,难以维护和扩展。

举个例子,我们可能有这样一个复杂的对象:

public class ComplexObject {
    private String name;
    private int age;
    private List<String> hobbies;
    private Map<String, Integer> scores;
    // 其他成员变量和方法

    public ComplexObject(String name, int age, List<String> hobbies, Map<String, Integer> scores) {
        this.name = name;
        this.age = age;
        this.hobbies = hobbies;
        this.scores = scores;
        // 初始化其他成员变量
    }
}

// 创建对象时需要进行大量初始化工作
List<String> hobbies = new ArrayList<>();
hobbies.add("reading");
hobbies.add("swimming");

Map<String, Integer> scores = new HashMap<>();
scores.put("math", 90);
scores.put("english", 80);

ComplexObject complexObject = new ComplexObject("Tom", 20, hobbies, scores);

上述代码中,我们需要创建一个 ComplexObject 对象,但是该对象的初始化过程比较复杂,需要对成员变量进行大量的初始化工作。下面是使用工厂模式来简化对象的创建过程:

public class ComplexObjectFactory {
    public static ComplexObject createComplexObject(String name, int age, List<String> hobbies, Map<String, Integer> scores) {
        // 进行复杂对象的初始化工作
        // ...
        ComplexObject complexObject = new ComplexObject(name, age, hobbies, scores);
        return complexObject;
    }
}

// 使用工厂模式创建对象
List<String> hobbies = new ArrayList<>();
hobbies.add("reading");
hobbies.add("swimming");

Map<String, Integer> scores = new HashMap<>();
scores.put("math", 90);
scores.put("english", 80);

ComplexObject complexObject = ComplexObjectFactory.createComplexObject("Tom", 20, hobbies, scores);

因此,工厂模式的出现就是为了解决这个问题。工厂模式将对象的创建过程封装在一个工厂类中,客户端只需要通过工厂类来创建对象,而不需要直接调用 new 关键字。这样可以将对象的创建和使用解耦,提高代码的可维护性和可扩展性。

同时,工厂模式也可以用来隐藏对象的具体实现,让客户端只关心对象的接口,而不需要关心对象的具体实现。这样可以在不影响客户端代码的情况下,灵活地改变对象的具体实现。

综上所述,工厂模式的出现主要是为了解决对象创建时的复杂性问题,并提高代码的可维护性、可扩展性和灵活性。

工厂模式的示例程序

下面我们通过一个示例程序来学习工厂模式。

假设我们需要开发一个游戏,游戏中有很多角色,每个角色都有自己的名字、生命值、攻击力和防御力。我们可以使用一个抽象的角色类来表示游戏中的角色:

public abstract class Role {
    protected String name;
    protected int hp;
    protected int atk;
    protected int def;

    public Role(String name, int hp, int atk, int def) {
        this.name = name;
        this.hp = hp;
        this.atk = atk;
        this.def = def;
    }

    public abstract void display();
}

然后我们可以定义一些具体的角色类,比如战士、法师和刺客:

public class Warrior extends Role {
    public Warrior(String name, int hp, int atk, int def) {
        super(name, hp, atk, def);
    }

    @Override
    public void display() {
        System.out.println("我是战士,我的名字是" + name);
    }
}

public class Mage extends Role {
    public Mage(String name, int hp, int atk, int def) {
        super(name, hp, atk, def);
    }

    @Override
    public void display() {
        System.out.println("我是法师,我的名字是" + name);
    }
}

public class Assassin extends Role {
    public Assassin(String name, int hp, int atk, int def) {
        super(name, hp, atk, def);
    }

    @Override
    public void display() {
        System.out.println("我是刺客,我的名字是" + name);
    }
}

现在我们需要创建一些角色对象,但是角色的创建过程比较复杂,需要对角色的属性进行初始化。因此,我们可以使用工厂模式来简化对象的创建过程:

public class RoleFactory {
    public static Role createRole(String name, String type) {
        Role role = null;
        switch (type) {
            case "warrior":
                role = new Warrior(name, 100, 10, 5);
                break;
            case "mage":
                role = new Mage(name, 80, 15, 3);
                break;
            case "assassin":
                role = new Assassin(name, 90, 12, 4);
                break;
        }
        return role;
    }
}

现在我们可以通过工厂类来创建角色对象了:

Role warrior = RoleFactory.createRole("张三", "warrior");
Role mage = RoleFactory.createRole("李四", "mage");
Role assassin = RoleFactory.createRole("王五", "assassin");

适配器模式

适配器模式是一种结构型设计模式,它可以将不兼容的接口转换为客户端期望的接口。适配器模式通过创建一个包装类来包装不兼容的对象,并使其与客户端代码兼容,从而实现不同类之间的协同工作。

如果你的项目中存在一些别人写的代码,但是这些代码的接口和你的项目不兼容,这时候你就可以使用适配器模式来解决这个问题,这也可以避免你修改别人的代码。

一个示例程序

下面我们通过一个示例程序来学习适配器模式。

// 定义一个被适配的类
class Adaptee {
    public String specificRequest() {
        return "Specific request";
    }
}

// 定义一个目标接口
interface Target {
    String request();
}

// 实现目标接口的类
class ConcreteTarget implements Target {
    public String request() {
        return "Target request";
    }
}

// 实现适配器类,将Adaptee的接口转换为Target的接口
class Adapter implements Target {
    private Adaptee adaptee;

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

    public String request() {
        return "Adapter: (translated) " + adaptee.specificRequest();
    }
}

// 客户端代码
public class Client {
    public static void main(String[] args) {
        // 使用ConcreteTarget类
        Target target = new ConcreteTarget();
        System.out.println(target.request()); // 输出:"Target request"

        // 使用Adapter类,将Adaptee的接口转换为Target的接口
        Adaptee adaptee = new Adaptee();
        Target adapter = new Adapter(adaptee);
        System.out.println(adapter.request()); // 输出:"Adapter: (translated) Specific request"
    }
}

观察者模式

观察者模式是一种设计模式,用于在对象之间建立一种一对多的依赖关系,使得当一个对象的状态发生改变时,其所有的依赖对象都会自动得到通知并进行相应的更新。观察者模式也被称为发布/订阅模式。

为什么要使用观察者模式

观察者模式最初由 Christopher Alexander 在他的著作《A Pattern Language》中提出,后来在计算机科学领域中得到了广泛的应用。最早的实现可以追溯到 Smalltalk 80 中的 Model-View-Controller(MVC)架构模式,后来又得到了更广泛的应用。

使用观察者模式可以将应用程序中的对象之间的耦合度降到最低,增加代码的灵活性和可维护性。观察者模式广泛应用于 GUI、Web 应用程序和游戏开发等领域。

例如,在 GUI 应用程序中,一个窗口(Subject)可能需要通知它的所有子组件(Observers)当它的大小或位置发生改变时进行重新布局。在游戏开发中,游戏世界中的所有对象(Subjects)可能需要通知它们的观察者(如 AI 控制器或玩家输入)当它们的状态发生改变时进行相应的更新。对于前端或者说状态驱动的应用,观察者模式是一个非常好的选择。Vue.js 和 React.js 都是基于观察者模式的框架。

观察者模式又被称为发布/订阅模式,它的主要目的是将发送者和接收者解耦,让两者之间的依赖关系变得更加松散。在观察者模式中,发送者和接收者之间的关系是一对多的关系,也就是说,一个发送者可以同时发送消息给多个接收者,而一个接收者也可以同时接收来自多个发送者的消息。因此,观察者模式可以用来实现消息队列、事件总线等功能。

典型的观察者模式

import java.util.ArrayList;
import java.util.List;

interface Subject {
    void attach(Observer observer);
    void detach(Observer observer);
    void notifyObservers();
}

interface Observer {
    void update(Object state);
}

class ObjectManager implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private Object state;

    public void attach(Observer observer) {
        observers.add(observer);
    }

    public void detach(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (Observer observer : observers) {
            observer.update(state);
        }
    }

    public void addObject(Object obj) {
        // Add the object to the manager
        System.out.println("Adding object " + obj);
        state = obj;
        notifyObservers();
    }

    public void removeObject(Object obj) {
        // Remove the object from the manager
        System.out.println("Removing object " + obj);
        state = null;
        notifyObservers();
    }
}

class ConsoleObserver implements Observer {
    public void update(Object state) {
        if (state == null) {
            System.out.println("Object removed");
        }
    }
}

在这个示例中,我们创建了一个 ObjectManager 类,它实现了 Subject 接口。 ObjectManager 类中有两个方法 addObject 和 removeObject,用于添加或删除对象。当这些方法被调用时,它们会更新 ObjectManager 类的状态并调用 notifyObservers 方法来通知所有的观察者对象。

现在我们创建一个观察者类 ConsoleObserver 来观察 ObjectManager 类的状态:

class ConsoleObserver implements Observer {
    public void update(Object state) {
        if (state == null) {
            System.out.println("Object removed");
        }
    }
}

当 ObjectManager 类的状态发生变化时,ConsoleObserver 类的 update 方法将被调用。在本例中,我们仅仅打印了一个简单的消息,但实际上可以执行任何需要的操作。

观察者模式的一些问题

观察者模式的一个问题是,如果观察者对象之间存在循环依赖关系,那么它们将无法正常工作。例如,如果观察者 A 依赖于观察者 B,而观察者 B 又依赖于观察者 A,那么当观察者 A 的状态发生变化时,它将无法通知观察者 B,反之亦然。

另一个问题是,如果观察者对象的数量过多,那么它们将无法正常工作。例如,如果观察者对象的数量超过了 JVM 的内存限制,那么当观察者 A 的状态发生变化时,它将无法通知所有的观察者对象。

除此以外,观察者模式还有以下一些问题:

  • 无法控制通知顺序:在观察者模式中,观察者和被观察者之间是松散耦合的,被观察者无法保证通知观察者的顺序,这可能导致观察者之间出现竞争条件或不一致的状态。
  • 内存泄漏:观察者模式中,观察者对象在注册时被添加到被观察者的观察者列表中,如果观察者没有被正确移除,就会导致内存泄漏问题。
  • 多线程并发问题:在多线程环境下,如果被观察者和观察者同时操作共享数据,就有可能导致并发问题,需要进行同步控制。
  • 性能问题:在观察者模式中,每当被观察者状态发生变化时,都需要通知所有的观察者,这可能导致性能问题,尤其是当观察者数量很大时。

要客服上面的问题,我们可以使用一些设计模式来改进观察者模式。例如,我们可以使用中介者模式来解决观察者模式中的循环依赖问题。我们也可以使用迭代器模式来解决观察者模式中的内存泄漏问题。我们还可以使用线程池来解决观察者模式中的多线程并发问题。

策略模式

策略模式是一种设计模式,它允许在运行时根据不同的需求选择不同的算法或策略,而不必改变使用算法的对象。这个模式基于“将算法封装成一个个独立的对象”,并且它可以使算法之间的耦合度低。在策略模式中,有一个被称为“上下文”的对象,它将调用不同的策略对象的方法,以达到所需的目的。策略对象是实现一个共同接口的一系列类,每个类都实现了自己的算法。上下文对象在运行时可以选择需要使用的算法对象,也可以动态地更改所使用的算法对象。

策略模式的优点是能够在不修改上下文对象的情况下改变算法,实现了算法和上下文之间的解耦。同时,它也提高了代码的可维护性和扩展性,因为新增一个算法只需要添加一个实现了共同接口的策略类即可。

使用策略模式减少 if-else 语句

策略模式的一个好例子是代替 if-else 语句。在 Java 中,我们可以使用 switch 语句来代替 if-else 语句,但是 switch 语句的缺点是它只能处理整数类型的数据,而且它的可读性也不是很好。例如,我们有一个类 Calculator,它有一个 calculate 方法,该方法接收一个 Operation 枚举类型的参数,根据不同的枚举值执行不同的操作:

public interface Operation {
    double calculate(double num1, double num2);
}

然后实现不同的操作类:

public class AddOperation implements Operation {
    @Override
    public double calculate(double num1, double num2) {
        return num1 + num2;
    }
}

public class SubtractOperation implements Operation {
    @Override
    public double calculate(double num1, double num2) {
        return num1 - num2;
    }
}

public class MultiplyOperation implements Operation {
    @Override
    public double calculate(double num1, double num2) {
        return num1 * num2;
    }
}

public class DivideOperation implements Operation {
    @Override
    public double calculate(double num1, double num2) {
        return num1 / num2;
    }
}

在 Calculator 类中,我们将 Operation 对象作为成员变量,并在构造函数中传入所需的 Operation 对象。然后,我们可以使用 calculate 方法来执行相应的操作:

public class Calculator {
    private Operation operation;

    public Calculator(Operation operation) {
        this.operation = operation;
    }

    public double calculate(double num1, double num2) {
        return operation.calculate(num1, num2);
    }
}

最后,我们可以使用如下代码来使用 Calculator 类:

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator(new AddOperation());
        System.out.println(calculator.calculate(2, 3));  // output: 5.0

        calculator = new Calculator(new SubtractOperation());
        System.out.println(calculator.calculate(2, 3));  // output: -1.0

        calculator = new Calculator(new MultiplyOperation());
        System.out.println(calculator.calculate(2, 3));  // output: 6.0

        calculator = new Calculator(new DivideOperation());
        System.out.println(calculator.calculate(6, 3));  // output: 2.0
    }
}

如果不使用策略模式,我们可以使用 if-else 语句来实现相同的功能:

public class Calculator {
    public double calculate(double num1, double num2, Operation operation) {
        if (operation == Operation.ADD) {
            return num1 + num2;
        } else if (operation == Operation.SUBTRACT) {
            return num1 - num2;
        } else if (operation == Operation.MULTIPLY) {
            return num1 * num2;
        } else if (operation == Operation.DIVIDE) {
            return num1 / num2;
        } else {
            throw new IllegalArgumentException("Invalid operation");
        }
    }
}

但是,这种实现方式有一个缺点,就是当我们需要添加新的操作时,我们需要修改 Calculator 类的源代码,这样就违反了开闭原则。如果我们使用策略模式来实现相同的功能,我们只需要添加一个新的 Operation 类,而不需要修改 Calculator 类的源代码,这样就能够遵循开闭原则。而且,策略模式书写的代码也更加简洁。

策略模式的缺点

尽管策略模式会使得代码显得更加整洁,但是在分支没有那么多的情况下,使用 if-else 语句也是可以的。策略模式的缺点是会增加代码的复杂度,因为我们需要创建一个策略类和一个上下文类,而且这两个类之间的耦合度也会增加。所以,我们需要根据实际情况来选择使用策略模式还是 if-else 语句。

除此以外,当我们使用策略模式时,我们需要考虑如何在运行时选择所需的策略。例如,我们可以使用工厂模式来创建策略对象,然后在运行时选择所需的策略对象。如果要添加一个新的策略,我们只需要添加一个新的策略类和一个新的工厂类即可。

然而,有时候我们需要添加很多的策略,在这种情况下可能使用责任链模式会更加合适。在责任链模式中,我们可以将不同的策略封装成一个个的处理器,然后将这些处理器组成一个链,然后在运行时选择所需的处理器。

参考资料