策略、模板方法和状态模式都在隔离变化,但变化的对象不同:策略替换一组算法,模板方法固定流程并开放部分步骤,状态模式让同一操作随内部状态产生不同响应。
下面沿用我读设计模式时的武器、饮品和投币机类比。它们是教学示例,不代表实现过这些业务系统;相关概念可结合《Head First 设计模式》阅读。
策略:角色把攻击行为交给一个接口
如果把所有武器行为写进角色父类,每加一种武器都可能影响不相关角色。把变化拆成接口,角色只负责委托:
interface WeaponBehavior {
void useWeapon();
}
class GameCharacter {
private WeaponBehavior weapon;
GameCharacter(WeaponBehavior weapon) {
setWeapon(weapon);
}
void setWeapon(WeaponBehavior weapon) {
this.weapon = java.util.Objects.requireNonNull(weapon);
}
void attack() {
weapon.useWeapon();
}
}
调用片段可以是:
GameCharacter player = new GameCharacter(
() -> System.out.println("使用匕首"));
player.attack();
player.setWeapon(() -> System.out.println("使用弓箭"));
player.attack();
角色不需要知道武器的实现细节。策略可以由配置、调用者或业务规则选择,不要求每个项目都在运行时切换。新增武器仍可能需要改装配、资源和权限规则,“增加一个类”不等于整个系统零修改。
模板方法:顺序固定,部分步骤交给子类
泡茶和泡咖啡都经历烧水、冲泡、倒杯和可选加料,变化的是具体冲泡与配料,而不是所有步骤都一样:
abstract class Drink {
public final void prepare() {
boilWater();
brew();
pourIntoCup();
if (wantsCondiments()) addCondiments();
}
private void boilWater() {
System.out.println("烧水");
}
private void pourIntoCup() {
System.out.println("倒入杯中");
}
protected abstract void brew();
protected abstract void addCondiments();
protected boolean wantsCondiments() {
return true;
}
}
子类实现冲泡与配料,需要不加料时覆写钩子 wantsCondiments() 返回 false。prepare() 的 final 保持流程骨架不被覆写;钩子提供默认行为,让子类可选扩展。
这里依靠继承扩展部分步骤,与角色持有策略对象的组合方式不同。不能仅因框架调用了一个可覆写方法,就认定任何实现都严格对应某一本书的类图。
状态:同一个动作,在不同状态下含义不同
投币机可以先写出转换规则,再考虑是否拆成状态类:
| 当前状态 | 投币 | 退币 | 转动摇杆 |
|---|---|---|---|
| 未投币且有库存 | 进入已投币 | 拒绝,无币可退 | 拒绝 |
| 已投币 | 拒绝重复投币 | 退币并回到未投币 | 进入出货流程 |
| 出货中 | 拒绝 | 拒绝 | 拒绝重复出货 |
| 售罄 | 拒绝 | 拒绝 | 拒绝 |
出货完成后根据剩余库存转回未投币或售罄。若库存不足或出货异常,退款与补偿需要另外定义,不由模式自动解决。
状态接口可以统一声明投币、退币和转动操作,具体状态决定响应;上下文持有当前状态并完成转换。这样避免每个操作重复一大段状态判断,但转换约束、并发和重复请求仍需设计。
怎么选择,而不是怎么套名字
需要替换同类算法,先考虑策略;需要稳定步骤和扩展点,考虑模板方法;行为由生命周期阶段决定,先画清状态转换,再判断状态对象是否有助维护。
策略由调用者选择、状态常由内部事件驱动,是有用的理解角度,不是不可突破的规则。少量清晰分支不必为消除 if 而拆成很多类。类似“包一层”的模式区别,见 装饰、适配、外观与代理。
参考:《Head First Design Patterns》出版方目录。这里引用其学习主题,不据此声称已核对某一版译本的全部章节。