[design-patterns]设计模式之一策略模式
设计模式
从今天开始开启设计模式专栏,我会系统的分析和总结每一个设计模式以及应用场景。那么首先,什么是设计模式呢,作为一个软件开发人员,程序人人都会写,但是写出一款逻辑清晰,扩展性强,可维护的程序就不是那么容易做到了。现实世界的问题复杂多样,如何将显示问题映射到我们编写的程序中本就是困难重重。另一方面,软件开发中一个不变的真理就是“一切都在变化之中”,这种变化可能来自于程序本身的复杂度,也可能来自于客户不断变化的需求,这就要求我们在编写程序中一定要考虑变化的因素,将变化的因素抽离出来,设计出一款低耦合的程序就成了程序设计中的难点。
设计模式就是用来解决以上的这些问题的一种方法。应用好设计模式,可以让我们免受程序设计中的变化的苦恼。从设计初至今,设计模式不是一成不变的,也并不是库,它比库更加高级,而且并不是用来解决某一种特定的问题,而是广泛的面向我们在程序开发中遇到的困境。在我们学习完一门语言的相关语法后,我相信设计模式应该作为紧接着学习的重点,它会让你对相关语法的认识更加深刻。比如说java中的接口就是一例,接口是让java流行的重要原因,但是初学完接口的使用,我们并不能很好的掌握接口,只有应用好设计模式,接口才能够大放异彩,我们也才能够真正的体会到接口给面向对象设计带来的魔力。实际上,在很多流行的开源库或者框架中,我们可以看到大量的使用设计模式的例子。
在网络中,有关于设计模式的讲解不在少数,在这里我并不会引出复杂的定义,而是以例子作为基础,道出我们在软件开发中面临的问题,然后引出设计模式,看看设计模式是如何帮助我们解决这些问题的。在之后的讲解中,我都会以java为主要语言,因为它的语法足够简单,使用其他语言作为主语言的开发者触类旁通即可。
最后,关于学习设计模式,希望大家能看一些经典的书籍,不要以其他网路上的博客(包括我的)做抓手学习。毕竟别人嚼过的不香,只有自己品味才能真正理解。书籍最经典的莫过于四人组(经常简称为GoF,即Group of Four)的《设计模式:可复用面向对象软件的基础》,初学可以看《Head First设计模式》,另有一本《大话设计模式》是国人写的评价也很高,同样适合初学者。
啰嗦了这么多,接下来开始步入正题。
设计模式之一策略模式
现在的你,在一家游戏开发公司供职。某一天,你的上司让你开发出一套简单的游戏,游戏中有多个角色,每个角色可以使用武器进行攻击。你打算先以角色入手,很快你设计出了以下的模型:
但是很快,你就发现这样设计并没有一开始想象的那么美好,因为你的上司让你给角色新增一个聊天功能,这时候问题出现了。
你很快想到因为所有角色都继承于Role,所以给Role添加一个聊天方法是理所应当的:
但是这个时候,你的同事告诉你不要这样做,因为他之前设计了一些怪物模型,同样继承于Role类,很明显,怪物也可以使用武器战斗,但是他们不能参与聊天。
你很郁闷,你的同事继承了你的类却没有告诉你,不过你告诉他不用大惊小怪,只要在怪物中重写聊天方法,让他们什么都不做就好了:
但是你的同事告诉你不要逗他,因为他写的怪物少说已经有了二十几个,你不能残忍的让你的同事挨个去重写chat方法。
当你们正在商讨解决方案时,你的上司告诉你,现在角色的武器太单一了,也就是说你不能把所有的战斗实现都放在父类中,我们需要更加多样化的武器。
你很苦恼,但是作为有经验的开发人员,你很快想到可以采用接口去实现,将所有的动作都抽象成一个个接口,让子类实现需要的接口,这样可以解决上司的需求和同事的问题:
但是,这一次是你自己将自己否定了,因为你的角色设计已经进行了很多,你不想将每个角色的方法都重新实现一遍,而且,这些武器很多都是重复的,作为受过良好的OO设计课程的开发人员,你不会强迫自己去写这些重复的代码。最重要的是,你的开发经验告诉你,这些武器很可能会发生改变,如果这样设计,当发生变化时,你就不得不去检查每一个角色类,并修改其中的代码。同样的,你的同事也同意你的看法。
设计原则
现在,你迫切的希望有一种设计模式能够解救你于水火之中。不过在揭晓这个设计模式之前,我们先回到原点,看一下我们遇到的问题到底是由什么原因造成的。
我们可以看到,继承不能很好的解决我们的问题,因为角色的行为是不断发生变化的,他们使用不同的武器和技能,并且你的上司很可能会不断地要求你添加新的行为。接口看起来不错,但是接口不具有实现代码,无法做到代码的复用,这意味着如果你想将某一种行为做统一的变化,就需要一个一个类去检查方法的实现。
要解决这个问题,首先我们需要明确一些设计原则,掌握了这些设计原则,我们才能更好的运用设计模式,解决问题。
面对以上的情况,我们有一个原则正好适用,那就是“封装变化”:
找出应用中可能需要变化的地方,把它们独立出来,不要和那些固定的代码混在一起。
那么对于当前的应用来讲,战斗以及聊天功能都是变化的功能,我们就应该将他们独立出来,分别设计战斗功能和聊天功能。接下来就是如何实现这两个功能了。这又会用到一个设计原则,那就是“接口编程”:
针对接口编程,而不是针对实现编程。
相信这个原则大家都非常清楚,我就不多讲了,接下来直接上代码,这里我只分析战斗功能,其他功能也是类似的:
设计实现
首先是接口设计:
1 public interface IFight { 2 void fight(); 3 }
这个接口很简单,只是提供了fight方法,接下来我们实现几个用各种武器战斗的类:
1 public class FightUseAxe implements IFight { 2 @Override 3 public void fight() { 4 System.out.println("使用斧子战斗"); 5 } 6 } 7 =============================================== 8 public class FightUseBlade implements IFight { 9 @Override 10 public void fight() { 11 System.out.println("使用剑战斗"); 12 } 13 } 14 =============================================== 15 public class FightUseKnife implements IFight { 16 @Override 17 public void fight() { 18 System.out.println("使用匕首战斗"); 19 } 20 }
注意上面的代码不能写在一个同一个文件中。
接下来我们对角色类进行重构:
1 public abstract class Role { 2 3 private IFight weapon; 4 5 public void fight() { 6 weapon.fight(); 7 } 8 9 public void setWeapon(IFight weapon) { 10 this.weapon = weapon; 11 } 12 13 public abstract void display(); 14 }
父类中我们实现了fight()方法,做到了统一管理,但是具体方法的实现实际上是交给子类去完成了,也就是IFight属性的赋值是在子类中完成的。接下来我们来看其中一个子类:
1 public class King extends Role { 2 @Override 3 public void display() { 4 System.out.println("显示国王的样子"); 5 } 6 7 public static void main(String[] args) { 8 Role role = new King(); 9 role.display(); 10 role.setWeapon(new FightUseAxe()); 11 role.fight(); 12 role.setWeapon(new FightUseBlade()); 13 role.fight(); 14 } 15 /** 16 * 运行结果: 17 * 显示国王的样子 18 * 使用斧子战斗 19 * 使用剑战斗 20 */ 21 }
我们可以看到,通过将战斗独立出来,我们实现了使用者(King)和武器的松耦合,即我们可以动态的改变角色使用的武器。同样的对于其他的角色也是一样,实际上,这是一种方法的委托,角色类不再实现战斗的具体方式,而是交给了成员属性IFight去执行。从另一方面来说,这是一种组合的概念,它和继承不同的地方在于,角色的战斗方式并不是继承而来的,而是和适当的战斗类来组合而成。在这个实例中,我们可以明显的看到组合的优势明显大于继承。
同样,这是一个很重要的设计原则,“多用组合,少用继承”。
其实,从更加抽象的角度来讲,例子中的战斗方式实际上是一种算法,通过策略模式,我们分离了使用算法的角色和算法之间的联系。因此我们可以给出策略模式的定义:
策略模式定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。
总结
今天我们学习了第一个设计模式即策略模式,当我们面对一些程序中可能会频繁变化的部分时可以采用此模式,它让算法的实现独立于使用算法的客户。同时我们还学习了三个面向对象设计中的重要原则:
- 封装变化
- 多用组合,少用继承
- 针对接口编程,而不针对实现编程
最后,为了巩固我们的学习成果,下面我们思考这样一个问题。在一片草原中,一群游牧民族养了一群羊和一群马,也就是说在这个草原中有三种生物,分别是人、马、羊、他们的动作有吃(eat)和睡(sleep),不过吃和睡的方式不同,马是站着吃站着睡,羊是站着吃趴着睡,人是坐着吃躺着睡。那么我们该怎样设计该OO模型,在实现功能的基础上,达到低耦合的要求,满足不断变化的需求呢?