策略模式

策略模式

鸭子游戏

我们通过一个鸭子游戏的案例来逐步理解策略模式。这个游戏的需求很简单,大家可以想象一下这样一个画面,
打开游戏界面,左边界面大部分区域是用来显示鸭子的,这些鸭子会在界面上游来游去,同时发出叫声,右面的小部分界面列出了当前游戏版本支持的鸭子类型,选择鸭子类型后就可以创建对应类型的鸭子出来,在左边的界面上显示出来。

V1版本

鸭子游戏V1版本实现,基于上面的需求,我们应用面向对象的知识,抽象出鸭子基类,它有游泳和叫两个行为,同时为了让每种鸭子在游戏界面上有不同的外观,在加上一个抽象的外观方法。
抽象的鸭子基类代码:

/**
 * 鸭子抽象类
 *  所有的鸭子都应该继承至该类
 */
public abstract class DuckV1 {

    /**
     * 抽象的游泳方法
     *  所有的鸭子都是在水里面游泳的。
     */
    public void swim(){
        System.out.println("在水里游泳");
    }

    /**
     * 鸭子叫
     *  所有的鸭子都是"呱呱呱"的叫
     */
    public void quack(){
        System.out.println("呱呱呱");
    }

    /**
     * 鸭子外观
     *  不同的鸭子有不同的外观
     */
    public abstract void display();
}

接下类,我们来实现两个具体的鸭子,“野鸭"和"红头鸭”。

/**
 * 野鸭
 */
public class MallardDuckV1 extends DuckV1 {
    @Override
    public void display() {
        System.out.println("野鸭的外观是绿头");
    }
}
/**
 * 红头鸭
 */
public class RedheadDuckV1 extends DuckV1{
    @Override
    public void display() {
        System.out.println("红头鸭的头是红色的");
    }
}

现在,我们来创建具体的鸭子,让它们玩起来吧。

/**
 * V1版鸭子游戏的测试
 */
public class DuckV1Main {

    public static void main(String[] args) {
        //一只野鸭在玩耍
        DuckV1 duck1 = new MallardDuckV1();
        duck1.display();
        duck1.swim();
        duck1.quack();
        System.out.println();

        //一只红头鸭在玩耍
        DuckV1 duck2 = new RedheadDuckV1();
        duck2.display();
        duck2.swim();
        duck2.quack();
        System.out.println();
    }
}

嗯,目前看起来还不错。

现在,市场反馈说,鸭子类型太少,新加入一种"橡皮鸭"进来。
按照目前的设计,新加入的橡皮鸭是一种具体的鸭子,只要继承我们的抽象鸭子就好了。
我们来实现一版看看:

/**
 * 橡皮鸭
 */
public class RubberDuckV1 extends DuckV1 {
    @Override
    public void display() {
        System.out.println("橡皮鸭好像都是黄色的");
    }
}

然后,创建一只橡皮鸭,它也加入玩耍的鸭子中去。

    //一只橡皮鸭
    DuckV1 duck3 = new RubberDuckV1();
    duck3.display();
    duck3.fly();
    duck3.swim();
    duck3.quack();

橡皮鸭玩耍时看起来像是这样的:

橡皮鸭好像都是黄色的
在水里游泳
呱呱呱

咦,感觉好像哪里不大对劲,橡皮鸭貌似是"吱吱吱"的叫吧,不是"呱呱呱"的叫,嗯,对,这是一个BUG。
好吧,我们让橡皮鸭重写鸭子基类的叫的方法,覆盖为"吱吱吱"

    @Override
    public void quack() {
        System.out.println("吱吱吱");
    }

这样,看起来问题解决了,新加入的橡皮鸭也可以愉快的玩耍了。

一段时间过后,迫于市场竞争的压力,鸭子游戏需要加入新的要素来提高市场占有率。老板召集一群策划大佬头脑风暴会议后,决定"让鸭子飞起来",以此来提高游戏的体验。
嗯,对于这个需求,由于我们是面向对象设计,看起来只需要在抽象基类中加上飞行方法就行了。
说干就干,先试试看,在鸭子基类中:

    /**
     * 新添加的飞的方法
     */
    public void fly(){
        System.out.println("鸭子飞吧...");
    }

接下来,我们测试下游戏:

 public static void main(String[] args) {
        //一只野鸭在玩耍
        DuckV1 duck1 = new MallardDuckV1();
        duck1.display();
        duck1.fly();
        duck1.swim();
        duck1.quack();
        System.out.println();

        //一只红头鸭在玩耍
        DuckV1 duck2 = new RedheadDuckV1();
        duck2.display();
        duck2.fly();
        duck2.swim();
        duck2.quack();
        System.out.println();

        //一只橡皮鸭
        DuckV1 duck3 = new RubberDuckV1();
        duck3.display();
        duck3.fly();
        duck3.swim();
        duck3.quack();
    }

重点关注橡皮鸭的玩耍姿势:

橡皮鸭好像都是黄色的
鸭子飞吧...
在水里游泳
吱吱吱

什么情况?橡皮鸭在界面上飞来飞去,飞得还很high。如果老板没有觉得这个"创意"很新颖的话,怕是要被骂得很惨。
为了让橡皮鸭不飞起来,好像只需要覆盖基类的飞行方法就行了吧,这和之前处理"叫"的方法是一样的。(我果然很厉害)

    @Override
    public void fly() {
        //橡皮鸭不会飞
    }

搞定,收工!

V1版本设计的UML图:
鸭子游戏V1

好了,我们来分析下,V1版本的核心设计是通过继承,来达到复用的目的。不过对于我们的鸭子游戏,大家思考一分钟,暴露出那些问题?(tips:发生变化)

如果鸭子基类中加入新的行为(跳舞),那么所有的鸭子都自动继承这些行为,即在基类中的变化会影响到所有子类,当子类发现这种行为不合适时(比如橡皮鸭不会跳舞),就需要覆盖基类的方法。如果鸭子类型变多越来越多,这个维护难度也会越来越大。
同理,新加入鸭子类型时,也需要被迫的去检查需要覆盖哪些方法,真是头大。

V2版本

嗯,我们需要来修改下我们的设计,那么用接口如何?
我们基类中的所有行为抽取出来,形成单独的接口,比如Quackable、Flyable。此时,有对应行为的鸭子就去实现对应的接口,没有相应行为的鸭子就不实现。
好,我们也试试实现这种设计,看看效果。
鸭子基类:

/**
 * 鸭子抽象类
 *  所有的鸭子都应该继承至该类
 */
public abstract class DuckV2 {

    /**
     * 抽象的游泳方法
     *  所有的鸭子都是在水里面游泳的。
     */
    public void swim(){
        System.out.println("在水里游泳");
    }

    /**
     * 鸭子外观
     *  不同的鸭子有不同的外观
     */
    public abstract void display();
}

“叫”接口:

/**
 * 叫接口
 */
public interface Quackable {
    void quack();
}

“飞”接口:

/**
 * 飞行接口
 */
public interface Flyable {
    void fly();
}

野鸭子实现,能叫也能飞:

/**
 * 野鸭
 */
public class MallardDuckV2 extends DuckV2 implements Flyable,Quackable {
    @Override
    public void display() {
        System.out.println("野鸭的外观是绿头");
    }

    @Override
    public void fly() {
        System.out.println("野鸭用翅膀在天上飞");
    }

    @Override
    public void quack() {
        System.out.println("呱呱呱");
    }
}

红头鸭子的实现,它也能叫和飞:

/**
 * 红头鸭
 */
public class RedheadDuckV2 extends DuckV2 implements Flyable,Quackable {
    @Override
    public void display() {
        System.out.println("红头鸭的头是红色的");
    }

    @Override
    public void fly() {
        System.out.println("野鸭用翅膀在天上飞");
    }

    @Override
    public void quack() {
        System.out.println("呱呱呱");
    }
}

最后,橡皮鸭的实现,会叫不会飞:

/**
 * 橡皮鸭
 */
public class RubberDuckV2 extends DuckV2 implements Quackable {
    @Override
    public void display() {
        System.out.println("橡皮鸭好像都是黄色的");
    }

    @Override
    public void quack() {
        System.out.println("橡皮鸭是吱吱吱的叫");
    }
}

OK,鸭子们,Let’s Play!

/**
 * V2版鸭子游戏的测试
 */
public class DuckV2Main {

    public static void main(String[] args) {
        //一只野鸭在玩耍
        DuckV2 duck1 = new MallardDuckV2();
        duck1.display();
        duck1.swim();
        ((Flyable)duck1).fly();
        ((Quackable)duck1).quack();
        System.out.println();

        //一只红头鸭在玩耍
        DuckV2 duck2 = new RedheadDuckV2();
        duck2.display();
        duck2.swim();
        ((Flyable)duck2).fly();
        ((Quackable)duck2).quack();
        System.out.println();

        //一只橡皮鸭
        DuckV2 duck3 = new RubberDuckV2();
        duck3.display();
        duck3.swim();
        ((Quackable)duck3).quack();
    }
}

满足需求,玩得欢实。

观察下V2版的UML图:

鸭子V2

V2版,通过将鸭子中的最有可能变化的行为单独抽取出来形成接口,已达到具体鸭子可以自由组合实现行为接口,来动态组合鸭子的行为。
但是我们非常容易发现V2的缺点,大量的代码重复(比如野鸭和红头鸭都是用翅膀飞,他们的飞的方法实现是重复的),客户端使用困难,需要知道具体鸭子实现了哪些行为接口,然后强制转为行为接口类型,才能调用对应行为的方法。

V3版本

针对V2的缺点,我们能不能这样处理,将行为接口的具体行为单独封装起来,比如飞行行为的具体实现包括,用翅膀飞,不能飞两种,那么提供这两种实现出来,具体鸭子通过组合的方式实现复用,这样就消除了V2版本中代码重复的问题。另外,在鸭子基类中定义鸭子的行为方法,但是实现通过组合鸭子的各种行为接口,调用行为接口的方法来实现行为,这样客户端看到的鸭子就是统一的类型,解决类型转换的问题。

说干就干,看看V3版本的实现:
先定义行为接口(和V2版本的接口一样),不过为了区别,这儿换个名字:

/**
 * 叫行为
 */
public interface QuackBehavior {
    void quack();
}
/**
 * 飞行行为
 */
public interface FlyBehavior {
    void fly();
}

分别提供行为接口的具体实现:

飞行为实现

/**
 * 使用翅膀飞
 */
public class FlyWithWings implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("煽动翅膀,飞向天空...");
    }
}
/**
 * 不能飞
 */
public class FlyNoWay implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("飞不动...");
    }
}

叫行为实现

public class Quack implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("呱呱呱的叫");
    }
}
public class Squeak implements QuackBehavior {
    @Override
    public void quack() {
        System.out.println("吱吱吱的叫");
    }
}

鸭子基类,组合鸭子行为

/**
 * 鸭子抽象类
 *  所有的鸭子都应该继承至该类
 */
public abstract class DuckV3 {

    //飞行行为
    FlyBehavior flyBehavior;

    //叫的行为
    QuackBehavior quackBehavior;

    /**
     * 抽象的游泳方法
     *  所有的鸭子都是在水里面游泳的。
     */
    public void swim(){
        System.out.println("在水里游泳");
    }

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

    //鸭子的叫委托给行为抽象了。
    public void quack(){
        quackBehavior.quack();
    }

    /**
     * 鸭子外观
     *  不同的鸭子有不同的外观
     */
    public abstract void display();
}

接下来,实现具体的鸭子了。
野鸭,它通过翅膀飞,呱呱呱的叫。

/**
 * 野鸭
 */
public class MallardDuckV3 extends DuckV3 {

    public MallardDuckV3() {
        //定义野鸭的行为
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("野鸭的外观是绿头");
    }
}

红头鸭,用翅膀飞,呱呱呱的叫

/**
 * 红头鸭
 */
public class RedheadDuckV3 extends DuckV3{
    public RedheadDuckV3() {
        flyBehavior = new FlyWithWings();
        quackBehavior = new Quack();
    }

    @Override
    public void display() {
        System.out.println("红头鸭的头是红色的");
    }
}

橡皮鸭,不会飞,吱吱吱的叫

/**
 * 橡皮鸭
 */
public class RubberDuckV3 extends DuckV3{

    public RubberDuckV3() {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Squeak();
    }

    @Override
    public void display() {
        System.out.println("橡皮鸭好像都是黄色的");
    }
}

按照惯例,先让鸭子们玩一会儿。

/**
 * V3版鸭子游戏的测试
 */
public class DuckV3Main {

    public static void main(String[] args) {
        //一只野鸭在玩耍
        DuckV3 duck1 = new MallardDuckV3();
        duck1.display();
        duck1.fly();
        duck1.quack();
        System.out.println();

        //一只红头鸭在玩耍
        DuckV3 duck2 = new RedheadDuckV3();
        duck2.display();
        duck2.quack();
        duck2.fly();
        System.out.println();

        //一只橡皮鸭
        DuckV3 duck3 = new RubberDuckV3();
        duck3.display();
        duck3.fly();
        duck3.quack();
        System.out.println();
    }
}

唉哟,不错哦。貌似鸭子们玩的都不错哦。(老板是不是加工资了……)

同样,观察下V3版本的UML图:

鸭子V3

V3版的实现解决V1和V2中的缺点,那么这个设计在应对新需求时表现怎么样?假设某个产品脑洞大开,在鸭子游戏中加入一种模型鸭的类型,这种鸭子不会叫,但是我们可以给它装上火箭推进器,让它飞起来。(老板说,这个点子不错,开发出来上线看看效果,开发心里。。。。)

还好,V3版的设计中,我们不会修改原有的代码,只需要扩展类就行了。
首先,我们要扩展我们行为实现,不会叫和火箭飞。

public class MuteQuack implements QuackBehavior {

    @Override
    public void quack() {
        //不会叫
    }
}
/**
 * 火箭助力
 */
public class FlyRocketPower implements FlyBehavior {
    @Override
    public void fly() {
        System.out.println("火箭助力飞行...");
    }
}

然后,实现模型鸭,组合这两种新的具体行为:

/**
 * 新增一个模型鸭
 */
public class ModelDuckV3 extends DuckV3{

    public ModelDuckV3() {
        //模型鸭不会飞,但装了火箭推进器
        flyBehavior = new FlyRocketPower();
        quackBehavior = new MuteQuack();
    }

    @Override
    public void display() {
        System.out.println("我是一只模型鸭");
    }
}

现在,让这只模型鸭加入进来看看:(DockV3Main中加入)

        //一只模型鸭
        DuckV3 duck4 = new ModelDuckV3();
        duck4.display();
        duck4.fly();
        duck4.quack();

输出

我是一只模型鸭
火箭助力飞行...

很好,这样我们就只通过扩展类的方式,来满足了新的需求(再奇葩也能实现),很好的满足了设计模式中的开闭原则。

到这里,我们就已经通过一个案例将我们今天要了解的策略模式引出了,下面我们看下正式的定义。

定义

策略模式定义了一组算法族,并分别封装起来,它们之间可以互相替换。策略模式让算法独立于使用算法的客户端,算法可以独立演进。

用代码说明一下.
抽象的策略(可以是接口、也可以是抽象类)

/**
 * 策略抽象(算法抽象)
 */
public interface Strategy {
    void calculate();
}

具体的策略算法实现

/**
 * 具体策略A
 */
public class ConcreteStrategyA implements Strategy {
    @Override
    public void calculate() {
        System.out.println("策略A的算法");
    }
}
/**
 * 具体策略B
 */
public class ConcreteStrategyB implements Strategy {
    @Override
    public void calculate() {
        System.out.println("策略B的算法");
    }
}

将策略组合进去的上下文

/**
 * 策略上下文,用来组合抽象策略
 */
public class Context {

    private Strategy strategy;

    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    public void calculate(){
        strategy.calculate();
    }

    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
}

使用策略的客户

/**
 * 客户
 */
public class StrategyMain {
    public static void main(String[] args) {
        Context context = new Context(new ConcreteStrategyA());
        context.calculate();

        context.setStrategy(new ConcreteStrategyB());
        context.calculate();
    }
}

策略模式UML图
策略UML

从鸭子游戏的设计过程中,我们可以总结一些面向对象的设计技巧:

  1. 封装变化 – 分离固定部分和变化部分,将变化的部分单独封装起来,鸭子游戏变化的就是鸭子的行为。
  2. 针对接口编程 – 组件(类)之间的依赖关系通过接口(抽象类)来解耦,让两边的组件(类)可以独自变化(只要接口方法签名不变)。鸭子基类只依赖行为的抽象接口。
  3. 多用组合,少用继承 – “有一个”(组合)在大部分时候比"是一个要好"(继承)。只有在确实需要继承的时候才使用继承。

扩展示例

为了加深理解,在来一个排序的例子。(这个例子URL是https://java2blog.com/strategy-design-pattern-java/)
原始需求,现有一个List,需要对其进行排序,且排序算法是可以指定的。

V1版本

假设,当前需要支持的排序算法只有两类,设计V1版本实现如下:

/**
 * 排序类型
 */
public enum SortingTypeV1 {
    MERGE_SORT, QUICK_SORT;
}
/**
 * 排序管理器
 */
public class SortingManagerV1 {
    List list;

    public SortingManagerV1(List list) {
        this.list = list;
    }

    public void sortListBaseOnType(SortingTypeV1 type){
        System.out.println("===================================");
        System.out.println("Sorting List based on Type");
        System.out.println("===================================");

        if(type == SortingTypeV1.MERGE_SORT){
            sortListUsingMergeSort();
        }else if(type == SortingTypeV1.QUICK_SORT){
            sortListUsingQuickSort();
        }else {
            throw new RuntimeException("not support type");
        }
    }

    private void sortListUsingQuickSort() {
        System.out.println("Sorting List using quick sort");
    }

    private void sortListUsingMergeSort() {
        System.out.println("Sorting List using merge sort");
    }
}

使用看看:

/**
 * 客户
 */
public class SortingMainV1 {
    public static void main(String[] args) {
        List list = Arrays.asList(new Integer[]{44,5,3,5,5,64,3});

        SortingManagerV1 sm = new SortingManagerV1(list);
        // Sorting using merge sort
        sm.sortListBaseOnType(SortingTypeV1.MERGE_SORT);

        System.out.println();
        // Sorting using quick sort
        sm.sortListBaseOnType(SortingTypeV1.QUICK_SORT);
    }
}

现在我们要加入一种新的堆排序算法,那么V1的变化会有,首先在SortingTypeV1中加入一个新的类型,
然后打开SortingManagerV1类,修改sortListBaseOnType方法的if-else,添加一个新的分支,接着新增一个
方法来实现堆排序。(这儿的代码未实现,读者可以自行实现感受下)

整个过程我们会发现,这会修改已有类的方法,也就是会对已经经过严格测试验证的类产生变动,从而带来稳定性问题和测试成本问题。违反开闭原则。

V2版本

好了,我们使用策略模式来实现。
首先,定义策略抽象,对应于这里就是排序策略。

/**
 * 抽象排序策略
 */
public interface SortingStrategy {

    void sortList(List list);
}

接下来,提供具体的策略算法,对应这里就是具体的排序算法。

/**
 * 快速排序算法
 */
public class QuickSortStrategy implements SortingStrategy{
    @Override
    public void sortList(List list) {
        System.out.println("Sorting List using quick sort");
    }
}
/**
 * 归并排序算法
 */
public class MergeSortStrategy implements SortingStrategy{
    @Override
    public void sortList(List list) {
        System.out.println("Sorting List using merge sort");
    }
}

然后,定义组合策略抽象的上下文,对应这里就是排序管理器

/**
 * 使用策略模式解决
 */
public class SortingManagerV2 {

    SortingStrategy sortingStrategy;
    List<Integer> list;

    public SortingManagerV2(SortingStrategy sortingStrategy, List<Integer> list) {
        this.sortingStrategy = sortingStrategy;
        this.list = list;
    }

    public void sortingList(){
        System.out.println("===================================");
        System.out.println("Sorting List based on Strategy");
        System.out.println("===================================");

        sortingStrategy.sortList(list);
    }

    public SortingStrategy getSortingStrategy() {
        return sortingStrategy;
    }

    public void setSortingStrategy(SortingStrategy sortingStrategy) {
        this.sortingStrategy = sortingStrategy;
    }
}

最后,测试下

/**
 * 客户
 */
public class SortingMainV2 {
    public static void main(String[] args) {
        List list = Arrays.asList(new Integer[]{44,5,3,5,5,64,3});

        SortingManagerV2 sm = new SortingManagerV2(new MergeSortStrategy(), list);
        sm.sortingList();

        System.out.println();

        sm.setSortingStrategy(new QuickSortStrategy());
        sm.sortingList();
    }
}

同样,现在加入一种新的堆排序算法,看看策略模式是如何应对的。
提供一种新的堆排序算法实现

/**
 * 堆排序算法
 */
public class HeapSortStrategy implements SortingStrategy{
    @Override
    public void sortList(List list) {
        System.out.println("Sorting List using heap sort");
    }
}

然后就可以测试了

    sm.setSortingStrategy(new HeapSortStrategy());
    sm.sortingList();

没毛病。发现使用策略模式解决这种问题,真是好用的不得了。

不过,策略模式也有它的缺点。它会产生很多类出来,实际使用中,需要权衡考虑。

参考资料

《Head First设计模式》

https://java2blog.com/strategy-design-pattern-java/

源码

https://gitee.com/cq-laozhou/design-pattern

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值