一、讨论会
有一个Game的游戏公司,正准备开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类游戏)。这类游戏都有一个基本功能,就是打怪(玩家通过攻击怪物,借此获得经验、虚拟货币和虚拟装备),并i企鹅根据玩家角色所装备的武器不公,攻击效果也不同。这天,Game公司的开发小组正在开会对打怪功能中的某一个功能点如何实现进行讨论,他们面前的屏幕上是这样的一个需求ppt:
各开发人员,面对这份需求,展开了热烈的讨论,下面看看在讨论会上都发生了什么。
二、实习生小贾的实现方式
在经过一番讨论后,项目组长Jindi觉得有必要整理一下各方的意见,他首先询问小贾的看法。小贾是某学校计算机系大三学生,对游戏开发特别感兴趣,目前是Game公司的一名实习生。
经过短暂的思考,小贾阐述了自己的意见:
“我认为,这个需求可以这么实现。HP当然是怪物的一个属性成员,而武器是角色的一个属性成员,类型可以使字符串,用于描述目前角色所装备的武器。角色类有一个攻击方法,以被攻击怪物为参数,当实施一次攻击时,攻击方法被调用,而这个方法首先判断当前角色装备了什么武器,然后据此对被攻击怪物的HP进行操作,以产生不同效果。”
而在阐述完后,小贾也飞快的在自己的电脑上写了一个Demo,来演示他的想法,Demo代码如下。
package com.game.test1;
/**
* 怪物类
* @author Administrator
*
*/
public class GuaiWu {
//定义一个保存怪物名称的变量
private String gw_name;
//定义一个保存怪物血量的变量
private int gw_hp;
public GuaiWu() {}
public GuaiWu(String gw_name,int gw_hp) {
this.gw_name=gw_name;
this.gw_hp=gw_hp;
}
//提供怪物名称的set/get方法
public String getGw_name() {
return gw_name;
}
public void setGw_name(String gw_name) {
this.gw_name = gw_name;
}
//提供怪物生命值的set/get方法
public int getGw_hp() {
return gw_hp;
}
public void setGw_hp(int gw_hp) {
this.gw_hp = gw_hp;
}
public void getInfo() {
System.out.println(gw_name+" "+gw_hp);
}
}
package com.game.test1;
/**
* 角色类
* @author Administrator
*
*/
public class JueSe {
//定义一个保存角色的名称
private String js_name;
//定义一个保存武器的变量
private String wuqi;
public JueSe(String js_name,String wuqi) {
this.js_name=js_name;
this.wuqi=wuqi;
}
public String getJs_name() {
return js_name;
}
public void setJs_name(String js_name) {
this.js_name = js_name;
}
public String getWuqi() {
return wuqi;
}
public void setWuqi(String wuqi) {
this.wuqi = wuqi;
}
public void getJsInfo() {
System.out.println(js_name+" "+wuqi);
}
/**
* 打怪物
*/
public void daGuai(GuaiWu gw) {
//判断怪物是否死亡
if(gw.getGw_hp()<=0) {
System.out.println(gw.getGw_name()+":已经死亡!");
}else {
//判断角色装备
if(wuqi.equals("木剑")) {
gw.setGw_hp(gw.getGw_hp()-20);
System.out.println(js_name+" 打"+gw.getGw_name()+"1次,怪物损失20hp,还剩"+gw.getGw_hp()+"hp");
}
//判断角色所持有的武器,从而对怪物进行操作
if(wuqi.equals("铁剑")) {
gw.setGw_hp(gw.getGw_hp()-50);
System.out.println(js_name+" 打"+gw.getGw_name()+"1次,怪物损失50hp,还剩"+gw.getGw_hp()+"hp");
}
if(wuqi.equals("魔剑")) {
//得到一个随机数
int random=((int)(Math.random()*10))+1;
if(random>5) {
gw.setGw_hp(gw.getGw_hp()-200);
System.out.println("产生暴击!!!");
System.out.println(js_name+" 打"+gw.getGw_name()+"1次,怪物损失200hp,还剩"+gw.getGw_hp()+"hp");
}else {
gw.setGw_hp(gw.getGw_hp()-100);
System.out.println(js_name+" 打"+gw.getGw_name()+"1次,怪物损失100hp,还剩"+gw.getGw_hp()+"hp");
}
}
}
}
}
package com.game.test1;
public class Test {
public static void main(String[] args) {
GuaiWu gw1=new GuaiWu("小妖",500);
GuaiWu gw2=new GuaiWu("大妖",1500);
gw1.getInfo();
gw2.getInfo();
JueSe js1=new JueSe("小剑客", "木剑");
JueSe js2=new JueSe("剑魔", "魔剑");
js1.getJsInfo();
js2.getJsInfo();
js1.daGuai(gw1);
js2.daGuai(gw2);
}
}
运行结果:
三、架构师的建议
小贾阐述完自己的想法并演示了Demo后,项目组长Jindi首先肯定了小贾的思考能力、编程能力以及初步的面向对象分析与设计的思想,并承认小贾的程序正确完成了需求中的功能。但同时,Jindi也指出小贾的设计存在一些问题,他请闻哥讲一下自己的看法。
闻哥是一名有五年软件架构经验的架构师,对软件架构、设计模式和面向对象思想有较深入的认识。他向Jindi点了点头,发表了自己的看法:
“小贾的思考能力是不错的,有着基本的面向对象分析设计能力,并且程序正确完成了所需要的功能。不过,这里我想从架构角度,简要说一下我认为这个设计中存在的问题。
首先,小贾设计的 角色 类的 打怪 方法很长,并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同。
再者,我认为这个设计比较大的一个问题是,违反了OCP原则。在这个设计中,如果以后我们增加一个新的武器,如倚天剑,每次攻击损失500HP,那么,我们就要打开JueSe ,修改daGuai方法。而我们的代码应该是对修改关闭的,当有新武器加入的时候,应该使用扩展完成,避免修改已有代码。
一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同武器攻击看成一个策略,那么引入策略模式(Strategy Pattern)是明智的选择。
最后说一个小的问题,被攻击后,减HP、死亡判断等都是怪物的职责,这里放在Role中有些不当。”
Tip:OCP原则,即开放关闭原则,指设计应该对扩展开放,对修改关闭。
Tip:策略模式,英文名Strategy Pattern,指定义算法族,分别封装起来,让他们之间可以相互替换,此模式使得算法的变化独立于客户。
闻哥边说,边画了一幅UML类图,用于直观表示他的思想。
Jindi让小贾按照闻哥的设计重构Demo,小李看了看闻哥的设计图,很快完成。相关代码如下:
package com.game.test1;
/**
* 角色类
* @author Administrator
*
*/
public class JueSe {
//定义一个保存角色的名称
private String js_name;
//定义一个保存武器的变量
private GongJiInterface wuqi;
public JueSe(String js_name,GongJiInterface wuqi) {
this.js_name=js_name;
this.wuqi=wuqi;
}
public String getJs_name() {
return js_name;
}
public void setJs_name(String js_name) {
this.js_name = js_name;
}
public GongJiInterface getWuqi() {
return wuqi;
}
public void setWuqi(GongJiInterface wuqi) {
this.wuqi = wuqi;
}
public void getJsInfo() {
System.out.println(js_name+" "+wuqi);
}
/**
* 打怪物
*/
public void daGuai(GuaiWu gw) {
this.wuqi.gongji(gw);
}
}
package com.game.test1;
/**
* 怪物类
* @author Administrator
*
*/
public class GuaiWu {
//定义一个保存怪物名称的变量
private String gw_name;
//定义一个保存怪物血量的变量
private int gw_hp;
public GuaiWu() {}
public GuaiWu(String gw_name,int gw_hp) {
this.gw_name=gw_name;
this.gw_hp=gw_hp;
}
public String getGw_name() {
return gw_name;
}
public void setGw_name(String gw_name) {
this.gw_name = gw_name;
}
public int getGw_hp() {
return gw_hp;
}
public void setGw_hp(int gw_hp) {
this.gw_hp = gw_hp;
}
/**
* 怪物自身的掉血操作
* @param hp
*/
public void diaoxue(int hp) {
//判断怪物的死活
if(this.gw_hp<=0) {
System.out.println("怪物-"+this.gw_name+",已经死亡!");
return;
}
this.gw_hp=this.gw_hp-hp;
if(this.gw_hp<=0){
System.out.println("怪物\""+this.gw_name+"\",已经死亡,无需攻击!!!");
}else {
System.out.println("怪物\""+this.gw_name+"\",损失"+hp+"hp,剩余"+this.gw_hp+"hp,仍可继续攻击!!!");
}
}
public void getInfo() {
System.out.println(gw_name+" "+gw_hp);
}
}
package com.game.test1;
/**
* 攻击接口
* @author Administrator
*
*/
public interface GongJiInterface {
//打怪方法
void gongji(GuaiWu gw);
}
package com.game.test1;
//木剑
public class MuJian implements GongJiInterface{
@Override
public void gongji(GuaiWu gw) {
gw.diaoxue(20);
}
}
package com.game.test1;
//铁剑
public class TieJian implements GongJiInterface{
@Override
public void gongji(GuaiWu gw) {
gw.diaoxue(50);
}
}
package com.game.test1;
//魔剑
public class MoJian implements GongJiInterface{
@Override
public void gongji(GuaiWu gw) {
int random=((int)(Math.random()*10))+1;
if(random>5) {
System.out.println("产生暴击!!");
gw.diaoxue(200);
}else {
gw.diaoxue(100);
}
}
}
package com.game.test1;
//倚天剑
public class YiTianJian implements GongJiInterface{
@Override
public void gongji(GuaiWu gw) {
gw.diaoxue(1200);
}
}
package com.game.test1;
public class Test {
public static void main(String[] args) {
GuaiWu gw1=new GuaiWu("小妖",500);
GuaiWu gw2=new GuaiWu("大妖",1500);
gw1.getInfo();
gw2.getInfo();
JueSe js1=new JueSe("小剑客", new MuJian());
JueSe js2=new JueSe("剑魔", new YiTianJian());
js1.getJsInfo();
js2.getJsInfo();
js1.daGuai(gw1);
js2.daGuai(gw2);
}
}
运行结果:
四、小贾的总结
Jindi显然对改进后的代码比较满意,他让小贾对照两份设计和代码,进行一个小结。小贾简略思考了一下,并结合闻哥对一次设计指出的不足,说道:
“我认为,改进后的代码有如下优点:
第一,虽然类的数量增加了,但是每个类中方法的代码都非常短,没有了以前Attack方法那种很长的方法,也没有了冗长的if…else,代码结构变得很清晰。
第二,类的职责更明确了。在第一个设计中,Role不但负责攻击,还负责给怪物减少HP和判断怪物是否已死。这明显不应该是Role的职责,改进后的代码将这两个职责移入Monster内,使得职责明确,提高了类的内聚性。
第三,引入Strategy[策略]模式后,不但消除了重复性代码,更重要的是,使得设计符合了OCP。如果以后要加一个新武器,只要新建一个类,实现IAttackStrategy接口,当角色需要装备这个新武器时,客户代码只要实例化一个新武器类,并赋给Role的Weapon成员就可以了,已有的Role和Monster代码都不用改动。这样就实现了对扩展开发,对修改关闭。”
Jindi和闻哥听后都很满意,认为小贾总结的非常出色。