设计模式之——状态模式与策略模式

如果你的简历里出现了"设计模式"的字样,那么作为面试官的我几乎都会问到一个问题: "状态模式与策略模式有哪些区别"。很多人一脸懵,我就知道这次愉快的技术交流无疾而终了。可能对于很多人来说,策略模式比较熟悉,可什么是状态模式,好多人还是比较迷糊的。此篇专题,我们就来聊聊状态模式与策略模式。

第一部分 状态模式

考虑这样的一个场景:一个电梯,有四种操作:运行停止开门关门。每一种操作成功后,都对应着状态的切换。每一种状态,又可以随着操作,向另一种状态切换。但是状态与状态之间又不是随意切换的。如下表所示:

  • 运行状态

    • 可以向停止状态切换
    • 不能再次切换到运行状态
    • 不能在电梯的运行过程中开门
    • 不能在电梯的运行过程中关门 - 因为运行的过程中,电梯的门必然是关的
  • 停止状态

    • 可以向运行状态切换
    • 可以向开门状态切换
    • 可以向关门状态切换
    • 不能再次切换到停止状态
  • 开门状态

    • 可以向关门状态切换
    • 不能再次切换到开门状态
    • 不能在开门的状态下运行
    • 不能在开门的状态下停止 - 因为开门状态下,电梯的状态必然是停止的
  • 关门状态

    • 可以向运行状态切换
    • 可以向开门状态切换
    • 不能向停止状态切换 - 因为在关门状态下,电梯必然是停止的
    • 不能再次切换到关门状态

说得比较复杂,看一个状态机图

有箭头的,就是允许;没有箭头的,就不允许

Lift-State.png

1 错误示范

if - else真是个害人精,它让我们在实现功能的时候,不必过多地思考,很多没有研习过状态模式的同学也是可以轻松实现的——只不过没那么优美罢了。

  • 新建一个枚举,列出四种状态
  • 电梯有4个方法
  • 电梯有1种状态
Lift-Design.png
  • LiftState.java

    package com.futureweaver.enums;
    
    // 电梯状态
    public enum LiftState {
        // 开门状态
        Opening,
    
        // 关门状态
        Closed,
    
        // 运行状态
        Running,
    
        // 停止状态
        Stoped
    }
    
  • Lift.java

    package com.futureweaver.domain;
    
    import com.futureweaver.enums.LiftState;
    
    // 电梯
    public class Lift {
        private LiftState state = LiftState.Closed;
    
        // 开门
        public void open() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 无法重复开门");
            } else if (state == LiftState.Closed) {
                state = LiftState.Opening;
                System.out.println("success: 开门");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 运行状态下不能开门");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Opening;
                System.out.println("success: 开门");
            }
        }
    
        // 关门
        public void close() {
            if (state == LiftState.Opening) {
                state = LiftState.Closed;
                System.out.println("success: 关门");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 无法重复关门");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 运行状态下,就一定是关门状态了");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 停止后就是关门状态了");
            }
        }
    
        // 运行
        public void run() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 电梯门没关,不能运行");
            } else if (state == LiftState.Closed) {
                state = LiftState.Running;
                System.out.println("success: 运行");
            } else if (state == LiftState.Running) {
                System.out.println("failure: 无法重复运行");
            } else/* if (state == LiftState.Stoped)*/ {
                state = LiftState.Running;
                System.out.println("success: 运行");
            }
        }
    
        // 停止
        public void stop() {
            if (state == LiftState.Opening) {
                System.out.println("failure: 开门状态下不会运行,自然也不需要停止");
            } else if (state == LiftState.Closed) {
                System.out.println("failure: 关门状态下不会运行,自然也不需要停止");
            } else if (state == LiftState.Running) {
                state = LiftState.Stoped;
                System.out.println("success: 停止");
            } else/* if (state == LiftState.Stoped)*/ {
                System.out.println("failure: 无法重复停止");
            }
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 输出

    failure: 无法重复关门
    failure: 无法重复关门
    success: 开门
    failure: 电梯门没关,不能运行
    failure: 无法重复开门
    failure: 开门状态下不会运行,自然也不需要停止
    

结论

从测试的结果可以看出,需求实现了,也没什么问题。但这是我们编写的简单代码,回过头再来审视一下Lift.java,我们做了大量的条件判断。同一个类当中的代码量又太多——如果状态不止4种,怎么办?如果状态与状态之间的切换,业务比较复杂,不能一两条代码就搞得定,又怎么办?

2 正确示范

在现实领域中,电梯状态,自然而然就是电梯的一个属性。然而在面向对象语言中,所谓万物皆对象,状态,自然也可以作为一个对象。既然状态可以作为对象,那么就可以利用多态来解决了。

  • 新建一个电梯状态的抽象类,定义4个操作: 打开关闭停止运行
  • 新建四个电梯状态的子类
  • 每个实际状态,自己判断能否向目标状态切换。如果能切换的话,创建目标状态对象,并向电梯发送修改状态的请求
Lift-Design-State-Pattern.png
  • Lift.java

    package com.futureweaver.domain;
    
    // 电梯
    public class Lift {
        private LiftState state = new ClosedState();
    
        public LiftState getState() {
            return state;
        }
    
        public void setState(LiftState state) {
            this.state = state;
        }
    
        // 开门
        public void open() {
            // 由状态对象自己处理切换行为
            state.open(this);
        }
    
        // 关门
        public void close() {
            // 由状态对象自己处理切换行为
            state.close(this);
        }
    
        // 运行
        public void run() {
            // 由状态对象自己处理切换行为
            state.run(this);
        }
    
        // 停止
        public void stop() {
            // 由状态对象自己处理切换行为
            state.stop(this);
        }
    }
    
  • LiftState.java

    package com.futureweaver.domain;
    
    // 电梯状态
    public abstract class LiftState {
    
        // 电梯状态的共同父类,无论向哪一个状态切换都可以,子类自己覆盖要阻止的操作
    
        public void open(Lift lift) {
            // 如果成功的话,直接修改电梯的"状态"属性
            lift.setState(new OpeningState());
            System.out.println("success: 开门");
        }
    
        public void close(Lift lift) {
            // 如果成功的话,直接修改电梯的"状态"属性
            lift.setState(new ClosedState());
            System.out.println("success: 关门");
        }
    
        public void stop(Lift lift) {
            // 如果成功的话,直接修改电梯的"状态"属性
            lift.setState(new StopedState());
            System.out.println("success: 停止");
        }
    
        public void run(Lift lift) {
            // 如果成功的话,直接修改电梯的"状态"属性
            lift.setState(new RunningState());
            System.out.println("success: 运行");
        }
    }
    
  • OpeningState.java

    package com.futureweaver.domain;
    
    public class OpeningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 无法重复开门");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 开门状态下不会运行,自然也不需要停止");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 电梯门没关,不能运行");
        }
    
    }
    
  • ClosedState.java

    package com.futureweaver.domain;
    
    public class ClosedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 无法重复关门");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 关门状态下不会运行,自然也不需要停止");
        }
    }
    
  • StopedState.java

    package com.futureweaver.domain;
    
    public class StopedState extends LiftState {
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 停止后就是关门状态了");
        }
    
        @Override
        public void stop(Lift lift) {
            System.out.println("failure: 无法重复停止");
        }
    }
    
  • RunningState.java

    package com.futureweaver.domain;
    
    public class RunningState extends LiftState {
        @Override
        public void open(Lift lift) {
            System.out.println("failure: 运行状态下不能开门");
        }
    
        @Override
        public void close(Lift lift) {
            System.out.println("failure: 运行状态下,就一定是关门状态了");
        }
    
        @Override
        public void run(Lift lift) {
            System.out.println("failure: 无法重复运行");
        }
    }
    
  • LiftTest.java

    package com.futureweaver.domain;
    
    import org.junit.Test;
    
    public class LiftTest {
        @Test
        public void testLift() {
            Lift lift = new Lift();
    
            lift.close();
            lift.close();
            lift.open();
            lift.run();
            lift.open();
            lift.stop();
        }
    }
    
  • 输出

    failure: 无法重复关闭
    failure: 无法重复关闭
    success: 开门
    failure: 电梯门没关,不能运行
    failure: 无法重复开门
    failure: 开门状态下不会运行,自然也不需要停止
    

结论

这种实现方式,电梯与电梯状态产生了双向依赖,属于一种紧耦合;电梯状态抽象父类,与电梯状态具体类又产生了双向依赖,属于一种紧耦合。理解起来有点绕,一条一条地说。

  • 电梯与电梯状态的通信

电梯具备一个电梯状态对象,当接收到操作请求时,电梯对象本身不做任何的判断和处理,而是交由状态对象处理。

当电梯状态接收到转换请求时,如果可以转换,那么我们想要得到的结果是:电梯的状态发生了变化。所以电梯状态需要向电梯对象发送"改变状态"的消息,那么电梯状态就需要知道,到底是哪一个电梯对象。所以电梯状态的转换操作,需要接收一个电梯对象的参数。

  • 抽象状态与具体状态的通信

具体状态需要继承自抽象状态。

抽象状态定义了默认方法,其中的默认方法就是: 所有的操作,都是合法的。既然是合法的,就要向目标状态切换。因为使用了状态模式,状态是一个对象了,所以需要创建具体状态的对象,再把创建好的状态对象,发送给电梯。

  • 说得比较复杂,结合看一下类图和序列图

    • 类图
Lift-Design-State-Pattern.png
  • 序列图
Lift-Sequence.png

3 状态模式总结

State-Pattern.png
  • 状态模式

    允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

    通过内聚,提升了灵活性、可维护性和可扩展性。

第二部分 状态模式与策略模式

在上一篇文章《设计模式在RESTful当中的应用》当中,已经聊过策略模式,乍一看它们的类图,是很相似的:

  • 状态模式
State-Pattern.png
  • 策略模式
Strategy-Pattern.png

策略模式与状态模式都把实际的行为,延迟到了子类,以此完成多态。同时,上下文(Context)面向的都是一个抽象类。(一些编程语言明确地区分了接口与抽象类,比如Java;而一些编程语言并没有明确地区分,比如C++。OOD本身与语言的关联是比较弱的,所以在OOP的时候,到底是面向接口还是抽象类,是需要酌情考虑的。)

状态模式与策略模式的区别

类结构相似,想要找出状态模式与策略模式的区别,就需要从它们的行为入手了

  • 策略模式

    在程序运行的过程中,策略与策略之间,是相互独立的,从而耦合度是比较松的。因为本身策略中封装的是一系列可以相互替换的算法,每一个策略是可以独立完成自己所要完成的工作的。

    上下文(Context)依赖于策略,而策略不依赖于上下文。因为策略在工作时,并不关心这个信息是谁发送过来的。

  • 状态模式: 允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

    在程序运行的过程中,状态与状态之间。比如从A状态,过渡到B状态,A状态是需要获取一个B状态(至于到底怎么获取,创建也好,使用注册表也好,使用享元也好,这个就要看具体业务了)的状态对象的。所以状态与状态之间是互相依赖的,耦合度是比较紧的。

    上下文(Context)依赖于状态,而状态又依赖于上下文。比如从A状态,过渡到B状态,A状态先获取一个B状态,之后要找到上下文,把上下文的状态给修改掉。所以上下文下状态之间是互相依赖的,耦合度也是比较紧的。

第三部分 总结

综上所述,策略模式实现起来比较简单,是真正利用了面向对象的多态技术,完成了算法的互换使用,并且既遵循了高内聚,又遵循了松耦合的设计原则。

而状态模式实现起来比较复杂,其亦是利用了面向对象的多态技术,完成状态与状态之间的过渡。虽然状态模式遵循了高内聚的设计原则,但却破坏了松耦合原则。

两者都是通过内聚,提升了灵活性可维护性可扩展性。但归根结底,两者的区别就在于:策略模式是松耦合、状态模式是紧耦合

打完收工

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容