一、 惊!某程序员手持板砖走向策划,原因竟然是?!
阿毅是某公司的程序员,最近负责项目关于武将模块的开发,需求是这样子的:武将需要有名称,品质,可以进行升级,觉醒,不同武将升级,觉醒的逻辑可能相同也可能不同;目前需要实现刘备和关羽两个武将。
”不就是简单的面向对象吗?我一天就能搞定“,阿毅心里想。一天过后,代码长这样子:
public abstract class Hero {
protected String name;
protected int quality;
protected abstract void levelUp();
protected abstract void wake();
}
public class LiuBei extends Hero {
public LiuBei(String name, int quality) {
this.name = name;
this.quality = quality;
}
@Override
protected void levelUp() {
//升级方案1
}
@Override
protected void wake() {
//觉醒方案1
}
}
public class GuanYu extends Hero {
public GuanYu(String name, int quality) {
this.name = name;
this.quality = quality;
}
@Override
protected void levelUp() {
//升级方案2
}
@Override
protected void wake() {
//觉醒方案2
}
}
看上去没什么大问题,很好的利用的面向对象继承的特点。过了一阵子,策划和阿毅说:”上次的工作做得不错,但是为了丰富我们游戏的多样性,我们决定再增加两个武将:张飞(升级方案1,觉醒方案2)和诸葛亮(升级方案2,觉醒方案3)。“
虽然增加了需求,但是这对阿毅来说只是小菜一碟,不到一天,他就提交了代码:
public class ZhangFei extends Hero {
public ZhangFei(String name, int quality) {
this.name = name;
this.quality = quality;
}
@Override
protected void levelUp() {
//升级方案1
}
@Override
protected void wake() {
//觉醒方案2
}
}
public class ZhuGe extends Hero {
public ZhuGe(String name, int quality) {
this.name = name;
this.quality = quality;
}
@Override
protected void levelUp() {
//升级方案2
}
@Override
protected void wake() {
//觉醒方案3
}
}
虽说有部分代码冗余,但是能完成策划的需求,阿毅并没有在意,他并不知道,他正在给自己埋下一颗定时炸弹。
随着游戏运营越来越久,武将已经多达100个,重复代码也越来越多。此时,策划又提出一个需求:”我们要给武将新增一个进阶玩法,一共有三种进阶方案。时间比较赶,一天内完成。“
由于武将的进阶方案并不完全相同,因此我们无法通过在父类实现通用方法的方式来完成,这也就意味着,阿毅需要在100个武将类中,逐个添加进阶的方法:
public abstract class Hero {
protected String name;
protected int quality;
protected abstract void levelUp();
protected abstract void wake();
protected abstract void advance();
}
public class LiuBei extends Hero {
public LiuBei(String name, int quality) {
this.name = name;
this.quality = quality;
}
@Override
protected void levelUp() {
//初级升级
}
@Override
protected void wake() {
//初级觉醒
}
@Override
protected void advance() {
//进阶方案X
}
}
//给其它99个类添加advance()方法
就在阿毅通宵敲代码,觉得大功即将告成之时,策划走过来和他说:”不好意思呀,我们刚刚开会,决定不要进阶这个功能了,改为天命升级,待会我把新需求发给你哈。“
此时的阿毅终于忍无可忍,操起手中的板砖。。。
二、原因分析
乍一看,上文似乎在讲一个勤勤恳恳的程序员被频繁加需求,改需求的策划逼得拿起板砖的故事,其实不然。
从程序出现大量冗余代码开始,阿毅就该意识到这一天迟早会降临。这种设计方式,哪怕仅仅改变某个升级方案的逻辑,也得同时修改几十个类。
也就是说,这种设计方式,使得代码极其难以维护!
来看看程序设计过程中,需要遵守的,阿毅却违反的几条规则:
- 将可能改变的代码抽取出来,与不会改变的代码分开;
- 面向接口编程,而不是面向实现;
- 有时候,比起继承,组合可能是个更好的选择。
接下来就来说说,策略模式是如何解决阿毅所遇到的问题的。
三、策略模式
Strategy Pattern defines a family of alogrithms, encapsulates each one, and makes them interchangable. Strategy lets the alogrithms vary from client that uses it.
策略模式定义了一个算法的集合,并对每一个算法进行封装,使它们能够被改变。策略模式也让客户端能够使用不同的算法。(翻译的不太好 o_o …)
光看定义有些难以理解,我们来看一下UML图。
-
阿毅的设计模式的UML:
- 策略设计模式UML:
将可能变化的方案的实现从Hero中抽离出来,通过面向接口编程,使得实现类的各种方案可变,易维护,减少大量重复代码。我们这里使用了组合的方式来实现,而不是继承。
具体的代码实现:
//升级策略
public interface LevelUpBehavior {
void levelUp();
}
public class LevelUp1 implements LevelUpBehavior {
@Override
public void levelUp() {}
}
public class LevelUp2 implements LevelUpBehavior {
@Override
public void levelUp() {}
}
//觉醒策略
public interface WakeBehavior {
void wake();
}
public class Wake1 implements WakeBehavior {
@Override
public void wake() {}
}
public class Wake2 implements WakeBehavior {
@Override
public void wake() {}
}
//武将共同接口
public abstract class Hero {
protected String name;
protected int quality;
//不同武将有不同的策略
protected LevelUpBehavior levelup;
protected WakeBehavior wake;
//面向接口编程,不需要知道具体是哪一个策略
public void levelUp() {
this.levelup.levelUp();
}
public void wake() {
this.wake.wake();
}
}
//武将具体实现
public class GuanYu extends Hero {
//构造函数中设置策略,也可以定义set()方法来改变策略
public GuanYu(String name, int quality) {
this.name = name;
this.quality = quality;
this.levelup = new LevelUp2();
this.wake = new Wake2();
}
}
public class LiuBei extends Hero {
public LiuBei(String name, int quality) {
this.name = name;
this.quality = quality;
this.levelup = new LevelUp1();
this.wake = new Wake1();
}
}
四、故事的结尾
原来,阿毅手中拿起的板砖并不是板砖,而是和板砖一样厚的《Head First Design Patterns》。看完里面讲解的策略模式后,阿毅重构了代码,最终将自己从堆积如山的重复代码中拯救出来。
”虽说加班让我掉了些许头发,但是学会了策略模式,我觉得还是挺赚的。“阿毅开心的提前下班了。
参考:Head First Design Pattern