GoF23:Strategy & Template Method(策略&模板方法)

GoF23

1、case:模拟鸭子

开发一款模拟鸭子游戏,包括各种鸭子及其不同行为

  • 会叫;
  • 会游泳;
  • 会飞;
  • ……

如何为鸭子添加行为?考虑以下哪种方案更合适!

1.1、方案尝试

继承

Duck超类

  1. 设计一个 Duck 超类;
  2. 将行为设计成超类方法;
  3. 各种鸭子继承此超类。

image-20220205160149917

  • quack():所有鸭子都会 “呱呱叫”,超类实现;
  • swim():所有鸭子都会 “游泳”,超类实现;
  • display():显示鸭子外观,子类实现;
  • fly():所有的鸭子都会 “飞行”,超类实现。

问题

  1. 并非所有的子类都会飞(如橡皮鸭);

  2. 由于使用继承,所有子类都会继承超类的相同行为,即使子类并不具备该行为(或子类的行为与父类不同);

  3. 此时,部分子类必须重写父类方法维护性差

    image-20220205154506761

实现

当涉及维护时,使用继承会导致复用性差。

Duck接口

  1. 设计一个 Duck 超类;
  2. 将行为设计成接口;
  3. 各种鸭子继承 Duck 超类,实现行为接口。

image-20220205161500169

  • 具备相应行为的子类,则实现相应的行为接口;
  • 不具备某些行为的子类,则无需实现相应的行为接口。

问题

大量代码重复,复用性差。

即:相同的行为,会导致相同的代码在多个子类中重复出现;

1.2、分析

  1. 方案存在问题
    • 继承:鸭子的行为在子类中各有不同,而继承会导致子类继承到相同的超类方法;
    • 行为接口:Java interface 不具备实现的代码,复用性差。
  2. 设计原则——封装变化之处将与应用中变化的部分抽取封装,以便修改或扩充,而不影响应用中的其它部分。

1.3、方案:封装变化

image-20220205163003117

  • 在 Duck 超类中,只有 fly() 和 quack() 存在较大的问题;
  • 因此将 “飞行” 和 “鸭叫” 两个行为,分别封装成一个类。每个类负责实现各自的动作。

Behavior:行为

将鸭子行为从 Duck 中独立出来,并在 Duck 中添加 setter,以便运行时动态设定行为

image-20220205163159574

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:整合行为

  1. 成员变量:鸭子行为,声明为接口类型(面向抽象编程);
  2. quack() 和 fly():委托 Behavior类 完成 ;
  3. display():抽象方法,子类实现;
  4. swim():超类实现;
  5. 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();
}

image-20220205174439116

测试 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();
}

image-20220205174848427

1.5、再分析

不再把 行为 当作 “一组方法”,而是把 行为 当作 “一簇算法”

image-20220205180551677

2、策略模式

2.1、定义

策略模式定义并封装算法族,使它们可以互相替换

  • 让算法的 变化 独立于 使用算法的客户
  • 把 行为 当作 “一簇算法”,而不是 “一组方法”;

2.2、OOP原则

  • 合成复用:多用组合/聚合,少用继承;
  • 里氏替换:(继承)父类的性质在子类中成立;
  • 开闭原则

2.3、类图

  • Clent:客户
    • 内部持有行为实例;
    • 当 Client 的方法被请求时,委托给 Behavior 类处理;
    • Client 可以在运行时动态修改行为。
  • Behavior:行为
    • 处理来自 Client 的请求;
    • 每个 ConcreteBehavior都提供了本状态对请求的实现;
    • 每个 ConcreteBehavior 是可以互相替换的。

image-20220212212210796

3、case:饮料制作

3.1、Coffee & Tea

简介

饮料店制作咖啡和茶:冲泡步骤相同,细节稍微不同

  • 咖啡
    1. 把水煮沸;
    2. 用沸水冲泡咖啡;
    3. 把咖啡倒进杯子;
    4. 加糖和牛奶。
    1. 把水煮沸;
    2. 用沸水冲泡茶叶;
    3. 把茶倒进杯子;
    4. 加柠檬。

分析

饮料冲泡算法步骤相同,实现有所不同

  1. 把水煮沸;
  2. 用沸水冲泡;
  3. 把饮料倒进杯子;
  4. 加调料。

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();
}

image-20220205191048870

3.4、prepare() 分析

prepare() 是模板方法

  • 是一个方法,作为算法模板;
  • 某些方法在超类中实现,某些方法由子类实现。

image-20220205192926389

4、模板方法模式

4.1、定义

模板方法模式在方法中定义算法的骨架,将一些步骤的实现延迟到子类中

  • 将算法定义成一组步骤,其中的任何步骤可以是具体的(超类实现),也可以是抽象的(子类实现);
  • 算法结构不变:模板方法定义为 final
  • 算法步骤的实现不同:abstract 的方法由子类实现;
  • OOP原则——依赖倒置:将算法抽象成一组步骤,面向抽象编程。

工厂方法是模板方法的一种特殊形式

4.2、类图

  • 抽象类
    • 包含模板方法、以及模板方法所用到的原语操作;
    • 模板方法中调用原语操作,即算法步骤;
    • 模板方法的本身 和 原语操作的具体实现 之间解耦;
  • 具体类:实现超类中抽象的操作

image-20220205205739014

4.3、代码表示

image-20220205211848313

4.4、hook:钩子

4.4.1、简介

钩子

  • 声明在抽象类中的方法,具有空的或默认的实现
  • 由子类决定是否进行挂钩(即重写钩子方法)。

image-20220206184530292

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 单元测试中,控制台无法输入的问题

  1. 点击 help → Edit custom VM options
  2. 添加一行代码:-Deditable.java.test.console=true
  3. 重启 IDEA 生效,解决问题。

也可以新建一个 main 方法作为测试。

@Test
public void testHook() {
    CoffeeWithHook coffee = new CoffeeWithHook();
    coffee.prepare();
}

image-20220206191117571

4.4.3、分析

  1. 抽象方法的使用:当算法步骤的实现需要由子类提供时,将方法声明为抽象,由子类实现。
  2. 钩子的使用:算法的部分步骤是可选时,可添加钩子方法。

4.5、Java API:数组排序

Java Arrays 提供了一个为 Object数组 排序的 sort() 方法。

  1. 实际上,有三个方法共同完成排序的功能;
  2. sort()legacyMergeSort() 是辅助方法,用于完成准备工作;
  3. 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()

真正的排序方法,可以看作一个模板方法

  1. compareTo():“抽象方法”,由 “子类” 实现;
    • 在使用 sort() 为 Object数组 排序时,待排序的类必须实现 Comparable接口的 compareTo() 方法;
    • :为 Student数组排序,则 Student类 需实现 Comparable接口,根据需求compareTo() 方法。
  2. 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 模板方法

策略模式 模板方法模式
定义 定义并封装算法族,使它们可以互相替换。 在方法中定义算法的骨架,将一些步骤的实现延迟到子类中。
侧重点 封装算法 封装算法子步骤
posted @ 2022-01-30 20:42  Jaywee  阅读(59)  评论(0编辑  收藏  举报

👇