最近在读一本《Head First 设计模式》书,书中用生动形象的例子来介绍设计模式,个人认为还是很不错的书,接下来会利用书中的例子介绍一些常用的设计模式,本文主要是设计模式入门+策略模式介绍。
一.模拟鸭子应用
1.背景
公司开发了一款很火爆的模拟鸭子游戏(具体怎么火起来的就先不关心啦),游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫,开始时我们的设计时这样的:
我们设计了一个抽象类Duck,包含quick方法(呱呱叫),swim方法(游泳)和display方法,一些鸭子的具体实现类,如MallardDuck,RedheadDuck等鸭子类,重写display方法,这样就满足当前系统的需要了。
2.变动
由于鸭子游戏过于火爆,很多公司开始抄袭我们的创意,竞争压力过大,因此主管们认为我们需要创新,决定让我们的鸭子可以飞。so,苦逼的我们需要改代码了,我们会怎么做呢?我们初步打算在抽象类Duck中加入fly方法试试吧。我们把Duck中加入了fly方法,上线第二天后,一个可怕的现象发生了,很多游戏中的橡皮鸭子在地图上满天飞(橡皮鸭不应该会飞),这是怎么回事呢?我们忽略了一个事实,并非Duck所有的子类都会飞,我们在Duck类中加上新的行为,会使得某些并不适合该行为的子类也具有该行为。对代码所做的局部修改,影响层面可不只是局部。为了解决橡皮鸭乱飞的问题,我们会想到覆写橡皮鸭的fly方法,可是如果以后我加入诱饵鸭,或者其他木头鸭子呢?情况变的有些复杂起来了。
3.一个解决方案
这时我们会想,fly放到Duck里貌似不是一个好的解决办法,我们可以把fly从超类中取出来,放进一个"Flyable接口"中,这样一来,只有会飞的鸭子才实现这个接口,同样的我们也可以设计一个"Quackable接口",因为并不是所有的鸭子都会叫。这种方式看起来可以解决上面的问题了。不过这种方式有什么问题呢?使用接口的实现没办法进行代码复用,这样会导致重复的代码变的非常多,考虑我们有大量的鸭子实现类,修改起来会非常麻烦。
4.把问题归零......
现在我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里不断地改变,并且让所有的子类都有这些行为是不恰当的。Flyable与Quackable接口一开始似乎还不错,但是Java接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着,无论何时需要修改某个行为,必须得往下追踪并在每一个定义此行为的类中修改它,一不小心就会造成新的错误。
有一个设计原则恰好适用此状况。
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他稳定的代码有所区分。
5.重新设计鸭子游戏
了解了上面的设计原则,我们知道把"变化和不会变化的部分"分开,我们准备简历两组类,一个是fly相关的,一个是quack相关的,因为Duck类内的fly和quack会随着鸭子的不同而改变。重新设计鸭子游戏之前,我们再来了解一个设计原则:针对接口编程,而不是针对实现编程。这里我们定义两个接口,FlyBehavior和QuackBehavior接口,再定义一些具体的实现类,如FlyWithWings,FlyNoWay,Squeak等实现类。重新设计之后,我们的鸭子包含两个属性,就是我们刚刚定义的两个接口,FlyBehavior和QuackBehavior。增加两个方法,performQuack和performFly方法来实现呱呱叫和飞行动作,方法的实现则为调用两个属性的具体实现。下面来看看具体的代码实现:
public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
@Override
public void fly() {
System. out.println("fly with wings" );
}
}
public class FlyNoWay implements FlyBehavior{
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("呱呱叫" );
}
}
public class MuteQuack implements QuackBehavior {
@Override
public void quack() {
System. out.println("i can not quack" );
}
}
上面定义了两组接口,和两个具体的实现类,接下来看看我们的Duck类
public abstract class Duck {
private FlyBehavior flyBehavior;
private QuackBehavior quackBehavior;
public void setFlyBehavior(FlyBehavior flyBehavior ) {
this.flyBehavior = flyBehavior ;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior ;
}
public void swim(){
System. out.println("i am swimming" );
}
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
public abstract void display();
}
再定义一个Duck的实现类
public class MallardDuck extends Duck {
public void display(){
System. out.println("i am a real mallard duck" );
}
}
接下来测试这段代码
public class MiniDuckTest {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.setFlyBehavior( new FlyWithWings());
mallard.setQuackBehavior( new Quack());
mallard.performFly();
mallard.performQuack();
}
}
二.策略模式
上面代码就使用了设计模式的策略模式,我们来看看策略模式的定义:
策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
策略模式的优缺点:
优点
1、可以动态的改变对象的行为
缺点
1、客户端必须知道所有的策略类,并自行决定使用哪一个策略类
2、策略模式将造成产生很多策略类
适用场景:
策略模式适用于经常变化的需求,如《大话设计模式 》中的示例:超市打折促销,超市经常有各种打折策略,经常变化,这种场景就很适合策略模式。再比如之前在介绍Lambda表达式中的例子"筛选苹果"需求,有时我们需要根据颜色筛选,有时我们需要根据重量筛选,再加上上面的鸭子游戏我们可以看到策略模式的功能是十分强大的。它能很好的将变化和不变的分离开,工作中如果有经常修改的部分我们可以考虑使用策略模式实现。
至少在在以下两种情况下,大家可以考虑使用策略模式:
- 几个类的主要逻辑相同,只在部分逻辑的算法和行为上稍有区别的情况。
- 有几种相似的行为,或者说算法,客户端需要动态地决定使用哪一种,那么可以使用策略模式,将这些算法封装起来供客户端调用。