课程定位、时间安排与提交方式
本讲是《专业实践》课程中一个小专题的组成部分:教师先讲清设计模式在工程里的位置,再给可照抄照改的样例;学生按作业要求独立完成 3~5 个模式的仓库。面向专业硕士研究生(专硕),强调动手与表达,而非死记硬背定义。
时间与提交
- 本节:课堂讲授 + 当堂可跟着敲的 Java / Python 最小示例。
- 截止:本周讲完后,下周内提交即可(具体时刻以课程群通知为准)。
- 提交方式:每位同学在自己常用的 Git 托管平台(GitHub、GitLab、Gitee 等)建好公开或教师可访问的仓库后,将仓库网址发到课程群;教师按链接检查并登记成绩。
成绩说明:本次作业重在给样例、统一格式与养成习惯;分数不是首要目的,但需达到 README、可运行、截图说明等底线要求,便于教师快速验收。
本课课堂示范(与作业区别)
课堂上教师会用 Java 与 Python 各走一遍下面两个教材经典例子:Observer(气象站)、Strategy(鸭子飞行行为)。作业仍要求每人自选 3~5 个不同模式分别建库;本页后半的「分步讲义」可直接当课堂板书稿使用。
参考书与版权声明
《Head First Design Patterns》(深入浅出设计模式)用图示与对话讲解 GoF 经典模式。课堂示范的例子尽量贴合书中叙事(气象站、鸭子),方便学生回到教材对照插图与章节。
作业实现语言建议以本课示范的 Java 或 Python 为主;若你更熟悉其他语言,也可与教师沟通,但须在 README 中写清环境与运行方式。
版权声明:课堂引用书名与教学模式概念属于合理使用;请勿在作业仓库中上传全书扫描件或盗版 PDF。
本节课目标(专硕)
- 理解「设计模式」对应的是可预期的变化方向,目标是解耦与可扩展,而不是背类图。
- 能对照教材故事,说出 Observer / Strategy 各自解决的变化是什么、关键角色有哪些。
- 在 Java 里会用 interface 表达角色契约;在 Python 里会用 typing.Protocol(或 ABC)模拟接口思路,并结合类型注解帮助阅读与重构。
- 能从「Hello World 级」的最小文件出发,一步一步拆出类、组合关系与调用顺序,跑通终端输出。
- 为下周作业做好准备:独立仓库、README、运行截图与简短图注。
模式地图(便于选题)
下列为 GoF 二十三式常用分类。作业要求从中自选 3~5 个(每个模式一个独立仓库)。本课课堂只精讲其中与教材高度贴合的两例:Observer、Strategy,其余模式请自学教材 + 作业实践。
| 类型 | 模式(示例) | 一句话直觉 |
|---|---|---|
| 创建型 | 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 分步:从空项目到可运行
- 第 0 步:Hello World。先建一个目录,创建
WeatherStation.java,写出main打印一行字,确认javac/java环境可用。 - 第 1 步:写角色契约(接口)。
Subject负责注册/移除/通知;Observer负责update(...);教材里的DisplayElement用于强调「展示」职责,可先保留为空实现再填充。 - 第 2 步:实现具体主题
WeatherData。内部一个List<Observer>;setMeasurements被调用时更新字段并调用notifyObservers。 - 第 3 步:实现具体观察者
CurrentConditionsDisplay。构造器里向SubjectregisterObserver(this);update保存读数并调用display打印终端。 - 第 4 步:在
main里串联。创建WeatherData,new 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 + 类型提示
- 第 0 步:单文件
weather_station.py,先print("ok")确认python3版本(建议 3.10+,Protocol 在标准库typing中)。 - 第 1 步:声明
Observer、Subject为Protocol,只写方法签名与->返回类型——这是给读者与工具的契约,不是运行时的父类继承。 - 第 2 步:实现
WeatherData,用List[Observer]保存观察者;方法名可用蛇形register_observer(Python 惯用法),与 Java camelCase 不同但角色一致。 - 第 3 步:实现
CurrentConditionsDisplay,在__init__里weather_data.register_observer(self)。 - 第 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 解决「算法/行为可替换」。
课时弹性:你点名要细讲的是 Observer;Strategy 与教材「鸭子」章节强绑定,适合作为第二段示范。若当堂时间紧,可将 Strategy 压缩为「现场只跑 examples + 学生对照本页自学」,把大部分时间留给 Observer 的 Java / Python 对比。
Java 分步
- Hello World:
MiniDuckSimulator只打印一行,确认环境。 - 定义
FlyBehavior及FlyWithWings/FlyNoWay两个具体策略。 - 写抽象类
Duck:字段protected FlyBehavior flyBehavior;方法performFly()委托给flyBehavior.fly()。 MallardDuck构造器里flyBehavior = new FlyWithWings();RubberDuck用FlyNoWay。- 在
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 分步
- 用
class FlyBehavior(Protocol):定义fly(self) -> None。 - 普通类
FlyWithWings、FlyNoWay实现该方法(无需显式implements)。 Duck基类持有_fly_behavior,perform_fly调用策略对象。- 子类在
__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 说明理由与环境。
- 将仓库推送到你可用的托管平台(GitHub、GitLab、Gitee 等),保证教师能打开链接阅读。
- 核心代码以键盘手敲为主;可使用 Trae CN 等工具辅助,但必须能独立讲解、修改与演示。
每个仓库必须包含
- README.md(重点),至少包含:
- 模式原理:意图、适用场景、关键参与者、与教材或 GoF 描述的对照(可用 Mermaid 简易类图)。
- 示例故事:你选用的具体场景是什么?与代码中类/函数如何一一对应?
- 运行说明:目录结构、依赖、编译或运行命令(写清 JDK / Python 版本)。
- 运行截图 + 图注:至少一张,说明截图证明的输出含义(例如「第二次 setMeasurements 触发 notify」)。
- 可运行入口(含
main或if __name__ == "__main__"等),禁止只有空接口。 - 建议有若干次 Git commit,体现迭代过程。
提交与截止
- 截止时间:本周课后,下周内在课程群提交即可(具体以群公告为准)。
- 提交方式:每位同学将各自仓库的 URL 发到课程群;教师按链接检查并登记成绩。
- 成绩定位:以完成度与文档清晰度为主,重在提供可仿照的样板仓库;请仍认真对待格式与可复现性。
自查清单(交作业前逐项打勾)
- 是否 3~5 个模式、且每个模式独立仓库?
- 是否已在课程群发送每个仓库的 URL(链接可打开、非失效)?
- README 是否讲清原理 + 你的例子 + 运行方式?
- 是否有运行截图且配有图注讲解?
- 代码是否能在他人机器上按 README 复现(JDK / Python 版本写清楚)?
- 能否用 3~5 分钟口述该模式解决了什么变化、你的例子哪里体现了这一点?
延伸阅读(教师备课可参考)
本地启动静态服务器
在本目录(含 index.html 与 css/)下打开终端,任选一种方式:
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(跨设备演示时需注意防火墙)。