设计模式之一——从魔兽争霸的兵种和技能看策略模式
很多人都喜欢玩魔兽争霸,里面的兵种很多,比如有Footman步兵、Knight骑士、Grunt兽人步兵,他们使用不同的武器,Footman步兵使用宝剑攻击、knight也使用sword宝剑攻击,grunt使用axe斧头攻击。我们如何来设计这几个兵种角色呢?
1 初步设计
首先我们想可以创建一个character角色类,他们都能够walk走路,但有不同的攻击行为,可以将character设计成抽象类,利用继承来得到具体的兵种。设计出来的类应该如图所示。
这种设计通过继承实现各兵种,footman、knight、grunt可以实现攻击行为。我们现在想增加兵种kedo科多兽,kedo使用mouth嘴吞噬敌人,还可以用drum战鼓增加我军的攻击力,是个辅助兵种,character中没有kedo对应的攻击和辅助方法,kedo不能从character继承得到。如果想让kedo继承character,必须修改character的类结构,增加用嘴攻击的方法和用鼓辅助的方法。
这里就能看到这个设计存在的问题,第一个是可扩展性不好,不能对适应变化的行为。第二个问题是footman和knight都用宝剑攻击,swordAttack方法在子类需要重复写代码,没有将代码复用。
这里我们引入软件设计的一个原则,就是区分不变的和变化的,将变化的封装起来。具体到character类来说,就是walk方法是不变的,不用动。attack和assist行为是变化的,我们给封装起来。
2 第一次改进
将attack和assist行为封装起来,我们考虑使用接口,设计两个接口Attackable和Assistable,让footman、knight、grunt、kedo分别实现相应的接口。设计出来的类如下图所示。
我们看这个设计方法,第一个可扩展的问题,如果还有其他的行为,我们可以再加入接口,这样可扩展的问题就解决了。第二个问题,代码重复的问题,由于footman和knight的swordAttack实现Attackable接口的attack方法,还是存在代码重复的问题。
解决代码重复的问题,我们考虑将attack和assist两种行为从character中分离出来。
3 第二次改进
3.1 分离出行为
我们设计两个行为的接口,AttackBehavior和AssistBehavior,子类来实现对应的attack和assist行为。接口和实现类的设计如图所示。
此处使用了软件设计的一个原则,针对接口编程,不要针对实现编程。针对接口编程,可以使用面向对象的多态特性,比如一个兵种有AttackBehavior的attack行为,这个attack行为在实现的时候可以是SwordAttack,也可以AxeAttack或者MouthAttack,增加灵活性。
3.2 整合兵种的行为
在Character类中增加两个实例变量,分别是attackBehavior和assistBehavior,声明为接口类型。Character设计如图所示。
3.3 代码实现
以下是Character类的代码实现
public class Character {
AttackBehavior attackBehavior;
AssistBehavior assistBehavior;
public void walk(){
System.out.println("I
can walk!");
}
public void performAttack(){
attackBehavior.attack();
}
public void performAssist(){
assistBehavior.assist();
}
}
以下是Kedo科多兽的主要代码
public class Kedo extends Character {
public Kedo(){
attackBehavior = new MouthAttack();
assistBehavior = new DrumAssist();
}
}
我们在这里将兵种的攻击或者辅助行为单独设计为类,与兵种分离开来,这种做法让兵种和行为都是独立的个体,他们的结合方式变成了组合,而不是一开始的继承。这里用到了软件设计中一个重要的原则,多用组合,少用继承。使用组合建立的系统具有很大的弹性。
我们的代码Kedo科多兽类,有用嘴吞噬和用战鼓辅助的行为,代码把他的行为固定了,如果可以不把他的行为固定,动态的设定他的行为,则可以增加代码的灵活性。我们可以进一步改进代码。
3.4动态设定行为
在Character类中,增加两个方法:
public void setAttackBehavior(AttackBehavior atb){
attackBehavior = atb;
}
public void setAssistBehavior(AssistBehavior asb){
assistBehavior = asb;
}
这样我们如果实现新的兵种,比如 Raider狼骑士 ,他是用大刀(暂时用宝剑代替)砍,我们可以不创建raider这个兵种类,直接实现raider具有宝剑攻击的行为。代码如下。
Character raider = new Character();
raider.setAttackBehavior(new SwordAttack());
raider.performAttack();
4 策略模式
现在我们回过头来再看看整个思路,兵种具有不同的攻击或辅助行为,我们把行为分离出来进行定义,这样就定义了一组行为(SwordAttack、AxeAttack、MouthAttack、DrumAssist),并把这些行为进行了封装,都封装到了类中,这些行为在地位上平等的,可以互相替代,行为独立于兵种之外,这就是策略模式的设计思路。
我们把行为扩展到更广义的算法,一组行为我们扩展为算法簇。再把上面的设计思路说一次,就是策略模式,定义了算法簇,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。