设计模式-行为型

观察者模式

简介

  • 定义了对象之间一对多依赖,让多个观察者对象同时监听某一个主题对象,当主题对象发生变化时,它的所有依赖者都会收到通知并更新

模式组合

角色 描述
Subject 被观察者接口,定义了注册、移除和通知观察者的方法
Observer 观察者接口,定义了接收被观察者通知并进行更新的方法
ConcreteSubject 具体的被观察者类,实现Subject接口,维护观察者列表,当状态变化时通知观察者
ConcreteObserver 具体的观察者类,实现Observer接口,定义了接收通知后进行更新操作的方法

实例

类图

image-20230626152258117

code

// 被观察者接口
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}

// 观察者接口
interface Observer {
void update(float temperature, float humidity, float pressure);
}

// 具体的被观察者类
class WeatherData implements Subject {
private List<Observer> observers;
private float temperature;
private float humidity;
private float pressure;

public WeatherData() {
observers = new ArrayList<>();
}

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

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

public void notifyObservers() {
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}

public void measurementsChanged() {
// 当天气数据发生变化时调用该方法
notifyObservers();
}

public void setMeasurements(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
measurementsChanged();
}
}

// 具体的观察者类
class WeatherDisplay implements Observer {
private float temperature;
private float humidity;
private float pressure;

public void update(float temperature, float humidity, float pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}

public void display() {
// 更新天气展示板上的数据显示
System.out.println("当前天气情况:温度 " + temperature + "℃,湿度 " + humidity + "%,气压 " + pressure + "Pa");
}
}

// 测试代码
public class ObserverPatternExample {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();

WeatherDisplay display1 = new WeatherDisplay();
WeatherDisplay display2 = new WeatherDisplay();

weatherData.registerObserver(display1);
weatherData.registerObserver(display2);

// 模拟天气数据变化
weatherData.setMeasurements(28.5f, 70.2f, 1013.2f);

// 移除一个观察者
weatherData.removeObserver(display2);

// 再次模拟天气数据变化
weatherData.setMeasurements(30.2f, 65.8f, 1012.5f);
}
}

模式特征

优点

  • 松耦合:被观察者和观察者之间是松耦合的关系,它们之间相互独立,可以独立地进行扩展和修改,一方的变化不会影响到另一方。
  • 可扩展性:可以轻松地增加新的观察者和被观察者,使系统具有更好的可扩展性。
  • 随时通知:被观察者状态发生变化时,会立即通知所有观察者,观察者可以及时作出响应。
  • 解耦:观察者模式将观察者和被观察者解耦,使它们之间的依赖关系降低,符合面向对象设计的原则。

缺点

  • 增加了复杂性:在使用观察者模式时,需要维护观察者列表并确保正确的通知顺序,这增加了系统的复杂性。
  • 更新通知顺序:观察者的更新通知顺序是不确定的,这可能导致观察者之间的依赖关系问题。
  • 观察者数量过多:当观察者数量过多时,被观察者通知所有观察者的时间和性能开销会增加。

策略模式

简介

  • 定义了一组算法类,将每个算法封装到单独的类中,并使他们可以相互替换,而不影响客户端的代码

模式组成

角色 描述
环境类(Context) 包含对策略的引用,可在运行时切换不同的策略。将具体的算法委托给策略对象执行。
抽象策略类(Strategy) 定义了通用的策略接口,所有具体策略类都必须实现该接口。通常是抽象类或接口,包含一个或多个用于执行策略的方法。
具体策略类(Concrete Strategy) 实现策略接口,提供具体的算法实现。每个具体策略类封装了一种特定的算法,可根据需要添加、删除或替换。

实例

类图

包 Strategy

code

// 环境类(Context)
class Context {
private Strategy strategy;

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

public void executeStrategy(int num1, int num2) {
int result = strategy.doOperation(num1, num2);
System.out.println("Result: " + result);
}
}

// 抽象策略类(Strategy)
interface Strategy {
int doOperation(int num1, int num2);
}

// 具体策略类(Concrete Strategy)
class AddStrategy implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}

// 具体策略类(Concrete Strategy)
class SubtractStrategy implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}

// 具体策略类(Concrete Strategy)
class MultiplyStrategy implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}

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

// 使用加法策略
context.setStrategy(new AddStrategy());
context.executeStrategy(5, 3); // Output: Result: 8

// 使用减法策略
context.setStrategy(new SubtractStrategy());
context.executeStrategy(5, 3); // Output: Result: 2

// 使用乘法策略
context.setStrategy(new MultiplyStrategy());
context.executeStrategy(5, 3); // Output: Result: 15
}
}

模式特征

策略模式的优点:

可扩展性:策略模式使得新增或修改算法变得简单,通过添加新的具体策略类,可以轻松地扩展系统的功能。

灵活性:客户端可以根据需要在运行时选择不同的策略,无需修改原有的代码,提供了更高的灵活性和可定制性。

代码复用:策略模式利用了面向对象的多态特性,可以让多个具体策略类共享相同的接口或抽象类,提高了代码的复用性。

解耦合:策略模式将算法的选择与使用算法的客户端代码解耦,客户端只需要关注如何使用策略,而无需关注具体的算法实现细节,降低了代码的耦合度。

策略模式的缺点:

增加类的数量:每个具体策略类都需要一个对应的类,当策略较多时,会增加类的数量,导致代码结构复杂。

客户端需要了解不同策略的区别:客户端在选择具体策略时需要了解不同策略的特点和适用场景,增加了客户端的理解和学习成本。

策略的切换开销:在运行时切换策略时,可能需要重新设置环境类的策略,会带来一定的切换开销,尤其是在需要频繁切换策略时。

责任链模式

简介

  • 允许多个对象依次处理请求,形成一个处理请求的链条,每个对象都有机会处理请求,但具体是哪个对象处理请求由运行时决定。

    模式组成

组成部分 描述
抽象处理者(Abstract Handler) 定义处理请求的接口,包含一个指向下一个处理者的引用。
具体处理者(Concrete Handler) 实现抽象处理者接口,并决定自己能处理的请求类型,如果可以处理就进行处理,否则将请求传递给下一个处理者。
客户端(Client) 发起请求的对象,将请求发送给第一个处理者。

实例

类图

image-20230626161056453

code

// 抽象处理者
abstract class Handler {
protected Handler successor; // 下一个处理者

public void setSuccessor(Handler successor) {
this.successor = successor;
}

public abstract void handleRequest(int request);
}

// 具体处理者A
class ConcreteHandlerA extends Handler {
@Override
public void handleRequest(int request) {
if (request >= 0 && request < 10) {
System.out.println("ConcreteHandlerA 处理请求:" + request);
} else if (successor != null) {
successor.handleRequest(request); // 传递给下一个处理者
}
}
}

// 具体处理者B
class ConcreteHandlerB extends Handler {
@Override
public void handleRequest(int request) {
if (request >= 10 && request < 20) {
System.out.println("ConcreteHandlerB 处理请求:" + request);
} else if (successor != null) {
successor.handleRequest(request); // 传递给下一个处理者
}
}
}

// 具体处理者C
class ConcreteHandlerC extends Handler {
@Override
public void handleRequest(int request) {
if (request >= 20 && request < 30) {
System.out.println("ConcreteHandlerC 处理请求:" + request);
} else if (successor != null) {
successor.handleRequest(request); // 传递给下一个处理者
}
}
}

// 客户端
public class Client {
public static void main(String[] args) {
Handler handlerA = new ConcreteHandlerA();
Handler handlerB = new ConcreteHandlerB();
Handler handlerC = new ConcreteHandlerC();

handlerA.setSuccessor(handlerB);
handlerB.setSuccessor(handlerC);

int[] requests = { 2, 12, 25, 30 };

for (int request : requests) {
handlerA.handleRequest(request);
}
}
}

模式特征

优点

责任链模式将发送者和接收者解耦,发送者无需知道具体的接收者,只需将请求发送给责任链的起始点即可,由责任链负责将请求传递给合适的接收者进行处理。
灵活性和可扩展性:责任链模式允许动态地添加、移除或重新排列处理者,可以根据需求对责任链进行灵活的组织和调整,而无需修改客户端代码。
可以确保请求被处理:由于责任链中的每个处理者都有机会处理请求,可以确保请求最终会被处理,而不会被忽略或丢失。

缺点

性能问题:如果责任链过长或者处理者的判断逻辑过于复杂,可能会导致性能下降,因为每个请求都要依次经过链中的所有处理者。
请求的处理不一定成功:由于责任链模式并不保证请求一定会被处理,如果没有合适的处理者或者处理者链配置错误,请求可能会被忽略或丢失。
可能导致调试困难:责任链模式中请求的处理路径是动态确定的,可能会导致在调试时难以确定请求的具体处理路径。

命令模式

简介

  • 将请求封装成一个对象,该对象会包含了执行操作的方法,发送者将命令对象传递给调用者,并在需要执行操作的时候触发命令并执行相应操作。

模式组成

模式组成 描述
命令接口 定义命令的执行方法,通常包括一个执行操作的方法。
具体命令 实现命令接口,持有一个接收者对象,并将请求委托给接收者执行具体的操作。
接收者 知道如何实施与执行一个请求相关的操作。
调用者 将命令对象传递给调用者,并可选择性地触发命令执行。

实例

code

当然,以下是一个简单的 Java 实例来演示命令模式:

首先,我们定义一个命令接口 Command,它包含了一个执行操作的方法 execute()

public interface Command {
void execute();
}

然后,我们创建一个具体命令类 ConcreteCommand,它实现了 Command 接口,并持有一个接收者对象,用于执行具体的操作:

public class ConcreteCommand implements Command {
private Receiver receiver;

public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}

public void execute() {
receiver.performAction();
}
}

接下来,我们定义一个接收者类 Receiver,它知道如何实施与执行请求相关的操作:

public class Receiver {
public void performAction() {
System.out.println("Receiver: Performing action...");
}
}

然后,我们创建一个调用者类 Invoker,它将接收到的命令对象传递给调用者,并在需要的时候触发命令的执行:

public class Invoker {
private Command command;

public void setCommand(Command command) {
this.command = command;
}

public void executeCommand() {
command.execute();
}
}

最后,我们可以在客户端代码中配置和使用命令模式:

public class Client {
public static void main(String[] args) {
// 创建接收者对象
Receiver receiver = new Receiver();

// 创建具体命令对象并设置接收者
Command command = new ConcreteCommand(receiver);

// 创建调用者对象并设置命令
Invoker invoker = new Invoker();
invoker.setCommand(command);

// 执行命令
invoker.executeCommand();
}
}

类图

image-20230702220522678

模式特征

优点

  1. 解耦发送者和接收者:命令模式将请求封装成对象,使得发送者和接收者之间解耦。发送者不需要知道接收者的具体实现细节,只需通过命令对象来执行请求。
  2. 容易扩展和修改:由于命令模式将请求封装成独立的对象,因此可以很容易地添加新的命令或修改现有命令,而不会对其他部分产生影响。这种可扩展性使得系统更加灵活和可维护。
  3. 支持撤销和重做:命令模式可以记录命令的历史,从而支持撤销和重做操作。通过保存命令的执行历史,可以在需要时回溯执行,实现撤销操作。
  4. 支持命令的排队和调度:命令模式可以将命令对象进行排队或者调度,从而实现更高级的控制和管理。可以按照特定的顺序执行命令,或者延迟执行命令。

缺点

  1. 类的增加:在使用命令模式时,每个具体命令都需要一个独立的类,这可能会导致类的数量增加,增加了系统的复杂性。
  2. 可能导致系统过于细粒度:如果系统中有大量的命令对象,每个对象都只封装了一个操作,可能会导致系统过于细粒度,增加了管理和维护的成本。
  3. 可能引入额外的开销:命令模式需要创建额外的对象来封装请求,可能会引入一定的开销。但在大多数情况下,这种开销是可以接受的。

状态模式

简介

  • 通过将不同状态抽象为独立的类,并将其与主对象关联,实现状态之间的切换和行为的动态变化。

模式组成

组成要素 描述
环境(Context) 环境类持有一个状态对象的引用,可以定义和维护对象的内部状态。它将状态相关的操作委托给当前状态对象,并在状态发生改变时更新当前状态对象。
抽象状态(State) 定义一个接口或抽象类,声明各个具体状态类共同的方法,这些方法可以是环境在某个特定状态下要执行的行为。
具体状态(Concrete State) 实现抽象状态接口或继承抽象状态类,具体状态类封装了特定状态下的行为。每个具体状态类负责处理与其状态相关的操作,并在必要时切换到其他状态。

实例

code

// 抽象状态类
interface OrderState {
void processOrder();
}

// 具体状态类:新订单状态
class NewOrderState implements OrderState {
@Override
public void processOrder() {
System.out.println("处理新订单...");
// 执行新订单状态下的业务逻辑
}
}

// 具体状态类:已支付状态
class PaidOrderState implements OrderState {
@Override
public void processOrder() {
System.out.println("处理已支付订单...");
// 执行已支付订单状态下的业务逻辑
}
}

// 具体状态类:已发货状态
class ShippedOrderState implements OrderState {
@Override
public void processOrder() {
System.out.println("处理已发货订单...");
// 执行已发货订单状态下的业务逻辑
}
}

// 环境类
class Order {
private OrderState currentState;

public Order() {
// 默认初始状态为新订单状态
currentState = new NewOrderState();
}

public void setState(OrderState state) {
currentState = state;
}

public void processOrder() {
currentState.processOrder();
}
}

// 客户端代码
public class StatePatternExample {
public static void main(String[] args) {
Order order = new Order();

// 处理新订单
order.processOrder();

// 切换为已支付状态
order.setState(new PaidOrderState());
order.processOrder();

// 切换为已发货状态
order.setState(new ShippedOrderState());
order.processOrder();
}
}

在上述示例中,抽象状态类OrderState定义了一个processOrder()方法,具体状态类NewOrderStatePaidOrderStateShippedOrderState分别实现了这个方法,封装了不同状态下的具体行为逻辑。Order类作为环境类,维护了当前状态对象的引用,并在processOrder()方法中委托给当前状态对象执行相应的行为。

客户端代码创建了一个订单对象order,初始状态为新订单状态,然后依次处理订单并切换状态,观察不同状态下的行为执行情况。

类图

image-20230702225657156

模式特征

优点

  1. 状态模式将对象的状态和行为进行了解耦,使得状态的变化可以独立于对象的行为变化。这提高了代码的可维护性和扩展性。
  2. 状态模式遵循开闭原则,可以通过添加新的具体状态类来增加新的状态,而无需修改现有的代码。
  3. 状态模式使得状态转换变得更加明确和可控。状态的切换逻辑集中在具体状态类中,不会分散在对象的各个方法中,使得代码更易理解和调试。
  4. 状态模式使得对象的状态变化可见,可以方便地观察和记录对象的状态变化历史。

缺点

  1. 状态模式增加了系统中类的数量,特别是在具体状态类较多的情况下,可能会导致类的数量增加,增加了系统的复杂性。
  2. 如果状态转换较为复杂,可能会导致状态模式的实现变得复杂,需要维护大量的状态类和状态转换逻辑。
  3. 当状态较少且简单时,引入状态模式可能会带来不必要的复杂性,增加了代码的理解和维护成本。

访问者模式

简介

  • 用于将数据结构和对数据结构的操作分离开来,允许定义新的操作而无需修改已有的数据结构。

模式组成

组成部分 描述
元素(Element) 表示数据结构中的对象。它定义了一个接受访问者对象的方法,让访问者可以对自身进行操作。
具体元素(Concrete Element) 实现了元素接口的具体类。每个具体元素类都会实现自己的接受访问者的方法,并在其中调用访问者对象的操作方法。
访问者(Visitor) 定义了对元素进行操作的接口,其中包含了为每个具体元素类定义的操作方法。
具体访问者(Concrete Visitor) 实现了访问者接口的具体类。每个具体访问者类都会实现对应的操作方法,以便对具体元素进行相应的操作。
对象结构(Object Structure) 存储元素对象并提供访问者访问的接口。它可以是一个集合、列表、树或其他数据结构。

实例

code

// 元素接口
interface Element {
void accept(Visitor visitor);
}

// 具体元素类A
class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitConcreteElementA(this);
}

public void operationA() {
System.out.println("ConcreteElementA operation");
}
}

// 具体元素类B
class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visitConcreteElementB(this);
}

public void operationB() {
System.out.println("ConcreteElementB operation");
}
}

// 访问者接口
interface Visitor {
void visitConcreteElementA(ConcreteElementA elementA);
void visitConcreteElementB(ConcreteElementB elementB);
}

// 具体访问者类
class ConcreteVisitor implements Visitor {
@Override
public void visitConcreteElementA(ConcreteElementA elementA) {
elementA.operationA();
System.out.println("ConcreteVisitor visits ConcreteElementA");
}

@Override
public void visitConcreteElementB(ConcreteElementB elementB) {
elementB.operationB();
System.out.println("ConcreteVisitor visits ConcreteElementB");
}
}

// 对象结构类
class ObjectStructure {
private List<Element> elements = new ArrayList<>();

public void addElement(Element element) {
elements.add(element);
}

public void accept(Visitor visitor) {
for (Element element : elements) {
element.accept(visitor);
}
}
}

// 客户端代码
public class Client {
public static void main(String[] args) {
// 创建具体元素对象
ConcreteElementA elementA = new ConcreteElementA();
ConcreteElementB elementB = new ConcreteElementB();

// 创建对象结构并添加元素
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.addElement(elementA);
objectStructure.addElement(elementB);

// 创建具体访问者对象
Visitor visitor = new ConcreteVisitor();

// 对象结构接受访问者访问
objectStructure.accept(visitor);
}
}

类图

image-20230702232125374

模式特征

优点

当讨论访问者模式时,可以列举一些其优点和缺点,以便全面评估该模式的适用性和潜在的问题。

优点:

  • 分离关注点:访问者模式可以将数据结构和操作分离开来,使得操作可以独立变化而不影响元素类的结构。这种分离可以提高代码的可维护性和可扩展性。
  • 增加新操作更容易:通过定义新的访问者类,可以很容易地增加新的操作,而无需修改元素类的代码。这符合开闭原则,使系统更灵活。
  • 集中相关操作:访问者模式可以将相关操作集中到访问者类中。这样,当需要对元素进行某个操作时,只需调用相应的访问者方法,而不需要在元素类中编写重复的操作代码。
  • 增加新元素相对容易:如果需要增加新的元素类,只需创建相应的具体元素类并实现接受访问者的方法即可。无需修改现有的访问者类。

缺点

  • 增加新元素困难:在访问者模式中,如果需要增加新的访问者类,需要修改所有已有的元素类,为它们添加接受新访问者的方法。这可能会导致元素类的修改和代码的重复。
  • 违反封装原则:访问者模式需要将具体元素类的内部结构暴露给访问者类,以便访问者能够进行操作。这可能违反了封装原则,降低了元素类的封装性。
  • 增加系统复杂性:引入访问者模式会增加系统中的类和接口数量,增加了代码的复杂性和理解难度。这可能使得系统变得更加复杂,特别是对于简单的数据结构而言,使用访问者模式可能过于繁琐。

模块方法模式

简介

  • 定义了一个操作中的算法的骨架。将一些步骤延迟到子类中实现。模板方块使得子类可以在不改变算法结构的情况下重新定义算法中某些步骤

    模式组成

模式组成 描述
模板方法(Template Method) 定义了算法的骨架,将算法的步骤定义为一系列抽象操作或具体操作的调用顺序。这个方法可以包含预定义的操作和钩子方法。
具体方法(Concrete Method) 在模板方法中定义的具体操作,是算法的固定部分。
抽象方法(Abstract Method) 在模板方法中定义的抽象操作,由子类实现。这些方法可以有不同的实现,以满足特定的需求。
钩子方法(Hook Method) 在模板方法中定义的具体操作,子类可以选择性地覆盖或扩展。这些方法在模板方法中有默认实现,但可以在子类中进行修改。
抽象类(Abstract Class) 包含模板方法和抽象方法的抽象类。它定义了算法的骨架,并规定了具体操作和抽象操作的调用顺序。
具体类(Concrete Class) 继承抽象类并实现其中的抽象方法,完成算法的具体步骤。它可以覆盖钩子方法来定制算法的行为。

实例

code

// 抽象类
abstract class AbstractClass {
// 模板方法
public void templateMethod() {
// 调用抽象方法
operation1();
// 调用具体方法
operation2();
// 调用钩子方法
if (hookMethod()) {
operation3();
}
}

// 抽象方法
protected abstract void operation1();

// 具体方法
protected void operation2() {
// 具体操作的实现
System.out.println("执行具体操作2");
}

// 钩子方法
protected boolean hookMethod() {
// 默认实现,子类可以选择性地覆盖该方法
return true;
}

// 抽象方法
protected abstract void operation3();
}

// 具体类A
class ConcreteClassA extends AbstractClass {
@Override
protected void operation1() {
System.out.println("执行具体操作1(来自具体类A)");
}

@Override
protected void operation3() {
System.out.println("执行具体操作3(来自具体类A)");
}
}

// 具体类B
class ConcreteClassB extends AbstractClass {
@Override
protected void operation1() {
System.out.println("执行具体操作1(来自具体类B)");
}

@Override
protected boolean hookMethod() {
return false;
}

@Override
protected void operation3() {
System.out.println("执行具体操作3(来自具体类B)");
}
}

// 测试代码
public class Main {
public static void main(String[] args) {
AbstractClass instanceA = new ConcreteClassA();
instanceA.templateMethod();

System.out.println("-----------------");

AbstractClass instanceB = new ConcreteClassB();
instanceB.templateMethod();
}
}

类图

image-20230702234441116

模式特征

优点

  1. 提供了一种框架或模板,用于定义算法的骨架,使得算法的具体步骤可以在子类中实现,提高了代码的可扩展性和复用性。
  2. 将算法的通用部分封装在模板方法中,具体实现延迟到子类中,使得算法的具体步骤可以灵活变化,而不影响算法的整体结构。
  3. 通过钩子方法,允许子类选择性地覆盖或扩展父类中的某些操作,从而实现个性化定制。
  4. 提高了代码的可维护性和可读性,将重复的代码逻辑放在模板方法中,避免了代码的重复编写。

缺点

  1. 引入了抽象类和具体类之间的继承关系,增加了系统的复杂性。如果继承关系设计不当,可能会导致类的层次结构变得复杂,难以维护。
  2. 由于模板方法已经定义了算法的骨架,因此在一定程度上限制了子类的自由度,子类只能实现父类定义好的抽象方法。
  3. 如果算法的变化点过多,可能需要定义大量的抽象方法,增加了子类的实现工作量。
  4. 模板方法模式的设计初衷是为了定义算法的骨架,对于一些具有多个算法簇的情况,可能会导致类的爆炸性增长,不利于系统的扩展和维护。