课程定位、时间安排与提交方式

本讲是《专业实践》课程中一个小专题的组成部分:教师先讲清设计模式在工程里的位置,再给可照抄照改的样例;学生按作业要求独立完成 3~5 个模式的仓库。面向专业硕士研究生(专硕),强调动手与表达,而非死记硬背定义。

时间与提交

  • 本节:课堂讲授 + 当堂可跟着敲的 Java / Python 最小示例。
  • 截止:本周讲完后,下周内提交即可(具体时刻以课程群通知为准)。
  • 提交方式:每位同学在自己常用的 Git 托管平台(GitHub、GitLab、Gitee 等)建好公开或教师可访问的仓库后,将仓库网址发到课程群;教师按链接检查并登记成绩。

成绩说明:本次作业重在给样例、统一格式与养成习惯;分数不是首要目的,但需达到 README、可运行、截图说明等底线要求,便于教师快速验收。

本课课堂示范(与作业区别)

课堂上教师会用 Java 与 Python 各走一遍下面两个教材经典例子:Observer(气象站)Strategy(鸭子飞行行为)。作业仍要求每人自选 3~5 个不同模式分别建库;本页后半的「分步讲义」可直接当课堂板书稿使用。

参考书与版权声明

《Head First Design Patterns》(深入浅出设计模式)用图示与对话讲解 GoF 经典模式。课堂示范的例子尽量贴合书中叙事(气象站、鸭子),方便学生回到教材对照插图与章节。

作业实现语言建议以本课示范的 Java 或 Python 为主;若你更熟悉其他语言,也可与教师沟通,但须在 README 中写清环境与运行方式。

版权声明:课堂引用书名与教学模式概念属于合理使用;请勿在作业仓库中上传全书扫描件或盗版 PDF。

本节课目标(专硕)

  1. 理解「设计模式」对应的是可预期的变化方向,目标是解耦与可扩展,而不是背类图。
  2. 能对照教材故事,说出 Observer / Strategy 各自解决的变化是什么、关键角色有哪些。
  3. 在 Java 里会用 interface 表达角色契约;在 Python 里会用 typing.Protocol(或 ABC)模拟接口思路,并结合类型注解帮助阅读与重构。
  4. 能从「Hello World 级」的最小文件出发,一步一步拆出类、组合关系与调用顺序,跑通终端输出。
  5. 为下周作业做好准备:独立仓库、README、运行截图与简短图注。

模式地图(便于选题)

下列为 GoF 二十三式常用分类。作业要求从中自选 3~5 个(每个模式一个独立仓库)。本课课堂只精讲其中与教材高度贴合的两例:ObserverStrategy,其余模式请自学教材 + 作业实践。

类型 模式(示例) 一句话直觉
创建型 Singleton、Factory Method、Abstract Factory、Builder、Prototype 「对象怎么被创建出来」以及「创建过程如何与使用方解耦」。
结构型 Adapter、Bridge、Composite、Decorator、Facade、Flyweight、Proxy 「如何把类或对象拼装成更大的结构」,同时保持灵活。
行为型 Command、Observer、Strategy、State、Template Method、Iterator、Chain of Responsibility … 「对象之间如何协作、职责如何分配、算法或流程如何可替换」。

选题建议:优先选你在小项目里真的能用到能讲清变化点的模式;避免为了「凑模式」而写空壳代码。

课堂讲授要点(教师可展开)

1. 从「变化」出发

模式背后往往是:需求里存在某种不稳定方向(例如算法多变、通知方式多变、对象创建方式多变)。好的设计把变化封装在少数扩展点,让其余代码稳定。

2. 意图优于名字

同样的代码外形可能被误贴标签。判断模式时优先对照 GoF Intent:这段结构究竟在解决什么问题?

3. 结构图与运行时序

讲解时建议白板或画图:关键类型、组合/继承关系、一次典型调用的时序(谁创建谁、谁调用谁、回调从哪来)。

4. 与重构的关系

许多模式体现为「在坏味道出现后如何安全演进」。可点到为止:小步重构、测试保护(若课程未要求单测,至少在 README 说明如何手动验证)。

5. 工具:Trae CN

允许使用 Trae CN 等 AI 辅助编码,但课堂练习与作业的核心代码路径须由你自己理解并能口述。建议在 README 中诚实标注:哪些部分主要手写、哪些借助了提示,以及你如何验证其正确性。

课堂示范一:观察者模式(Observer)— 气象站

教材故事:气象数据(温度、湿度、气压)更新时,多个布告板(当前状况、统计、预报等)需要自动刷新。若把「更新显示」写在气象站类里 if-else 维护所有布告板,会难以扩展。Observer 让主题(Subject)维护观察者列表,数据变化时统一 notify,每个观察者自己实现 update

Python 与「接口」:Python 没有 interface 关键字。课堂推荐用 typing.Protocol 描述「必须具备的方法签名」,配合类型检查工具(如 mypy)或 IDE 提示,在心智模型上对齐 Java 的接口;运行时仍靠鸭子类型工作。

Java 分步:从空项目到可运行

  1. 第 0 步:Hello World。先建一个目录,创建 WeatherStation.java,写出 main 打印一行字,确认 javac / java 环境可用。
  2. 第 1 步:写角色契约(接口)。Subject 负责注册/移除/通知;Observer 负责 update(...);教材里的 DisplayElement 用于强调「展示」职责,可先保留为空实现再填充。
  3. 第 2 步:实现具体主题 WeatherData内部一个 List<Observer>setMeasurements 被调用时更新字段并调用 notifyObservers
  4. 第 3 步:实现具体观察者 CurrentConditionsDisplay构造器里向 Subject registerObserver(this)update 保存读数并调用 display 打印终端。
  5. 第 4 步:在 main 里串联。创建 WeatherDatanew CurrentConditionsDisplay(weatherData),再两次调用 setMeasurements,观察两行输出。

Java · 核心接口与主题(节选)

public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

public class WeatherData implements Subject {
    private final java.util.List<Observer> observers = new java.util.ArrayList<>();
    private float temperature, humidity, pressure;

    public void setMeasurements(float t, float h, float p) {
        this.temperature = t;
        this.humidity = h;
        this.pressure = p;
        notifyObservers();
    }

    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(temperature, humidity, pressure);
        }
    }
    // register/remove 同理补全
}

Java · 观察者注册与展示(节选)

public class CurrentConditionsDisplay implements Observer, DisplayElement {
    private float temperature, humidity;

    public CurrentConditionsDisplay(Subject weatherData) {
        weatherData.registerObserver(this);
    }

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

    public void display() {
        System.out.printf("当前状况:%.1f°F,湿度 %.1f%%%n", temperature, humidity);
    }
}
课堂可强调的两句话(打开)

依赖方向:具体观察者依赖 Subject 接口,而不是依赖 WeatherData 的具体类(构造器参数类型用接口)。

推送时机:「谁负责调用 notify」要约定清楚——本例由 setMeasurements 在数据落库后统一通知,避免观察者轮询。

Python 分步:Protocol + 类型提示

  1. 第 0 步:单文件 weather_station.py,先 print("ok") 确认 python3 版本(建议 3.10+,Protocol 在标准库 typing 中)。
  2. 第 1 步:声明 ObserverSubjectProtocol,只写方法签名与 -> 返回类型——这是给读者与工具的契约,不是运行时的父类继承。
  3. 第 2 步:实现 WeatherData,用 List[Observer] 保存观察者;方法名可用蛇形 register_observer(Python 惯用法),与 Java camelCase 不同但角色一致。
  4. 第 3 步:实现 CurrentConditionsDisplay,在 __init__weather_data.register_observer(self)
  5. 第 4 步:main() 里两次 set_measurements,终端应打印与 Java 版语义一致的行。

Python · Protocol 契约(节选)

from typing import Protocol, List

class Observer(Protocol):
    def update(self, temp: float, humidity: float, pressure: float) -> None: ...

class WeatherData:
    def __init__(self) -> None:
        self._observers: List[Observer] = []

    def register_observer(self, o: Observer) -> None:
        self._observers.append(o)

    def notify_observers(self) -> None:
        for o in self._observers:
            o.update(self._temperature, self._humidity, self._pressure)

完整可运行代码见本页 示例工程examples/python-observer/weather_station.py

课堂示范二:策略模式(Strategy)— 鸭子飞行

教材故事:不同鸭子飞行方式不同(真飞、不会飞、甚至火箭飞)。若把飞行动作写在 Duck 基类的各个子类里,会出现大量重复或难以在运行时改变行为。Strategy 把「会变的部分」抽成接口/协议,由 Duck 组合一个 FlyBehavior,通过 setXxxBehavior 替换。

本示范与 Observer 形成对照:Observer 解决「一对多通知」;Strategy 解决「算法/行为可替换」。

课时弹性:你点名要细讲的是 ObserverStrategy 与教材「鸭子」章节强绑定,适合作为第二段示范。若当堂时间紧,可将 Strategy 压缩为「现场只跑 examples + 学生对照本页自学」,把大部分时间留给 Observer 的 Java / Python 对比。

Java 分步

  1. Hello World:MiniDuckSimulator 只打印一行,确认环境。
  2. 定义 FlyBehaviorFlyWithWings / FlyNoWay 两个具体策略。
  3. 写抽象类 Duck:字段 protected FlyBehavior flyBehavior;方法 performFly() 委托给 flyBehavior.fly()
  4. MallardDuck 构造器里 flyBehavior = new FlyWithWings()RubberDuckFlyNoWay
  5. main 里对橡皮鸭调用 setFlyBehavior(new FlyWithWings())performFly(),演示运行时替换策略

Java · 组合与委托(节选)

public abstract class Duck {
    protected FlyBehavior flyBehavior;

    public void performFly() {
        flyBehavior.fly();
    }

    public void setFlyBehavior(FlyBehavior fb) {
        this.flyBehavior = fb;
    }

    public abstract void display();
}

Python 分步

  1. class FlyBehavior(Protocol): 定义 fly(self) -> None
  2. 普通类 FlyWithWingsFlyNoWay 实现该方法(无需显式 implements)。
  3. Duck 基类持有 _fly_behaviorperform_fly 调用策略对象。
  4. 子类在 __init__set_fly_behavior(...)main 里演示替换。

Python · 策略字段(节选)

class Duck:
    def __init__(self) -> None:
        self._fly_behavior: FlyBehavior = FlyNoWay()

    def set_fly_behavior(self, fb: FlyBehavior) -> None:
        self._fly_behavior = fb

    def perform_fly(self) -> None:
        self._fly_behavior.fly()

完整代码见 examples/java-strategy/examples/python-strategy/duck_simulator.py

可运行示例工程(供课堂演示与学生拷贝)

以下目录与本讲义网页位于同一项目根目录 test-project/ 下,可直接打开文件对照讲解。

路径 说明 运行
examples/java-observer/ 气象站 Observer,6 个 .java 文件 javac *.java && java WeatherStation
examples/python-observer/ weather_station.py 单文件 python3 weather_station.py
examples/java-strategy/ 鸭子 Strategy javac *.java && java MiniDuckSimulator
examples/python-strategy/ duck_simulator.py python3 duck_simulator.py

教师本机若尚未安装 JDK,请先安装后再演示 Java 部分;Python 示例已在 3.x 环境验证可运行。

作业布置

总要求

  • 从经典设计模式(建议以教材与 GoF 分类为准)中自选 3~5 个模式;每个模式一个独立 Git 仓库(命名示例:经典设计模式讲解-01-观察者模式经典设计模式讲解-02-策略模式 …)。
  • 实现语言优先使用本课示范的 Java 或 Python;若选其他语言,须在 README 说明理由与环境。
  • 将仓库推送到你可用的托管平台(GitHubGitLabGitee 等),保证教师能打开链接阅读。
  • 核心代码以键盘手敲为主;可使用 Trae CN 等工具辅助,但必须能独立讲解、修改与演示。

每个仓库必须包含

  1. README.md(重点),至少包含:
    • 模式原理:意图、适用场景、关键参与者、与教材或 GoF 描述的对照(可用 Mermaid 简易类图)。
    • 示例故事:你选用的具体场景是什么?与代码中类/函数如何一一对应?
    • 运行说明:目录结构、依赖、编译或运行命令(写清 JDK / Python 版本)。
    • 运行截图 + 图注:至少一张,说明截图证明的输出含义(例如「第二次 setMeasurements 触发 notify」)。
  2. 可运行入口(含 mainif __name__ == "__main__" 等),禁止只有空接口。
  3. 建议有若干次 Git commit,体现迭代过程。

提交与截止

  • 截止时间:本周课后,下周内在课程群提交即可(具体以群公告为准)。
  • 提交方式:每位同学将各自仓库的 URL 发到课程群;教师按链接检查并登记成绩。
  • 成绩定位:以完成度与文档清晰度为主,重在提供可仿照的样板仓库;请仍认真对待格式与可复现性。

自查清单(交作业前逐项打勾)

  • 是否 3~5 个模式、且每个模式独立仓库
  • 是否已在课程群发送每个仓库的 URL(链接可打开、非失效)?
  • README 是否讲清原理 + 你的例子 + 运行方式
  • 是否有运行截图且配有图注讲解
  • 代码是否能在他人机器上按 README 复现(JDK / Python 版本写清楚)?
  • 能否用 3~5 分钟口述该模式解决了什么变化、你的例子哪里体现了这一点?

延伸阅读(教师备课可参考)

本地启动静态服务器

在本目录(含 index.htmlcss/)下打开终端,任选一种方式:

Python 3

cd "/mnt/c/Users/Administrator/Desktop/test-project"
python3 -m http.server 8080

浏览器访问:http://127.0.0.1:8080/

Node.js(如已安装 npx)

npx --yes serve -l 8080 .

若仅在 WSL 中访问,也可将地址栏中的主机名换成本机 IP(跨设备演示时需注意防火墙)。