您的位置:首页 > 编程语言

重构 - 理解设计模式的捷径(4 设计模式的引入 - 工厂模式)

2009-11-21 02:22 441 查看
 

3.2 三思而后“new” – 工厂方法模式的引入

3.2.1 直接new的后果

    之前已经提到了工厂方法模式,所以现在就把它引出来吧。回到之前那个问题,客户在玩了一段时间之后,感觉游戏太简单了,敌人老是那么“有规律”地出现嘛,所以要求增加游戏难度,怎么办?
    还是有两种设计方案,先看第一种,在具体的状态类的Handle方法中直接把所有的业务逻辑写出来,不分离职责。由于院子类的接口最多,所以就以它为例子来说明。根据地图,院子可以通向四个区域,所以首先可以生成一个0 – 5之间的随机数,然后加以判断,看它对不对应相邻的那四个区域之间的任何一个。如果没有对应的,重新生成,直到成功为止。如果有对应的,恭喜,可以产生新的状态类的实例了。但产生之前还要做个判断,因为还不知道它具体是哪个区域,这就需要一个庞大的switch-case分支了(代码有了坏味道)。判断结束后,才可以生成具体的实例状态。好了,暂时先看看这样的代码怎么样:



    单单看这一个的话,似乎不是很复杂,但是考虑到还有其他五个区域,每个区域都要重复写这样的代码的时候,可能本来摄氏30度的“热情”瞬间就降到了摄氏1度了。谁愿意做这种重复工作呢?这还不算增加新状态类的情况。如果考虑新的状态加进来,然后多两个接口,那么估计热情就要降到0度以下了。写代码要动点脑筋,代码哪个程序员都可以写,但高质量的代码却不是那么容易写出来的。
    还是先看看这样的设计问题出在哪里吧。
    重点注意图中用红笔圈出来的地方。这些都是代码的“坏味道” 。不妨先注意那些有许多new的地方。院子类需要判断,更衣室类也需要判断,书房类依然需要判断,这不合理。重复的地方太多。 还有上面的一连串的 “&&” 判断,每个具体类的Handle函数都要做这样的工作,也是重复,而且最关键的是Handle函数本来是被设计来干什么的?答案是切换状态。那么它要管随机数判断干什么?要管具体状态的生成干什么?统统不需要!它只负责拿来用就可以了,这些具体的工作都应该交给合适的对象去做。这就和之前提到的单一职责原则联系到一起了。所以代码出问题的关键地方就在于:违背“单一职责”原则。

3.2.2 重构 – 引入工厂模式

    首先看实例化的问题怎么解决吧。可以考虑单独写一个函数CreateInstance来生成实例,听上去不错。但这只能解决院子状态类的问题,其他的状态怎么办?对了,要复用,应该考虑单独写一个类,专门负责生成状态的具体实例,就像生产具体的产品的工厂那样。所以这里就可以引入工厂模式了。首先定义一个CStateFactory的工厂类,然后定义一个生成实例的方法CreateState(int whicharea),其功能就是根据参数whicharea来生成对应的状态对象。





    然后我们再看看Handle里的代码:



    看起来精简了很多了,过去需要十多行的代码,现在只需要两行就实现了,而且最主要的是工厂类可以被其他子状态类复用,如果有新增加的状态,依然可以复用,只需要在工厂方法中增加对应的状态生成的分支就可以了(当然,更厉害的可以直接利用配置文件来动态生成状态的个数,利用反射机制将switch-case分支彻底“消灭” )。不过还不够简洁,注意到上图的命名了吗?“重构的Handle方法(1)” ,不意味着还有“(2)”出来吗?那么应该怎么重构呢?请注意下面红色圈出来的地方:



    为什么说这段代码有问题?前面讲过了,它属于具体的业务判断逻辑,不属于这个方法的职责。所以首先可以想到把它分离出去成为一个函数,专门负责判断。不过这样还是有问题,为什么?因为这只能解决这一个类的问题,其他状态类呢?难道还要写重复的代码?(回忆一下利用工厂模式的重构过程)。分离成函数本身没有错,肯定要有专门负责判断的函数,但是少了些思考。少了什么思考?仔细看看这段代码,什么是共同的?什么是不变的?对了,判断的业务逻辑是一样的,都是看随机数对应的状态是否落在子区域的相邻区域内。所以缺少的就是抽象!要将代码共同的地方抽象出来,放到抽象类或者接口中,然后利用多态性在子类中具体化,最后通过抽象接口统一调用。这就是面向对象的“抽象” 的思维方式。实际上面向对象的思想用一句调侃的方式来解释的话,就是教会程序员如何高明地“偷懒”。具体到这段代码,首先应该在抽象类CState中定义一个数组exitValid[6],代表“假定中”的6个相邻的区域哪些是有效的,它将在6个具体的子类中根据每个子类的相邻接口具体化。然后需要在抽象类中定义一个GetRandState方法,并直接在该类中实现。其功能就是实现上图中红色的那段代码的作用,返回一个“合法”的随机状态。具体的实现如下:
 







    可以看到,GetRandState函数由于利用exitValid属性的功能而被大大简化了,最主要的是,它可以充分实现面向对象多态的特性,由具体的子类来具体化exitValid属性后再调用统一的GetRandState接口。现在我们来看看再次重构过的Handle的样子:
 



    又简洁了许多。要不是考虑到C++内存泄露的问题,甚至可以直接写成如下形式:

 



    不过这终究不是Java,所以还是用对象比较保险。这样,重构的工作就算告一段落了。其他的状态类所要做的工作和院子类是类似的,具体的做法就不用列出来了。可以指出的一点是,exitValid属性的抽象以及GetRandState方法的设计的思路,实质上就是使用了另外一个设计模式:模板方法模式。模板方法模式的核心思想就是把公共的地方抽象到父类中,将具体的操作延迟到子类中,最终通过父类的统一接口来调用。回到前面一节所提到的问题:面对这种需求,那个“循环” 的解决方案还能用吗?用一句比较市侩的话来描述就是:门都没有!肯定嘛,循环的方式怎么可能搞定随机的需求呢?所以必须得引入状态模式、工厂模式以及模板方法模式来解决。

3.2.3 小结

       最后我们再来对比一下,看变化过业务逻辑后的Handle和变化之前的Handle之间有什么不同:

 



 

    咋一看,没有什么不同,其实不然。旧的业务逻辑由于是循环切换状态,所以新的状态是什么是可以确定的,所以生成新的方法的语句就在写死在了具体的子类的代码中;而新的业务逻辑由于增加了随机性,所以不能再写死在代码中了,取而代之的是引入工厂方法模式,动态地生成新的状态,而且代码就转移到了基类中给予实现。换句话说,就是一个是变的,一个是不变的;一个是常量,一个是变量;一个是静态的,一个是动态的。这一点光看嵌套的层次就知道了。新的业务逻辑足足嵌套了三层。所以相对而言,新的Handle方法具有更大的灵活性和可重用性。面向对象设计的一个思路就是抽象,提炼出“不变”的地方,然后把它用“变量”表示出来,接着利用继承,在子类中实例化,再利用多态,通过抽象接口统一调用。这个设计套路很实用,对于解决这类问题也相当有效。它充分借用了抽象的基础以及面向接口编程的设计原则。可以看到现在的设计方案不管是增加几个状态,都可以轻松地进行扩展和维护了。当然,这种设计仅仅是针对“随机性”切换区域的需求比较适合,如果需求变化了,玩家只需要玩那种规律性的切换,那么还是前面一种设计更合适,因为此时“随机性”的设计方案并不满足这个需求了。

    引入工厂模式之后,现在的设计的关键之处就是GetRandState方法,因为它是业务逻辑的核心部分,更加具体一点就是随机数产生的方式(也就是randState产生的方式),这是需求对应的关键业务逻辑。如果要增加游戏的灵活性,这个业务逻辑就是需要重点设计的地方。毕竟rand()这个方法实质上还是有“规律”的,这一点在进行单元测试的时候尤为明显,它几乎每次都是以“固定”的模式产生所谓的“随机数” 。随机函数结果并不随机。所以采用它并不是最好的选择,这里这么做仅仅是为了方便演示和说明。在实际的开发中绝对不可能简单地用一个rand()就解决了,因为它是业务逻辑的关键之处。

    所以结论就是:如果需要new的地方,三思,考虑工厂模式能否帮的上忙。当然,这里采用的仅仅是最原始的工厂方法,没有写子工厂类,具体在设计中可以根据需要灵活运用。还有,之所以引入工厂方法,完全是因为需求的改变,不同的需求对应着不同的设计方案。一切取决于需求及其变化,这是软件设计的关键之处。
    当然,还可以再把问题扩展一点。现在玩家的需求又变了,要求可以在游戏中自主选择难度, “初级”,“高级” ,还有一个“专家级” 的,并增加了“专家级” 的业务逻辑的定义:在“随机性”的基础上,系统人物可以根据玩家人物的属性情况选择要切换的目标区域,当玩家人物的属性处于劣势的时候,系统人物将优先切换到玩家人物所处的区域(当然要满足相邻的条件);否则优先切换到玩家人物不在的区域(也要满足相邻的条件)。
    怎么办呢?请读者自己思考吧(提示:工厂模式依然有效!)。相信经过前面的演化过程的介绍,解决这个问题应该不再是太难的事情了。
    附:工厂方法模式的经典定义。
        Factory Method:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。Factory Method使一个类的实例化延迟到其子类。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息