而本节要讨论的工厂方法模式是简单工厂模式的进一步抽象化和推广。它比简单工厂模式聪明的地方在于, 它不再作为一个具体的交通警察的面貌出现,而是以交通警察局的面貌出现。它把具体的车辆交通交给下面去管理。换言之,工厂方法模式里不再只由一个工厂类决定那一个产品类应当被实例化,这个决定被交给子类去作。处于工厂方法模式的中心位置上的类甚至都不去接触那一个产品类应当被实例化这种细节。这种进一步抽象化的结果,是这种新的模式可以用来处理更加复杂的情形。
为什么需要工厂方法模式
现在,让我们继续考察我们的小花果园。在<简单工厂模式>一节里,我们在后花园里引进了水果类植物, 构造了简单工厂模式来处理, 使用一个FruitGardener类来负责创立水果类的实例。见下图。
![]() 图1. 简单工厂模式。FruitGardener掌握所有水果类的生杀大权。 |
在这一节里,我们准备再次引进蔬菜类植物,比如
西红柿 (Tomato)
土豆 (Potato)
西芥兰花 (Broccoli)
蔬菜与花和水果当然有共同点,可又有不同之处。蔬菜需要喷洒(dust)杀虫剂(pesticide)除虫, 不同的蔬菜需要喷洒不同的杀虫剂,等等。怎么办呢?
那么,再借用一下简单工厂模式不就行了? 再设计一个专管蔬菜类植物的工厂类,比如
![]() 图2. 简单工厂模式。VeggieGardener掌握所有蔬菜类的生杀大权 |
这样做一个明显的不足点就是不够一般化和抽象化。在FruitGardener和VeggieGardener类之间明显存在很多共同点, 这些共同点应当抽出来一般化和框架化。这样一来,如果后花园的主人决定再在园子里引进些树木类植物时, 我们有框架化的处理方法。本节所要引入的工厂方法模式就符合这样的要求。
简单工厂模式的回顾
有必要首先回顾一下简单工厂模式的定义,以便于比较。
![]() 图3. 简单工厂模式的类图定义 |
从上图可以看出,简单工厂模式涉及到以下的角色
工厂类 (Creator)
担任这个角色的是工厂方法模式的核心,是与应用程序紧密相关的,直接在应用程序调用下,创立产品实例的那个类。
工厂类只有一个,而且是实的。见下面的位图
![]() |
产品 (Product)
担任这个角色的类是工厂方法模式所创立的对象的父类,或它们共同拥有的接口。
实产品 (Concrete Product)
担任这个角色的类是工厂方法模式所创立的任何对象所属的类。
实产品类可以是分布在一维数轴上的分立点 1,2,3,...中的任何一个,见下面的位图
工厂方法模式的定义
从上图可以看出,工厂方法模式涉及到以下的角色 抽象工厂接口(Creator) 担任这个角色的是工厂方法模式的核心,它是与应用程序无关的。任何在模式中创立对象的工厂类必须实现这个接口。 实工厂类 (Conrete Creator) 担任这个角色的是与应用程序紧密相关的,直接在应用程序调用下,创立产品实例的那样一些类。 实工厂类可以是分布在一维数轴上的分立点 1,2,3,...中的任何一个,见下面的位图
产品 (Product) 担任这个角色的类是工厂方法模式所创立的对象的父类,或它们共同拥有的接口。 实产品 (Concrete Product) 担任这个角色的类是工厂方法模式所创立的任何对象所属的类。 实产品类可以是分布在二维平面上的分立点 (1,1), (1,2), (2,3),...中的任何一个,见下面的位图
工厂方法模式和简单工厂模式在定义上的不同是很明显的。工厂方法模式的核心是一个抽象工厂类,而不像简单工厂模式, 把核心放在一个实类上。工厂方法模式可以允许很多实的工厂类从抽象工厂类继承下来, 从而可以在实际上成为多个简单工厂模式的综合,从而推广了简单工厂模式。 反过来讲,简单工厂模式是由工厂方法模式退化而来。设想如果我们非常确定一个系统只需要一个实的工厂类, 那么就不妨把抽象工厂类合并到实的工厂类中去。而这样一来,我们就退化到简单工厂模式了。 与简单工厂模式中的情形一样的是,ConcreteCreator 的factory() 方法返还的数据类型是一个接口 PlantIF,而不是哪一个具体的产品类。这种设计使得工厂类创立哪一个产品类的实例细节完全封装在工厂类内部。 工厂方法模式又叫多形性工厂模式,显然是因为实工厂类都有共同的接口,或者都有共同的抽象父类。 |
工厂方法模式在小花果园系统中的实现
好了,现在让我们回到小花果园的系统里,看一看怎样发挥工厂方法模式的威力,解决需要接连不断向小花果园引进不同类别的植物所带来的问题。
首先,我们需要一个抽象工厂类,比如叫做 Gardener,作为两个实工厂类 FruitGardener 及 VeggieGardener 的父类。 Gardener 的 factory() 方法可以是抽象的,留给子类去实现,也可以是实的,在父类实现一部分功能,再在子类实现剩余的功能。我们选择将 factory() 做成抽象的,主要是因为我们的系统是一个示范系统,内容十分简单,没有要在父类实现的任何功能。
![]() 图5. 工厂方法模式在小花果园系统中的实现 |
抽象工厂类 Gardener 是工厂方法模式的核心,但是它并不掌握水果类或蔬菜类的生杀大权。相反地,这项权力被交给子类,即 VeggieGardener 及 FruitGardener。
package com.javapatterns.factorymethod; abstract public class Gardener { public abstract PlantIF factory(String which) throws BadFruitException; } |
代码清单1. 父类 Gardener。
package com.javapatterns.factorymethod; public class VeggieGardener extends Gardener { public PlantIF factory(String which) throws BadPlantException { if (which.equalsIgnoreCase("tomato")) { return new Tomato(); } else if (which.equalsIgnoreCase("potato")) { return new Potato(); } else if (which.equalsIgnoreCase("broccoli")) { return new Broccoli(); } else { throw new BadPlantException("Bad veggie request"); } } } |
代码清单2. 子类 VeggieGardener。
package com.javapatterns.factorymethod; public class FruitGardener extends Gardener { public PlantIF factory(String which) { if (which.equalsIgnoreCase("apple")) { return new Apple(); } else if (which.equalsIgnoreCase("strawberry")) { return new Strawberry(); } else if (which.equalsIgnoreCase("grape")) { return new Grape(); } else { throw new BadPlantException("Bad fruit request"); } } } |
代码清单3. 子类 FruitGardener。
package com.javapatterns.factorymethod; public class Broccoli implements VeggieIF, PlantIF { public void grow() { log("Broccoli is growing..."); } public void harvest() { log("Broccoli has been harvested."); } public void plant() { log("Broccoli has been planted."); } private static void log(String msg) { System.out.println(msg); } public void pesticideDust(){ } } |
代码清单4. 蔬菜类 Broccoli。其它的蔬菜类与 Broccoli 相似,因此不再赘述。
package com.javapatterns.factorymethod; public class Apple implements FruitIF, PlantIF { public void grow() { log("Apple is growing..."); } public void harvest() { log("Apple has been harvested."); } public void plant() { log("Apple has been planted."); } private static void log(String msg) { System.out.println(msg); } public int getTreeAge(){ return treeAge; } public void setTreeAge(int treeAge){ this.treeAge = treeAge; } private int treeAge; } |
代码清单5. 水果类 Apple。与<简单工厂模式>一节里的Apple类相比,唯一的区别就是多实现了一个接口 PlantIF。其它的水果类与 Apple 相似,因此不再赘述。
package com.javapatterns.factorymethod; public class BadPlantException extends Exception { public BadPlantException(String msg) { super(msg); } } |
代码清单6. 例外类 BadPlantException。
工厂方法模式应该在什么情况下使用
既然工厂方法模式与简单工厂模式的区别很是微妙,那么应该在什么情况下使用工厂方法模式,又应该在什么情况下使用简单工厂模式呢?
一般来说,如果你的系统不能事先确定那一个产品类在哪一个时刻被实例化,从而需要将实例化的细节局域化,并封装起来以分割实例化及使用实例的责任时,你就需要考虑使用某一种形式的工厂模式。
在我们的小花果园系统里,我们必须假设水果的种类随时都有可能变化。我们必须能够在引入新的水果品种时,能够很少改动程序,就可以适应变化以后的情况。因此,我们显然需要某一种形式的工厂模式。
如果在发现系统只用一个产品类等级(hierarchy)就可以描述所有已有的产品类,以及可预见的未来可能引进的产品类时,简单工厂模式是很好的解决方案。因为一个单一产品类等级只需要一个单一的实的工厂类。
然而,当发现系统只用一个产品类等级不足以描述所有的产品类,包括以后可能要添加的新的产品类时,就应当考虑采用工厂方法模式。由于工厂方法模式可以容许多个实的工厂类,以每一个工厂类负责每一个产品类等级,因此这种模式可以容纳所有的产品等级。
在我们的小花果园系统里,不只有水果种类的植物,而且有蔬菜种类的植物。换言之,存在不止一个产品类等级。而且产品类等级的数目也随时都有可能变化。因此,简单工厂模式不能满足需要,为解决向题,我们显然需要工厂方法模式。
关于模式的实现
在实现工厂方法模式时,有下面一些值得讨论的地方。
第一丶在图四的类图定义中,可以对抽象工厂(Creator) 做一些变通。变通的种类有
抽象工厂(Creator) 不是接口而是抽象类。一般而言,抽象类不提供一个缺省的工厂方法。 这样可以有效地解决怎样实例化事先不能预知的类的问题。
抽象工厂(Creator) 本身是一个实类,并提供一个缺省的工厂方法。 这样当最初的设计者所预见的实例化不能满足需要时,后来的设计人员就可以用实工厂类的factory() 方法来置换(Override))父类中factory()方法。
第二丶在经典的工厂方法模式中,factory()方法是没有参量的。在本文举例时加入了参量,这实际上也是一种变通。
第三丶在给相关的类和方法取名字时,应当注意让别人一看即知你是在使用工厂模式。
COM技术架构中的工厂方法模式
在微软(Microsoft)所提倡的COM(Component Object Model)技术架构中, 工厂方法模式起着关键的作用。
在COM架框里,Creator接口的角色是由一个叫作IClassFactory的COM接口来担任的。而实类ConcreteCreator的角色是由实现IClassFactory接口的类CFactory(见下图)来担任的。一般而言,对象的创立可能要求分配系统资源,要求在不同的对象之间进行协调等等。因为IClassFactory的引进,所有这些在对象的创立过程中出现的细节问题, 都可以封装在一个实现IClassFactory接口的实的工厂类里面。这样一来, 一个COM架构的支持系统只需要创立这个工厂类CFactory的实例就可以了。
![]() 图6. 微软(Microsoft)的COM(Component Object Model)技术架构是怎样工作的。 |
在上面的序列活动(Sequence Activity)图中,用户端调用COM的库函数CoCreateInstance。 CoCreateInstance在COM架框中以CoGetClassObject实现。 CoCreateInstance会在视窗系统的Registry里搜寻所要的部件(在我们的例子中即CEmployee)。如果找到了这个部件,就会加载支持此部件的DLL。当此DLL加载成功后, CoGetClassObject就会调用DllGetClassObject。后者使用new操作符将工厂类CFactory实例化。
下面,DllGetClassObject会向工厂类CFactory搜询IClassFactory接口,返还给CoCreateInstance。 CoCreateInstance接下来利用IClassFactory接口调用CreateInstance函数。此时,IClassFactory::CreateInstance调用new操作符来创立所要的部件(CEmployee)。此外,它搜询IEmployee接口。在拿到接口的指针后, CoCreateInstance释放掉工厂类并把接口的指针返还给客户端。
客户端现在就可以利用这个接口调用此部件中的方法了。
EJB技术架构中的工厂方法模式
升阳(Sun Microsystem)倡导的EJB(Enterprise Java Beans)技术架构是一套为爪哇语言设计的, 用来开发企业规模应用程序的组件模型。我们来举例看一看EJB架构是怎样利用工厂方法模式的。请考察下面的序列活动图。
![]() 图7. 在升阳所提倡的EJB技术架构中, 工厂方法模式也起着关键的作用 |
在上面的图中,用户端创立一个新的 Context 对象,以便利用 JNDI 伺服器寻找 EJBObject。在得到这个 Context 对象后,就可以使用 JNDI 名, 比如"Employee", 来拿到 EJB 类 Employee 的 Home 接口。使用 Employee 的 Home 接口,客户端可以创立 EJB 对象,比如 EJB 类 Employee 的实例 emp, 然后调用 Employee 的各个方法。
// 取到 JNDI naming context Context ctx = new InitialContext (); // 利用ctx 索取 EJB Home 接口 EmployeeHome home = (EmployeeHome)ctx.lookup("Employee"); // 利用Home 接口创立一个 Session Bean 对象 // 这里使用的是标准的工厂方法模式 Employee emp = home.create (1001, "John", "Smith"); // 调用方法 emp.setTel ("212-657-7879"); |
代码清单7. EJB架构中,Home接口提供工厂方法以便用户端可以动态地创立EJB类Employee的实例。
JMS技术架构中的工厂方法模式
JMS定义了一套标准的API,让爪哇语言程序能通过支持JMS标准的MOM(Message Oriented Middleware 面向消息的中间伺服器)来创立和交换消息(message)。我们来举例看一看JMS(Java Messaging Service)技术架构是怎样使用工厂方法模式的。
![]() 图8. 在JMS技术架构中, 工厂方法模式无处不在 |
在上面的序列图中,用户端创立一个新的 Context 对象,以便利用 JNDI 伺服器寻找 Topic 和 ConnectionFactory 对象。在得到这个 ConnectionFactory 对象后, 就可以利用 Connection 创立 Session 的实例。有了 Session 的实例后,就可以利用 Session 创立 TopicPublisher的实例,并利用Session创立消息实例。
Properties prop = new Properties(); prop.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory"); prop.put(Context.PROVIDER_URL, "file:C:/temp"); // 取到 JNDI context Context ctx = new InitialContext(prop); // 利用ctx 索取工厂类的实例 Topic topic = (Topic) ctx.lookup("myTopic"); TopicConnectionFactory tcf = (TopicConnectionFactory) ctx.lookup("myTCF"); // 利用工厂类创立Connection,这是典型的工厂模式 TopicConnection tCon = tcf.createTopicConnectoin(); // 利用Connection创立Session的实例,又是工厂模式 TopicSession tSess = tCon.createTopicSession(false, Session.AUTO_ACKNOWLEDGE); // 利用Session创立Producer的实例,又是工厂模式 TopicPublisher publisher = tSess.createPublisher(topic); // 利用Session创立消息实例,又是工厂模式 TextMesage msg = tSess.createTextMessage("Hello from Jeff"); //发送消息 publisher.publish(msg); |
代码清单8. JMS架构中,工厂模式被用于创立 Connection, Session, Producer 的实例
问答题
第1题、在这一节和上一节的类图中,我注意到Apple类的类图与Strawberry类的类图有一点点不同。在Apple类的类图左上角有一个夹子样的标识。请问这个标识代表什么意思。
第2题、在这一节的类图4中,我注意到 ConcreteProduct 类只出现一次,但实现 Product 接口的类实际上可以有很多。这是否可以用在联接 Product 和 ConcreteProduct 之间的线旁注上 1,2,... 表示呢? 记得我在UML图中曾见过这种记号。
第3题、请问在本节的小花果园系统的源代码清单4里,Broccoli 类实现两个接口,VeggieIF 和 PlantIF。只有 PlantIF 才与工厂模式有关。为什么不把 VeggieIF 接口合并到 PlantIF 接口中去?
第4题、请问在工厂方法模式中,产品(Product) 何时应是抽象类,何时应是接口?
第5题、请问在工厂方法 (factory())中,为什么要使用 if 语句作过程性判断来决定创立哪一个产品类,而不使用多形性原则 (Polymorphsm) 来创立产品类?
问答题答案
第1题、Apple类有性质(property),而Strawberry类没有性质。
一个类的成员变量叫做属性(attribute)。性质与属性的区别在于性质是带着一套取值丶赋值方法的属性。一个类有了属性,其类图左上角就会有一只夹子。有些人认为,一个爪哇类有了属性才能被称做爪哇豆(Java Bean)。这只夹子就表示这个类是一只豆。
一个企业爪哇豆,或 EJB (Enterprise JavaBean) 的类图左上角也会有一只夹子,夹子上面有一个E字以示与普通的爪哇豆的不同(请见下图)。
![]() |
第2题、不能。在图4中联接 Product 和 ConcreteProduct 之间的线有两条,一条表示两者之间的推广关系 (即有向上箭头的),另一条表示两者之间的关联关系(即有向下箭头的)。在推广关系线旁写数字没有意义。在关联关系线旁写数字是有意义的,类旁的数字可以表明类的实例的数目。
原来的问题是关于类的数目而不是类的实例的数目,因此是错的。
没有任何必要用数字标明这一点,而且UML也不提供这种标记。
第3题、在面向对象的编程,特别是爪哇语言的编程中,接口常常用来标志一种身份(identity)。 VeggieIF 和 PlantIF 接口代表两种不同的身份。VeggieIF 表明 Broccoli 类属于蔬菜类等级, PlantIF 接口表明 Broccoli 类属于工厂的产品类。
因此,虽然把两个接口合并起来可能在功能上是行得通的,在原则上是不应鼓励这样做的。
第4题、在工厂方法模式中,产品(Product)可以永远是抽象类。但在一些情形下可仪简化为接口。
如果所实产品类( Concrete Product) 之间有共同的逻辑,这部分公有的代码就应当转移到产品 (Product) 中去,这样产品就必须是抽象类而不可能是接口。
反过来,如果所实产品类( Concrete Product) 之间没有任何共同的逻辑,那么产品(Product)就没有任何逻辑代码,它就应当被作为接口,而不是抽象类。但这不是必须的,仅是建议而已。
第5题、多形性原则 (Polymorphism) 是在对象被创立之后才存在的,因此不能使用多形性来创立对象。factory() 方法必然是非常过程性 (procedural)的。