孪生兄弟状态模式与策略模式有什么区别,究竟该如何选择

都说状态模式和策略模式很像,它们的 UML 类图一样。这也说明,单纯从代码角度来讲,它们的本质一样,其实都是多态的应用。但它们实际所代表的的事物特征是有本质区别的,选择哪个设计模式,代表了你看待业务场景的角度。从合理角度地对业务进程抽象,选择恰当的设计模式,才能让代码有更好的结构。
这篇文章重点说说我对状态模式和策略模式区别的理解,以及如何选择。

一、策略模式

关于策略模式,我之前写过一篇笔记,不过是 C# 写的。策略模式解决了代码逻辑分支较多,对不同的分支,采取不同措施的问题。不熟悉策略模式的,也可以上集回顾: 扯一扯 C#委托和事件?策略模式?接口回调?

策略模式简介

在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。这种类型的设计模式属于行为型模式。

意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。

策略模式的模型代码

  • 策略的抽象,定一个策略接口,声明不同策略方案所需实现的方法:
public interface Stragety {
    void function();
}
  • 具体的策略类,定义不同的策略类,实现策略抽象接口:
public class StrategyA implements Stragety {

    @Override
    public void function() {
        System.out.println("invoke StrategyA function ...");
    }
}
public class StrategyB implements Stragety {
    @Override
    public void function() {
        System.out.println("invoke StrategyB function ...");
    }
}
  • 操作策略的上下文环境,Context 类:
public class Context {
    private Stragety stragety;

    public Context() {
    }

    public Context(Stragety stragety) {
        this.stragety = stragety;
    }

    public void setStragety(Stragety stragety) {
        this.stragety = stragety;
    }

    public void function() {
        if (stragety == null) {
            System.out.println("not set strategy...");
            return;
        }
        stragety.function();
    }
}

最后调用的测试代码如下:

public class Test {
    public static void main(String[] args) {
        Context context = new Context();
        context.function();
        context.setStragety(new StrategyA());
        context.function();
        context.setStragety(new StrategyB());
        context.function();
    }
}

结果如下:

二、状态模式

状态模式中的行为是由状态来决定的,在状态模式(State Pattern)中,类的行为是基于它的状态改变的。我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。这种类型的设计模式也属于行为型模式。

状态模式简介

意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。

主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。

何时使用:代码中包含大量与对象状态有关的条件语句。

如何解决:将各种具体的状态类抽象出来。

关键代码:状态模式的接口中通常有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和策略模式一样,也可以用于消除 if...else 等条件选择语句。

状态模式的模型代码

  • 状态的抽象,定一个状态接口,声明不同状态下所需实现的方法:
public interface State {
    void function1();

    void function2();
}

  • 具体的状态类,定义不同的状态类,实现状态抽象接口:
public class StateA implements State {
    @Override
    public void function1() {
        System.out.println("invoke StateA function1 ...");
    }

    @Override
    public void function2() {
        System.out.println("invoke StateA function2 ...");
    }
}
public class StateB implements State {
    @Override
    public void function1() {
        System.out.println("invoke StateB function1 ...");
    }

    @Override
    public void function2() {
        System.out.println("invoke StateB function2 ...");
    }
}
  • 维护状态的上下文环境,Context 类:
public class Context {
    private State state;

    public Context() {
    }

    public Context(State originalState) {
        this.state = originalState;
    }

    public void setState(State state) {
        this.state = state;
    }

    public void setStateA() {
        setState(new StateA());
    }

    public void setStateB() {
        setState(new StateB());
    }

    public void function1() {
        state.function1();
    }

    public void function2() {
        state.function2();
    }
}

最后调用的测试代码如下:

public class Test {
    public static void main(String[] args) {
        Context context = new Context();
        context.setStateA();
        context.function1();
        context.function2();
        context.setStateB();
        context.function1();
        context.function2();
    }
}

结果如下:

三、状态模式和策略模式的区别

通过上面模型代码的对比,有的同学可能发现了,乍一看代码,其实两者几乎没有没什么区别,都是在玩多态的语法糖。这让我想起了经典书籍《重构,改善既有代码的设计》第一章中的那个例子,重构篇中有如下一段话——

可以用多态来取代switch语句,但是因为:一部影片可以在生命周期内修改自己的分类,一个对象却不能在生命周期内修改自己所属的类。所以这里不能用策略模式,用多态取代switch,而应该用状态模式(State)。       
                                 
这是一个State模式还是一个Strategy模式?答案取决于Price类究竟代表计费方式,还是代表影片的某个状态。

也就是说对于一个场景的抽象选择策略模式还是状态模式,取决于你对这个场景的认知。我的理解是策略,即方法,不同的策略实现类中相同的方法,地位应该是平等的。举几个例子,早餐选择吃面包,中餐选择吃米饭,这是策略上的决定;交通工具是选择乘坐公交车还是乘坐地铁;在中国选择说中文,在美国选择说英语...

而状态之间,往往伴随着转换,即状态迁移,可能是在时序上的状态迁移,也可能是在某个状态上执行某种行为(调用某个方法)的时候,转化为另外一种状态。它的重点应该是状态迁移,譬如烧水过程中,水温可以当做状态;手机移动数据蓝牙WiFi的开关、汽车行驶的速度;在中国的时候说中文,在美国的时候说英语...都可以抽象成状态。咦,等等!!!在中国选择说中文,在美国选择说英语,到底抽象成策略模式还是状态模式?

其实这就还是要看你是怎么看待这个场景了,你要把它当做两中平等的场景,只是在不同的场景中,做一个选择,则可以抽象成策略模式,如果你把在中国和在美国当做两种状态,并且两种状态可以发生转换,比如处于在中国这种状态下,有一个搭乘飞机飞到了中国的行为,状态变成了在中国,此时就应该考虑抽象成状态模式。

  1. 状态转换场景中,最好不要由客户端来直接改变状态(也不是绝对不可以),而是客户端做了某种其它操作引起状态迁移,也就是说客户端最好不要直接创建一个具体 State 实现类的实例对象,通过 setState() 方法来设置。
  2. 状态转换也可能是对象的内部行为造成的。

这么一想,状态模式和策略模式代码实现还是有区别的,下面我结合以上想法将状态模式模型代码做修改。
针对第一点,比如,客户端执行了 actionB 方法方法,使得状态改变成 StateB , 针对第二点,假设,在状态 StateB 中,执行 function2 的时候,会切换状态为 StateA。此时代码应该是这样的:

  • 定一个状态接口:
public interface State {
    void function1();

    void function2(Context context);
}

  • 具体的状态类:
public class StateA implements State {
    @Override
    public void function1() {
        System.out.println("invoke StateA function1 ...");
    }

    @Override
    public void function2(Context context) {
        System.out.println("invoke StateA function2 ...");
    }
}
public class StateB implements State {
    @Override
    public void function1() {
        System.out.println("invoke StateB function1 ...");
    }

    @Override
    public void function2(Context context) {
        System.out.println("invoke StateB function2 ...");
        context.setStateA();
    }
}
  • 增加一个可能在其它操作中改变状态的方法,封装成接口:
public interface Others {
    void actionB();
}
  • 维护状态的上下文环境,Context 类:
public class Context implements Others{
    private State state;

    public Context() {
    }

    // 为了方便下文说明,标记为--------MARK1
    public Context(State originalState) {
        this.state = originalState;
    }

    // 为了方便下文说明,标记为--------MARK2
    public void setState(State state) {
        this.state = state;
    }

    public void setStateA() {
        setState(new StateA());
    }

    public void setStateB() {
        setState(new StateB());
    }

    public void function1() {
        state.function1();
    }

    public void function2() {
        state.function2(this);
    }

    @Override
    public void actionB() {
        System.out.println("invoke actionB ...");
        setStateB();
    }
}

最后调用的测试代码如下:

public class Test {
    public static void main(String[] args) {
        Context context = new Context();
        context.setStateA();
        context.function1();
        context.actionB();
        context.function1();
        context.function2();
        context.function1();
        context.function2();
    }
}

此时的执行结果如下:

四、总结

在现实世界中,策略和状态是两种完全不同的思想。虽然状态模式和策略模式拥有相似的结构,虽然它们都基于开闭原则,但是,它们的意图是完全不同的。当我们对状态和策略进行建模时,这种差异会导致完全不同的问题。对状态进行建模时,状态迁移是一个核心内容,状态模式帮助对象管理状态;而在选择策略时,状态迁移与此毫无关系。另外,策略模式允许一个客户选择或提供一种策略,而这种思想在状态模式中完全没有,所以在状态模式中,如果需要避免客户端的不安全操作,我们完全可以不提供代码中标记为 MARK1 的构造器,并将代码中标记为 MARK2 的方法私有化。

从代码上理解:是谁促使了行为的改变。状态模式中,状态转移由 Context 或 State 自己管理。如果你在State中管理状态转移,那么它必须持有Context的引用。例如,在上面代码中,StateB 的 function2() 方法需要调用 setState()方法去改变它的状态,它就需要传入一个 Context 类型参数。而策略模式中,Strategy 从不持有Context的引用,是客户端把所选择的 Strategy 传递给Context。由于状态模式和策略模式在工作中的使用场景比较多(我自己最近项目就有用到),所以本文重点分析记录状态模式和策略模式的异同,来加深我自己对它们的理解。也希望能帮助到有缘看到本文的朋友。

posted @ 2018-10-24 08:02  SharpCJ  阅读(810)  评论(0编辑  收藏  举报