【完结】OK!用大白话说清楚设计模式(三)

208 阅读33分钟

前情提要,请浏览:用大白话说清楚设计模式(一):用大白话说清楚设计模式(二)。本篇是第三篇,不多废话直接开始。

一、前言

设计模式(Design Patterns)是软件开发中用来解决常见问题的通用、可重用的解决方案。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式并不是直接可用的代码,而是模板或指导原则,开发者可以根据具体需求加以实现。

分类

  1. 创建型模式(Creational Patterns)
    • 啥意思:关注对象的创建过程,旨在提高对象实例化的灵活性和效率。
    • 包括:单例模式、建造者模式、原型模式、工厂方法模式、抽象工厂模式。
  1. 结构型模式(Structural Patterns)
    • 啥意思:关注类和对象之间的组合与关系,帮助构建清晰的系统结构。
    • 包括:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
  1. 行为型模式(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 的,不能改),这个方法里按顺序调用几个步骤。
  • 这些步骤里,有些是抽象方法(没写具体内容),留给子类去实现。
  • 子类继承父类,把这些抽象方法填上具体内容。
分类

模板方法模式的核心思想不复杂,分类也不多,但根据用法可以分成两种:

  1. 基本模板方法模式
    父类定好模板方法和需要子类实现的抽象方法,子类直接实现这些方法。这是最常见的形式。
  2. 带钩子(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): 触发命令的家伙,拿着命令对象喊“执行”。
分类

命令模式有几种变体,但核心思想是一样的,区别在于功能复杂度:

  1. 简单命令模式
    最基础的版本,命令直接调用执行者的方法,没啥花样。
  2. 带参数的命令模式
    命令对象可以带点“额外信息”,比如点菜时指定菜名和份数。
  3. 支持撤销的命令模式
    在命令里加个 undo() 方法,能把操作撤销回去,比如取消订单。
  4. 宏命令(Macro Command)
    一个命令包含多个子命令,一次执行一堆操作,比如一键点好几道菜。

开发中,简单命令模式支持撤销的命令模式用得最多。

使用场景

命令模式特别适合以下几种情况:

  1. 需要把请求封装成对象
    • 比如图形界面(GUI)里,按钮点击可以封装成命令,方便管理。
    • 或者网络通信,把请求打包成命令对象传过去。
  1. 需要支持撤销或重做
    • 比如文本编辑器,用户可以撤销上一步操作。
    • 游戏里,玩家可以取消某个行动。
  1. 需要控制请求的执行时机
    • 比如命令队列,先把命令存起来,晚点再执行。
    • 或者记录操作日志,追踪历史。
  1. 想让发送者和执行者解耦
    • 发送者不用知道执行者是谁,只管发命令。
    • 执行者也不关心谁发的命令,只管干活。

常见的例子有: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了,就能读档回到之前的状态重新来过。

在备忘录模式里,有三个关键角色:

  1. 发起人(Originator)**
    **这是需要保存状态的主角。比如游戏里的马里奥,它知道自己的状态(生命值、位置等),也能把自己打包成“存档”,还能从“存档”里恢复自己。
  2. 备忘录(Memento)**
    **这是“存档”本身,里面装着发起人的状态数据。它就像一个密封的小盒子,保存了某个时刻的信息,但别人不能随便打开乱改。
  3. **管理者(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。
角色

这种模式主要关心对象之间怎么互动、怎么分工。在状态模式里,有几个关键角色:

  1. 上下文(Context)
    这是主角,比如我们的超级英雄。它会“拿着”一个状态对象,把具体的行为交给这个状态去处理。
  2. 状态接口(State)
    一个模板,规定了所有状态都要有哪些行为,比如“攻击”和“防御”。
  3. 具体状态(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();
    }
}

访问者模式

访问者模式就是把数据和操作分开,让操作可以灵活地加到数据上

通俗讲:想象一下,你去一个博物馆参观,里面有各种展品,比如画作、雕塑、化石等等。你作为一个游客,可以对这些展品做不同的事:有的游客喜欢静静地欣赏,有的喜欢拍照留念,还有的可能会写评论。博物馆的展品本身没变,但不同游客来了以后,可以根据自己的兴趣对展品做不同的事。


角色

在访问者模式里,有几个关键角色:

  1. 访问者接口(Visitor)
    就像给“游客”定个规矩,告诉他们可以对哪些东西做什么事。
  2. 具体访问者(ConcreteVisitor)
    每个“游客”有自己的特点,比如一个喜欢拍照,一个喜欢欣赏。
  3. 元素接口(Element)
    这是“展品”的模板,规定它们得能接受游客来访问。
  4. 具体元素(ConcreteElement)
    具体的“展品”,比如画作、雕塑,它们会实现接受访问者的功能。
  5. 对象结构(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);
    }
}

中介者模式

中介者模式就是用一个中间人来管理对象间的通信

俗讲 :想象一下,你在租房,房东和租客之间通常不会直接沟通,而是通过一个中介。中介负责传递信息、协调双方需求,比如租客想看房,中介安排时间;房东想涨租金,中介通知租客。这样,房东和租客都不用直接打交道,省心又高效。

分类

在中介者模式里,有两个关键角色:

  1. 中介者(Mediator)
    就像房产中介,它知道所有对象的存在,负责协调它们之间的通信。
  2. 具体对象(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语句,或者简单的规则

角色

里面有几个关键角色:

  1. 抽象表达式(Abstract Expression)
    • 这是一个“模板”,告诉大家怎么干活。
    • 通常是个接口或抽象类,定义了所有“翻译官”都要实现的方法,比如“解释(interpret)”。
  1. 终结符表达式(Terminal Expression)
    • 这是语言里最简单的小单位,比如“2”或“3”这样的数字。
    • 它直接返回自己的值,不需要再拆分。
  1. 非终结符表达式(Nonterminal Expression)
    • 这是稍微复杂点的部分,比如“加号(+)”或“减号(-)”。
    • 它负责把几个小单位组合起来,计算结果。
  1. 上下文(Context)
    • 这是一个“助手”,提供一些额外的信息。
    • 比如,句子里的变量值可以存在这里,解释的时候用得上。
  1. 客户端(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
    }
}