设计模式与生活⑩:地铁没变,你换了个状态

0 阅读8分钟

周二,没什么特别。

小王照旧早起、吃早餐、坐地铁,到公司做日常开发。中午吃完饭,再去公司旁边遛一圈。普通的牛马的一天。

地铁上倒有两个小片段。

刚上去没座,他一只手扶杆,一只手刷手机。等他反应过来,已经坐过了一站。

后来有了座,他靠着窗户,没一会儿睡着了。

scene-subway.jpg

还是那班地铁,人已经从刷手机切到了睡觉

还是小王,还是“列车到站”这件事,可他的反应已经不一样:刷手机时没留意,坐着清醒时会看站名,睡着后又听不见报站。

今天聊的就是这个思路:**状态模式(State)——对象的内部状态变了,处理同一件事的方式也跟着变。**系列第十篇。

01 先把所有反应塞进一个方法

最直白的写法,是给小王记一个状态,再在到站时判断:

enum RideState { STANDING, SEATED, SLEEPING }

class Commuter {
    private RideState state = RideState.STANDING;

    void arriveAtStation() {
        switch (state) {
            case STANDING -> System.out.println("站着刷手机,没留意报站");
            case SEATED   -> System.out.println("抬头确认站名,准备下车");
            case SLEEPING -> System.out.println("靠窗睡着,没有听见报站");
        }
    }
}

当前状态记在一个字段里,行为都挤在一个方法里

三个分支不多,甚至挺清楚,别急着说它烂。

麻烦通常不是第一天出现的。以后再加上“戴着耳机”“正在打电话”“快到换乘站”,每种状态下的处理再复杂一点,这个方法就会同时塞进两件事:我现在是什么状态,以及这个状态该怎么反应。每加一种情况,都得回来翻它。

状态模式要问的不是“这里能不能用 switch”,而是:**这些分支,是不是同一个对象在不同状态下的不同行为?**如果是,就可以把这些行为各自安顿下来。

02 把三种状态请出去

先给所有状态留一个共同的接口。每种状态各自回答:列车到站时,我该怎么处理。

interface CommuterState {
    void onStationArrived();
}

class StandingScrollingState implements CommuterState {
    public void onStationArrived() {
        System.out.println("站着刷手机,没留意报站");
    }
}

class SeatedAwakeState implements CommuterState {
    public void onStationArrived() {
        System.out.println("抬头确认站名,准备下车");
    }
}

class SleepingState implements CommuterState {
    public void onStationArrived() {
        System.out.println("靠窗睡着,没有听见报站");
    }
}

每种状态只管自己的反应

小王还是那个小王,只是当前状态换成了对应的对象:

class Commuter {
    private CommuterState state = new StandingScrollingState();

    void takeSeat() {
        state = new SeatedAwakeState();
    }

    void dozeOff() {
        state = new SleepingState();
    }

    void arriveAtStation() {
        state.onStationArrived();   // 交给当前状态处理
    }
}

找到座位、打起瞌睡会换状态,到站处理只负责委托

单看到站处理,调用方从头到尾只说一句“到站了”:

Commuter xiaoWang = new Commuter();
xiaoWang.arriveAtStation();  // 站着刷手机,没留意报站

xiaoWang.takeSeat();
xiaoWang.arriveAtStation();  // 抬头确认站名,准备下车

xiaoWang.dozeOff();
xiaoWang.arriveAtStation();  // 靠窗睡着,没有听见报站

外部事件没变,处理它的状态变了

state.png

同一件事交给不同状态,得到的处理自然不同

这里把状态切换放在 Commuter 里,是为了看得清楚。状态对象也可以在处理事件时决定下一个状态,两种写法都可以。重要的不是切换必须写在哪,而是切换规则要有明确的去处,别散落在各个调用方手里。

以后再添“戴着耳机听歌”,可以新增一个状态类,把对应行为放进去,再在合适的地方接上切换。注意,是“不用往 arriveAtStation() 里继续塞分支”,不是“其他地方一行都不用改”——新状态从哪儿进来、能去哪儿,仍然要写清楚。

03 跟策略、观察者,分别差在哪儿

单看代码,状态模式和策略模式很像:都是一个接口、几个实现,再把事情委托出去。

策略模式(Day 4)管“选哪种做法”:早餐可以自己做,也可以楼下买,目标是吃一顿早餐,方案由你挑。状态模式管“当前状态怎么做”:小王不是给“到站”挑了一个算法,而是他已经从站着刷手机,变成了坐着、又变成了睡着;状态还可能随着事件继续迁移。

跟观察者模式(Day 8)也要分开:**观察者管“变化发生后通知谁”,状态管“接到事件后由当前状态怎么处理”。**快递到了,可以通知一群订阅者;通知送到小王这里,他醒着还是睡着、怎么回应,则是另一件事。

业务里常见的是订单。待支付时调用 cancel(),可以直接取消;已经发货时再调用同一个方法,就得按另一套规则处理。调用方不必在每个地方重复判断订单走到哪一步,当前订单状态才知道自己能做什么。

不过,**看到订单里有个状态枚举,不等于用了状态模式。**只有状态相关行为被拆成对应对象,并通过当前状态对象完成委托,才是这里说的 GoF 状态模式。

04 人不是代码,这两处不完全像

第一处:真实的人不是只有清清楚楚的三档。小王可能半睡半醒,可能听见报站却懒得睁眼,也可能身体坐着、脑子还停在没改完的 bug 上。代码里的状态则必须可识别,迁移也得有明确规则,不能拿“有点困,但又不完全困”当一个模糊分支混过去。

第二处:现实里睡着,不需要谁郑重其事地调用一次 dozeOff()。代码里不会自己凭空换状态,某段逻辑总要触发这次迁移。

所以,地铁类比只负责说明“同一事件由不同状态给出不同反应”。它不负责证明人的所有心理活动都能被切成几个类——真这么干,就不是设计模式,是难为程序员了。

05 什么时候,别急着拆状态

**只有几个简单分支时,别拆。**像开头那样,三个状态各打印一句话,枚举加 switch 反而一眼能看完。为了套模式,把十几行变成几个文件,读者只会更累。

**状态只是个标签时,别拆。**如果它只用于展示、筛选或存库,不同状态下没有不同的行为,一个枚举就够了。状态模式解决的是行为差异,不是给每个名词都发一个类。

**几个独立维度,别硬揉成一个状态。**站着还是坐着、醒着还是睡着、戴没戴耳机,如果都能独立变化,全塞进一个 State 很容易拼出一大堆组合类。该拆成几个独立状态维度就拆开,别硬造“站着戴耳机但快睡着状态”。

还有一点:**状态模式不会自动保证迁移合法。**睡着的人能不能直接变成“正在下车”,待支付订单能不能直接变成“已完成”,这些约束仍然要由你明确写出来。

判断标准可以落在一句问题上:**同一个对象面对同一事件,是否会因状态不同而有明显不同、并且正在变复杂的行为?**会,才值得考虑状态模式。

06 普通的一天,也有自己的状态

中午吃完饭,小王还会照例去公司旁边遛一圈。早上那点迷糊,并不会把周二变成什么特别的一天。

但回头看,那班地铁倒把一件事演示得挺完整:人没变,事件没变,状态一变,行为就变了。

跟前面九篇对一句:单例管造几个,工厂管怎么造,模板方法管事情怎么走,策略管这一步用什么做法,装饰器管外面加几层,命令管把要做的事做成对象,代理管谁能碰到真东西,观察者管变化后通知谁,外观管一堆零件只留一个入口,状态管当前状态下怎么做。

带走一句话:状态 = 同一个对象,状态不同,对同一件事的处理也不同。

地铁没变,站还是一站一站过。只是刷手机的你、看站名的你、靠着窗睡着的你,已经不是同一个状态。

普通的一天也值得好好过。中午那圈遛弯,能走就下楼走一圈。


参考资料