1 IGame游戏公司的故事
1.1 讨论会
话说有一个叫IGame的游戏公司,正在开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类的游戏)。一般这类游戏都有一个基本的功能,就是打怪(玩家攻击怪物,借此获得经验、虚拟货币和虚拟装备),并且根据玩家角色所装备的武器不同,攻击效果也不同。这天,IGame公司的开发小组正在开会对打怪功能中的某一个功能点如何实现进行讨论,他们面前的大屏幕上是这样一份需求描述的ppt:
图1.1 需求描述ppt
各个开发人员,面对这份需求,展开了热烈的讨论,下面我们看看讨论会上都发生了什么。
1.2 实习生小李的实现方式
在经过一番讨论后,项目组长Peter觉得有必要整理一下各方的意见,他首先询问小李的看法。小李是某学校计算机系大三学生,对游戏开发特别感兴趣,目前是IGame公司的一名实习生。
经过短暂的思考,小李阐述了自己的意见:
“我认为,这个需求可以这么实现。HP当然是怪物的一个属性成员,而武器是角色的一个属性成员,类型可以使字符串,用于描述目前角色所装备的武器。角色类有一个攻击方法,以被攻击怪物为参数,当实施一次攻击时,攻击方法被调用,而这个方法首先判断当前角色装备了什么武器,然后据此对被攻击怪物的HP进行操作,以产生不同效果。”
而在阐述完后,小李也飞快的在自己的电脑上写了一个Demo,来演示他的想法,Demo代码如下。
Code:怪物
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace IGameLi
7: {
8: /// <summary>
9: /// 怪物
10: /// </summary>
11: internal sealed class Monster
12: {
13: 14: /// 怪物的名字
15: 16: public String Name { get; set; }
17:
18: 19: /// 怪物的生命值
20: 21: public Int32 HP { get; set; }
22:
23: public Monster(String name,Int32 hp)
24: {
25: this.Name = name;
26: this.HP = hp;
27: }
28: }
29: }
Code:角色
class Role
private Random _random = new Random();
14:
/// 表示角色目前所持武器的字符串
17: public String WeaponTag { get; set; }
19:
/// 攻击怪物
22: /// <param name="monster">被攻击的怪物</param>
24: public void Attack(Monster monster)
25: {
if (monster.HP <= 0)
27: {
28: Console.WriteLine("此怪物已死");
29: return;
30: }
31:
32: if ("WoodSword" == this.WeaponTag)
33: {
34: monster.HP -= 20;
35: 36: {
37: Console.WriteLine("攻击成功!怪物" + monster.Name + "已死亡");
38: }
39: else
40: {
41: Console.WriteLine("损失20HP");
42: }
43: }
44: else "IronSword" == 45: {
46: monster.HP -= 50;
47: 48: {
49: Console.WriteLine(50: }
51: 52: {
53: Console.WriteLine("损失50HP");
54: }
55: }
56: "MagicSword" == 57: {
58: Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
59: monster.HP -= loss;
60: if (200 == loss)
61: {
62: Console.WriteLine("出现暴击!!!");
63: }
64:
65: 66: {
67: Console.WriteLine(68: }
69: 70: {
71: Console.WriteLine("损失" + loss + "HP");
72: }
73: }
74: 75: {
76: Console.WriteLine("角色手里没有武器,无法攻击!");
77: }
78: }
79: }
80: }
Code:测试代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
5:
namespace IGameLi
7: {
class Program
9: {
static void Main(string[] args)
11: {
12: //生成怪物
13: Monster monster1 = new Monster("小怪A",50);
14: Monster monster2 = "小怪B",96)">15: Monster monster3 = "关主",200);
16: Monster monster4 = "最终Boss",1000);
17:
//生成角色
19: Role role = new Role();
20:
//木剑攻击
22: role.WeaponTag = "WoodSword";
23: role.Attack(monster1);
24:
//铁剑攻击
26: role.WeaponTag = "IronSword";
27: role.Attack(monster2);
28: role.Attack(monster3);
29:
30: //魔剑攻击
31: role.WeaponTag = "MagicSword";
32: role.Attack(monster3);
33: role.Attack(monster4);
34: role.Attack(monster4);
35: role.Attack(monster4);
36: role.Attack(monster4);
37: role.Attack(monster4);
38:
39: Console.ReadLine();
40: }
41: }
42: }
程序运行结果如下:
图1.2 小李程序的运行结果
1.3 架构师的建议
小李阐述完自己的想法并演示了Demo后,项目组长Peter首先肯定了小李的思考能力、编程能力以及初步的面向对象分析与设计的思想,并承认小李的程序正确完成了需求中的功能。但同时,Peter也指出小李的设计存在一些问题,他请小于讲一下自己的看法。
小于是一名有五年软件架构经验的架构师,对软件架构、设计模式和面向对象思想有较深入的认识。他向Peter点了点头,发表了自己的看法:
“小李的思考能力是不错的,有着基本的面向对象分析设计能力,并且程序正确完成了所需要的功能。不过,这里我想从架构角度,简要说一下我认为这个设计中存在的问题。
首先,小李设计的Role类的Attack方法很长,并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同。
再者,我认为这个设计比较大的一个问题是,违反了OCP原则。在这个设计中,如果以后我们增加一个新的武器,如倚天剑,每次攻击损失500HP,那么,我们就要打开Role,修改Attack方法。而我们的代码应该是对修改关闭的,当有新武器加入的时候,应该使用扩展完成,避免修改已有代码。
一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同武器攻击看成一个策略,那么引入策略模式(Strategy Pattern)是明智的选择。
最后说一个小的问题,被攻击后,减HP、死亡判断等都是怪物的职责,这里放在Role中有些不当。”
Tip:OCP原则,即开放关闭原则,指设计应该对扩展开放,对修改关闭。
Tip:策略模式,英文名Strategy Pattern,指定义算法族,分别封装起来,让他们之间可以相互替换,此模式使得算法的变化独立于客户。
小于边说,边画了一幅UML类图,用于直观表示他的思想。
图1.3 小于的设计
Peter让小李按照小于的设计重构Demo,小李看了看小于的设计图,很快完成。相关代码如下:
Code:IAttackStrategy接口
interface IAttackStrategy
void AttackTarget(Monster monster);
11: }
12: }
Code:木剑
void AttackTarget(Monster monster)
12: monster.Notify(20);
13: }
14: }
15: }