设计模式之策略模式
前言
最近在读《Head First设计模式》一书,为加强记忆及便于日后查阅,在博客将自己的学习过程进行记录。谨以本篇策略模式作为设计模式系列的开篇章,并在后续过程中不断丰富自己的博客内容,欢迎各位进行浏览。
问题:现有一游戏SimDuck,游戏采用OO技术开发,其中会出现各种各样的鸭子,可以一边游泳一边呱呱叫。现在要对游戏内容进行丰富,使得鸭子能够飞起来。
一、解决思路
思路1:继承
在Duck父方法中加入fly()方法,简单快捷的实现鸭子的飞行
存在问题:
若加入塑料橡皮鸭,木头诱饵鸭,继承Duck类之后,会出现橡皮鸭、诱饵鸭飞行的闹剧,因而在父类中加入fly()方法的思路不可行。
思路2:改进继承
Flyable与Quackable虽然解决了一些问题,但却造成代码无法复用,若有上百个鸭子类,每个都要修改飞行类,这简直就是一个噩梦。因而,我们要考虑其他的实现思路.
思路3:策略模式实现
接下来我们就用策略模式的方式实现SimDuck的需求。
由上面分析可知,鸭子的quack()行为有“呱呱”、“吱吱”...... fly()行为有能自己飞的、不能飞的、依靠外力飞的...... 因而选择将这些不同的部分拿出来,建一组新类代表每个行为。
在设计这些行为类的过程中,我们想要鸭子的行为可以动态的改变,并得到指定行为的鸭子实例。因而我们选择使用接口的方式构建鸭子行为,类图如下所示:
这样的设计,可以让飞行与和呱呱叫的动作被其他对象复用,同时可以在新增行为的过程中,不会影响到既有的行为类,这样就有了继承的“复用”好处,却又没有继承带来的麻烦。
整合鸭子的行为
在Duck类中“加入两个实力变量”,分别为flyBehavior和quackBehavior,声明为接口类型,每个鸭子对象都会动态的设置变量以在运行时引用正确的行为类型。
Dack类代码如下所示:
public class Duck {
QuackBehavior quackBehavior;
FlyBehavior flyBehavior;
public abstract void display();
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack;
}
public void setFlyBehavior(FlyBehavior fb){
flyBehavior = fb;
}
public void setQuackBehavior(QuackBehavior qb){
quackBehavior = qb;
}
}
fly算法族的代码如下所示:
public interface flyBehavior{
public void fly();
}
public class FlyWithWings implents flyBehavior{
public void fly(){
System.out.println("I'm flying");
}
}
public class FlyNoWay implents flyBehavior{
public void fly(){
System.out.println("I can't fly");
}
}
quack算法族的代码如下所示:
public interface quackBehavior{
public void quack();
}
public class Quack implents quackBehavior{
public void quack(){
System.out.println("Quack");
}
}
public class MuteQuack implents quackBehavior{
public void quack(){
System.out.println("...silence...");
}
}
public class Squeak implents quackBehavior{
public void quack(){
System.out.println("Squeak");
}
}
最后我们编辑一下测试类:
public class MiniDuckSimulator{
public static void main(String[] args){
Duck mallard = new MallardDuck();
mallard.proformQuack();
mallard.profpormFly();
}
}
动态行为设定
现在加入模型鸭,其通过火箭实现飞行
public class ModelDuck extends Duck{
public ModelDuck(){
quackBehavior = new Quack;
flyBehavior = new FlyNoWay();
}
public void display(){
System.out.printin("I;m a model duck");
}
}
新建FlyBehavior类型FlyRocketPowered
public class FlyRocketPowered implements FlyBehavior{
public void fly(){
System.out.println("Im fly with rocket!");
}
}
修改测试类使得模型鸭具有火箭动力
public class MiniDuckSimulator{
public static void main(String[] args){
Duck mallard = new MallardDuck();
mallard.proformQuack();
mallard.profpormFly();
Duck model = new DuckModel();
model.proformFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
}
}
至此,我们已经完成了鸭子模拟器的设计,我们看一下整体的类结构,如下图所示:
策略模式定义了算法族,各个算法族分别封装起来,每个算法族内的类可以相互替换,使得算法的变化独立于算法使用者。
二、设计原则
在上面的过程中,我们发现前两种思路存在缺点,而走在我们前面的优秀程序员们就针对这些缺点总结了相应的软件设计原则:
设计原则一:分开变化与不变化
即,找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。这个原则几乎是每个设计模式背后的精神所在,所有的模式都提供了一套方法让系统中的某部分改变不会影响其它部分。将会变化的部分封装起来,以后可以轻易的针对此部分进行改动或扩充,不影响其他部分。在SimDuck设计过程中我们将变化的fly与quack独立出来,使得整个系统变得更有弹性。
设计原则之二:针对接口编程而不是针对实现编程
在SimDuck原有的设计中,行为来自Duck父类的具体实现,或由子类继承接口后自行实现。这两种做法都是依赖于实现编程。在策略模式实现中我们将行为写在实现FlyBehavior和QuackBehavior的类中。
针对接口编程的关键在于多态,利用多态,程序员可以实现针对超类型编程,执行时根据实际状况执行真正的行为。“针对超类型编程”可以更加准确的说为变量的声明类型应为超类型,通常为一个抽象类或接口,故只要具体实现次超类型的类所产生的对象,都可以指定给这个变量。即声明类时可以不用理会以后执行的真正的对象类型。
设计原则之三:多用组合,少用继承
使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以在运行时动态地改变行为,只要组合的对象符合正确的额接口标准即可。
三、策略模式总结
一个系统有许多类,区分它们的只是他们直接的行为,在这种情况下,定义算法族,把它们封装成实现同一个接口的类, 使它们可相互替换。
使用场景
1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。
3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
策略模式的优缺点
策略模式的优点:
- 算法可以自由的切换,通过实现抽象策略,通过封装角色对其封装,保证对外提供“可自由切换”的策略。
- 避免使用多重条件判断,如果有多重策略,那么每个策略只需实现自己的方法,至于采用何种策略,可以通过其他模块决定。
- 扩展性良好,可以在现有的系统中任意的加入新的策略,只需继承IStrategy接口,符合OCP原则。
策略模式缺点:
- 策略类数量增多,每个策略都是一个类,复用的可能性很小,类数量增多
- 所有的策略都需要对外暴露,上层模块必须知道有哪些策略,然后才能知道采用哪种策略,可以通过使用工厂方法模式、代理模式和享元模式修正。
注意:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
本文主要参考自《Head First设计模式》一书
另参考文章:
https://www.cnblogs.com/wolf-sun/p/3534573.html
http://www.runoob.com/design-pattern/strategy-pattern.html
https://www.cnblogs.com/zhanglei93/p/6081019.html