16.无尽加班何时休 - 状态模式(State Pattern)

16.1 加班, 又是加班!

  • 时间: 4月19日   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     “小菜, 你们加班没完没了了?” 大鸟为晚上十点才到家的小菜打开了房门.

     “嗨, 没办法, 公司的项目很急, 所以要求加班.”
     “有这么急吗? 这星期四天来你都在加班, 有加班费吗? 难道周末还要继续?”
     “哪来的什么加班费, 估计周末也逃不了了…..”
     “你刚刚讲到, 上午状态好, 中午想睡觉, 下午渐回复, 加班苦煎熬. 其实是一种状态的变化, 不同的时间, 会有不同的状态. 你现在用代码来实现一下.”
     “其实就是根据时间的不同, 作出判断来实现, 是吧? 这不是大问题.”

16.2 工作状态–函数版

     半小时后, 小菜的第一版程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 写程序方法
public static void writeProgram() {
if (hour < 12) {
System.out.println("当前时间: " + hour + "点 上午工作, 精神百倍");
} else if (hour < 13) {
System.out.println("当前时间: " + hour + "点 饿了, 午饭: 犯困, 午休.");
} else if (hour < 17) {
System.out.println("当前时间: " + hour + "点 小屋状态还不错, 继续努力");
} else {
if (workFinished) {
System.out.println("当前时间: " + hour + "点 下班回家了");
} else {
if (hour < 21) {
System.out.println("当前时间: " + hour + "点 加班哦, 疲惫至极");
} else {
System.out.println("当前时间: " + hour + "点 不行了, 睡着了");
}
}
}
}

     主程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) {
hour = 9;
writeProgram();
hour = 10;
writeProgram();
hour = 12;
writeProgram();
hour = 13;
writeProgram();
hour = 14;
writeProgram();
hour = 17;
writeProgram();
workFinished = false;
writeProgram();
hour = 19;
writeProgram();
hour = 22;
writeProgram();
}

     “小菜, 都学了这么长时间的面向对象的开发, 怎么还写面向过程的代码啊?”
     “啊, 我都习惯思维了, 你的意思是说要分一个类出来.”
     “这是起码的面向对象的思维啊, 只要应该有个 ‘工作’ 类, 你的 ‘写程序’ 方法是类方法, 而 ‘钟点’, ‘ 任务完成’ 其实就是类的什么?”
     “对外属性是吧?”
     “还问什么, 还不快去重写.” 大鸟不答反而催促道.

16.3 工作状态 — 分类版

     十分钟后, 小菜写出了第二版程序
     工作类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Work {
private int hour;
public int getHour() { return hour; }
public void setHour(int hour) { this.hour = hour; }
private boolean finish = false;
public boolean isFinish() { return finish; }
public void setFinish(boolean finish) { this.finish = finish; }
public void writeProgram() {
if (hour < 12) {
System.out.println("当前时间: " + hour + "点 上午工作, 精神百倍");
} else if (hour < 13) {
System.out.println("当前时间: " + hour + "点 饿了, 午饭: 犯困, 午休.");
} else if (hour < 17) {
System.out.println("当前时间: " + hour + "点 下午状态还不错, 继续努力");
} else {
if (finish) {
System.out.println("当前时间: " + hour + "点 下班回家了");
} else {
if (hour < 21) {
System.out.println("当前时间: " + hour + "点 加班哦, 疲惫至极");
} else {
System.out.println("当前时间: " + hour + "点 不行了, 睡着了");
}
}
}
}
}

     客户端程序如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) {
// 紧急项目
Work emergencyWork = new Work();
emergencyWork.setHour(9);
emergencyWork.writeProgram();
emergencyWork.setHour(10);
emergencyWork.writeProgram();
emergencyWork.setHour(12);
emergencyWork.writeProgram();
emergencyWork.setHour(13);
emergencyWork.writeProgram();
emergencyWork.setHour(14);
emergencyWork.writeProgram();
emergencyWork.setHour(17);
emergencyWork.writeProgram();
emergencyWork.setFinish(false);
emergencyWork.setHour(19);
emergencyWork.writeProgram();
emergencyWork.setHour(22);
emergencyWork.writeProgram();
}

     结果表现如下

1
2
3
4
5
6
7
8
当前时间: 9点 上午工作, 精神百倍
当前时间: 10点 上午工作, 精神百倍
当前时间: 12点 饿了, 午饭: 犯困, 午休.
当前时间: 13点 下午状态还不错, 继续努力
当前时间: 14点 下午状态还不错, 继续努力
当前时间: 17点 加班哦, 疲惫至极
当前时间: 19点 加班哦, 疲惫至极
当前时间: 22点 不行了, 睡着了

16.4 方法过长是坏味道

     “若是 ‘任务完成’ , 则17点, 19点, 22点都是 ‘下班回家’的状态.”
     “好, 现在我问你, 你这样的代码有什么问题?” 大鸟问道.
     “我觉得没什么问题, 不然我早就改了.”
     “仔细看看, MartinFowler 曾在 <<重构>> 中写过一个很重要的代码的坏味道, 叫做 ‘Long Method’, 方法如果过长其实极有可能有坏味道了.”
     “你的意思是workwritePrograms 方法过长了?不过这里面好多的判断, 好像是不太好, 但是我也想不出来什么办法解决它.”
     “你要知道, 这个方法很长, 他有很多判断分支, 也即是说他承担的责任过大了. 无论任何状态, 都需要通过它来改变, 这实际上是很糟糕的.”
     “哦, 对的, 面向对象的设计其实就是希望代码责任的分解. 这个类违背了 ‘单一职责原则’, 但是如何做呢?”
     “说的不错, 由于 writePrograms 的方法里有这么多的判断, 使得任何需求的改动或增加, 都需要都要去更改这个方法了, 比如, 你们的老板感觉加班有些过分, 对公司办公室的管理和员工的安全都不利, 于是发了一通知, 不管任务再多, 20点之前必须离开公司. 这样的需求非常的合理, 所以满足需求你就要更改这个方法, 但是真正需要改变的地方就只有17点到22点的状态, 但目前整个代码确实对整个方法做改动, 维护出错的风险很大.”
     “你解释了这么多, 我的理解就是这个方法违反了 ‘开放封闭原则’.”
     “哈, 小菜总结的很好, 对这几个原则理解的很透吗, 那我们应该如何做?”
     “把这些分支想办法分解成一个又一个类, 增加是不会影响到其他的类, 然后状态的变化在各自的类中完成.” 小菜说道, “理论讲讲很容易, 但实际如何做, 我不知道.”
     “当然, 这需要丰富的经验积累, 但实际上你是不需要发明重复的 ‘轮子’ 的, 因为 GoF 已经为我们针对这类问题提供了解决方案, 那就是 ‘状态模式’”

16.5 状态模式(State pattern)

状态模式(State Pattern), 当一个对象的内在状态改变时, 允许改变其行为, 这个对象看起来像是改变了其类.[DP]

     “状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂的情况下. 把状态的判断逻辑转移到表示不同状态的一系列类当中, 可以把复杂判断的逻辑简化. 当然, 如果这个状态判断很简单, 那就没有必要用 ‘状态模式’ 了”



     State 类, 抽象状态类, 定义一个接口以封装与 Context 的一个特定的状态的相关行为.

1
2
3
public abstract class State {
public abstract void handle(Context context);
}

     ConcreteState 类, 具体状态, 每一个子类实现一个与 Context 的一个状态相关的行为.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ConcreteStateA extends State {
@Override
public void handle(Context context) {
// 设置 ConcreteStateA 的下一个状态是 ConcreteStateB
context.setState(new ConcreteStateB());
}
}
public class ConcreteStateB extends State {
@Override
public void handle(Context context) {
// 设置 ConcreteStateB 的下一个状态是 ConcreteStateA
context.setState(new ConcreteStateA());
}
}

     Context 类, 维护一个 ConcreteState 子类的实例, 这个实例定义当前的状态.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Context {
private State state;
public Context(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void setState(State state) {
this.state = state;
System.out.println("当前状态:" + state.getClass().getSimpleName());
}
public void request() {
state.handle(this);
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
// 设置 Context 的初始状态为 ConcreteStateA
Context c = new Context(new ConcreteStateA());
// 不断请求, 同事更改状态
c.request();
c.request();
c.request();
c.request();
}

16.6 状态模式的好处与用处

     状态模式的好处是将与特定状态相关的行为局部化, 并将不同状态的行为分割开来[DP].
     “是不是就是将将特定的状态行为都放入一个对象中, 由于所有与状态相关的代码存在有某一个ConcreteState 中, 所以通过定义新的子类可以很容易的增加新的状态和转换[DP].
     “说白了, 这样做的目的及时为了消除消除庞大的条件分支语句, 大的分支判断会使他们难以修改和扩展, 就像我们最早说的刻板印刷一样, 任何改动和变化都是致命的. 状态模式通过把各种状态转移逻辑分布到 State 的子类之间, 来减少相互间的依赖, 好比把整个版面改成一个有一个的活字, 此时就容易维护和扩展了.”
     “什么时候应该考虑使用状态模式呢?”
     “当一个对象的行为取决于他的状态, 并且他必须在运行时刻根据状态改变他的行为时, 就可以考虑使用状态模式了. 另外, 如果业务需求某项业务有过个状态, 通常都是一些枚举常量, 状态的变化都是依靠大量的多分支判断语句来实现, 此是应该考虑将每一种业务状态定义为一个 State 的子类. 这样这些对象就可以不依赖与其他对象而独立的变化了, 某一天客户需要更改需求, 增加或减少业务状态或改变状态流程, 对你来说都不是困难的事.”
     “哦, 明白了, 这种需求还是很常见的.”
     “现在在回过头来改改你的代码, 那个Long Method 你会改了吗?”
     “哦, 学了设计模式, 有点感觉了, 我试试看.”

16.7 工作状态–状态模式版

     半小时后小菜写出了第三版的代码



     抽象状态类, 定义一个抽象的方法 ‘写程序’

1
2
3
public abstract class State {
public abstract void writeProgram(Work w);
}

     上午和中午的工作状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 上午工作状态
public class ForenoonState extends State {
@Override
public void writeProgram(Work w) {
if (w.getHour() < 12) {
System.out.println("当前时间: " + w.getHour() + "点 上午工作, 精神百倍");
} else {
// 超过12点, 则转入中午工作状态
w.setCurrent(new AfternoonState());
w.writeProgram();
}
}
}
// 中午工作状态
public class NoonState extends State {
@Override
public void writeProgram(Work w) {
if (w.getHour() < 13) {
System.out.println("当前时间: " + w.getHour() + "点 饿了, 午饭: 犯困, 午休.");
} else {
// 超过13点, 则转入下午工作状态
w.setCurrent(new AfternoonState());
w.writeProgram();
}
}
}

     下午和晚上的工作状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 下午工作状态
public class AfternoonState extends State {
@Override
public void writeProgram(Work w) {
if (w.getHour() < 17) {
System.out.println("当前时间: " + w.getHour() + "点 下午状态还不错, 继续努力");
} else {
// 超过17点, 则转入晚上工作状态
w.setCurrent(new EveningState());
w.writeProgram();
}
}
}
// 晚间工作状态
public class EveningState extends State {
@Override
public void writeProgram(Work w) {
if (w.isFinished()) {
// 如果完成则转入下班状态
w.setCurrent(new RestState());
w.writeProgram();
} else {
if (w.getHour() < 21) {
System.out.println("当前时间: " + w.getHour() + "点 加班哦, 疲惫至极");
} else {
// 超过21 点, 则转入睡眠工作状态
w.setCurrent(new SleepState());
w.writeProgram();
}
}
}
}

     睡眠状态和下班休息状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 睡眠状态
public class SleepState extends State {
@Override
public void writeProgram(Work w) {
System.out.println("当前时间: " + w.getHour() + "点 下班回家了");
}
}
// 下班休息状态
public class RestState extends State {
@Override
public void writeProgram(Work w) {
System.out.println("当前时间: " + w.getHour() + "点 不行了, 睡着了");
}
}

     工作类, 此时没有了过长的分支判断语句.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 工作类
public class Work {
private State current;
private double hour;
private boolean finished;
public Work() {
this.current = new ForenoonState();
}
public double getHour() { return hour; }
public void setHour(double hour) { this.hour = hour; }
public boolean isFinished() { return finished; }
public void setFinished(boolean finished) { this.finished = finished; }
public void setCurrent(State current) {
this.current = current;
}
public void writeProgram() {
current.writeProgram(this);
}
}

     客户端代码, 没有任何改动. 但我们的程序却更加灵活易变了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] args) {
// 紧急项目
Work emergencyWork = new Work();
emergencyWork.setHour(9);
emergencyWork.writeProgram();
emergencyWork.setHour(10);
emergencyWork.writeProgram();
emergencyWork.setHour(12);
emergencyWork.writeProgram();
emergencyWork.setHour(13);
emergencyWork.writeProgram();
emergencyWork.setHour(14);
emergencyWork.writeProgram();
emergencyWork.setHour(17);
emergencyWork.writeProgram();
emergencyWork.setFinished(false);
emergencyWork.setHour(19);
emergencyWork.writeProgram();
emergencyWork.setHour(22);
emergencyWork.writeProgram();
}

     结果表现如下

1
2
3
4
5
6
7
8
当前时间: 9.0点 上午工作, 精神百倍
当前时间: 10.0点 上午工作, 精神百倍
当前时间: 12.0点 下午状态还不错, 继续努力
当前时间: 13.0点 下午状态还不错, 继续努力
当前时间: 14.0点 下午状态还不错, 继续努力
当前时间: 17.0点 加班哦, 疲惫至极
当前时间: 19.0点 加班哦, 疲惫至极
当前时间: 22.0点 下班回家了

     “此是的代码, 如果要完成我所说的, ‘员工必须在20点之前离开公司’, 我们只需要怎么样?”
     “增加一个 ‘强制下班状态类’, 并改动一下, ‘傍晚工作状态了’ 类的判断就可以了. 而这是不影响其他状态的代码的. 这样做确实非常好.”
     “幺, 都半夜12点半了, 快点睡觉吧.” 大鸟提醒道.
     “…..”

~感谢捧场,您的支持将鼓励我继续创作~