刚接触设计模式的第一课,工厂模式与抽象工厂,确实感觉到了设计模式的抽象与强大作用力,学习过程中自己动手设计了一个小游戏的角色。
假定一个游戏中有很多怪物角色,如蜘蛛,马,猴子,等,此游戏有多种游戏级别,先假定为3级。
设计游戏时必须考虑到:
1,游戏的角色可扩展性
2,游戏易于维护(如,游戏中怪物角色易于管理)
暂时不考虑其他的问题,先说说角色的设计问题。
游戏中有很多怪物角色,也分为三级,即怪物也有三个级别,那么,怎么设计角色的继承体系呢?
至少有以下两种策略:
a),游戏维护一个怪物超类,所有怪物直接继承,将这些子类再作为超类,供“三个级别”怪物类继承,类结构大致为:
CMonster
CHouse :public CMonster,......
CHighLevelHouse :public CHouse,CLowLevelHouse :public CHouse.....
b),游戏维护一个怪物超类,让三个级别继承此超类,再分别让每个怪物继承之,类结构大致为:
CMonster
CHighLevelMonster : public CMonster,CLowLevelMonster : public CMonster.......
CHighLevelHouse : public CHighLevelMonster,CLowLevelHouse: public CLowLevelMonster....
暂且不论这两种继承方案怎样,下面就这两种方案分别使用工厂模式和抽象工厂得到怪物对象。
一,使用工厂模式
使用工厂模式的目的在于代码的客户不需要亲自实例化一个对象,客户只需要操心:“我要得到什么”,而不用操心:“我要使用哪一个具体的类去获得”;考虑在可预见的将来客户需要的对象改变了,那么,他是否不得不去更改那一段new Chouse()的代码?但如果使用工厂方法,替代这种“硬编码”,或言之“过程性编码”,则他不需要更改任何代码,而只需要更改工厂里生产此对象的方法,将生产的对象替换一下就能做到。这是很具有优越性的,假定程序中出现了一万次new Chouse(),后来我们不再需要Chouse这个对象,而是需要重写或者有较Chouse有大更改的ChouseChanged 类实例,这就意味着一天悲惨的查找->替换工作的开始!但如果使用了工厂方法,以一种封闭的方法产生客户需要的对象,就只需要在工厂内部修改生成的方法,替换一次即可!
以上是使用的目的!但如何使用呢,下面就来深入的了解工厂模式。工厂从某种角度上就是一种透明的机器,这种机器有很多型号,有的能生产猴子对象,有的能生成蜘蛛对象,等等。那么,为什么不是“一部透明的机器”呢,因为它要生产很多种对象,“一部机器”是不能生产末知种类,末知数量的对象的。那,为什么不将某参数传入此机器,让它能根据参数生产特定的对象呢?答案在于,可扩展性。如前所言,工厂并不知道它要生产怎样的对象,生产多少种类型的对象,如果单纯地以参数去界定,若以后再增加了一/多种对象,则还需要修改工厂的生产方法,可能是增加switch里面的case语句,这样就涉及到维护问题了,一旦忘记,则导致新加的对象创建不成功!记住,工厂本应是一个抽象的概念,不是一个具体的概念。所以生产多种对象,必须有多种具体的工厂!如,生产猴子的猴子工厂,生产蜘蛛的蜘蛛工厂,等。这就是工厂模式。
二,使用抽象工厂模式
再来说说抽象工厂模式,其实本人觉得抽象工厂模式也就是工厂模式,只是它是前一种模式复杂一些的模式,但本质还是一样的,如果你会用工厂模式,那么你一定会用抽象工厂模式!
已经知道了为什么要使用工厂模式,现在让我们开始分析在那个游戏中,怎样通过工厂模式去获得怪物。
现在的情况稍微复杂了一些,增加了游戏的难易级别,则不能简单的直接使用工厂模式,因为无法满足三个级别的限制,那么,抽象工厂便出现了,其实质是将工厂再次向上抽象,产生继承得到多个抽象的工厂。
考虑将工厂归类,则至少有以下两种策略:
a),定义三个“级别工厂”:高级怪物工厂,中级怪物工厂,低级怪物工厂,让每个工厂去生产所有类型的怪物
b),定义多个“怪物工厂”:猴子工厂,蜘蛛工厂,马工厂,让每个工厂去生产三种怪物(高级,中级,低级)
首先,我们先决策哪一种策略更优,把“更优”换一种说法,即是文章最开始说到的两个条件:可扩展性,可维护性。先考察可扩展性:假设游戏以后增加了几十种,几百种,新的怪物,则b中的怪物工厂则飙增到相同的数目,相比之下 a 中的工厂数目则不会增加。但 a 也会付出惨重的代价:每个工厂里面,增加几十个,几百个生产对象的方法。有一个经验,“集中地增加代码,而不是分散地增加或者修改既有代码”往往表现出更优的可扩展性。显然,定义怪物工厂可获得更好的可扩展性。因为在b中,新增加一种怪物,只需要增加一个工厂,同时在里面写入三个方法分别产生三种级别的怪物即可,相比a中的在三个工厂中都增加一个方法(不能遗忘),显然更优一些!但b也不是最优解,因为如果游戏级别一旦增加,则需要在每个怪物工厂中去增加相应的代码,而a中则只需要再新增加一个工厂,并将其它工厂里的代码直接拷到下面即可工作。但我们之前有约定:怪物的易变性大于级别的易变性,毕竟,一个游戏的游戏级别是不会常变的,这也符合假定!如果要彻底解决这个问题,我们得时刻记住一句话:“永远要对变化的东西抽象(封装)”。于是,我们自然会想到,使用继承去获得游戏级别变化下的可扩展性,我们可以在b的基础上再使用一层抽象,让怪物工厂里的生产怪物的方法是抽象的,(C++里使用vitual修饰),让每个怪物工厂被三个类(目前是三个级别)去继承,可能是这样的类结构:
CFactory
CHouseFactory : public CFactory,CMonkeyFactory : public CFactrory,.......
CHighLevelFactory : public CHosueFactory,CLowLevelFactory : public CHouseFactory....
这样会获得完全的扩展性,但与之相应的代价是子类膨胀问题,子类巨多!每个怪物工厂类有三个子类!
这里可以做权衡,减少可扩展性,增加新函数代替子类:可能的结构如下:
CFactory
{
virtual CHighLevelMonster* getHighLevelMonster();
virtual CMiddleLevelMonster* getMiddleLevelMonster();
virtual CLowLevelMonster* getLowLevelMonster();
//可能只有这三种级别
}
CHouseFactory : public CFactory
{
CHighLevelMonster* getHighLevelMonster();
CMiddleLevelMonster* getMiddleLevelMonster();
CLowLevelMonster* getLowLevelMonster();
}
说明:
1,在超类工厂CFactory中任何获得对象的方法都是virtual的
2,抽象工厂中的获得对象的方法的返回值类型必定不同!(第一个是CHighLevelMonster*,第二个是CMiddleLevelMonster*,第三个是CLowLevelMonster*)。谨记:如果它们的返回值一样,则退化为了工厂模式,而不是抽象工厂模式!之所以叫抽象工厂,是因为抽象工厂一旦实例化,则可以实例化为多种类型的工厂,每一种工厂用来生产相关,但不同类型的对象,各种实例化出来的工厂之间的差别在于,它们生产这些对象的方式或者结果不同!游戏需要三种级别的怪物,则每个工厂按自己的方式去生成它们,(“不同的方式”体现在,每个工厂生产出的怪物(即使级别相同,但)不同)
三,反过来再谈继承方案
在文章的最开始,我们提出了两种继承策略,
a),游戏维护一个怪物超类,所有怪物直接继承,将这些子类再作为超类,供“三个级别”怪物类继承,类结构大致为:
CMonster
CHouse :public CMonster,......
CHighLevelHouse :public CHouse,CLowLevelHouse :public CHouse.....
b),游戏维护一个怪物超类,让三个级别继承此超类,再分别让每个怪物继承之,类结构大致为:
CMonster
CHighLevelMonster : public CMonster,CLowLevelMonster : public CMonster.......
CHighLevelHouse : public CHighLevelMonster,CLowLevelHouse: public CLowLevelMonster....
我们前面使用的抽象工厂模式正是基于的第二种继承方案,所以被抽象的工厂是“怪物工厂”,而不是“级别工厂”,然后在各个工厂里生产出的是不同级别的怪物。若我们使用第一种继承方案,由正好对应了将工厂归类中的 a) ,定义三个“级别工厂”:高级怪物工厂,中级怪物工厂,低级怪物工厂,让每个工厂去生产所有类型的怪物此时,形成的是“级别工厂”,在每个级别工厂里,生产出的是级别相同但属性不同的怪物,可能的代码是这样的:
CFactory
{
virtual CMonkey* getMonkey();
virtual CHouse* getHouse();
virtual CSpiter* getSpiter();
.............//省略掉其它怪物
}
CHighLevelFactory : public CFactory
{
CMonkey* getMonkey();
CHouse* getHouse();
CSpiter* getSpiter();
....................
}
根据之前的讨论,b)方法的抽象工厂模式较好,所以继承方案应该是后一种要好一些
四,结语
经过之前的分析,对工厂模式和抽象工厂模式有了一定的认识,可能还存在不足之处,望读者指出,一同探讨!