"Head First"设计模式读书笔记——策略模式
问题描述:
目前的任务是实现一个FPS类游戏的各种角色(友军、敌军、平民和狗、猫、鸭子等动物)以及他们的各种行为(攻击、游泳等)。
设计方案一
很简单,只要实现一个角色超类,将角色的各种行为放入超类中,其他具体角色都继承次角色超类就可以了。类图如下所示(由于不同的角色有不同的外观,所以display设定为抽象方法,由子类自己实现)。
现在想想,这个设计有什么潜在的问题?对了,游戏开发完成后,你在享受游戏的乐趣的同时可能会发现竟然有一只猫在游泳,还有一只鸭子在向友军攻击...。这些是有悖常理,绝对不允许出现的。也许会有人想到一种解决方案,就是在子类中覆盖父类中相应的方法。但是这样一来,如果系统需要加入新的角色,程序员必须跟踪并可能覆盖fight和swim方法,永无止境的噩梦。必须更改设计方案。
设计方案二
针对上一个方案出现的问题,首先将会变化的行为(fight和swim)抽出来,各自放入单独的接口fightAction和swimAction中,只有具有此类行为的角色才会实现相应的接口。设计图如下:
稍稍动动脑筋就会发现,这种设计方案其实超傻。如果有成千上万种角色,就意味着程序员要重复编写成千上万次同样的代码。完全消除了代码复用的优势,几乎可以荣登史上最傻方案榜了。毫不犹豫,淘汰,必须更改方案。
设计方案三
在上一篇文章”面向对象设计原则”中的第一个原则是“封装变化”,这个原则可以理解为:“找出应用程序中需要变化的部分,把它们独立出来,不要和那些不需要变化的代码混在一起,以便以后可以轻易的改动或扩充此部分而不至影响到其他部分。”基于此,把角色的行为从角色中抽离出来。
为了实现“封装变化”,首先建立两组类,其中一个关乎fight行为的,另一个是关乎swim行为的,各自实现各自的行为,并将该两组类完全脱离角色类。
以上一切工作都是为了使得程序更具有弹性,易于扩展。所以,当然不能让子类来实现设定行为的方法,必须放在父类中。
”面向对象设计原则”中的第三个原则是“针对接口编程,而不是针对实现编程”,注意,此处讲到的接口并不是特指某个C#或java的接口,而是泛指实现某个超类性(类或接口)的某个方法。在此设计方案中利用接口代表行为,fight行为用FightAction接口代表,swim行为用SwimAction接口代表,这样角色本身不会负责实现某个行为,而是由专门的“行为类”(实现行为接口的类)来负责。设计方案如下图所示:
父类Character中用performFight和performSwim方法代替了方案一和方案二中的fight和swim方法。在performFight方法和performSwim方法中调用fightAction和swimAction中的fight和swim方法实现角色的行为。而每一个角色子类的fightAction和swimAction成员是在角色本身实例化的时候指定的。利用此方案实现的系统,决不会出现具有攻击行为的鸭子和会游泳的猫,同时也保证了系统的可扩展性,有新的行为加入的时候(如fightWithKnife)只需要添加一个新的行为类即可以实现。程序员甚至可以在父类中添加一个setFightAction(FightAction fightAction)或setSwimAction(SwimAction swimAction)方法来实现每一个角色在运行的时候动态的改变行为(如用枪攻击改为用刀攻击)。
总结
以上陈述的“设计方案三”利用的既是策略模式,有心的你也许会发现,该方案同样遵循了”面向对象设计原则”中的第二个原则是“多用组合,少用继承”。每一个角色都具有一个fightAction和swimAction用于委托处理fight行为和swim行为,这就是组合。“组合”不同于“继承”之处在于角色的行为不是继承来的,而是由行为对象组合而来。
引用策略模式的官方定义:“策略模式定义了算法族,分别封装起来,让它们直之间可以互相替换,此模式让算法的变化独立于使用算法的客户”。不难发现,该模式主要适用于不同客户(角色)使用不同策略(行为),并且策略本身将来可能通过不同方式实现,需要对客户隐藏具体策略的实现细节,完全独立的情况。策略模式提供了一种替代继承的方法,既保持了继承的优点(代码复用),同时也比继承更加灵活(算法独立,可任意扩展)。该模式还避免了多重条件语句的出现,使系统更易于扩展和维护。