前情提要,请浏览:用大白话说清楚设计模式(一),:用大白话说清楚设计模式(二)。本篇是第三篇,不多废话直接开始。
一、前言
设计模式(Design Patterns)是软件开发中用来解决常见问题的通用、可重用的解决方案。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式并不是直接可用的代码,而是模板或指导原则,开发者可以根据具体需求加以实现。
分类
- 创建型模式(Creational Patterns)
-
- 啥意思:关注对象的创建过程,旨在提高对象实例化的灵活性和效率。
- 包括:单例模式、建造者模式、原型模式、工厂方法模式、抽象工厂模式。
- 结构型模式(Structural Patterns)
-
- 啥意思:关注类和对象之间的组合与关系,帮助构建清晰的系统结构。
- 包括:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式(Behavioral Patterns)
-
- 啥意思:关注对象之间的通信和职责分配,优化对象间的协作。
- 包括:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
1.创建型模式(Creational Patterns)
2.结构型模式(Structural Patterns)
3.行为型模式(Behavioral Patterns)
策略模式
是把可变的行为(比如不同的算法或策略)封装成独立的类,让它们可以互相替换,而不影响使用这些行为的代码。简单来说,就是让你在运行时灵活选择用哪种“策略”来完成任务。
通俗讲:你去餐厅吃饭,付款时可以选择现金、信用卡、支付宝或微信。每种支付方式就是一种“策略”,餐厅(代码里叫客户端)不用管你具体怎么付,只要你付钱就行
在代码里,策略模式通常会这样实现:
- 定义一个策略接口,规定所有策略的行为。
- 写多个具体策略类,实现这个接口,每个类代表一种具体策略。
- 用一个上下文类(Context) 来调用策略,上下文类里会持有一个策略接口的引用,客户端可以动态设置或切换这个策略。
分类
策略模式的核心思想很简单,稍微分一下:
- 简单策略模式
客户端直接挑一个具体策略来用,手动设置。
- 工厂策略模式
结合工厂模式,通过一个工厂类来创建策略对象,客户端不用自己去实例化具体策略。
- 上下文策略模式
上下文类根据情况自动选择策略,比如根据配置文件或用户输入来决定用哪个策略。
使用场景
策略模式特别适合以下几种情况:
- 有多种不同的算法或行为
比如:
排序算法:快速排序、冒泡排序、归并排序。
支付方式:支付宝、微信支付、银联支付。
压缩文件:ZIP、RAR、7Z。
- 需要在运行时动态选择算法
比如用户可以切换APP的主题皮肤,或者根据网络情况选择不同的数据加载方式。
- 不想写一堆 if-else 判断
如果不用策略模式,你可能得用一堆条件语句来决定用哪个算法,代码会变得又乱又难改。策略模式能让代码更干净。
- 希望客户端和具体策略解耦
客户端不用知道具体策略怎么实现的,只管用就行,这样加新策略时也不用改老代码,符合“开闭原则”(对扩展开放,对修改关闭)
示例
// 策略接口
public interface PaymentStrategy {
void pay(int amount); // 支付方法,传入支付金额
}
// 2. 具体策略类
// 支付宝支付
public class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("使用支付宝支付 " + amount + " 元");
}
}
// 微信支付
public class WechatPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("使用微信支付 " + amount + " 元");
}
}
// 银联支付
public class UnionPayStrategy implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("使用银联支付 " + amount + " 元");
}
}
// 3. 上下文类
// 上下文类
public class PaymentContext {
private PaymentStrategy paymentStrategy;
// 设置支付策略
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// 执行支付
public void pay(int amount) {
if (paymentStrategy != null) {
paymentStrategy.pay(amount);
} else {
System.out.println("请先选择支付方式");
}
}
}
// 4.客户端(比如订单系统)可以根据用户选择,动态设置支付策略并执行支付
public class Client {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// 用户选择支付宝支付
PaymentStrategy alipay = new AlipayStrategy();
context.setPaymentStrategy(alipay);
context.pay(100);
// 用户选择微信支付
PaymentStrategy wechatPay = new WechatPayStrategy();
context.setPaymentStrategy(wechatPay);
context.pay(200);
// 用户选择银联支付
PaymentStrategy unionPay = new UnionPayStrategy();
context.setPaymentStrategy(unionPay);
context.pay(300);
}
}
模板方法模式
把一个固定的流程写在一个方法里,但把一些可以变化的部分交给子类去实现。简单来说,就是父类搭一个框架,子类来填内容。
通俗讲:你去餐厅点“套餐”,套餐的流程固定,比如先上汤,再上主菜,最后上甜点。但具体是什么汤、什么主菜、什么甜点,每家餐厅都不一样
在代码中,模板方法模式通常这样实现:
- 有一个抽象父类,里面定义一个模板方法(一般是 final 的,不能改),这个方法里按顺序调用几个步骤。
- 这些步骤里,有些是抽象方法(没写具体内容),留给子类去实现。
- 子类继承父类,把这些抽象方法填上具体内容。
分类
模板方法模式的核心思想不复杂,分类也不多,但根据用法可以分成两种:
- 基本模板方法模式
父类定好模板方法和需要子类实现的抽象方法,子类直接实现这些方法。这是最常见的形式。 - 带钩子(Hook)的模板方法模式
除了抽象方法,父类还可以加一些“钩子方法”(有默认实现的方法),子类可以选择改不改这些钩子方法,来稍微调整流程。
使用场景
模板方法模式特别适合以下几种情况:
- 有固定流程,但细节会变
比如:
做菜:准备材料、烹饪、装盘,每道菜步骤一样,但具体怎么做不同。
游戏开发:启动流程(加载资源、初始化、开始游戏),但每款游戏内容不同。
测试框架:测试流程(准备、执行、清理),但每个测试的内容不同。
- 不想写重复代码
如果多个类有差不多一样的流程,只是细节不同,用模板方法模式把流程抽到父类,子类只管实现自己的部分,省得重复写流程。
- 让子类只管细节,不管整体
父类控制整个流程,子类只用专注自己的实现,职责分得清楚。
- 方便扩展,不改老代码
想加新功能?直接写新子类就行,不用动父类或老子类,符合“开闭原则”。
示例
// 抽象父类
public abstract class Dish {
// 模板方法,用 final 防止子类改流程
public final void makeDish() {
prepareIngredients(); // 准备材料
cook(); // 烹饪
if (needsSalt()) { // 用钩子方法决定是否加盐
addSalt();
}
plate(); // 装盘
}
// 钩子方法,子类可以重写
protected boolean needsSalt() {
return true; // 默认加盐
}
protected void addSalt() {
System.out.println("加盐");
}
// 抽象方法,子类必须实现
protected abstract void prepareIngredients();
protected abstract void cook();
protected abstract void plate();
}
////////////////////
// 意大利面
public class Pasta extends Dish {
@Override
protected void prepareIngredients() {
System.out.println("准备面条、酱料、蔬菜");
}
@Override
protected void cook() {
System.out.println("煮面条,炒酱料");
}
@Override
protected void plate() {
System.out.println("将面条和酱料混合,装盘");
}
}
// 牛排
public class Steak extends Dish {
@Override
protected void prepareIngredients() {
System.out.println("准备牛排、调料、配菜");
}
@Override
protected void cook() {
System.out.println("煎牛排,烤配菜");
}
@Override
protected void plate() {
System.out.println("将牛排和配菜摆盘");
}
}
public class Client {
public static void main(String[] args) {
// 做意大利面
Dish pasta = new Pasta();
pasta.makeDish();
System.out.println();
// 做牛排
Dish steak = new Steak();
steak.makeDish();
}
}
观察者模式
当一个对象(我们叫它被观察者或主题)的状态发生变化时,所有依赖它的对象(叫观察者)都会自动收到通知,然后更新自己的状态。简单来说,就是“一个变,大家跟着变”。
通俗讲:你关注了一个微信公众号,这个公众号就是被观察者,你就是观察者。每当公众号发新文章时,你会收到推送通知。公众号不需要知道你具体是谁,你也不用自己去查有没有新文章,系统自动通知你。观察者模式在软件里也是这个逻辑:它让对象之间建立一种自动的通知机制,避免了直接的硬绑定。
在代码里,观察者模式通常包含:
- 被观察者(Subject) :负责维护一个观察者列表,当自己的状态变化时,通知所有观察者。
- 观察者(Observer) :定义一个更新方法,等着被通知后执行操作。
分类
观察者模式有两种常见的实现方式:
- 推模型(Push Model)
被观察者在通知观察者时,直接把自己的状态信息(比如新数据)推送给观察者。
优点:观察者不用自己去拿数据,效率高。
缺点:如果状态信息太多,传起来可能有点费劲。 - 拉模型(Pull Model)
被观察者只告诉观察者“我变了”,观察者收到通知后,自己去找被观察者拿数据。
优点:观察者可以灵活决定要不要拿数据,或者拿哪些数据。
缺点:多了一步主动获取的操作。
使用场景
观察者模式特别适合以下几种情况:
- 一个对象变化,其他对象要跟着变
比如天气预报:天气数据更新时,手机、电视上的显示面板要跟着更新。
股票价格:价格一变,订阅的用户要收到通知。
社交媒体:有人发新帖,关注者要看到更新。
- 对象间松耦合
被观察者和观察者通过接口通信,不需要知道对方的具体实现,改起来很方便。
- 广播通信
一个对象要通知多个对象,比如发布-订阅系统。
- 事件驱动系统
比如界面上的按钮,用户一点击(事件),其他组件就要响应。
示例
// 观察者接口
public interface Observer {
void update(String weather); // 接收天气数据并更新
}
// 2. 具体观察者
// 手机显示
public class PhoneDisplay implements Observer {
@Override
public void update(String weather) {
System.out.println("手机显示:天气更新为 " + weather);
}
}
// 电视显示
public class TVDisplay implements Observer {
@Override
public void update(String weather) {
System.out.println("电视显示:天气更新为 " + weather);
}
}
// 3. 被观察者(主题)
import java.util.ArrayList;
import java.util.List;
// 被观察者
public class WeatherData {
private List<Observer> observers = new ArrayList<>(); // 观察者列表
private String weather; // 天气数据
// 注册观察者
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(weather); // 推模型:直接把天气数据传过去
}
}
// 更新天气数据并通知
public void setWeather(String weather) {
this.weather = weather;
notifyObservers();
}
}
// 4. 客户端代码
public class Client {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
// 创建观察者
Observer phone = new PhoneDisplay();
Observer tv = new TVDisplay();
// 注册观察者
weatherData.registerObserver(phone);
weatherData.registerObserver(tv);
// 更新天气数据
weatherData.setWeather("晴天");
// 模拟天气变化
weatherData.setWeather("雨天");
}
}
迭代器模式
给一个方法,使能按顺序访问一个集合(比如列表、数组、树)里的每个元素,而不用管这个集合内部是怎么组织的。简单来说,迭代器就像一个“遥控器”,你按一下“下一个”,就能看到集合里的下一个东西,完全不用操心集合是怎么存数据的
通俗讲:你在看电视,遥控器就是迭代器,按一下“下一台”,就切换到下一个频道。你不用知道电视台的节目单是怎么排的,只管按按钮看就行
在代码里,迭代器模式通常有这几个角色:
- 迭代器接口(Iterator):规定了遍历的基本操作,比如“还有没有下一个”(hasNext())和“给我下一个”(next())。
- 具体迭代器(Concrete Iterator):实现接口,具体干活,负责从集合里取元素。
- 集合接口(Aggregate):定义一个方法,专门用来生成迭代器。
- 具体集合(Concrete Aggregate):实现集合接口,管理数据并提供对应的迭代器。
分类
- 外部迭代器
由你(客户端)来控制遍历的过程,比如你手动调用 next() 去取下一个元素。
优点:很灵活,你想怎么遍历就怎么遍历。
缺点:你得自己写代码控制,稍微麻烦点。
- 内部迭代器
迭代器自己把遍历干完,比如提供一个 forEach 方法,直接把所有元素循环一遍给你。
优点:用起来简单,代码少。
缺点:不够灵活,你没法控制遍历的细节。
使用场景
迭代器模式特别适合以下几种情况:
- 想访问集合元素,但不想暴露内部结构
比如你自己写了个集合类,不希望别人知道数据是怎么存的,但又得让人能遍历里面的东西。
- 需要多种遍历方式
比如一个集合可以正着遍历、倒着遍历,或者按条件挑着遍历。
- 统一不同集合的遍历方式
比如你有数组、链表、树这些不同的集合,但希望用一样的方法去访问它们。
- 把集合和遍历逻辑分开
集合只管存数据,遍历交给迭代器,各干各的,代码更清晰。
常见的例子有:列表遍历、数据库查询结果、文件目录扫描等。
示例:
// 迭代器接口
public interface Iterator {
boolean hasNext(); // 还有没有下一个元素
Object next(); // 取下一个元素
}
// 具体迭代器
public class BookShelfIterator implements Iterator {
private BookShelf bookShelf; // 要遍历的书架
private int index; // 当前位置
public BookShelfIterator(BookShelf bookShelf) {
this.bookShelf = bookShelf;
this.index = 0; // 从第0本书开始
}
@Override
public boolean hasNext() {
return index < bookShelf.getLength(); // 检查是否还有书
}
@Override
public Object next() {
Book book = bookShelf.getBookAt(index); // 取当前书
index++; // 位置后移
return book;
}
}
// 集合接口
public interface Aggregate {
Iterator iterator(); // 返回一个迭代器
}
import java.util.ArrayList;
import java.util.List;
// 具体集合
public class BookShelf implements Aggregate {
private List<Book> books; // 用List存书
public BookShelf() {
this.books = new ArrayList<>();
}
public void addBook(Book book) {
books.add(book); // 添加一本书
}
public Book getBookAt(int index) {
return books.get(index); // 取某本书
}
public int getLength() {
return books.size(); // 书架上有几本书
}
@Override
public Iterator iterator() {
return new BookShelfIterator(this); // 创建书架的迭代器
}
}
// 书类
public class Book {
private String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class Client {
public static void main(String[] args) {
// 创建书架,添加几本书
BookShelf bookShelf = new BookShelf();
bookShelf.addBook(new Book("设计模式"));
bookShelf.addBook(new Book("Java 编程"));
bookShelf.addBook(new Book("数据结构"));
// 用迭代器遍历书架
Iterator iterator = bookShelf.iterator();
while (iterator.hasNext()) {
Book book = (Book) iterator.next();
System.out.println(book.getName());
}
}
}
责任链模式
把一群能处理请求的对象连成一条链,请求就像接力棒一样沿着这条链传下去,直到有个对象说“我来处理”为止
通俗讲:就像你去银行办业务,柜员A不会办,就把你推给柜员B,B还不会就推给C,直到有人能搞定你的需求。
责任链模式通常有这几个角色:
- 处理者接口(Handler):定个规矩,告诉大家怎么处理请求。
- 具体处理者(Concrete Handler):链子里的每个“人”,自己试着处理请求,搞不定就传给下一个人。
- 客户端(Client):发起请求的人,把请求交给链子的第一个处理者。
分类
责任链模式有两种玩法:
- 纯责任链
每个处理者要么自己处理请求,要么把请求传给下一个人,不能既处理又传。
特点:请求要么被某个处理者解决,要么传到链尾没人管。
就像请假审批,要么组长批了,要么传给经理,绝不会组长批一半再给经理。
- 不纯责任链
处理者可以先处理请求,然后再传给下一个人。
特点:一个请求可能被好几个人处理。
比如日志系统,低级别日志处理器记完后,还可以传给高级别处理器再记一遍。
开发中,纯责任链更常见,因为它更符合“找一个能干活的”这个初衷。
使用场景
责任链模式不是随便用的,它特别适合以下几种情况:
- 多个对象能处理请求,但不知道谁会处理
比如公司请假:小事找组长,大事找经理,再大事找老板,具体谁批得看假条内容。
或者界面事件:用户点了个按钮,事件先给按钮处理,按钮不行就给面板,最后给窗口。
- 想让请求者和处理者不直接挂钩
请求者只管发请求,不用知道谁处理;处理者只管接活,不用管谁发的。这样改代码的时候,双方都不用大动干戈。
- 处理顺序可以动态调整
比如审批流程,今天是组长→经理→老板,明天可以改成经理→老板→组长,链子随便调。
- 不想写一堆 if-else 判断
如果不用责任链,你可能得写一堆条件:如果天数小于1给组长,否则如果小于3给经理……代码会变得很乱。
现实中的例子有:日志系统(不同级别处理器)、浏览器的事件冒泡、Web 服务器的请求过滤器(比如 Servlet Filter)。
示例:
// 处理者接口
public interface Approver {
void setNext(Approver next); // 设置链子里的下一个人
void approveLeave(int days); // 处理请假请求
}
// 组长
public class TeamLeader implements Approver {
private Approver next; // 下一个处理者
@Override
public void setNext(Approver next) {
this.next = next;
}
@Override
public void approveLeave(int days) {
if (days <= 1) {
System.out.println("组长批准了 " + days + " 天假");
} else if (next != null) {
System.out.println("组长不能批准,转给经理");
next.approveLeave(days);
} else {
System.out.println("无法批准 " + days + " 天假");
}
}
}
// 经理
public class Manager implements Approver {
private Approver next;
@Override
public void setNext(Approver next) {
this.next = next;
}
@Override
public void approveLeave(int days) {
if (days <= 3) {
System.out.println("经理批准了 " + days + " 天假");
} else if (next != null) {
System.out.println("经理不能批准,转给老板");
next.approveLeave(days);
} else {
System.out.println("无法批准 " + days + " 天假");
}
}
}
// 老板
public class Boss implements Approver {
private Approver next;
@Override
public void setNext(Approver next) {
this.next = next;
}
@Override
public void approveLeave(int days) {
System.out.println("老板批准了 " + days + " 天假");
}
}
public class Client {
public static void main(String[] args) {
// 搭好责任链:组长 → 经理 → 老板
Approver teamLeader = new TeamLeader();
Approver manager = new Manager();
Approver boss = new Boss();
teamLeader.setNext(manager);
manager.setNext(boss);
// 请假1天
System.out.println("请假1天:");
teamLeader.approveLeave(1);
System.out.println();
// 请假2天
System.out.println("请假2天:");
teamLeader.approveLeave(2);
System.out.println();
// 请假5天
System.out.println("请假5天:");
teamLeader.approveLeave(5);
}
}
命令模式
把一个请求(命令) 封装成一个对象。封装成对象后,你就可以像操作普通对象一样去操作这个请求,比如传递它、存起来、排个队,甚至撤销它。简单来说,就是把“要做的事”打包成一个东西,让你能更灵活地控制。
通俗讲:你去餐厅吃饭,点菜时把想吃的菜写在点菜单上,服务员把单子拿给厨房,厨房照着单子做菜。你(发送者)不用管厨房(执行者)怎么做菜,服务员(调用者)也不用管是谁点的菜,只负责把单子传过去。
在代码里,命令模式通常有这几个角色:
- 命令接口(Command): 定义一个执行命令的方法,比如 execute()。
- 具体命令(Concrete Command): 实现命令接口,里面写具体的操作逻辑,通常会调用执行者的方法。
- 执行者(Receiver): 真正干活的家伙,负责执行具体任务。
- 调用者(Invoker): 触发命令的家伙,拿着命令对象喊“执行”。
分类
命令模式有几种变体,但核心思想是一样的,区别在于功能复杂度:
- 简单命令模式
最基础的版本,命令直接调用执行者的方法,没啥花样。 - 带参数的命令模式
命令对象可以带点“额外信息”,比如点菜时指定菜名和份数。 - 支持撤销的命令模式
在命令里加个 undo() 方法,能把操作撤销回去,比如取消订单。 - 宏命令(Macro Command)
一个命令包含多个子命令,一次执行一堆操作,比如一键点好几道菜。
开发中,简单命令模式和支持撤销的命令模式用得最多。
使用场景
命令模式特别适合以下几种情况:
- 需要把请求封装成对象
-
- 比如图形界面(GUI)里,按钮点击可以封装成命令,方便管理。
- 或者网络通信,把请求打包成命令对象传过去。
- 需要支持撤销或重做
-
- 比如文本编辑器,用户可以撤销上一步操作。
- 游戏里,玩家可以取消某个行动。
- 需要控制请求的执行时机
-
- 比如命令队列,先把命令存起来,晚点再执行。
- 或者记录操作日志,追踪历史。
- 想让发送者和执行者解耦
-
- 发送者不用知道执行者是谁,只管发命令。
- 执行者也不关心谁发的命令,只管干活。
常见的例子有:GUI 事件处理(按钮点击)、事务管理、线程池任务提交、日志记录等。
示例
// 1. 执行者(Receiver)
// 执行者:电视
public class TV {
public void turnOn() {
System.out.println("电视打开了");
}
public void turnOff() {
System.out.println("电视关闭了");
}
public void changeChannel(int channel) {
System.out.println("切换到频道 " + channel);
}
}
// 2. 命令接口(Command)
// 命令接口
public interface Command {
void execute();
}
// 3. 具体命令(Concrete Command)
// 开电视命令
public class TurnOnCommand implements Command {
private TV tv;
public TurnOnCommand(TV tv) {
this.tv = tv; // 绑定执行者
}
@Override
public void execute() {
tv.turnOn(); // 调用执行者的方法
}
}
// 关电视命令
public class TurnOffCommand implements Command {
private TV tv;
public TurnOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.turnOff();
}
}
// 换频道命令
public class ChangeChannelCommand implements Command {
private TV tv;
private int channel;
public ChangeChannelCommand(TV tv, int channel) {
this.tv = tv;
this.channel = channel; // 带参数的命令
}
@Override
public void execute() {
tv.changeChannel(channel);
}
}
// 4. 调用者(Invoker)
// 调用者:遥控器
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command; // 设置要执行的命令
}
public void pressButton() {
command.execute(); // 按按钮,触发命令
}
}
// 5. 客户端代码
public class Client {
public static void main(String[] args) {
// 创建执行者
TV tv = new TV();
// 创建具体命令
Command turnOn = new TurnOnCommand(tv);
Command turnOff = new TurnOffCommand(tv);
Command changeChannel = new ChangeChannelCommand(tv, 5);
// 创建调用者
RemoteControl remote = new RemoteControl();
// 开电视
remote.setCommand(turnOn);
remote.pressButton();
// 换频道
remote.setCommand(changeChannel);
remote.pressButton();
// 关电视
remote.setCommand(turnOff);
remote.pressButton();
}
}
备忘录模式
某个对象(比如游戏角色)把自己现在的状态打包成一个“存档”,交给别人保管。需要的时候,再把这个“存档”拿回来,恢复到之前的样子。这样既方便又不会暴露对象的内部细节。
通俗讲:想象玩游戏,比如《马里奥》。你的马里奥在某个关卡g了,你会想:“早知道存个档就好了!”备忘录模式就相当于游戏里的“存档”功能。你可以在某个时刻保存状态(比如位置、生命值、金币数),然后如果g了,就能读档回到之前的状态重新来过。
在备忘录模式里,有三个关键角色:
- 发起人(Originator)**
**这是需要保存状态的主角。比如游戏里的马里奥,它知道自己的状态(生命值、位置等),也能把自己打包成“存档”,还能从“存档”里恢复自己。 - 备忘录(Memento)**
**这是“存档”本身,里面装着发起人的状态数据。它就像一个密封的小盒子,保存了某个时刻的信息,但别人不能随便打开乱改。 - **管理者(Caretaker)
**这是负责保管“存档”的人。它只管存着备忘录,需要时拿出来给你,但它不会去偷看或修改里面的内容。
使用场景
备忘录模式不是随便用的,它适合下面这些情况:
- 需要保存和恢复状态
比如你在编辑一个文档,想支持“撤销”功能,就可以把每一步的操作保存下来,随时退回到上一步。 - 不想暴露对象内部细节
如果一个对象很复杂,你不想让外界直接操作它的属性,但又得提供恢复功能,备忘录模式就很合适。 - 需要快照功能
像游戏存档、数据库回滚这种需要记录“某个瞬间”的场景,都可以用备忘录模式。
简单说,想要“时间倒流”而且不希望别人乱动你的东西,这个模式就派得上用场。
示例:
假设有个游戏角色,有等级(level)和生命值(health),我们希望能保存和恢复它的状态
// 1. 发起人(GameRole
// 游戏角色类,能创建“存档”和从“存档”恢复
public class GameRole {
private int level; // 等级
private int health; // 生命值
public GameRole(int level, int health) {
this.level = level;
this.health = health;
}
public void setLevel(int level) {
this.level = level;
}
public void setHealth(int health) {
this.health = health;
}
public int getLevel() {
return level;
}
public int getHealth() {
return health;
}
// 创建存档
public Memento createMemento() {
return new Memento(level, health);
}
// 从存档恢复
public void restoreFromMemento(Memento memento) {
this.level = memento.getLevel();
this.health = memento.getHealth();
}
@Override
public String toString() {
return "角色状态 [等级=" + level + ", 生命值=" + health + "]";
}
}
//2. 备忘录(Memento)
// “存档”类,专门存角色状态。为了安全,里面的数据是只读的
public class Memento {
private final int level; // 等级
private final int health; // 生命值
public Memento(int level, int health) {
this.level = level;
this.health = health;
}
public int getLevel() {
return level;
}
public int getHealth() {
return health;
}
}
// 3. 管理者(Caretaker)
// 保管“存档”的类,用一个栈(Stack)来保存多个存档,方便取回
import java.util.Stack;
public class Caretaker {
private final Stack<Memento> mementoStack = new Stack<>();
// 保存存档
public void saveMemento(Memento memento) {
mementoStack.push(memento);
}
// 取出存档
public Memento getMemento() {
if (!mementoStack.isEmpty()) {
return mementoStack.pop();
}
return null;
}
}
// 4. 测试代码
public class Main {
public static void main(String[] args) {
// 创建角色,初始状态:等级1,生命值100
GameRole role = new GameRole(1, 100);
Caretaker caretaker = new Caretaker();
System.out.println("初始状态: " + role);
// 保存当前状态
caretaker.saveMemento(role.createMemento());
// 改变状态:升级到2级,生命值掉到80
role.setLevel(2);
role.setHealth(80);
System.out.println("改变后的状态: " + role);
// 恢复到之前的状态
Memento memento = caretaker.getMemento();
if (memento != null) {
role.restoreFromMemento(memento);
System.out.println("恢复后的状态: " + role);
}
}
}
初始状态: 角色状态 [等级=1, 生命值=100]
改变后的状态: 角色状态 [等级=2, 生命值=80]
恢复后的状态: 角色状态 [等级=1, 生命值=100]
状态模式
状态模式就是让对象根据自己的状态,自动切换不同的行为,而不是一大堆 if-else
通俗讲:想象你是一个游戏里的超级英雄,这个英雄在不同的状态下会有不同的表现:
- 正常状态:可以正常地打怪,攻击力是100,防御力是50。
- 受伤状态:挨了一顿揍,攻击力掉到50,防御力也只有20。
- 狂暴状态:嗑了药,攻击力飙到150,但防御力变得脆弱,只有30。
角色
这种模式主要关心对象之间怎么互动、怎么分工。在状态模式里,有几个关键角色:
- 上下文(Context)
这是主角,比如我们的超级英雄。它会“拿着”一个状态对象,把具体的行为交给这个状态去处理。 - 状态接口(State)
一个模板,规定了所有状态都要有哪些行为,比如“攻击”和“防御”。 - 具体状态(ConcreteState)
每个状态的实际“身份”,比如正常状态、受伤状态、狂暴状态,分别定义了在那种状态下具体怎么攻击、怎么防御。
使用场景
状态模式在这些情况下特别好用:
- 对象的行为跟状态有关
比如游戏角色的状态变化(正常、受伤、狂暴),或者订单的处理流程(待支付、已支付、已发货),状态不同,行为就不同。 - 不想写一堆条件判断
如果不用状态模式,你可能会写一堆 if-else 或 switch-case,状态多了代码就乱成一团。状态模式把每种状态单独封装,干净多了。 - 状态之间转换很复杂
如果状态之间还有跳转规则(比如受伤后不能直接狂暴),状态模式可以让每个状态自己管自己的跳转逻辑,维护起来更方便。
一句话,只要你的对象有多种状态,每种状态下行为不一样,状态模式就是个好帮手。
示例
假设有个超级英雄,有三种状态:正常、受伤、狂暴。每种状态下攻击力和防御力都不一样。
// 1. 状态接口(State)
public interface State {
void attack(); // 攻击行为
void defend(); // 防御行为
}
// 2. 具体状态类
// -- 正常状态(NormalState)
public class NormalState implements State {
@Override
public void attack() {
System.out.println("正常攻击,攻击力:100");
}
@Override
public void defend() {
System.out.println("正常防御,防御力:50");
}
}
// --受伤状态(InjuredState)
public class InjuredState implements State {
@Override
public void attack() {
System.out.println("受伤攻击,攻击力:50");
}
@Override
public void defend() {
System.out.println("受伤防御,防御力:20");
}
}
// -- 狂暴状态(BerserkState)
public class BerserkState implements State {
@Override
public void attack() {
System.out.println("狂暴攻击,攻击力:150");
}
@Override
public void defend() {
System.out.println("狂暴防御,防御力:30");
}
}
// 3. 上下文(GameRole)
// --英雄类,里面有个状态对象,英雄的行为会交给当前状态去执行
public class GameRole {
private State state; // 当前状态
public GameRole() {
// 一开始是正常状态
this.state = new NormalState();
}
// 切换状态的方法
public void setState(State state) {
this.state = state;
}
// 攻击和防御,交给当前状态处理
public void attack() {
state.attack();
}
public void defend() {
state.defend();
}
}
// 4. 测试代码
public class Main {
public static void main(String[] args) {
GameRole hero = new GameRole();
// 正常状态
System.out.println("--- 正常状态 ---");
hero.attack();
hero.defend();
// 切换到受伤状态
hero.setState(new InjuredState());
System.out.println("--- 受伤状态 ---");
hero.attack();
hero.defend();
// 切换到狂暴状态
hero.setState(new BerserkState());
System.out.println("--- 狂暴状态 ---");
hero.attack();
hero.defend();
}
}
访问者模式
访问者模式就是把数据和操作分开,让操作可以灵活地加到数据上
通俗讲:想象一下,你去一个博物馆参观,里面有各种展品,比如画作、雕塑、化石等等。你作为一个游客,可以对这些展品做不同的事:有的游客喜欢静静地欣赏,有的喜欢拍照留念,还有的可能会写评论。博物馆的展品本身没变,但不同游客来了以后,可以根据自己的兴趣对展品做不同的事。
角色
在访问者模式里,有几个关键角色:
- 访问者接口(Visitor)
就像给“游客”定个规矩,告诉他们可以对哪些东西做什么事。 - 具体访问者(ConcreteVisitor)
每个“游客”有自己的特点,比如一个喜欢拍照,一个喜欢欣赏。 - 元素接口(Element)
这是“展品”的模板,规定它们得能接受游客来访问。 - 具体元素(ConcreteElement)
具体的“展品”,比如画作、雕塑,它们会实现接受访问者的功能。 - 对象结构(ObjectStructure)
相当于“博物馆”,管理着一堆展品,让游客可以挨个访问。
通过这些角色,访问者模式把数据和操作解耦,让你能灵活地给对象添加新功能。
使用场景
访问者模式不是随便用的,它在特定情况下特别好使:
- 对象结构稳定,但操作经常变
比如一个公司的员工和部门结构固定,但你可能需要对它们做不同的操作:算工资、做报表、发通知等等。用访问者模式,你可以随时加新的操作,不用改员工或部门的代码。 - 需要对一堆不同对象做多种操作
假设你有各种图形(圆形、矩形、三角形),想对它们做不同的事(画出来、算面积、旋转),访问者模式能把这些操作集中管理。 - 不想让对象的代码变乱
如果你不想在对象的类里塞一堆乱七八糟的操作代码,用访问者模式可以把操作抽出来,让类的代码保持干净。
一句话总结:当数据结构固定,但你想对它做各种花样操作时,访问者模式很合适。
示例
下面我们用一个博物馆的例子,来写点代码看看访问者模式怎么用。假设博物馆里有两种展品:画作和雕塑,然后有两个游客,一个喜欢欣赏,一个喜欢拍照
// 1. 元素接口(Element)
public interface ArtPiece {
void accept(Visitor visitor);
}
// 2. 具体元素类- 每种展品都得照规矩来
// -- 画作(Painting
public class Painting implements ArtPiece {
@Override
public void accept(Visitor visitor) {
visitor.visitPainting(this);
}
public void display() {
System.out.println("展示一幅美丽的画作");
}
}
// -- 雕塑(Sculpture)
public class Sculpture implements ArtPiece {
@Override
public void accept(Visitor visitor) {
visitor.visitSculpture(this);
}
public void display() {
System.out.println("展示一座精美的雕塑");
}
}
// 3. 访问者接口(Visitor)
public interface Visitor {
void visitPainting(Painting painting);
void visitSculpture(Sculpture sculpture);
}
// 4. 具体访问者类-每个游客有自己的操作方式
// -- 欣赏者(Appreciator)
public class Appreciator implements Visitor {
@Override
public void visitPainting(Painting painting) {
System.out.println("欣赏画作的细节");
painting.display();
}
@Override
public void visitSculpture(Sculpture sculpture) {
System.out.println("欣赏雕塑的工艺");
sculpture.display();
}
}
//-- 摄影师(Photographer)
public class Photographer implements Visitor {
@Override
public void visitPainting(Painting painting) {
System.out.println("给画作拍照");
painting.display();
}
@Override
public void visitSculpture(Sculpture sculpture) {
System.out.println("给雕塑拍照");
sculpture.display();
}
}
// 5. 对象结构(Museum)
// -博物馆负责管理展品,让访问者挨个操作。
import java.util.ArrayList;
import java.util.List;
public class Museum {
private List<ArtPiece> artPieces = new ArrayList<>();
public void addArtPiece(ArtPiece artPiece) {
artPieces.add(artPiece);
}
public void accept(Visitor visitor) {
for (ArtPiece artPiece : artPieces) {
artPiece.accept(visitor);
}
}
}
// 6. 测试代码
public class Main {
public static void main(String[] args) {
// 创建博物馆,添加展品
Museum museum = new Museum();
museum.addArtPiece(new Painting());
museum.addArtPiece(new Sculpture());
// 创建两个访问者
Visitor appreciator = new Appreciator();
Visitor photographer = new Photographer();
// 欣赏者访问
System.out.println("--- 欣赏者访问 ---");
museum.accept(appreciator);
// 摄影师访问
System.out.println("--- 摄影师访问 ---");
museum.accept(photographer);
}
}
中介者模式
中介者模式就是用一个中间人来管理对象间的通信。
通俗讲 :想象一下,你在租房,房东和租客之间通常不会直接沟通,而是通过一个中介。中介负责传递信息、协调双方需求,比如租客想看房,中介安排时间;房东想涨租金,中介通知租客。这样,房东和租客都不用直接打交道,省心又高效。
分类
在中介者模式里,有两个关键角色:
- 中介者(Mediator)
就像房产中介,它知道所有对象的存在,负责协调它们之间的通信。 - 具体对象(Colleague)
就像房东和租客,它们只跟中介者通信,不直接与其他对象交互。
通过这两个角色,中介者模式让对象间的通信集中管理,降低了系统的耦合度。
使用场景
中介者模式在这些情况下特别有用:
- 对象间交互复杂
比如一个聊天室里,用户 A 发消息要通知用户 B、C、D,如果直接让用户间通信,代码会乱成一团。用中介者模式,聊天室作为中介,管理所有消息的发送和接收。 - 一个对象的改变会影响其他对象
比如在 GUI 界面中,点击一个按钮会触发多个组件的更新。如果不用中介者,每个组件都得知道其他组件的存在,维护起来很麻烦。用中介者模式,界面控制器作为中介,统一管理组件间的交互。 - 想降低对象间的耦合
当你希望对象之间不要直接依赖,而是通过一个中间层来通信时,中介者模式是个好选择。
一句话总结:当对象间通信关系复杂、耦合度高时,中介者模式能帮你理清关系。
示例
简单的聊天室例子,来展示中介者模式怎么用。假设有个聊天室,用户可以通过聊天室发送消息给所有人,而不是直接给其他用户发消息
// 1. 中介者接口(Mediator)
public interface ChatRoom {
void sendMessage(String message, User user);
}
// 2. 具体中介者(ConcreteMediator)
import java.util.ArrayList;
import java.util.List;
public class ConcreteChatRoom implements ChatRoom {
private List<User> users = new ArrayList<>();
public void addUser(User user) {
users.add(user);
}
@Override
public void sendMessage(String message, User sender) {
for (User user : users) {
if (user != sender) { // 不发给自己
user.receiveMessage(message);
}
}
}
}
// 3. 具体对象(User)
public class User {
private String name;
private ChatRoom chatRoom;
public User(String name, ChatRoom chatRoom) {
this.name = name;
this.chatRoom = chatRoom;
}
public void sendMessage(String message) {
System.out.println(name + " 发送消息: " + message);
chatRoom.sendMessage(message, this);
}
public void receiveMessage(String message) {
System.out.println(name + " 收到消息: " + message);
}
}
// 4. 测试代码
public class Main {
public static void main(String[] args) {
// 创建聊天室
ConcreteChatRoom chatRoom = new ConcreteChatRoom();
// 创建用户并加入聊天室
User alice = new User("Alice", chatRoom);
User bob = new User("Bob", chatRoom);
User charlie = new User("Charlie", chatRoom);
chatRoom.addUser(alice);
chatRoom.addUser(bob);
chatRoom.addUser(charlie);
// Alice 发消息
alice.sendMessage("大家好!");
// Bob 发消息
bob.sendMessage("你好,Alice!");
}
}
解释器模式
- 先定义一套“语言规则”(文法),告诉系统这些句子长什么样。
- 再提供一个“翻译工具”(解释器),把句子拆开、分析,最后执行出结果。
通俗讲:解释器模式就像一个“翻译官”。假如你有一堆奇怪的句子(比如“2 + 3 - 1”),它能把这些句子翻译成你想要的结果(比如“4”)。在软件里,这种“句子”可以是任何需要解释的东西,比如数学公式、SQL语句,或者简单的规则
角色
里面有几个关键角色:
- 抽象表达式(Abstract Expression)
-
- 这是一个“模板”,告诉大家怎么干活。
- 通常是个接口或抽象类,定义了所有“翻译官”都要实现的方法,比如“解释(interpret)”。
- 终结符表达式(Terminal Expression)
-
- 这是语言里最简单的小单位,比如“2”或“3”这样的数字。
- 它直接返回自己的值,不需要再拆分。
- 非终结符表达式(Nonterminal Expression)
-
- 这是稍微复杂点的部分,比如“加号(+)”或“减号(-)”。
- 它负责把几个小单位组合起来,计算结果。
- 上下文(Context)
-
- 这是一个“助手”,提供一些额外的信息。
- 比如,句子里的变量值可以存在这里,解释的时候用得上。
- 客户端(Client)
-
- 这是“老板”,负责把句子组织好(通常是搭一个抽象语法树,简称AST),然后交给翻译官去执行。
使用场景
解释器模式不是万能的,它适合用在这些地方:
- 简单的语言或规则:你需要解释和执行一些不太复杂的语句,比如“a + b”或者“SELECT * FROM table”。
- 规则不常变:如果语言规则经常改来改去,维护起来会很麻烦。
- 性能要求不高:解释器模式会把句子拆成很多小块,处理复杂东西时可能会慢。
常见的例子:
- 数学表达式计算器:比如算“2 + 3 - 1”。
- SQL解析器:把SQL语句翻译成数据库能懂的操作。
- 配置文件解析:读取简单的配置规则并执行。
- 简单脚本语言:比如写个小工具解释自定义命令。
示例
一个简单的数学计算器
// 1. 抽象表达式:定个规矩
public interface Expression {
int interpret(Context context); // 每个表达式都要能“解释”自己,返回结果
}
// 2. 终结符表达式:处理数字
public class NumberExpression implements Expression {
private int number; // 存一个数字,比如2、3
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context context) {
return number; // 直接返回数字,不用算
}
}
// 3. 非终结符表达式:处理加减法
// -加法
public class AddExpression implements Expression {
private Expression left; // 左边的表达式,比如2
private Expression right; // 右边的表达式,比如3
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context); // 左边 + 右边
}
}
// -减法
public class SubtractExpression implements Expression {
private Expression left; // 左边的表达式,比如2+3
private Expression right; // 右边的表达式,比如1
public SubtractExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context); // 左边 - 右边
}
}
// 4. 上下文:存点全局信息
public class Context {
// 可以放一些变量值啥的,这里先不用
}
// 5. 客户端:搭好句子,交给解释器
public class Client {
public static void main(String[] args) {
// 搭树:(2 + 3) - 1
Expression two = new NumberExpression(2); // 2
Expression three = new NumberExpression(3); // 3
Expression add = new AddExpression(two, three); // 2 + 3
Expression one = new NumberExpression(1); // 1
Expression subtract = new SubtractExpression(add, one); // (2 + 3) - 1
Context context = new Context();
int result = subtract.interpret(context); // 开始解释
System.out.println("Result: " + result); // 输出:Result: 4
}
}