今天要和大家一起分享下在《Head First 设计模式》学习到的内容,很实用的两个模式:策略模式,状态模式。
为什么要说这两个模式呢。在模式图中,是一样的,他俩就像孪生兄弟,但是目的却不同。
状态模式:
书中说明了列举了下面情况。
糖果公司要求实现糖果机功能。状态图如下。
一般情况会用实例变量来持有目前状态,然后定义每个状态值。
final static int SOLD_OUT = 0;// 售罄
final static int NO_QUARTER = 1;// 没有25分钱
final static int HAS_QUARTER = 2;// 有25分钱
final static int SOLD = 3;// 出售糖果
int state = SOLD_OUT;// 初始状态为售罄
还有能对糖果机做的操作,如:投入25分钱,退回25分钱,转动曲柄,发放糖果
针对每个动作都会做出一个函数,这些函数利用条件语句来决定每个状态内什么行为是什么。
如“投入25分钱”
public void insertQuarter() {
if (state == HAS_QUARTER) {
System.out.println("You can't insert another quarter");
} else if (state == NO_QUARTER) {
state = HAS_QUARTER;// 转换到另一个状态
System.out.println("You inserted a quarter");
} else if (state == SOLD_OUT) {
System.out.println("Yon can't insert a quarter, the machine is sold out");
} else if (state == SOLD) {
System.out.println("Please wait, we're already giving you a gumball");
}
}
每个可能的状态都需要条件语句检查,然后对每一个可能的状态展现适当的行为,但是也可以转换到另一个状态像状态图中所描绘的那样。
例如:当状态是NO_QUARTER的时候表示糖果机的初始状态,等待投入25分钱硬币。当调用insertQuarter方法的时候,状态就会由NO_QUARTER转换到HAS_QUARTER.
在代码中的操作如下
else if (state == NO_QUARTER) {
state = HAS_QUARTER;// 转换到另一个状态
System.out.println("You inserted a quarter");
}
并且针对其他的无效操作,做出相应的处理。一个函数中,每个状态都会做出if判断,来对每种状态做出处理。
整体代码如下:
public class GumballMachine { final static int SOLD_OUT = 0; final static int NO_QUARTER = 1; final static int HAS_QUARTER = 2; final static int SOLD = 3; int state = SOLD_OUT; int count = 0; public GumballMachine(int count) { this.count = count; if (count > 0){ state = NO_QUARTER; } } public void insertQuarter() { if (state == HAS_QUARTER) { System.out.println("You can't insert another quarter"); } else if (state == NO_QUARTER) { state = HAS_QUARTER; System.out.println("You inserted a quarter"); } else if (state == SOLD_OUT) { System.out.println("Yon can't insert a quarter, the machine is sold out"); } else if (state == SOLD) { System.out.println("Please wait, we're already giving you a gumball"); } } public void turnCrank() { if (state == SOLD) { System.out.println("Turning twice doesn't get you another gumball"); } else if (state == NO_QUARTER) { System.out.println("You turned but there's no quarter"); } else if (state == SOLD_OUT) { System.out.println("You turned, but there's no gumballs"); } else if (state == HAS_QUARTER) { System.out.println("You turned..."); state = SOLD; dispense(); } } //省略其他方法 }
当需求变动时,糖果机需要10%的概率可以发放两粒糖果。
这可有的忙了。
先要在下面加上赢家状态
final static int SOLD_OUT = 0;// 售罄 final static int NO_QUARTER = 1;// 没有25分钱 final static int HAS_QUARTER = 2;// 有25分钱 final static int SOLD = 3;// 出售糖果
//添加一个赢家状态 WINNER int state = SOLD_OUT;// 初始状态为售罄
public void turnCrank() 方法有的忙了,你必须检测顾客是否是赢家,还要决定切换到赢家状态还是售出糖果状态。public void insertQuarter() 等方法要加入一个新的条件判断来处理“赢家”状态。0.0 要是有100个方法怎么办。一个个老实加吧。
没有需求不变的项目,尤其是奇葩客户以及大公司的设计,式样。0.0
只能让我们代码变得有弹性,维护性更高,才能让我们更高效的利用我们程序员宝贵的时间。否则费大劲改来改去,bug还一堆。以后很多代码都不敢动,重复的代码越来越多。很多都重写一套。噩梦啊。
此时就要遵守一个设计原则,“封装变化”。要让变化影响的范围越小越好。
针对上面的实现代码,不要维护现有的,重写它以便于将状态对象封装在各自的类中,然后在动作发生时委托给当前状态。
我们要做的是:
1.首先,我们先定义一个接口State。在这个接口中糖果机的每个动作都有一个对应的方法。
2.然后为机器中的每个状态实现状态类。这些类负责在对应的状态下进行机器的行为。
3.最后,我们要摆脱旧的条件代码,取而代之的方式是,将动作委托到状态类。
我们把一个状态的所有行为放在一个类中。这样以来我们将行为局部化了,并使得事情容易改变和理解。
定义状态接口
此时糖果机变成了
public class GumballMachine2 { State soldOutState; State noQuarterState; State hasQuarterState; State soldState; State state = soldOutState; int count = 0; public int getCount() { return count; } public GumballMachine2(int numberGumballs){ soldOutState = new SoldOutState(this); noQuarterState = new NoQuarterState(this); hasQuarterState = new HasQuarterState(this); soldState = new SoldState(this); this.count = numberGumballs; if(numberGumballs > 0){ state = noQuarterState; } } public void insertQuarter(){ state.insertQuarter(); } public void ejectQuarter(){ state.ejectQuarter(); } public void turnCrank(){ state.turnCrank(); state.dispense(); } void setState(State state){ this.state = state; } void releaseBall(){ System.out.println("A gumball comes rolling out the slot..."); if(count != 0){ count = count - 1; } } public State getNoQuarterState(){ return noQuarterState; } public State getHasQuarterState() { return hasQuarterState; } public State getSoldState() { return soldState; } public State getSoldOutState() { return soldOutState; } }具体的行为,只用类中的state变量处理就可以。初始状态是noQuarterState。public class NoQuarterState implements State{ GumballMachine2 gumballMachine; public NoQuarterState(GumballMachine2 gumballMachine){ this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("Yon inserted a quarter"); gumballMachine.setState(gumballMachine.getHasQuarterState()); } @Override public void ejectQuarter() { System.out.println("You haven't inserted a quarter"); } @Override public void turnCrank() { System.out.println("You turned, but there is no quarter"); } @Override public void dispense() { System.out.println("You need to pay first"); } }
调用insertQuarter函数后,把糖果机gumballMachine的状态变为 hasQuarter。
同一个行为不同状态会做出不同的处理。并且行为也会改变状态。
把当初的if判断拆分成了类结构,具体的操作在类中修改。以封装变化的部分为宗旨。
我们做到了什么:
* 将每个状态的行为局部化到它自己的类中。
* 将容易产生问题的if语句删除,以方便日后的维护。
* 让每个状态“对修改关闭”,让糖果机“对扩展开放”,因为可以加入新的状态类
* 创建一个新的代码基和类结构,这更能映射糖果公司的图,而且更容易阅读和理解。
我们来看看状态模式的定义:
状态模式 允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
乍一看这个定义是什么啊。简直崩溃!!我来解释下。
第一句“允许对象在内部状态改变时改变它的行为”,此处的对象就是糖果机GumballMachine,状态就是它持有的当前状态的引用 State state。因为这个模式将状态封装成为独立的类,
并将动作委托到代表当前状态的对象,我们知道行为会随着内部状态而改变。以上例子中:当糖果机在No_QUARTER和HAS_QUARTER两种不同的状态时,你投入25分钱,
就会得到不同的行为(机器接受25分钱和机器拒绝收钱)
第二句“对象看起来好像修改了它的类”,从客户的角度来看,如果说你使用的对象能够完全改变它的行为,那么你会觉得,这个实际上是从别的类实例化而来的。然而实际上,
你知道我们是在使用组合通过简单的引用不同的状态对象来造成类改变的假象。
状态模式就介绍到这里,具体可以参考《Head First设计模式》,还有其他文章。学习模式我觉得比较好的方式就是对比。下面我们来介绍下和它差不多的策略模式。
对比之后就会加深我们对模式的理解。对于应用场景的问题,很多同学想问,这个模式能用在哪里。这个还要看具体的业务,我无法给除准确的回答。但是,学习
设计模式到一定程度之后,已经心中有很多设计原则,即使不用设计模式,也会写出扩展性很高的代码。其实各个模式的中心思想都是一样的,“封装变化”,“多用组合少用继承”,
“类应该只有一个改变的理由”等设计原则。具体设计原则可以去搜索下,放在脑子里,才会运用自如。而不是就看到糖果机,才知道用状态模式。0.0
策略模式:
场景是鸭子应用。类图如下:
这是我们经常处理问题的方式。找到类型的共同点,抽出个抽象模型。Duck。可以保证代码的复用。
一切看起来是那么好,但是需求变更的时候,噩梦就来了。
假如客户要求鸭子有飞的行为。我们首先想到的是在父类上加fly方法就可以搞定。
如果子类有橡皮鸭,根本不会飞的鸭子。拥有这个方法就没有意义了。有的人解决方案是,fly里面什么也不处理就可以啦。但是有另一个子类,不会quack,也不会fly呢。
那这两个方法都要什么都不做。如果还要在父类上加上N个行为的方法呢。所有的子类都要改变一下。有的做出具体的操作,有的把方法做出空实现。
这样看啦,继承不是我们想要的解决方案。
聪明的程序员想到了,把fly从超类中抽出来。放到一个flyable接口中。只有会飞的鸭子实现此接口。
看起来不错的设计,但是还是有问题。如果有48个子类,每个子类的飞行行为有相同的有不同的,那么每个鸭子子类都要实现飞行行为,重复代码增多。
综上我们再回顾下问题:
并非所有的子类都有飞行和呱呱叫行为,所以继承并不适合我们。虽然flyable和quackable可以解决部分问题,但是代码无法复用。
此时来了一个设计原则帮助我们。找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
如果每次新的需求一来,都会使某方面的代码发生变化,你可以确定这部分代码需要被抽出来,和其他稳定的代码有所区分。
这个原则可以换成这种说法,把会变化的部分取出并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分。
这几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让“系统中某部分改变不会影响其他部分”。
该是把鸭子的行为从Duck中取的时候了!
鸭子的行为将被放在分开的类中,此类专门提供某行为接口的实现。这样,鸭子就不需要知道行为的实现细节。
我们利用接口代表每个行为,比方说,FlyBehavior和QuackBehavior,而行为的每个实现都将实现其中一个接口。所以鸭子类不会负责实现FlyBehavior和QuackBehavior,
反而是由我们制造一组其他类专门实现FlyBehavior和QuackBehavior,这就成为“行为”类。由行为类而不是Duck类来实现行为接口。
以前的做法是:行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来。这两种做法都是依赖于“实现”,我们被实现绑得死死的,没办法改行为。
这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了。
而我们可以新增一些行为,不会影响到既有的行为类,也不会影响“使用”到飞行行为的鸭子类。这样一来有了继承的“复用”好处,却没有了继承所带来的包袱。
鸭子会将飞行和呱呱叫动作“委托”别人处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。
在Duck类中加入两个实例变量,分别为flyBehavior和quackBehavior,声明为接口类型,每个鸭子对象都会动态的设置这些变量以在运行时引用正确的行为类型。
用方法performFly和performQuack取代Duck类中的fly和quack。
上代码
public abstract class Duck { FlyBehavior flyBehavior; QuackBehavior quackBehavior; public Duck() { } public abstract void display(); public void performFly(){ flyBehavior.fly(); } public void performQuack(){ quackBehavior.quack(); } public void setFlyBehavior(FlyBehavior flyBehavior){ this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior){ this.quackBehavior = quackBehavior; } public void swim(){ System.out.println("All ducks can swim, even decoys!"); } }
public interface FlyBehavior { public void fly(); }public class FlyWithWings implements FlyBehavior{ @Override public void fly() { System.out.println("I'm flying!"); } }public class FlyNoWay implements FlyBehavior{ @Override public void fly() { System.out.println("I can not fly!"); } }public interface QuackBehavior { public void quack(); }public class Quack implements QuackBehavior{ @Override public void quack() { System.out.println("Quack!"); } }public class Squeak implements QuackBehavior{ @Override public void quack() { System.out.println("Squeak"); } }其中Duck中的public void setFlyBehavior(FlyBehavior flyBehavior){ this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior){ this.quackBehavior = quackBehavior; }方法是用来动态设置行为的此时我们又多了一个设计原则,多用在组合,少用继承。Duck中的flyBehavior和quackBehavior变量就是组合形式。
现在大家来看看策略模式的定义
策略模式 定义算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
看看和状态模式的类图是不是很像。但是这两个模式的意图不同。
总结:
状态模式在开始的时候就会设置状态值,从什么状态开始。然后随着时间改变自己的状态。虽然策略模式也可以通过声明set行为的方法来替换行为,但是状态模式的状态改变
是都定义好的,改变行为是建立在状态模式的方案中。也就是说,状态的转换已经在状态模式的方案中定义好了。
而策略模式会控制对象使用什么策略。
今天我们学到的原则
OO原则:
封装变化
多用组合,少用继承