GoF23:Strategy & Template Method(策略&模板方法)
1、case:模拟鸭子
开发一款模拟鸭子游戏,包括各种鸭子及其不同行为
- 会叫;
- 会游泳;
- 会飞;
- ……
如何为鸭子添加行为?考虑以下哪种方案更合适!
1.1、方案尝试
继承
Duck超类
- 设计一个 Duck 超类;
- 将行为设计成超类方法;
- 各种鸭子继承此超类。
- quack():所有鸭子都会 “呱呱叫”,超类实现;
- swim():所有鸭子都会 “游泳”,超类实现;
- display():显示鸭子外观,子类实现;
- fly():所有的鸭子都会 “飞行”,超类实现。
问题
-
并非所有的子类都会飞(如橡皮鸭);
-
由于使用继承,所有子类都会继承超类的相同行为,即使子类并不具备该行为(或子类的行为与父类不同);
-
此时,部分子类必须重写父类方法,维护性差。
实现
当涉及维护时,使用继承会导致复用性差。
Duck接口
- 设计一个 Duck 超类;
- 将行为设计成接口;
- 各种鸭子继承 Duck 超类,实现行为接口。
- 具备相应行为的子类,则实现相应的行为接口;
- 不具备某些行为的子类,则无需实现相应的行为接口。
问题
大量代码重复,复用性差。
即:相同的行为,会导致相同的代码在多个子类中重复出现;
1.2、分析
- 方案存在问题
- 继承:鸭子的行为在子类中各有不同,而继承会导致子类继承到相同的超类方法;
- 行为接口:Java interface 不具备实现的代码,复用性差。
- 设计原则——封装变化之处:将与应用中变化的部分抽取封装,以便修改或扩充,而不影响应用中的其它部分。
1.3、方案:封装变化
- 在 Duck 超类中,只有 fly() 和 quack() 存在较大的问题;
- 因此将 “飞行” 和 “鸭叫” 两个行为,分别封装成一个类。每个类负责实现各自的动作。
Behavior:行为
将鸭子行为从 Duck 中独立出来,并在 Duck 中添加 setter,以便运行时动态设定行为。
FlyBehavior
public interface FlyBehavior {
void fly();
}
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("I'm flying with wings...");
}
}
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("I can't fly...");
}
}
QuackBehavior
public interface QuackBehavior {
void quack();
}
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("Quacking...");
}
}
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("Squeaking...");
}
}
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("...");
}
}
Duck:整合行为
- 成员变量:鸭子行为,声明为接口类型(面向抽象编程);
- quack() 和 fly():委托 Behavior类 完成 ;
- display():抽象方法,子类实现;
- swim():超类实现;
- setter:注入 Behavior 实例,也可以构造器注入。
public abstract class Duck {
protected QuackBehavior quackBehavior;
protected FlyBehavior flyBehavior;
public void quack(){
quackBehavior.quack();
}
public void swim(){
System.out.println("I'm swimming...");
}
public abstract void display();
public void fly(){
flyBehavior.fly();
}
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
}
Duck:子类
- 注入 Behavior:构造器注入默认值(硬编码),也可以运行时通过 setter 动态修改;
- 实现 display()
public class MallardDuck extends Duck{
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
@Override
public void display() {
System.out.println("I'm Mallard duck");
}
}
测试
测试 MallardDuck 方法
@Test
public void test() {
MallardDuck duck = new MallardDuck();
duck.display();
duck.quack();
duck.swim();
duck.fly();
}
测试 setter
@Test
public void testSetter() {
MallardDuck duck = new MallardDuck();
duck.quack();
duck.fly();
System.out.println("\n===== setter =====\n");
// 动态设置行为
duck.setQuackBehavior(new MuteQuack());
duck.setFlyBehavior(new FlyNoWay());
duck.quack();
duck.fly();
}
1.5、再分析
不再把 行为 当作 “一组方法”,而是把 行为 当作 “一簇算法”。
2、策略模式
2.1、定义
策略模式:定义并封装算法族,使它们可以互相替换。
- 让算法的 变化 独立于 使用算法的客户;
- 把 行为 当作 “一簇算法”,而不是 “一组方法”;
2.2、OOP原则
- 合成复用:多用组合/聚合,少用继承;
- 里氏替换:(继承)父类的性质在子类中成立;
- 开闭原则
2.3、类图
- Clent:客户
- 内部持有行为实例;
- 当 Client 的方法被请求时,委托给 Behavior 类处理;
- Client 可以在运行时动态修改行为。
- Behavior:行为
- 处理来自 Client 的请求;
- 每个 ConcreteBehavior都提供了本状态对请求的实现;
- 每个 ConcreteBehavior 是可以互相替换的。
3、case:饮料制作
3.1、Coffee & Tea
简介
饮料店制作咖啡和茶:冲泡步骤相同,细节稍微不同。
- 咖啡
- 把水煮沸;
- 用沸水冲泡咖啡;
- 把咖啡倒进杯子;
- 加糖和牛奶。
- 茶
- 把水煮沸;
- 用沸水冲泡茶叶;
- 把茶倒进杯子;
- 加柠檬。
分析
饮料冲泡:算法步骤相同,实现有所不同。
- 把水煮沸;
- 用沸水冲泡;
- 把饮料倒进杯子;
- 加调料。
3.2、代码实现
Drink抽象类
设计一个 Drink 抽象类,定义一个 final方法 和 四个制作方法。
- prepare():final,封装算法步骤,避免子类覆盖;
- boil():final,超类中实现,子类通用;
- brew():abstract,不同饮料的处理方法不同,由子类实现;
- pour():final,超类中实现,子类通用;
- addCondiment():abstract,不同饮料添加的调料不同,由子类实现。
public abstract class Drink {
public final void prepare() {
boil();
brew();
pour();
addCondiment();
}
private final void boil() {
System.out.println("Boiling water...");
}
protected abstract void brew();
private final void pour() {
System.out.println("Pouring into a cup...");
}
protected abstract void addCondiment();
}
实现类
继承 Drink超类,实现 brew() 和 addCondiment()。
// 咖啡
public class Coffee extends Drink {
@Override
protected void brew() {
System.out.println("Dripping the coffee through filter...");
}
@Override
protected void addCondiment() {
System.out.println("Adding sugar and milk...");
}
}
// 茶
public class Tea extends Drink{
@Override
protected void brew() {
System.out.println("Steeping the tea...");
}
@Override
protected void addCondiment() {
System.out.println("Adding lemon...");
}
}
3.3、测试
@Test
public void test() {
Coffee coffee = new Coffee();
coffee.prepare();
System.out.println("====================================");
Tea tea = new Tea();
tea.prepare();
}
3.4、prepare() 分析
prepare() 是模板方法
- 是一个方法,作为算法模板;
- 某些方法在超类中实现,某些方法由子类实现。
4、模板方法模式
4.1、定义
模板方法模式:在方法中定义算法的骨架,将一些步骤的实现延迟到子类中。
- 将算法定义成一组步骤,其中的任何步骤可以是具体的(超类实现),也可以是抽象的(子类实现);
- 算法结构不变:模板方法定义为 final;
- 算法步骤的实现不同:abstract 的方法由子类实现;
- OOP原则——依赖倒置:将算法抽象成一组步骤,面向抽象编程。
注:工厂方法是模板方法的一种特殊形式。
4.2、类图
- 抽象类
- 包含模板方法、以及模板方法所用到的原语操作;
- 模板方法中调用原语操作,即算法步骤;
- 模板方法的本身 和 原语操作的具体实现 之间解耦;
- 具体类:实现超类中抽象的操作
4.3、代码表示
4.4、hook:钩子
4.4.1、简介
钩子
- 声明在抽象类中的方法,具有空的或默认的实现;
- 由子类决定是否进行挂钩(即重写钩子方法)。
4.4.2、DrinkWithHook
Drink抽象类
- 在原有代码基础上,添加钩子方法 isAddCondiment();
- 本例中钩子方法,用于判断是否添加调料。
public abstract class DrinkWithHook {
public final void prepare() {
boil();
brew();
pour();
if (isAddCondiment()) {
addCondiment();
}
}
private void boil() {
System.out.println("Boiling water...");
}
protected abstract void brew();
private void pour() {
System.out.println("Pouring into a cup...");
}
protected abstract void addCondiment();
protected boolean isAddCondiment() {
return true;
}
}
实现类
- 继承 DrinkWithHook 超类。实现抽象方法;
- 重写钩子方法 isAddCondiment(),加入自己的逻辑。
public class CoffeeWithHook extends DrinkWithHook {
@Override
protected void brew() {
System.out.println("Dripping the coffee through filter...");
}
@Override
protected void addCondiment() {
System.out.println("Adding sugar and milk...");
}
@Override
protected boolean isAddCondiment() {
System.out.println("=== Would you like sugar and milk? (y/n) ===");
String answer = new Scanner(System.in).next();
return "y".equals(answer.toLowerCase());
}
}
测试
PS:在 JUnit 单元测试中,控制台无法输入的问题
- 点击 help → Edit custom VM options
- 添加一行代码:
-Deditable.java.test.console=true
- 重启 IDEA 生效,解决问题。
也可以新建一个 main 方法作为测试。
@Test
public void testHook() {
CoffeeWithHook coffee = new CoffeeWithHook();
coffee.prepare();
}
4.4.3、分析
- 抽象方法的使用:当算法步骤的实现需要由子类提供时,将方法声明为抽象,由子类实现。
- 钩子的使用:算法的部分步骤是可选时,可添加钩子方法。
4.5、Java API:数组排序
Java Arrays 提供了一个为 Object数组 排序的 sort() 方法。
- 实际上,有三个方法共同完成排序的功能;
- sort() 和 legacyMergeSort() 是辅助方法,用于完成准备工作;
- mergeSort() 是真正的排序方法,可以看作一个模板方法。
sort()
-
委托 legacyMergeSort() 方法;
-
传入 legacyMergeSort() 需要的参数,即数组 a;
public static void sort(Object[] a) { if (LegacyMergeSort.userRequested) legacyMergeSort(a); else ComparableTimSort.sort(a, 0, a.length, null, 0, 0); }
legacyMergeSort()
- 委托 mergeSort() 方法;
- 为待排序数组 a 创造一个拷贝 aux,并传入 mergeSort() 需要的参数。
private static void legacyMergeSort(Object[] a) {
Object[] aux = a.clone();
mergeSort(aux, a, 0, a.length, 0);
}
mergeSort()
真正的排序方法,可以看作一个模板方法。
- compareTo():“抽象方法”,由 “子类” 实现;
- 在使用 sort() 为 Object数组 排序时,待排序的类必须实现 Comparable接口的 compareTo() 方法;
- 例:为 Student数组排序,则 Student类 需实现 Comparable接口,根据需求compareTo() 方法。
- swap():“具体方法”,由本类提供。
private static void mergeSort(Object[] src,
Object[] dest,
int low,
int high,
int off) {
int length = high - low;
if (length < INSERTIONSORT_THRESHOLD) {
for (int i=low; i<high; i++)
for (int j=i; j>low &&
((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
swap(dest, j, j-1);
return;
}
// ...
}
5、策略 vs 模板方法
策略模式 | 模板方法模式 | |
---|---|---|
定义 | 定义并封装算法族,使它们可以互相替换。 | 在方法中定义算法的骨架,将一些步骤的实现延迟到子类中。 |
侧重点 | 封装算法 | 封装算法子步骤 |