--“Ignore how they were created”,工厂方法模式应用
说起这个工厂模式,一时还真不知道该如何说起。反正这是我的开发日志,不提理论的东西,理论的东西那里都有,我只想把具体实践记录下来给师弟师妹们作个参考,积累点经验。所有这些文字都是集中讲一点――“在什么情况下为什么用某种模式好,为什么好,为什么在那种情况下能想起来用?”。
研究生院项目中“明显”使用了“工厂方法模式”。其实在遇到具体问题的时候,即使我们不知道有这个模式存在,我们也肯定会造一个类似的东西出来。但是,肯定没有书上论述的那么好,那么全面。我想这就是看书的好处吧。
工厂方法出现的必然(我的理解,一个很狭隘并幼稚理的人的理解)
刚开始使用这个东西的时候,只是感觉是单纯的一种模式,用于创建需要的对象。
但是随着使用和思考的深入,越发发现它给我的启示不只在于单纯的对象创建,而是告诉我应该怎么理解“产品”,怎么得到“产品”,怎么消费“产品”,以至于以后怎么设计“产品”。
下面这个线索是我对它出现必然性的理解:
1. “针对接口编程”
这是OO世界中经典的规范,不管你主动还是被动,你天天都在用这个东西。
接口是共性的表示,在对象的世界中,共性和个性的辩证关系是最重要的关系。在万千的对象中,通过它们之间的共性和个性,可以形成最基本对象层级架构。
假设我们的讨论域中有以下一些对象:“学生”、“大学生”、“小学生”、“中学生”;我们不用细想,学过一天OO的人都可以为这些耳熟能详的对象们,通过个性和共性的关系得出下面的结构图。
把这些对象之间的关系定义成这样是顺理成章的。
下一步肯定是让客户端“使用”这个接口啦。也就是如下图:
2. 接口和具体类的矛盾
勿庸置疑,我们只希望Client跟接口Student打交道,让它根本就不知道Student有哪些子类,绝对不希望直接跟它们打交道。
但这里出现的困难是,接口都是“假”的,都是由具体类upcast的。
如果Client要使用接口Student,Client中必须会出现下面的代码:
Student marco = new Small_Student();
只要一出现这个代码,就说明Client不只跟Student打交道了,它知道了Small_Student类,这违反了我们预先的想法。
3. 找“人”帮我去创建“接口对象”
从上图体现出来的结构看,Client只想跟Student打交道的目的是实现不了的了。
最简单的方法就是找另外的“帮手”去帮我生成这个“接口对象”。这个帮手它知道“接口对象”的具体类型,但是它为客户端提供的却一定是“接口类型”。这就符合我们的要求了。如图:
这样,Client就可以既用到了“Student接口对象”,又不用因为“只有具体类才能创建对象”的规则,而必须对其子类结构有完全的了解。它成功的解决了2中的矛盾。
而“负责创建具体类对象的任务”全部都落在了这个“帮手”身上,这个“帮手”(Student_Factory)就是工厂模式中的工厂类,更具体的说,它就是“简单工厂模式”中的“简单工厂类”。
我觉得,即使一点都不知道工厂模式,一旦我遇到了2里说的矛盾,我也会用这样的方法处理。
4. 这个“帮手”不符合“开-闭原则”
这个帮手的确不错了,而且一跃成为系统中最重要的对象了,所有“创建具体类的逻辑”都放进去了,也就是因为重要,万一挂了不就惨了。
再者,它不符合“开-闭”原则,我不能在不修改这个帮手的情况下添加任何一个产品。在这个例子中就是,如果那天我有病非要加一个“幼儿园学生”进来,那您就必须修改这个“帮手”的代码了,这个“帮手”现在就变成Version2(如下图)了,这个二版的帮手,可以在“代码”层实现对一版(还没有添加幼儿园学生之前)的通用,但这种保证在“开-闭”原则看来,还是不够的,不保险的,它要的是在类的结构上的保证。声明一下,这是我很感性的理解,不正确的可能性很高。
5. 让“帮手”也多态
这里可以尝试让“帮手”也多态一下,这样“每种学生类创建的任务”都被分派到了多态出来的类中去了。这时,再有新的学生类型加进来,添加一个对应的帮手就可以了。这样虽然类多了一些,但是符合“开-闭”原则,书上称之为“工厂方法模式”。如图:
假如Client现在要使用一个小学生,代码如下:
2 Student_Factory factory = new Small_Student_Factory();
3 // 求助这个帮手,帮我创建一个
4 Student primaryStudent = factory.produce();
5 // 这时就可以使用这个小学生了,让它玩玩游戏吧
6 primaryStudent.playGames();
7
在这里还是要强调两点:
n 虽然实际上Client的确使用了一个小学生对象(Small_Student),但这里Client也认为它就是Student对象,这里一定要用Student接口来隐藏它的具体类。
n 另外,却不需要用Student_Factory这个接口来隐藏它的具体类,因为,Client实际就是通过“选择它的具体类”这招儿来“选择创建的学生类型”。这里的Student_Factory更多的功能不是“隐藏”具体类,而是“规范”具体类。
项目实践
扯淡到此,该联系我们的项目啦。
由于是做研究生院的项目,其中巨大的需求就是要让同学能在网上提交各种申请单,申请退学的,申请转专业的,申请复学的,申请保留学籍的,除了申请女朋友的外,应有尽有。 对于这些单子,用最最基本OO思维,根据共性个性分析方式,抽象出一个申请单接口,和若干的具体类。
当然,除了概念上感性上吻合以外,在项目中它们也要“真”的有巨大的共性才行,如都要提交,修改,删除,审核,打印等功能。
靠,既然都这样了,肯定就用一接口规定它了。
想到这里,加上有了上面的思考,不用说了,就用工厂方法模式啦。
图中Ent_Shift就是申请单接口。等于前面分析的Student接口。还可以看到有很多具体类,前面例子中是代表各种学生,这里就是代表各种申请单。
当然,这里还有很多工厂,也就是前面一直叫的“帮手”。
Bean_Shift就是工厂接口,相当于前面的Student_Factory接口。还有很多的具体类就是生产各种申请单的工厂类。
下面就是使用“申请单工厂方法模式”的一段客户端代码:
2 Ent_Shift es = null ;
3 // 声明申请单工厂接口
4 Bean_Shift bs = null ;
5 // 根据传入的申请单类型数字身成对应的申请单工厂
6 switch (shiftTypeID) {
7 case Bean_Shift.SHIFT_CHANGETEAAP :
8 bs = new Bean_Shift_CHANGETEAAP();
9 break;
10 case Bean_Shift.SHIFT_RESERVEAP :
11 bs = new Bean_Shift_RESERVEAP();
12 break;
13 case Bean_Shift.SHIFT_RENEWAP :
14 bs = new Bean_Shift_RENEWAP();
15 break;
16 //省略了别的申请单……………………
17 default :
18 this.forwardErr(req, resp, "所选择的异动类别不存在");
19 }
20
21 try {
22 //调用工厂接口的生产方法
23 es = bs.getBlankShift(stuID);
24
25 } catch (Exception e) {
26 this.forwardErr(req, resp, DB_ERROR);
27 }
28 // 调用单子的提交方法
29 es.submit(req);
30
31 // 发给页面去显示
32 es.fowrardWithSessionObject(
33 req,
34 resp,
35 Ent_Shift.nameInSessionAndRequest);
36
升华
个人比较同意《Design Pattern Explained》中作者讲的,要用好很多的模式,其中都有一个思路,就是用接口或抽象类来隐藏子类的不同。
我每当看到这时,老是会被一种思路困扰――“new只能new具体类啊,这咋能隐藏呢,这隐藏还有什么用呢?”。
作者仿佛也曾经有过我的这个傻B苦恼,它的解决方法就是:根本在使用对象的时候,特别是设计阶段,尽量不去想对象是在那里被new的。他认为:反正有了工厂模式后,你总有办法把他们new出来的。
所以,我用了工厂模式后更发的启发是:以后设计的时候不要想一个Client怎么创建一个对象,尽管放心大胆的先继续想,直接使用就好了。反正最后我还有工厂模式呢。
因此俺的副标题才是“Ignore how they were created”,呵呵。
MARCO ZHANG 2006年2月16日3:52:10