小新大学毕业后进入一家游戏公司,最近公司准备研发一款叫皇室战争的卡牌对战游戏。玩家可以通过在战场上放置骷髅兵,王子,巨人,骑士,气球兵等各种各样的角色。这些角色相互拼杀最终摧毁敌方的国王塔赢得胜利。小新被分在了人物模型设计组跟着一位叫吉勇的资深程序员共同参与这项工作。小新从小就很喜欢玩游戏,拿到了任务后,非常积极,刷刷刷的就在黑板上画出了类图准备和他的师傅吉勇讨论。
吉勇老师请看图:所有的人物拥有不同的外表 (display)和攻击能力(attack)。角色的外表都各不相同,那么display()在Character中是一个抽象方法。Character的子类实现他们各自的display方法让不同的角色拥有不同的外表。而对于attack方法我们在Character中做一个默认实现。这样所有的角色就都能拥有攻击的能力。如果攻击的方式不同,那么我们只需要在子类中重写(Override)attack方法就好。
吉勇老师:看起来很有道理的样子,其实并不是这样。年轻人积极是好事,但是设计不能太过急躁,得反复斟酌。每一个角色都拥有攻击的能力这并不是一个好的设计。如果我们加入天使(Angel)这样的辅助类角色,她只能给友军回复生命,并不能攻击别人,你的设计就不灵了,一款好的游戏怎么能没有奶妈?
小新:看来是个问题,那我换一种方式。把攻击的能力定义为一个接口,如果想拥有攻击能力的只需要实现Attackable(攻击)接口。至于治疗也是一样,谁想拥有治疗技能,那么只需要实现Cureable(治疗)接口。
吉勇老师:恩,这是一个更加没有道理的设计。这让我们补了一个坑,但同时又挖了另外一个坑。接口的确让攻击和治疗以技能的形式呈现。但是人物的技能是通过实现接口的方式获得的。这样耦合度很高。你想想这样的情况,有些角色攻击方式是一样的,比如野蛮人(Barbarians)和骷髅兵(Skeletons)他们都用宝剑攻击敌人。按照你的设计野蛮人和骷髅兵需要各自实现一次Attackable接口,然后我们把用宝剑攻击的代码重复写两次。这样的设计导致野蛮人和骷髅兵用同样的方式攻击敌人但是代码却不能得到复用。我们重复的代码就会变得非常多。我们要设计一套代码,我传什么样的参数给他,他就用什么方式攻击或者辅助。今天野蛮人用剑,我一秒钟就让他改用长枪。这样的设计才能应对更多的变化。
小新:老师,我该如何去设计呢?
吉勇老师:首先,我给你第一点建议“找出应用中可能需要变化的地方,把他分离出来。不要和那些不变化的代码混在一起”。把那些会变化的部分封装起来,让其他部分不受影响。这样系统会更有弹性。
小新:分离“变化”与“不变”。野蛮人用剑,骷髅兵用剑,王子用长枪,天使不能攻击只能恢复友军生命。他们在战场上的行为是变化的。但是不变的是他们都会有行为。那么我需要把这些角色的行为抽象到一个行为接口(behavior)中去。用剑,还是用长枪这些具体的行为类实现这个behavior接口。
吉勇老师:很不错,这么快就领悟到了。那么在角色类(Character)中,调用的方法是具体用剑,用枪,还是治疗呢,你想想。
小新:都不是,应该调用Behavior接口。在写Character的时候,我哪里会知道今后具体会有多少种的行为呢,现在用剑和枪,以后你还可能用原子弹。但是我知道的是,你应该在战场上有一个行为Behavior,我只调用你的behavior,而我并不关心你具体的行为是什么。
吉勇老师:这是我想给你的第二个建议“面向接口编程,而不是面向实现编程”。这样使得你在写Character代码的时候并不需要关心Behavior的具体实现是什么。那么Behavior与Character是什么样的关系呢?你考虑过吗?
小新:刚才我是用Character实现Attackable接口获得attack技能。这样的方式并没有真正将角色和技能分开。角色中包含一个技能行为,而不是角色是一个技能行为。他们是组合关系。
吉勇老师:孺子可教也,我的第三个建议是“多用组合,少用继承”。让那些行为通过组合获得,而不是通过继承获得。再辛苦你画一个类图,并实现他吧。
代码如下:
public abstract class Character {
Behavior behavior = null;
public void setBehavior(Behavior weaponBehavior) {
this.behavior = weaponBehavior;
}
public void behavior(){
String action = behavior.behavior();
System.out.println(display()+action);
}
abstract String display();
}
public class Barbarians extends Character {
@Override
String display() {
// TODO Auto-generated method stub
return "四个光膀子野蛮人";
}
}
public class Skeletons extends Character {
@Override
String display() {
return "四个小骷髅兵";
}
}
public class Prince extends Character{
@Override
public String display() {
return "穿着盔甲的王子";
}
}
public class Angel extends Character{
@Override
String display() {
// TODO Auto-generated method stub
return "天使";
}
}
public interface Behavior {
public String behavior();
}
public class SwordBehavior implements Behavior {
@Override
public String behavior() {
// TODO Auto-generated method stub
return "使用大宝剑砍敌人";
}
}
public class LanceBehavior implements Behavior {
@Override
public String behavior() {
// TODO Auto-generated method stub
return "使用长枪戳敌人";
}
}
public class CureBehavior implements Behavior {
@Override
public String behavior() {
// TODO Auto-generated method stub
return "使用魔法治疗队友";
}
}
public class test {
public static void main(String[] args) {
// TODO Auto-generated method stub
Prince prince = new Prince();
Behavior behavior = new LanceBehavior();
prince.setBehavior(behavior);
prince.behavior();
Skeletons skeletons = new Skeletons();
behavior = new SwordBehavior();
skeletons.setBehavior(behavior);
skeletons.behavior();
Barbarians barbarians = new Barbarians();
behavior = new SwordBehavior();
barbarians.setBehavior(behavior);
barbarians.behavior();
Angel angel = new Angel();
behavior = new CureBeavior();
angel.setBehavior(behavior);
angel.behavior();
}
}
运行结果
穿着盔甲的王子使用长枪戳敌人
四个小骷髅兵使用大宝剑砍敌人
四个光膀子野蛮人使用大宝剑砍敌人
天使使用魔法治疗队友
吉勇老师:这样的设计就灵活多了。老板让我们再添加两个新角色,气球兵(Balloon)和炸弹兵(Bomber),他们一个在热气球里投掷炸弹攻击敌人,一个在地面投掷炸弹攻击敌人。
小新:全是小菜一碟。我们只需要添加三个类:Balloon, Bomber继承Character;BombBehavior实现Behavior接口就搞定。丢炸弹攻击敌人的行为还可以在气球兵(Balloon)和炸弹兵(Bomber)中得到复用。以后再来点什么炸弹王子也能复用BomerBehavior。在分开了人物和攻击方式后。如果哪天老板要让王子拥有投掷炸弹的能力,只需要在测试类Test中,用setBehavior()方法给王子设置一个BombBehavior。王子就不再是一个用长枪戳敌人的王子了,而是一个炸弹王子了。(Test类只是针对本程序的例子,而更好的做法不用修改任何代码,王子的行为写在配置文件中,另外添加一些读取配置文件的类,通过修改配置文件就能修改王子的修为,还改个屁的代码)
吉勇老师:完全正确,这就是松耦合的好处。我们通过定义算法族,再将他们封装起来,让算法族之间可以相互替换。这样使得算法的变化独立于调用他的客户代码。这么机智的设计就是策略模式。