本系列文章简介:
设计模式是在软件开发过程中,经过实践和总结得到的一套解决特定问题的可复用的模板。它是一种在特定情境中经过验证的经验和技巧的集合,可以帮助开发人员设计出高效、可维护、可扩展和可复用的软件系统。设计模式提供了一种在设计和编码过程中的指导,它用于解决常见的设计问题,并提供了一种标准化的方案。设计模式能够帮助开发人员降低系统的复杂性,提高代码的可读性和可维护性。本系列文章将详细讲解Java中的23中设计模式 ,并配有图文解析及相应的代码示例,欢迎大家订阅《Java技术栈高级攻略》专栏,一起学习,一起涨分!
目录
1、引言
2、设计模型详解
2.1 行为型模式(Behavioral)
2.1.1 解释器模式(Interpreter Pattern)
2.1.1.1 简介
2.1.1.2 优缺点
2.1.1.3 使用场景
2.1.1.4 使用案例
2.1.2 模板方法模式(Template Method Pattern)
2.1.2.1 简介
2.1.2.2 优缺点
2.1.2.3 使用场景
2.1.2.4 使用案例
2.1.3 观察者模式(Observer Pattern)
2.1.3.1 简介
2.1.3.2 优缺点
2.1.3.3 使用场景
2.1.3.4 使用案例
2.1.4 策略模式(Strategy Pattern)
2.1.4.1 简介
2.1.4.2 优缺点
2.1.4.3 使用场景
2.1.4.4 使用案例
2.1.5 命令模式(Command Pattern)
2.1.5.1 简介
2.1.5.2 优缺点
2.1.5.3 使用场景
2.1.5.4 使用案例
2.1.6 职责链模式(Chain of Responsibility Pattern)
2.1.6.1 简介
2.1.6.2 优缺点
2.1.6.3 使用场景
2.1.6.4 使用案例
2.1.7 迭代器模式(Iterator Pattern)
2.1.7.1 简介
2.1.7.2 优缺点
2.1.7.3 使用场景
2.1.7.4 使用案例
2.1.8 中介者模式(Mediator Pattern)
2.1.8.1 简介
2.1.8.2 优缺点
2.1.8.3 使用场景
2.1.8.4 使用案例
2.1.9 备忘录模式(Memento Pattern)
2.1.9.1 简介
2.1.9.2 优缺点
2.1.9.3 使用场景
2.1.9.4 使用案例
2.1.10 状态模式(State Pattern)
2.1.10.1 简介
2.1.10.2 优缺点
2.1.10.3 使用场景
2.1.10.4 使用案例
2.1.11 访问者模式(Visitor Pattern)
2.1.11.1 简介
2.1.11.2 优缺点
2.1.11.3 使用场景
2.1.11.4 使用案例
3、结语
1、引言
设计模式是一种解决常见软件设计问题的经验总结,它提供了一套可重用的设计思想和方法,帮助开发人员更好地组织和设计他们的代码。在软件开发中,我们经常会遇到一些常见的问题,比如如何实现代码的灵活性、可扩展性、可维护性和可复用性,以及如何减少代码的耦合性等。设计模式通过定义一些通用的解决方案来解决这些问题,从而提高代码的质量和可维护性。
设计模式的概念最早由四位名为Gang of Four(GoF)的作者提出,他们在《设计模式:可复用面向对象软件的基础》一书中总结了23种常见的设计模式。这些设计模式包括创建型模式、结构型模式和行为型模式,它们分别用于解决对象创建、对象组合和对象交互等问题。每个设计模式都有其固定的结构和用法,开发人员可以根据具体的问题选择合适的设计模式来解决。
本文将跟随《Java设计模式大全:23种常见的设计模式详解(二)》的进度,继续介绍Java中常见的设计模式,详细解释它们的原理、应用场景和使用方法。通过学习这些设计模式,开发人员可以更好地理解软件设计的原则和思想,提高自己的设计能力和编码水平。设计模式不仅是一种编码技巧,更是一种思维方式和设计原则的体现。希望通过本文的介绍,读者能够更好地掌握和应用设计模式,写出更优雅、可扩展和可维护的代码。
2、设计模型详解
2.1 行为型模式(Behavioral)
2.1.1 解释器模式(Interpreter Pattern)
2.1.1.1 简介
Interpreter pattern)是一种行为型设计模式,它用于解释一种语言或表示法。它定义了一种语法规则,并使用该语法规则来解析和执行语言中的表达式。
解释器模式的结构包括以下几个角色:
- 抽象表达式(Abstract Expression):定义了一个抽象的解释操作,所有具体表达式都需要实现该接口。
- 终结符表达式(Terminal Expression):表示一个终结符表达式,它是解释器中的最基本的元素。
- 非终结符表达式(Nonterminal Expression):表示一个非终结符表达式,它由一个或多个终结符表达式组合而成。
- 上下文(Context):包含解释器需要的全局信息,通常被解释器用于存储和传递解释器需要的数据。
2.1.1.2 优缺点
优点:
-
灵活性高:解释器模式可以根据需要改变或扩展语言的语法规则,从而使得语言更加灵活,并且能够适应不同的需求。
-
可扩展性强:由于解释器模式使用了抽象语法树来表示语言的语法规则,因此可以很容易地添加新的表达式或规则。
-
易于实现:解释器模式的实现相对简单,只需要实现抽象语法树的解释器和相应的表达式类即可。
缺点:
-
可维护性较差:由于解释器模式使用了抽象语法树来表示语言的语法规则,因此在修改或扩展语法规则时,可能需要修改大量的代码。
-
性能较低:由于解释器模式需要解释和执行抽象语法树,因此在运行时会消耗较多的时间和资源。
-
可读性较差:由于解释器模式涉及到大量的类和对象之间的交互,因此代码的可读性较低,可理解性较差。
总的来说,解释器模式适用于需要动态定义或扩展语言的场景,但由于其较低的性能和可维护性,其应用范围相对较小。
2.1.1.3 使用场景
解释器模式适用于以下情况:
-
当需要解释一种特定的语言或语法,并且希望能够执行特定的操作时,可以使用解释器模式。例如,编程语言编译器、数据库查询语句解析器等。
-
当需要构建一个简单的语法规则,并且可以灵活地修改和扩展这些规则时,可以使用解释器模式。解释器模式可以将语法规则表示为抽象语法树,并且可以通过修改或添加新的解释器来改变或扩展语法规则。
-
当需要解决一类类似问题的场景,并且问题可以通过一种语法规则来描述时,可以使用解释器模式。解释器模式可以将这类问题的解决方式抽象为一种语法规则,并且可以通过解释器来执行这种规则。
-
当需要对一组对象进行一系列操作,并且这些操作可以表示为一种语法规则时,可以使用解释器模式。解释器模式可以通过定义不同的解释器来执行这些操作。
总之,解释器模式适用于需要解释和执行一种特定语言或语法规则的场景,以及需要将一类问题的解决方式抽象为一种语法规则并执行的场景。
2.1.1.4 使用案例
解释器模式是一种行为型设计模式。它用于解决将解释语言与应用程序的代码相互分离的问题。
在Java中,解释器模式可以用于处理一些特定的规则或语法。下面以一个简单的示例来说明如何使用解释器模式。
假设我们有一个简单的计算器应用程序,它可以计算四则运算表达式。用户输入一个表达式,程序会解析该表达式并计算出结果。
首先,我们需要定义一个抽象表达式接口,该接口定义了一个解释方法interpret,用于解释表达式:
public interface Expression {
int interpret();
}
然后,我们可以创建具体的表达式类,实现该接口。例如,我们可以创建一个AddExpression类,用于计算两个数字的加法表达式:
public class AddExpression implements Expression {
private final int operand1;
private final int operand2;
public AddExpression(int operand1, int operand2) {
this.operand1 = operand1;
this.operand2 = operand2;
}
@Override
public int interpret() {
return operand1 + operand2;
}
}
接下来,我们可以创建一个表达式解析器类,该类负责解析用户输入的表达式并生成相应的表达式对象:
import java.util.Stack;
public class ExpressionParser {
public static Expression parse(String expression) {
Stack<Expression> stack = new Stack<>();
String[] tokens = expression.split(" ");
for (String token : tokens) {
if (isOperator(token)) {
Expression operand2 = stack.pop();
Expression operand1 = stack.pop();
Expression operator = getOperator(token, operand1, operand2);
stack.push(operator);
} else {
int value = Integer.parseInt(token);
Expression operand = new NumberExpression(value);
stack.push(operand);
}
}
return stack.pop();
}
private static boolean isOperator(String token) {
return token.equals("+") || token.equals("-") || token.equals("*") || token.equals("/");
}
private static Expression getOperator(String token, Expression operand1, Expression operand2) {
switch (token) {
case "+":
return new AddExpression(operand1, operand2);
case "-":
return new SubtractExpression(operand1, operand2);
case "*":
return new MultiplyExpression(operand1, operand2);
case "/":
return new DivideExpression(operand1, operand2);
default:
throw new IllegalArgumentException("Unknown operator: " + token);
}
}
}
在上面的代码中,我们使用了一个栈来保存解析出的表达式对象。遇到操作符时,从栈中弹出两个操作数,并根据操作符创建相应的表达式对象,然后将新的表达式对象压入栈中。遇到数字时,将其解析为NumberExpression对象并压入栈中。
最后,我们可以在主程序中使用解释器模式来计算用户输入的表达式:
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter an expression: ");
String expression = scanner.nextLine();
Expression parsedExpression = ExpressionParser.parse(expression);
int result = parsedExpression.interpret();
System.out.println("Result: " + result);
}
}
用户输入的表达式将会被解析并计算出结果。
这就是一个简单的使用解释器模式的示例。通过使用解释器模式,我们可以将解释语言的语法和应用程序的代码相互分离,实现更加灵活和可扩展的程序设计。
2.1.2 模板方法模式(Template Method Pattern)
2.1.2.1 简介
模板方法模式是一种行为型设计模式,它定义了一个算法的骨架,将一些步骤延迟到子类中实现。模板方法模式使得子类可以在不改变算法结构的情况下重新定义算法的某些步骤。
在模板方法模式中,定义了一个抽象类,其中包含了一个模板方法,该方法定义了算法的骨架。具体的步骤在抽象类中被实现,并且可以在子类中进行扩展或重写。
2.1.2.2 优缺点
优点:
- 模板方法模式提供了一种简单且可扩展的方式来定义算法的骨架结构。
- 它通过将算法的公共部分提取到父类中,可以提高代码的复用性和可维护性。
- 子类可以通过重写父类的方法来实现算法的特定部分,这样可以灵活地定制算法的行为。
- 模板方法模式可以在不改变算法结构的情况下,对其中的某些步骤进行个性化定制。
缺点:
- 模板方法模式在一定程度上增加了类的继承关系,导致类的数量增多,可能会增加系统的复杂性。
- 如果算法的结构变化较大,可能需要修改父类的方法,导致所有子类都需要相应地改变,违反了开闭原则。
- 某些情况下,使用模板方法模式可能会导致代码的流程不够清晰,降低了代码的可读性和可维护性。
2.1.2.3 使用场景
以下是一些模板方法模式的常见使用场景:
-
框架或库的设计:当设计一个框架或库时,可以使用模板方法模式来定义框架或库的操作流程,并让用户根据需要实现具体的步骤。
-
数据库访问:在访问数据库时,可以使用模板方法模式来定义通用的数据库操作流程,例如打开连接、执行查询、关闭连接等步骤,并让具体的数据库访问类去实现具体的查询操作。
-
流程控制:在一个流程中有多个步骤,可以使用模板方法模式来定义流程的骨架,让子类实现具体的步骤,例如流程中的数据验证、数据处理、数据存储等。
-
单元测试:在编写单元测试时,可以使用模板方法模式来定义测试的流程,例如初始化测试环境、执行测试方法、验证结果等。
-
数据导入导出:在数据导入导出的过程中,可以使用模板方法模式来定义数据的读取、转换、写入等步骤。不同的数据源可以通过子类来实现具体的读写操作。
总而言之,模板方法模式适用于需要定义一个算法的骨架,并允许子类在不改变算法结构的情况下重新定义算法的某些步骤的场景。它可以提供代码的复用性和扩展性,并可以在不同的环境下实现定制化的行为。
2.1.2.4 使用案例
模板方法模式是一种行为设计模式,它定义了一个算法的骨架,但将一些步骤的实现推迟到子类中。这样,子类可以在保持算法结构不变的同时,重新定义某些步骤的具体实现。
在Java中,模板方法模式通常用于设计框架、库或模板,以提供一个通用的算法骨架,同时允许子类根据特定需求进行定制。
以下是一个使用模板方法模式的示例,模拟制作饮料的过程:
abstract class Beverage {
// 模板方法,定义制作饮料的算法骨架
public final void prepareBeverage() {
boilWater();
brew();
pourIntoCup();
if (addCondiments()) {
addExtras();
}
}
// 具体步骤的默认实现
private void boilWater() {
System.out.println("烧开水");
}
private void pourIntoCup() {
System.out.println("倒入杯中");
}
// 抽象方法,由子类实现
protected abstract void brew();
// 钩子方法,子类可以选择是否覆盖
protected boolean addCondiments() {
return true;
}
// 钩子方法,子类可以选择是否覆盖
protected void addExtras() {
System.out.println("加入额外的调料");
}
}
class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("用沸水冲泡咖啡");
}
@Override
protected boolean addCondiments() {
return true;
}
@Override
protected void addExtras() {
System.out.println("加入牛奶和糖");
}
}
class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("用沸水冲泡茶叶");
}
@Override
protected boolean addCondiments() {
return false;
}
}
public class TemplateMethodExample {
public static void main(String[] args) {
Beverage coffee = new Coffee();
coffee.prepareBeverage();
System.out.println();
Beverage tea = new Tea();
tea.prepareBeverage();
}
}
在这个示例中,Beverage
是一个抽象类,它定义了制作饮料的算法骨架,并提供了一些具体步骤的默认实现和一些可选的钩子方法。
Coffee
和Tea
是具体的子类,它们继承了Beverage
并实现了抽象方法和钩子方法。Coffee
选择了覆盖addCondiments()
和addExtras()
方法,而Tea
选择了保留默认的实现。
在main()
方法中,我们创建了一个Coffee
对象和一个Tea
对象,并分别调用它们的prepareBeverage()
方法。这样,我们可以看到制作咖啡和茶的过程,以及它们在具体步骤上的差异。
2.1.3 观察者模式(Observer Pattern)
2.1.3.1 简介
观察者模式(Observer Pattern)是一种行为型设计模式,也被称为发布-订阅模式。
在观察者模式中,有两个主要角色:主题(Subject)和观察者(Observer)。主题对象维护了一个观察者列表,并提供了一种注册和删除观察者的机制。观察者对象则定义了一个更新方法,在主题对象有变化时被调用。
观察者模式可以用来实现对象间的松耦合,当一个对象的状态发生变化时,所有依赖于它的对象都能得到通知并自动更新。这种模式适用于存在一对多关系的场景,其中一个对象的状态会影响到其他多个对象。
2.1.3.2 优缺点
观察者模式有以下的优点和缺点:
优点:
- 解耦性:观察者模式能够使得被观察者和观察者之间的关系松耦合,它们之间并不直接进行相互调用,通过中介者(被观察者)来进行通信,从而使系统更加灵活、可扩展和可维护。
- 扩展性:由于观察者模式中的观察者和被观察者之间的关系是松耦合的,因此可以很方便地增加或删除观察者,而不会影响到其他相关的对象和代码。
- 符合开闭原则:观察者模式使得新增观察者非常容易,无需修改现有代码。符合开闭原则,增强了系统的可维护性和可扩展性。
缺点:
- 引起性能问题:如果观察者过多或者观察者之间的交互逻辑较为复杂,会导致观察者模式的性能问题。因为当被观察者发生改变时,需要通知所有的观察者,如果观察者过多或者处理逻辑耗时较长,会影响到系统的性能。
- 造成循环引用:观察者模式中观察者和被观察者之间是相互依赖的关系,如果不加以限制,容易造成循环引用,导致系统出现问题。
总的来说,观察者模式在一些需要实现对象间松耦合、事件通知和触发的场景中非常适用。但在一些性能要求较高、观察者过多或者观察者之间存在复杂交互的场景中,需要谨慎使用该模式。
2.1.3.3 使用场景
观察者模式适用于以下场景:
-
当一个对象的改变需要通知其他多个对象,并且这些对象的数量以及对其的依赖关系可能变化时,可以使用观察者模式来实现。
-
当一个抽象模型有两个方面,其中一个方面依赖于另一个方面的状态变化,可以使用观察者模式来解耦。
-
当一个对象改变时,需要通知其他对象,但是不希望这些对象和主对象发生紧耦合关系时,可以使用观察者模式来实现。
-
当某个对象的状态改变需要触发其他相关对象的行为时,可以使用观察者模式来实现。
-
当一个对象需要以事件驱动的方式与其他对象进行通信时,可以使用观察者模式来实现。
总而言之,观察者模式适用于对象之间存在一对多关系的场景,通常用于事件处理、GUI开发、消息通知等领域。
2.1.3.4 使用案例
观察者模式是一种行为型设计模式,用于在对象之间建立一种一对多的依赖关系,当一个对象的状态发生改变时,它的所有依赖者都会收到通知并自动更新。这种模式可以降低对象之间的耦合性,使得系统更加灵活和可扩展。
下面是一个使用观察者模式的示例:假设我们有一个商店类Shop,它负责售卖商品,并且用一个Observer接口表示商店的观察者。当商店售卖商品时,所有观察者都会收到通知并更新自己的状态。
首先,定义一个Observer接口,它包含一个update方法用于更新观察者的状态:
public interface Observer {
void update();
}
然后,定义一个Shop类作为观察者的被观察者:
import java.util.ArrayList;
import java.util.List;
public class Shop {
private List<Observer> observers = new ArrayList<>();
// 添加观察者
public void addObserver(Observer observer) {
observers.add(observer);
}
// 移除观察者
public void removeObserver(Observer observer) {
observers.remove(observer);
}
// 售卖商品
public void sellProduct() {
// 售卖商品的逻辑
System.out.println("售卖商品");
// 通知所有观察者更新状态
notifyObservers();
}
// 通知所有观察者更新状态
private void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
接下来,定义两个具体的观察者类:Customer和Inventory,它们实现Observer接口并实现update方法:
public class Customer implements Observer {
@Override
public void update() {
System.out.println("顾客收到通知,更新购物车");
}
}
public class Inventory implements Observer {
@Override
public void update() {
System.out.println("库存收到通知,更新商品数量");
}
}
最后,在客户端代码中使用观察者模式:
public class Client {
public static void main(String[] args) {
// 创建商店对象
Shop shop = new Shop();
// 创建两个观察者对象
Customer customer = new Customer();
Inventory inventory = new Inventory();
// 添加观察者
shop.addObserver(customer);
shop.addObserver(inventory);
// 商店售卖商品
shop.sellProduct();
}
}
运行客户端代码,输出如下:
售卖商品
顾客收到通知,更新购物车
库存收到通知,更新商品数量
以上示例中,Shop类是观察者的被观察者,Customer和Inventory类是具体的观察者。当商店售卖商品时,所有观察者都会收到通知并更新自己的状态。这种方式可以使得购物车和库存在商店售卖商品时进行相应的更新操作。
2.1.4 策略模式(Strategy Pattern)
2.1.4.1 简介
策略模式是一种行为设计模式,它允许在运行时选择算法的一种方式。它定义了一系列算法,将每个算法封装在独立的类中,并使它们可以相互替换。这样可以使算法的选择与算法的使用分离开来,使得算法可以独立于使用它的客户端变化。
在策略模式中,有三个核心角色:策略接口(Strategy Interface)、具体策略(Concrete Strategies)和上下文(Context)。
策略接口定义了算法的通用接口,它包含了一个或多个方法,具体策略类实现了这个接口,并提供了具体的算法实现。上下文类持有一个策略接口的引用,它的主要作用是将具体的算法委托给策略对象来执行。上下文类还提供了一些方法,用于选择具体的算法和调用策略对象的方法。
2.1.4.2 优缺点
优点:
- 策略模式提供了一种灵活的方式来选择和切换算法,使得算法可以独立于客户端代码进行修改和演进。这种解耦使得系统更易于维护、扩展和重用。
- 策略模式可以减少大量的if-else或switch-case语句,提高代码的可读性和可维护性。
- 策略模式封装了具体的算法实现,使得算法可以独立于其他部分进行测试和调试。
缺点:
- 策略模式增加了系统中类的数量,可能会导致类的增加复杂性,从而增加理解和维护的难度。
- 客户端需要了解所有不同的策略类,并在使用时进行选择。这可能增加了客户端的复杂性。
- 当策略类的数量较多时,可能需要创建大量的具体策略类,这可能会增加代码大小和开发时间。
总的来说,策略模式在需要根据不同的情况选择不同的算法时非常有用,特别是当有许多算法可选时。它提供了一种灵活的方式来切换算法,使系统更加可扩展和可维护。然而,它也可能增加系统的复杂性,特别是当策略类的数量较多时。在使用策略模式时,需要权衡这些优点和缺点来决定是否使用。
2.1.4.3 使用场景
策略模式适用于以下情况:
-
当一个系统中存在多个类似的算法,但各个算法在具体实现上有所不同的情况下,可以使用策略模式来将每个算法封装成独立的策略类,使得算法的变化独立于客户端使用它们的变化。
-
当一个系统需要在多个算法中选择一个进行使用,但又不希望客户端直接和算法之间发生耦合的情况下,可以使用策略模式来将算法的选择和使用进行分离,客户端只需要调用策略的接口即可。
-
当一个对象有多种行为,且不同行为需要根据不同的情况来选择的情况下,可以使用策略模式来将每种行为封装成独立的策略类,根据具体情况来选择使用哪种行为。
-
当一个系统需要动态地在多个算法中切换的情况下,可以使用策略模式来动态地替换算法,而无需修改客户端代码。
-
当一个类中包含了大量的条件语句来选择不同的行为时,可以使用策略模式来减少条件语句的数量,使得代码更清晰、可维护。
2.1.4.4 使用案例
策略模式是一种行为型设计模式,它允许在运行时动态地改变一个对象的行为。在Java中,策略模式可以通过接口和实现多态来实现。
示例: 假设我们有一个图形绘制器,它可以绘制不同形状的图形,例如圆形、矩形和三角形。我们希望能够在运行时选择要绘制的图形。
首先,我们需要定义一个图形接口,其中包含一个绘制方法:
public interface Shape {
void draw();
}
然后,我们可以实现具体的图形类,例如圆形、矩形和三角形:
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("绘制矩形");
}
}
public class Triangle implements Shape {
@Override
public void draw() {
System.out.println("绘制三角形");
}
}
接下来,我们可以定义一个绘制器类,它具有一个绘制方法,并接受一个图形对象作为参数:
public class DrawContext {
private Shape shape;
public void setShape(Shape shape) {
this.shape = shape;
}
public void drawShape() {
shape.draw();
}
}
最后,我们可以在使用策略模式的客户端中进行如下使用:
public class Client {
public static void main(String[] args) {
DrawContext drawContext = new DrawContext();
// 绘制圆形
drawContext.setShape(new Circle());
drawContext.drawShape();
// 绘制矩形
drawContext.setShape(new Rectangle());
drawContext.drawShape();
// 绘制三角形
drawContext.setShape(new Triangle());
drawContext.drawShape();
}
}
在上述示例中,我们定义了一个图形接口 Shape,并实现了圆形、矩形和三角形类作为具体的图形类。然后,我们定义了一个绘制器类 DrawContext,它具有一个 setShape 方法用于设置要绘制的图形对象,以及一个 drawShape 方法用于绘制图形。
在客户端中,我们创建了一个 DrawContext 对象,并使用 setShape 方法设置要绘制的图形对象。然后,我们调用 drawShape 方法来绘制图形。
通过使用策略模式,我们可以在运行时动态地改变要绘制的图形,而不需要修改绘制器类的代码。这种灵活性使得策略模式在需要根据不同的条件选择不同算法或行为的场景中非常有用。
2.1.5 命令模式(Command Pattern)
2.1.5.1 简介
命令模式(Command Pattern)是一种行为设计模式,它将请求封装成一个对象,从而允许不同的请求参数化客户端,将请求排列在队列中或者记录请求日志,以及支持可撤销的操作。
在命令模式中,将请求封装为一个命令对象,并定义了一个统一的接口来执行这个命令。命令对象通常包含了一个接收者对象,即真正执行请求的对象。通过将请求者与接收者解耦,可以让请求者与接收者之间的关系灵活地变化,而无需修改请求者的代码。
命令模式的主要目的是将请求的发送者和接收者解耦,从而实现请求的发送者和接收者之间的松耦合。使用命令模式可以增加新的命令,而无需修改现有的代码,符合开闭原则。
命令模式的角色有:
- Command(命令):定义命令的接口,声明执行的方法。
- ConcreteCommand(具体命令):实现命令接口,负责执行具体的操作。
- Invoker(调用者):负责调用命令对象执行命令。
- Receiver(接收者):负责执行命令的具体操作。
- Client(客户端):创建命令对象并设置命令的接收者。
2.1.5.2 优缺点
命令模式的优点包括:
-
解耦请求发送者和请求接收者:命令模式通过将请求封装在命令对象中,使得请求发送者和请求接收者之间的关系解耦,使得系统更加灵活。
-
容易扩展和维护:新增命令类非常容易,增删命令不会影响到其他部分的代码,维护起来也更加方便。
-
支持撤销和重做:命令模式可以通过保存命令对象的历史记录,实现撤销和重做操作。
-
支持任务队列:命令模式可以将请求保存在队列中,实现请求的异步执行。
命令模式的缺点包括:
-
类的数量增加:引入命令对象会导致类的数量增加,增加了系统的复杂性。
-
命令执行速度较慢:命令模式需要将请求封装在命令对象中,然后再传递给接收者执行,相比直接调用接收者的方法,会有一定的性能损失。
-
对象状态的一致性:命令模式需要维护命令对象和接收者的一致性,可能需要额外的代码来保证两者的状态信息一致。
2.1.5.3 使用场景
命令模式的使用场景包括:
-
执行操作的顺序很重要,需要支持撤销和重做操作。命令模式可以将操作封装成命令对象,通过调用命令对象的执行方法执行操作,并且可以通过调用命令对象的撤销和重做方法来撤销和重做操作。
-
需要将操作的请求者和操作的执行者解耦。命令模式可以将请求者和执行者分离,请求者只需要调用命令对象的执行方法,而不需要知道具体的执行细节。
-
需要支持命令的批量执行和撤销。命令模式可以将一系列操作封装成命令对象,并且可以通过调用命令对象的批量执行和撤销方法来批量执行和撤销操作。
-
需要支持操作的日志记录和回放。命令模式可以在命令对象的执行方法中记录操作的日志信息,并且可以通过回放命令对象的执行方法来重现操作。
-
需要支持命令的延迟执行。命令模式可以将操作封装成命令对象,并且可以在需要的时候才执行命令对象的执行方法。
总之,命令模式适用于各种需要将操作封装成对象,并且需要对操作进行控制、组织、记录和协调的场景。
2.1.5.4 使用案例
命令模式是一种行为设计模式,它用于将请求封装成一个对象,从而使不同的请求可以被不同的对象处理。
以下是一个使用命令模式的示例,用于演示用户在一个文本编辑器中执行撤销和重做操作。
首先,我们需要定义一个接口Command
,用于表示所有命令的通用行为:
public interface Command {
void execute();
void undo();
void redo();
}
然后,我们可以实现不同的命令类,比如InsertCommand
和DeleteCommand
:
public class InsertCommand implements Command {
private Document document;
private String text;
public InsertCommand(Document document, String text) {
this.document = document;
this.text = text;
}
public void execute() {
document.insert(text);
}
public void undo() {
document.delete(text);
}
public void redo() {
execute();
}
}
public class DeleteCommand implements Command {
private Document document;
private String text;
public DeleteCommand(Document document, String text) {
this.document = document;
this.text = text;
}
public void execute() {
document.delete(text);
}
public void undo() {
document.insert(text);
}
public void redo() {
execute();
}
}
接下来,我们需要定义一个文本编辑器Editor
,它包含一个撤销栈和一个重做栈,以及一些方法用于执行和撤销命令:
import java.util.Stack;
public class Editor {
private Document document;
private Stack<Command> undoStack;
private Stack<Command> redoStack;
public Editor(Document document) {
this.document = document;
this.undoStack = new Stack<>();
this.redoStack = new Stack<>();
}
public void executeCommand(Command command) {
command.execute();
undoStack.push(command);
redoStack.clear();
}
public void undo() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.redo();
undoStack.push(command);
}
}
}
最后,我们可以创建一个示例程序,演示命令模式的使用:
public class Main {
public static void main(String[] args) {
Document document = new Document();
Editor editor = new Editor(document);
Command insertCommand = new InsertCommand(document, "Hello, World!");
editor.executeCommand(insertCommand);
System.out.println(document.getContent()); // 输出: Hello, World!
Command deleteCommand = new DeleteCommand(document, "World");
editor.executeCommand(deleteCommand);
System.out.println(document.getContent()); // 输出: Hello, !
editor.undo();
System.out.println(document.getContent()); // 输出: Hello, World!
editor.redo();
System.out.println(document.getContent()); // 输出: Hello, !
}
}
在这个示例中,用户可以通过执行InsertCommand
和DeleteCommand
来向文本编辑器中插入和删除文本。同时,用户还可以使用undo
和redo
方法来撤销和重做操作。命令模式的优点在于可以将命令的执行和撤销逻辑封装到不同的命令类中,使得代码更加可维护和扩展。
2.1.6 职责链模式(Chain of Responsibility Pattern)
2.1.6.1 简介
职责链模式(Chain of Responsibility Pattern)是一种行为型设计模式,用于解耦请求发送者和接收者,将多个接收对象连接成一条链,然后沿着这条链传递请求,直到有一个接收者能够处理它。
在职责链模式中,每个接收者都有一个指向下一个接收者的引用,形成一个链。当一个请求从链的起始位置发送时,沿着链的每个接收者依次判断是否能够处理请求,如果能够处理则处理请求,否则将请求传递给下一个接收者。这样,请求可以从链的起始位置一直传递到链的末尾,直到找到能够处理它的接收者。
2.1.6.2 优缺点
职责链模式的优点:
- 解耦:将请求的发送者和接收者解耦,降低系统的耦合度。
- 可扩展性:可以动态添加/修改/删除处理者,扩展性高。
- 灵活性:可以根据实际需求灵活地组合处理者,可以灵活地调整处理流程。
- 简化对象:每个处理者只需要关注自己的处理逻辑,无需知道其他处理者的存在,使得对象的职责更加清晰。
职责链模式的缺点:
- 不能保证请求一定会被处理:如果没有合适的处理者处理请求,请求可能会被无视。
- 可能导致系统性能下降:每个请求都需要从链头依次传递到链尾,可能导致系统的性能下降。
- 调试困难:由于一个请求可能经过多个处理者处理,如果出现问题,可能需要逐个排查处理者的逻辑,增加了调试的难度。
2.1.6.3 使用场景
职责链模式适用于以下场景:
- 对于一个请求需要经过多个对象处理,并且具体哪个对象处理该请求在运行时决定。
- 需要动态地给某个对象指定处理请求的对象集合。
- 对于一个请求的处理需要多个对象参与,但是每个对象的处理逻辑不同。
- 不希望请求的发送者和接收者之间耦合在一起,让请求的发送者只需要知道第一个处理者即可。
实际上,职责链模式在很多情况下都可以使用。比如,系统中的权限控制、日志记录、异常处理等都可以使用职责链模式来实现。
2.1.6.4 使用案例
职责链模式是一种行为设计模式,用于将请求的发送者和接收者解耦,并将其组织成一个链。当一个请求在链上被发送时,链中的每个对象都有机会处理该请求,直到有一个对象处理了它为止。
在Java中,职责链模式可以用于处理请求的分发和处理,比如过滤器链和异常处理链等场景。
以下是一个示例,展示了职责链模式在Java中的使用:
首先,我们定义一个请求类Request,其中包含一个请求类型和请求内容。
public class Request {
private String type;
private String content;
public Request(String type, String content) {
this.type = type;
this.content = content;
}
public String getType() {
return type;
}
public String getContent() {
return content;
}
}
接下来,我们定义一个抽象处理类Handler,其中包含一个后继处理器(也就是下一个处理者)和一个处理请求的方法。
public abstract class Handler {
protected Handler successor;
public void setSuccessor(Handler successor) {
this.successor = successor;
}
public abstract void handleRequest(Request request);
}
然后,我们创建两个具体的处理类,分别是EmailHandler和SmsHandler,用于处理不同类型的请求。
public class EmailHandler extends Handler {
@Override
public void handleRequest(Request request) {
if (request.getType().equals("email")) {
System.out.println("EmailHandler handles the request: " + request.getContent());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
public class SmsHandler extends Handler {
@Override
public void handleRequest(Request request) {
if (request.getType().equals("sms")) {
System.out.println("SmsHandler handles the request: " + request.getContent());
} else if (successor != null) {
successor.handleRequest(request);
}
}
}
最后,我们使用职责链模式将处理类连接成一个链,并测试其功能。
public class Client {
public static void main(String[] args) {
Handler emailHandler = new EmailHandler();
Handler smsHandler = new SmsHandler();
emailHandler.setSuccessor(smsHandler);
Request request1 = new Request("email", "Send an email");
Request request2 = new Request("sms", "Send a sms");
Request request3 = new Request("phone", "Make a phone call");
emailHandler.handleRequest(request1);
emailHandler.handleRequest(request2);
emailHandler.handleRequest(request3);
}
}
运行以上代码,输出结果如下:
EmailHandler handles the request: Send an email
SmsHandler handles the request: Send a sms
在这个示例中,我们使用职责链模式将EmailHandler和SmsHandler连接成一个处理链。当一个请求被发送时,会从链的开头开始进行处理,直到有一个处理者处理了它为止。
这个示例模拟了一个简单的请求处理场景,请求的类型可以是email、sms或phone。EmailHandler负责处理email类型的请求,SmsHandler负责处理sms类型的请求。如果一个请求的类型不属于这两种类型,则会被传递给下一个处理者进行处理。这样,我们就实现了请求的分发和处理的解耦。
2.1.7 迭代器模式(Iterator Pattern)
2.1.7.1 简介
迭代器模式是一种行为型设计模式,它允许我们访问一个容器对象的元素,而不需要暴露该对象的内部表示。迭代器模式提供了一种统一的方式来遍历各种不同类型的容器,而无需关心其内部实现细节。
在迭代器模式中,容器对象通常会实现一个迭代器接口,该接口定义了访问容器元素的方法。迭代器对象负责追踪当前迭代的位置,并提供访问和遍历容器元素的方法,如在容器中移动到下一个元素、获取当前元素等。
迭代器模式在很多编程语言中都有广泛的应用,如Java中的Iterator接口和C#中的IEnumerable和IEnumerator接口等。
总结来说,迭代器模式提供了一种通用的方式来遍历容器对象的元素,而不需要了解容器的内部结构,从而提高了代码的可维护性和灵活性。
2.1.7.2 优缺点
优点:
- 可以隐藏遍历集合的具体实现,使代码更加简洁清晰。
- 提供了一种统一的遍历集合的方式,减少了遍历代码的重复性。
- 可以在遍历集合的同时对集合进行修改,而不会导致遍历过程中的错误。
缺点:
- 使用迭代器模式会增加代码的复杂性,特别是对于简单的集合而言,使用迭代器可能比直接遍历更加复杂。
- 迭代器模式需要实现一个迭代器类,如果集合的结构发生变化,可能需要同时修改迭代器类的实现。
- 迭代器模式对于底层集合的数据结构有一定的依赖,不同的集合可能需要不同的迭代器实现。
2.1.7.3 使用场景
迭代器模式主要用于遍历集合(如数组、列表、树等)中的元素,而不需要暴露集合的内部结构。以下是几个常见的使用场景:
-
需要遍历一个集合但又不想暴露其内部结构:例如,一个类维护了一个私有的列表,可以使用迭代器模式来提供一种遍历该列表的方式,而不暴露列表的实现细节。
-
需要对一个集合提供多种遍历方式:例如,一个类维护了一个列表,可以提供不同的迭代器来支持正向遍历、逆向遍历等不同的遍历方式。
-
需要在遍历过程中对集合进行修改:例如,在遍历一个列表的同时,可能需要移除其中的某些元素,可以使用迭代器模式来实现。
-
需要对多个集合进行统一的遍历操作:例如,一个类维护了多个列表,可以使用迭代器模式来提供一种统一的遍历方式,而不需要关心不同列表的具体实现。
-
需要按照一定的顺序遍历集合中的元素:例如,一个类维护了一个无序集合,可以使用迭代器模式来提供一种有序的遍历方式。
总的来说,迭代器模式适用于需要遍历集合中的元素,而不关心集合内部结构的情况。它提供了一种统一的遍历接口,并且可以方便地支持不同的遍历方式和对集合的修改操作。
2.1.7.4 使用案例
迭代器模式是一种行为型设计模式,它用于提供一种统一的方法来访问一个容器对象中的各个元素,而不需要暴露该容器对象的内部结构。
在Java中,迭代器模式经常被用于遍历集合类的元素。下面是一个使用迭代器模式的示例,假设我们有一个自定义的集合类 Bookshelf,用于存储书籍对象。
首先,我们定义一个 Book 类用于表示书籍的信息:
public class Book {
private String title;
private String author;
public Book(String title, String author) {
this.title = title;
this.author = author;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
}
接下来,我们定义一个 Bookshelf 类表示书架,其中包含一个数组用于存储 Book 对象,并实现 Iterable 接口:
import java.util.Iterator;
public class Bookshelf implements Iterable<Book> {
private Book[] books;
private int count;
public Bookshelf(int capacity) {
books = new Book[capacity];
count = 0;
}
public void addBook(Book book) {
if (count < books.length) {
books[count++] = book;
}
}
@Override
public Iterator<Book> iterator() {
return new BookIterator();
}
private class BookIterator implements Iterator<Book> {
private int currentIndex;
public BookIterator() {
currentIndex = 0;
}
@Override
public boolean hasNext() {
return currentIndex < count;
}
@Override
public Book next() {
if (hasNext()) {
return books[currentIndex++];
}
return null;
}
}
}
在上述代码中,Bookshelf 类实现了 Iterable 接口,并重写了 iterator() 方法来返回一个 BookIterator 对象。BookIterator 类实现了 Iterator 接口,可以用于遍历 Bookshelf 中的书籍。
现在,我们可以使用迭代器来遍历 Bookshelf 中的书籍了:
public class Main {
public static void main(String[] args) {
Bookshelf bookshelf = new Bookshelf(5);
bookshelf.addBook(new Book("Book 1", "Author 1"));
bookshelf.addBook(new Book("Book 2", "Author 2"));
bookshelf.addBook(new Book("Book 3", "Author 3"));
Iterator<Book> iterator = bookshelf.iterator();
while (iterator.hasNext()) {
Book book = iterator.next();
System.out.println(book.getTitle() + " by " + book.getAuthor());
}
}
}
输出结果为:
Book 1 by Author 1
Book 2 by Author 2
Book 3 by Author 3
在上述示例中,我们通过迭代器遍历了 Bookshelf 中的书籍,并打印出了每本书的标题和作者。通过使用迭代器模式,我们可以方便地遍历集合类中的元素,而不需要了解集合类的内部实现细节。
2.1.8 中介者模式(Mediator Pattern)
2.1.8.1 简介
中介者模式是一种行为型设计模式,它用于解耦系统中的各个组件之间的关系,通过引入一个中介者对象来统一管理组件之间的通信。中介者模式将系统中的组件之间的复杂的相互依赖关系转化为一个简单的中介者对象与各个组件直接通信的关系。
在中介者模式中,各个组件不再直接与其他组件通信,而是通过中介者对象来进行通信。当一个组件需要与其他组件进行通信时,它将消息发送给中介者对象,中介者对象负责将消息传递给目标组件。这样,每个组件只需要与中介者对象进行通信,而不需要与其他组件直接通信,从而实现了组件之间的解耦。
2.1.8.2 优缺点
中介者模式的优点:
- 减少了对象之间的直接通信,简化了对象之间的耦合关系,使得对象之间的通信更加灵活、可复用。
- 将对象之间的通信集中在中介者对象中,减少了系统中对象之间的复杂性,提高了系统的可维护性和可扩展性。
- 中介者模式可以灵活地扩展和修改系统中的各个组件,只需修改中介者类或新增一个中介者类即可,不需要修改其他类。
- 可以将系统中的复杂交互逻辑封装在中介者对象中,使得系统更加清晰、易于理解。
中介者模式的缺点:
- 中介者模式会增加一个中介者对象的复杂性,如果中介者对象设计不合理,可能会成为系统的瓶颈,影响系统的性能。
- 如果系统中的对象之间的交互关系非常复杂,中介者模式可能会导致中介者对象过于庞大和复杂,难以维护和扩展。
- 中介者模式将系统中的交互逻辑集中在一个对象中,可能会使得该对象的复杂性增加,难以理解和维护。
2.1.8.3 使用场景
中介者模式的使用场景如下:
-
对象之间存在复杂的交互关系:当多个对象之间存在复杂的交互关系,每个对象都需要直接与其他对象通信时,可以使用中介者模式。中介者作为一个中心协调者,可以封装对象之间的交互,简化对象之间的关系。
-
减少对象之间的耦合:中介者模式可以降低对象之间的依赖程度,使得对象之间的耦合度低。当需要修改对象之间的交互时,只需修改中介者对象,而不必修改各个对象之间的关系。
-
业务逻辑复杂的系统:在一个系统中,如果业务逻辑非常复杂,对象之间的交互关系错综复杂,可以使用中介者模式来简化系统的架构和设计。通过引入中介者对象,可以将复杂的交互逻辑集中处理,使得系统更加可维护和可扩展。
-
多个对象共享同一个资源:当多个对象需要共享同一个资源时,可以使用中介者模式来协调资源的访问。中介者可以控制和管理资源的访问,避免资源的冲突和竞争。
-
分布式系统中的消息传递:在分布式系统中,各个节点之间需要进行消息的传递和协调。可以使用中介者模式来管理消息的传递和协调,实现分布式系统的通信和协作。
2.1.8.4 使用案例
中介者模式是一种行为型设计模式,它通过将一组对象的交互行为封装在一个中介者对象中,来减少对象之间的直接耦合。在这种模式中,对象不再直接与其他对象进行通信,而是通过中介者来进行通信。这种模式可以降低系统的复杂性,提高可维护性。
下面是一个简单的示例,展示了如何使用中介者模式来实现一个聊天室的功能。
// 定义一个中介者接口
interface ChatRoomMediator {
void sendMessage(User user, String message);
}
// 实现中介者接口
class ChatRoom implements ChatRoomMediator {
@Override
public void sendMessage(User user, String message) {
System.out.println("[" + user.getName() + "]发送消息: " + message);
}
}
// 定义一个用户类
class User {
private String name;
private ChatRoomMediator chatRoomMediator;
public User(String name, ChatRoomMediator chatRoomMediator) {
this.name = name;
this.chatRoomMediator = chatRoomMediator;
}
public String getName() {
return name;
}
public void sendMessage(String message) {
chatRoomMediator.sendMessage(this, message);
}
}
// 客户端代码
public class MediatorPatternExample {
public static void main(String[] args) {
// 创建一个中介者
ChatRoomMediator chatRoom = new ChatRoom();
// 创建几个用户
User user1 = new User("User1", chatRoom);
User user2 = new User("User2", chatRoom);
User user3 = new User("User3", chatRoom);
// 用户发送消息
user1.sendMessage("Hello, everyone!");
user2.sendMessage("Hi, User1!");
user3.sendMessage("Nice to meet you!");
/*
输出结果:
[User1]发送消息: Hello, everyone!
[User2]发送消息: Hi, User1!
[User3]发送消息: Nice to meet you!
*/
}
}
在上述示例中,我们定义了一个中介者接口ChatRoomMediator
,该接口包含一个sendMessage
方法。然后我们实现了一个具体的中介者类ChatRoom
来实现ChatRoomMediator
接口。
我们还定义了一个User
类,该类有一个sendMessage
方法,该方法会调用中介者对象的sendMessage
方法来发送消息。
在客户端代码中,我们创建了一个中介者对象chatRoom
,以及几个用户对象。然后用户对象通过调用sendMessage
方法来发送消息,而不是直接与其他用户对象进行通信。这样就实现了通过中介者来进行通信的功能。
通过中介者模式,我们将用户对象之间的交互逻辑封装在了中介者对象中,使得系统的扩展和维护更加方便。同时,中介者模式还可以降低对象之间的直接耦合,提高系统的松耦合性。
2.1.9 备忘录模式(Memento Pattern)
2.1.9.1 简介
备忘录模式(Memento Pattern)是一种行为型设计模式,它允许在不暴露对象内部状态的情况下捕获和恢复对象的内部状态。该模式通过保存对象的状态到备忘录对象中,以便在需要时可以将对象恢复到之前的状态。
在备忘录模式中,主要包含以下几个角色:
- Originator(原发器):负责创建一个备忘录对象,用于存储当前对象的内部状态,并可以使用备忘录对象恢复到之前的状态。
- Memento(备忘录):用于存储Originator对象的内部状态。
- Caretaker(负责人):负责保存和管理备忘录对象。
使用备忘录模式可以提供一种简单的方式来实现对象的撤销操作、恢复到之前的状态、保存历史状态等功能。它可以避免在对象中暴露状态的细节,保持对象的封装性;同时也可以减少对象的内存开销,因为备忘录只保存对象的状态而不保存对象的具体数据。
2.1.9.2 优缺点
优点:
- 将对象的状态保存在备忘录中,可以在需要时轻松地恢复到先前的状态。
- 可以保护对象的封装性,因为备忘录对象只能由原始对象访问和修改。其他对象无法访问备忘录对象。
- 备忘录模式可以提供对象的历史记录,可以随时回滚到先前的状态。
缺点:
- 备忘录模式可能会增加内存使用,因为需要存储多个备忘录对象。
- 如果原始对象的状态非常大或者频繁变化,备忘录模式可能会导致性能问题。
- 备忘录模式需要额外的代码来管理备忘录对象的创建、存储和检索,增加了代码复杂性。
2.1.9.3 使用场景
备忘录模式的使用场景包括以下几种:
-
需要保存状态历史的情况:备忘录模式适用于需要保存对象状态历史的情况。例如,在文档编辑器中,可以使用备忘录模式保存编辑历史,以便可以撤销和重做操作。
-
需要实现快照功能的情况:备忘录模式适用于需要实现对象快照功能的情况。例如,在游戏中,可以使用备忘录模式保存游戏进度快照,以便在需要时回到之前的进度。
-
需要实现撤销和重做功能的情况:备忘录模式适用于需要实现撤销和重做功能的情况。例如,在图形绘制软件中,可以使用备忘录模式保存绘制历史,以便可以撤销和重做绘制操作。
-
需要实现状态的持久化和恢复功能的情况:备忘录模式适用于需要实现状态的持久化和恢复功能的情况。例如,在电子商务系统中,可以使用备忘录模式保存用户购物车的状态,以便在用户退出后可以恢复购物车中的商品。
总的来说,备忘录模式适用于需要保存和恢复对象状态的情况,或者需要实现撤销和重做功能的情况。
2.1.9.4 使用案例
备忘录模式是一种行为型设计模式,它允许在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。
在Java中,备忘录模式常用于需要保存和恢复对象状态的场景,比如文本编辑器的撤销操作、游戏的存档功能等。
下面是一个示例,演示了如何使用备忘录模式来保存和恢复游戏的状态:
首先,我们需要定义一个备忘录类 GameMemento
,用于存储游戏状态:
public class GameMemento {
private int level;
private int score;
public GameMemento(int level, int score) {
this.level = level;
this.score = score;
}
public int getLevel() {
return level;
}
public int getScore() {
return score;
}
}
然后,我们定义一个游戏类 Game
,它可以保存和恢复游戏状态,并提供一些操作方法:
public class Game {
private int level;
private int score;
public void play() {
// 模拟游戏进行中的操作
level++;
score += 100;
System.out.println("Playing game: level " + level + ", score " + score);
}
public GameMemento save() {
return new GameMemento(level, score);
}
public void restore(GameMemento memento) {
level = memento.getLevel();
score = memento.getScore();
System.out.println("Restoring game: level " + level + ", score " + score);
}
}
最后,我们可以在客户端代码中使用备忘录模式,保存和恢复游戏状态:
public class Client {
public static void main(String[] args) {
Game game = new Game();
// 初始状态
game.play();
// 保存游戏状态
GameMemento memento = game.save();
// 进行一些操作
game.play();
game.play();
// 恢复游戏状态
game.restore(memento);
}
}
输出结果:
Playing game: level 1, score 100
Playing game: level 2, score 300
Playing game: level 3, score 500
Restoring game: level 2, score 300
可以看到,通过备忘录模式,我们可以保存游戏状态,并在需要时恢复到之前的状态。这样用户就能够在游戏中进行撤销操作,或者在游戏存档时进行恢复。
2.1.10 状态模式(State Pattern)
2.1.10.1 简介
状态模式是一种行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为。它将对象的行为封装在不同的状态对象中,以实现状态切换时的行为改变。
在状态模式中,对象通过将其内部状态委托给不同的状态对象,在不同状态下具有不同的行为。状态对象可以根据对象的内部状态来改变对象的行为。通过将状态转换的逻辑从对象中提取出来,可以使对象的行为更加灵活和可扩展。
状态模式通常包括以下角色:
- 环境类(Context):维护一个具体状态对象的引用,并将请求委托给当前状态对象进行处理。
- 抽象状态类(State):定义一个接口,用于封装与环境类的一个特定状态相关的行为。
- 具体状态类(Concrete State):实现抽象状态类的接口,封装与特定状态相关的行为。
通过使用状态模式,可以实现对象在不同状态下的行为变化,而无需使用大量的条件语句。它可以提高代码的可读性和可维护性,同时也符合开闭原则(对扩展开放,对修改关闭)。
2.1.10.2 优缺点
优点:
-
状态模式将对象的状态封装在不同的状态类中,使得状态转换更加明确和简单。
-
状态模式将对象的行为与状态解耦,增加新的状态或者新的行为相对容易。
-
状态模式符合开闭原则,增加新的状态类不需要修改其他代码。
-
状态模式可以通过改变对象的状态来改变对象的行为,使得代码更加灵活可扩展。
缺点:
-
状态模式增加了系统的类和对象的个数,增加了系统的复杂性。
-
状态模式的实现需要维护状态之间的转换,增加了系统的维护成本。
-
如果状态转换比较频繁,可能会导致系统的性能下降。
2.1.10.3 使用场景
下面是一些状态模式的使用场景:
-
在任务或工作流中,根据不同的状态执行不同的操作。例如,在一个订单处理系统中,订单可以处于待支付、已支付、已发货等状态,根据订单状态的不同,需要执行相应的操作,如生成发货单、生成退款单等。
-
在游戏中,角色的行为可能会根据当前状态进行切换。例如,在一个角色扮演游戏中,角色可能处于正常状态、攻击状态、防御状态等,根据不同的状态,角色可以执行不同的行为,如攻击敌人、防御自己等。
-
在电子商务网站中,购物车的行为可能会根据当前的状态进行不同的处理。例如,在购物车中,可以添加商品、删除商品、结算等操作,根据购物车的状态,可以执行相应的操作,如只有在购物车中有商品时才能结算。
-
在多媒体播放器中,根据当前的播放状态进行相应的操作。例如,在一个音乐播放器中,可以播放、暂停、停止等操作,根据当前的播放状态,可以决定是否执行相应的操作。
总的来说,状态模式适用于对象的行为需要根据状态进行不同处理的情况,能够将每个状态的处理逻辑封装在不同的状态类中,并根据状态进行切换,提高了代码的可维护性和可扩展性。
2.1.10.4 使用案例
状态模式是一种行为型设计模式,它允许对象在内部状态发生改变时改变其行为。在状态模式中,对象会根据当前状态选择不同的行为。
下面是一个在Java中使用状态模式的示例:
首先,我们定义一个接口State
来表示对象的状态,其中包含一个handle()
方法用于处理当前状态的行为:
public interface State {
void handle();
}
然后,我们创建具体的状态类,实现State
接口。每个具体状态类表示对象的一种状态,并实现了相应的行为。例如,我们创建一个IdleState
表示对象处于空闲状态:
public class IdleState implements State {
@Override
public void handle() {
System.out.println("Object is in idle state");
}
}
接下来,我们创建一个上下文类Object
,它包含一个状态对象的引用,并提供了一些方法用于设置和获取状态,以及调用当前状态的行为:
public class Object {
private State currentState;
public void setState(State state) {
this.currentState = state;
}
public void performAction() {
currentState.handle();
}
}
最后,我们可以使用状态模式来管理对象的状态。例如:
public class Main {
public static void main(String[] args) {
Object object = new Object();
// 设置初始状态为IdleState
object.setState(new IdleState());
// 执行当前状态的行为
object.performAction();
// 切换到其他状态
object.setState(new RunningState());
object.performAction();
object.setState(new SleepingState());
object.performAction();
}
}
在上述示例中,我们创建了一个Object
对象,并设置其初始状态为IdleState
。然后,我们调用performAction()
方法来执行当前状态的行为。
通过切换状态,我们可以实现不同的行为。例如,当切换到RunningState
时,输出为"Object is running";当切换到SleepingState
时,输出为"Object is sleeping"。
这就是一个简单的在Java中使用状态模式的示例。它可以帮助我们根据对象的不同状态来选择不同的行为,使代码更加简洁和可维护。
2.1.11 访问者模式(Visitor Pattern)
2.1.11.1 简介
访问者模式是一种对象行为型模式。它的目的是将算法与数据结构进行分离,使得算法可以独立于数据结构的变化而变化。通过定义一个访问者(Visitor)类,该类封装了对某种数据结构(如对象结构)中各个元素的操作,从而将数据结构与操作解耦。
访问者模式的核心思想是将对象结构中的各个元素的操作封装到访问者类中,该类提供了统一的接口来访问对象结构中的元素。这样一来,即使需要对对象结构中的元素进行扩展,也无需修改访问者类,只需创建一个新的访问者类即可。
访问者模式包含以下几个角色:
- 抽象访问者(Visitor):定义了对对象结构中各个元素的操作方法,它可以通过重载方法来实现不同的访问操作。
- 具体访问者(ConcreteVisitor):具体实现了抽象访问者定义的各个操作。
- 抽象元素(Element):定义了访问者可以访问的对象的接口。
- 具体元素(ConcreteElement):实现了抽象元素接口,通常会提供一个accept()方法,用于接受访问者的访问。
- 对象结构(ObjectStructure):用于存储元素对象,实现了一个集合,它可以遍历所有的元素,并提供一个高层接口,以供访问者访问。
使用访问者模式可以将算法与数据结构解耦,使得代码更加灵活和可扩展。然而,访问者模式在增加新的元素类时,需要修改访问者类的代码,这可能会导致访问者类的代码变得庞大和复杂。因此,在使用访问者模式时,需要权衡代码的灵活性和复杂性。
2.1.11.2 优缺点
访问者模式的优点包括:
- 通过将数据结构和数据操作分离,使得新增的操作更容易添加到现有系统中,而无需修改已有的代码。
- 可以在不改变数据结构的情况下定义新的操作。
- 将相关的操作封装在一个访问者对象中,使得代码更加清晰和易于维护。
- 可以在不影响数据结构的情况下,在不同对象间共享和复用访问者对象。
访问者模式的缺点包括:
- 增加了系统的复杂性,引入了新的抽象层,使得代码变得更加难以理解和阅读。
- 在增加新的数据结构时,需要修改现有的访问者类,违背了开闭原则。
- 运用访问者模式时,访问者对象需要访问并操作数据结构中的每一个对象,在数据结构较大时可能会导致性能问题。
2.1.11.3 使用场景
访问者模式通常在以下场景中使用:
-
当一个数据结构中的元素对象有不同的操作方式,并且需要执行这些操作时,可以使用访问者模式。该模式能够将数据结构与操作分离,使得操作可以独立变化,而不影响数据结构。
-
当需要对一个复杂的对象结构进行操作,且需要对不同类型的元素执行不同的操作时,可以使用访问者模式。访问者模式能够将对象结构与操作解耦,简化结构的复杂性。
-
当一个对象结构中的元素对象的类别很少改变,但经常需要在该对象结构上定义新的操作时,可以使用访问者模式。通过定义不同的访问者,可以在不修改对象结构的情况下增加新的操作。
-
当多个对象结构需要共享一些公共操作时,可以使用访问者模式。通过定义一个访问者接口,不同的对象结构可以共享该接口,以执行共同的操作。
2.1.11.4 使用案例
访问者模式是一种行为型设计模式,它允许你在不修改对象结构的前提下定义新的操作。
假设我们有一个图形库,包含多种不同类型的图形,比如圆形、矩形等。我们要对这些图形进行不同的操作,比如计算面积、计算周长等。
首先,我们需要定义一个抽象图形类Shape,包含一个抽象的accept方法,用于接受访问者访问。
interface Shape {
void accept(Visitor visitor);
}
然后,我们实现具体的图形类,比如圆形Circle和矩形Rectangle,它们都实现accept方法,将自己作为参数传给访问者。
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double getRadius() {
return radius;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
class Rectangle implements Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double getWidth() {
return width;
}
public double getHeight() {
return height;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
接下来,我们需要定义访问者接口Visitor,包含多个visit方法,用于对不同类型的图形进行具体的操作。
interface Visitor {
void visit(Circle circle);
void visit(Rectangle rectangle);
}
然后,我们实现具体的访问者类,比如计算面积的AreaVisitor和计算周长的PerimeterVisitor。
class AreaVisitor implements Visitor {
private double area;
public double getArea() {
return area;
}
@Override
public void visit(Circle circle) {
area = Math.PI * circle.getRadius() * circle.getRadius();
}
@Override
public void visit(Rectangle rectangle) {
area = rectangle.getWidth() * rectangle.getHeight();
}
}
class PerimeterVisitor implements Visitor {
private double perimeter;
public double getPerimeter() {
return perimeter;
}
@Override
public void visit(Circle circle) {
perimeter = 2 * Math.PI * circle.getRadius();
}
@Override
public void visit(Rectangle rectangle) {
perimeter = 2 * (rectangle.getWidth() + rectangle.getHeight());
}
}
最后,我们可以使用访问者模式对图形进行操作。
public class Main {
public static void main(String[] args) {
Shape[] shapes = {new Circle(5), new Rectangle(3, 4)};
AreaVisitor areaVisitor = new AreaVisitor();
for (Shape shape : shapes) {
shape.accept(areaVisitor);
System.out.println("Area: " + areaVisitor.getArea());
}
PerimeterVisitor perimeterVisitor = new PerimeterVisitor();
for (Shape shape : shapes) {
shape.accept(perimeterVisitor);
System.out.println("Perimeter: " + perimeterVisitor.getPerimeter());
}
}
}
运行结果:
Area: 78.53981633974483
Area: 12.0
Perimeter: 31.41592653589793
Perimeter: 14.0
这个示例中,我们通过访问者模式实现了对不同类型的图形进行不同的操作,而不需要修改图形类的结构。这样可以保持图形类的封装性和灵活性。
3、结语
文章至此,已接近尾声!希望此文能够对大家有所启发和帮助。同时,感谢大家的耐心阅读和对本文档的信任。在未来的技术学习和工作中,期待与各位大佬共同进步,共同探索新的技术前沿。最后,再次感谢各位的支持和关注。您的支持是作者创作的最大动力,如果您觉得这篇文章对您有所帮助,请考虑给予一点打赏。