本文通过简单的模拟鸭子应用做起,在模拟鸭子游戏中,会出现各种鸭子,鸭子可以游泳、可以呱呱叫。
看看模拟鸭子程序的初期类图
现在客户想让鸭子可以飞行,于是同意了这个需求,类图变成了下面这样
这是,可怕的问题发生了,有许多“橡皮鸭子”可以在游戏界面飞来飞去,这是在设计上的疏忽,因为在超类上加上 fly(),就会导致所有的子类都具备 fly(),连那些不该具备 fly()的子类也无法免除。
利用接口如何?
我可以把 fly()从超类中取出来,放进一个“Flyable 接口”中。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用来设计一个“Quackable 接口”,因为不是所有的鸭子都会叫。
我们知道,并非“所有”的子类都具有飞行和呱呱叫的行为,所以继承并不是适当的解决方式。虽然 Flyable 与 Quackable 可以解决“一部分”问题(不会再有会飞的橡皮鸭),但是却造成代码无法复用,这只能算是从一个恶梦跳进另一个恶梦。
这个时候我们想到一个设计原则
找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
分开变化和不会变化的部分
我们知道 Duck 类内的 fly()和 quack()会随着鸭子的不同而改变。
下面开始设计鸭子的行为
整合鸭子的行为
首先,在 Duck 类中“加入两个实例变量”,分别为“flyBehavior”与“quackBehavior”,声明为接口类型(而不是具体类实现类型),每个鸭子对象都会动态地设置这些变量以在运行时引用正确的行为类型。
编写 Duck 类
public abstract class Duck {
// 为行为接口类型声明两个引用变量,所有鸭子类都继承它们
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck() {}
abstract void display();
public void performFly() {
// 委托给行为类
flyBehavior.fly();}
public void performQuack() {
// 委托给行为类
quackBehavior.quack();}
public void swim() {System.out.println("All ducks float, even decoys!");
}
}
编写 MallardDuck 类
public class MallardDuck extends Duck {public MallardDuck() {quackBehavior = new Quack();
flyBehavior = new FlyWithWings();}
public void display() {System.out.println("I'm a real Mallard duck");
}
}
编写 ModelDuck 类
public class ModelDuck extends Duck {public ModelDuck() {flyBehavior = new FlyNoWay();
quackBehavior = new Quack();}
public void display() {System.out.println("I'm a model duck");
}
}
编写 FlyBehavior 接口与两个行为实现类
FlyBehavior 接口
public interface FlyBehavior {public void fly();
}
FlyNoWay
public class FlyNoWay implements FlyBehavior {public void fly() {System.out.println("I can't fly");
}
}
FlyWithWings
public class FlyWithWings implements FlyBehavior {public void fly() {System.out.println("I'm flying!!");
}
}
编写 QuackBehavior 接口与三个实现类
public interface QuackBehavior {public void quack();
}
Quack 实现类
public class Quack implements QuackBehavior {public void quack() {System.out.println("Quack");
}
}
MuteQuack
public class MuteQuack implements QuackBehavior {public void quack() {System.out.println("<< Silence >>");
}
}
Squack
public class Squeak implements QuackBehavior {public void quack() {System.out.println("Squeak");
}
}
编写测试类
public class MiniDuckSimulator {public static void main(String[] args) {MallardDuck mallard = new MallardDuck();
mallard.display();
mallard.performQuack();
mallard.performFly();
Duck model = new ModelDuck();
model.display();
model.performQuack();
model.performFly();}
}
运行结果
策略模式的定义
Define a family of algorithms,encapsulate each one,and make them interchangeable.(定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。)
通用类图
策略模式使用的就是面向对象的继承和多态机制,非常容易理解和掌握,我们再来看看策略模式的三个角色:
-
Context 封装角色
它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。 -
Strategy 抽象策略角色
策略、算法家族的抽象,通常为接口,定义每个策略或算法必须具有的方法和属性。各位看官可能要问了,类图中的 AlgorithmInterface 是什么意思,嘿嘿,algorithm 是“运算法则”的意思,结合起来意思就明白了吧。 -
ConcreteStrategy 具体策略角色
实现抽象策略中的操作,该类含有具体的算法
策略模式的应用
策略模式的优点
- 算法可以自由切换
-
避免使用多重条件判断
如果没有策略模式,我们想想看会是什么样子?一个策略家族有 5 个策略算法,一会要使用 A 策略,一会要使用 B 策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
-
扩展性良好
在现有的系统中增加一个策略太容易了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大地符合了 OCP 原则。
策略模式的缺点
-
策略类数量增多
每一个策略都是一个类,复用的可能性很小,类数量增多。
- 所有的策略类都需要对外暴露
策略模式的使用场景
- 多个类只有在算法或行为上稍有不同的场景
-
算法需要自由切换的场景
例如,算法的选择是由使用者决定的,或者算法始终在进化,特别是一些站在技术前沿的行业,连业务专家都无法给你保证这样的系统规则能够存在多长时间,在这种情况下策略模式是你最好的助手。 - 需要屏蔽算法规则的场景
参考书籍:《Head First 设计模式》《设计模式之禅》