- 模式:工程化实现及扩展(设计模式Java 版)
- 王翔 孙逊
- 7735字
- 2020-08-28 05:18:36
4.1 说明
Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses.
——Design Patterns : Elements of Reusable Object-Oriented Software
工厂类型要解决的是怎么new()的问题。
读者可能觉得这有什么关系呢?我需要用某个对象直接new()出来就可以了,逻辑上需要几个苹果,我就new()几个苹果。例如:“Apple apple = new Apple()”。
但事情远没有那么简单,当你把代码提交后,没几天测试部门告诉你编译时出现错误。奇怪?我什么都没做,这不是“人在家中坐,麻烦天上来”吗?过去一看,原来是开发Apple 类的同事为了让对象看上去更有质感,增加了锥光效果,构造函数签名有点变化。其实需要你做的事情不多,就是略微修改后重新编译。但麻烦才刚刚开始,以后各种麻烦会不断出现——大家觉的苹果太单调,增加了樱桃类(Cherry)、芒果类(Mango)……虽然每件事都不大,就是多new()几次,多编译几次,但麻烦总是不断,别人代码动一动你也要跟着一起动。
小事情带来大麻烦,为什么?
问题的症结在于我们对对象的创建过程没有控制,因此我们就从这里入手,看看怎么解决。本章的工厂方法模式和后续几章的创建型模式,就要集中解决这类问题。
笔者一直认为工厂方法模式是GOF 23个模式中最具启发效果的,它告诉我们可以通过增加新的对象专门管理“变化”。例如,我们为了解决new()引起的“变化”,就引入了工厂类型,由新增的工厂类型专门处理new()相关的“变化”,确保客户程序不受这些变化的直接影响。
4.2 简单工厂
4.2.1 最简单的工厂类
作为后面要介绍的工厂方法模式和抽象工厂模式的 “预备”内容,我们首先用最朴实的方式完成一个工厂,通过它分析工厂与抽象类型间的构造关系,其代码如下:
Java
interface Product {} class ProductA implements Product{} class ProductB implements Product{}
Unit Test
public class SimpleFactoryFixture {
class Factory{ /** * 由工厂类决定到底实例化哪个子类 * @return 符合客户程序要求的子类实例 */ public Product newInstance(){ return new ProductA(); } }
@Test public void testSimpleFactory(){ assertTrue(new Factory().newInstance() instanceof ProductA); } }
采用工厂类型与直接使用new()有什么不同?
●我们对待加工的类型做了抽象(Interface Product)。
●定义了专门负责构造的类型——Factory。
●通过Product接口隔离了客户程序与具体类型(ProductX)的依赖关系,在客户程序视野内根本就不存在ProductX,只有Product接口,而Product接口相对于各个具体的ProductX而言更加稳定,不容易出现“变化”。
●即使ProductX增加、删除方法或属性,也无碍大局,只要按照要求实现Product接口就可以,客户程序无须关心ProductX的变化。Factory将ProductX的变化与客户程序隔开了。如果站在工程的角度,上面的示例还有些不尽如人意的地方:
●对于开发人员而言,较之直接实例化new ProductX()要多写一个工厂类型(Factory),当ProductX变化的时候,Factory类型需要反复编译,如果没有其他控制措施,客户程序也“闲不下来”,它也要经常“陪同”编译。
●好的需求分析师可以在实施之前分析清楚85%的需求,好的架构师在把这些需求转换为实际技术框架的时候大概能做到忠于90%的需求,作为开发人员,设计的时候能够详细处理95%的内容就很不错了——100% - 85%×90%×95% = 27.3%,也就是说,即便你身在一个精英团队,也可能有1/4的内容到了编码的时候还无法得到准确分析,ProductX的变化在所难免。
此外,还有一个效率问题。如果构造一个Factory实例,并用它获取一个抽象类型实例后就不再使用,资源上有些浪费,尤其当这个过程非常频繁的时候。项目中可以通过如下几种方式解决:
●把工厂实例作为参数注入到操作中,而不是在每个方法内部自行创建,如下面的ProductConsumer类就定义为通过构造注入方式获得工厂类型实例:
Java
class ProductConsumer{
Factory factory;
/** * 从外部注入的引用,而不是每次操作时都要获取一个新的Factory类型实例 * @param factory 工厂类型实例 */ public ProductConsumer(Factory factory){this.factory = factory;}
public void handleWithProductMethod1(){ Product product = factory.newInstance(); // 其他需要依赖于Product的操作 }
public void handleWithProductMethod2(){ Product product = factory.newInstance(); // 其他需要依赖于Product的操作 } }
●把工厂设计为单件(Singleton)方式,因为工厂的职责相对单一,所有客户程序共享唯一的一个Factory类型实例。
●使用静态类,构造静态工厂,将工厂所有负责创建类型实例的方法都声明为Static。虽然在经典的设计模式书籍示例中并没有采用,但它未尝不是一个有效节省资源的途径,而且它给实例化各种抽象对象的方法起了一个名字,客户程序可以通过这些命名上更富含义的方法获得各种抽象对象。
不过切记:静态类不能被继承,所以看起来静态类更像以前的API集合,有点不那么“面向对象”的味道。其示例代码如下:
Java 简单静态工厂
class StaticFactory{
public enum Category{ A, B }
private static final Category DEFAULT_CATEGORY = Category.A;
public static Product newInstance(Category category){ switch(category){ case A: return new ProductA(); case B: return new ProductB(); default: throw new UnsupportedOperationException (); } }
public static Product newInstance(){ return newInstance(DEFAULT_CATEGORY); } }
Unit Test
@Test public void testStaticFactory()throws ClassNotFoundException{ assertTrue(StaticFactory.newInstance() instanceof ProductA); assertTrue(StaticFactory.newInstance(Category.A) instanceof ProductA); assertTrue(StaticFactory.newInstance(Category.B) instanceof ProductB); }
静态工厂的静态结构如图4-1所示。
图4-1 静态工厂的静态结构
4.2.2 简单工厂的局限性
上例的简单工厂(和简单静态工厂)大体解决了外部new()的问题,它把目标实例的创建工作交给一个外部的工厂完成,是设计思想模式化的一个很不错的引子。但如果应用中需要工厂的类型非常多,各个工厂类型的职责又雷同——就是一个new()的替代品,此时用我们面向对象的“嗅觉”不难发现这时候就需要进一步抽象了,于是出现了新的发展:工厂方法模式和抽象工厂模式。
4.3 经典回顾
作为简单工厂的一个扩展,工厂方法模式的意图就是把类的实例化过程延迟到子类,将new()的工作交给工厂完成。同时,对工厂自身进行抽象,然后客户程序基于工厂的抽象行为构造所需的类型实例。
它适合如下情景:
●客户程序需要隔离自己与待new()类型间的耦合关系。
●开发阶段客户程序还无法明确预知需要new()的具体类型。
●客户程序将new()的工作交给外部对象完成。
●各种需要new()的对象虽然可以统一抽象为某个接口,但它们的继承关系太过烦琐,层次也比较复杂,这时同样可以通过工厂方法解决这个问题。工厂方法模式主要有4个角色。
●抽象产品类型(Product):对待构造类型抽象后的接口。
●具体产品类型(ConcreteProduct):具体待构造类型,需要由工厂延迟实例化。
●工厂类型的抽象接口(Factory)。
●具体工厂类型(ConcreteFactory)。
经典的工厂方法模式的静态结构如图4-2所示。
图4-2 经典的工厂方法模式的静态结构
示例代码如下:
Java
/**抽象的工厂类型描述*/ public interface Factory { /** * 工厂类型的职责——构造实例 * @return 客户程序所需的实例 */ Product newInstance(); }
Java 两个具体工厂类型
class FactoryA implements Factory{ @Override public Product newInstance() { return new ProductA(); } }
class FactoryB implements Factory{ @Override public Product newInstance() { return new ProductB(); } }
Java Unit Test
@Test public void testClassicFactoryMethod(){ Factory factory = new FactoryA(); assertTrue(factory.newInstance() instanceof ProductA); factory = new FactoryB(); assertTrue(factory.newInstance() instanceof ProductB); }
通过这个例子可以看到,当客户程序作为Product消费者的时候可以把频繁变化的某个ProductX隔离在自己的视线之外,自身不会受到它的影响。
4.4 解耦工厂类型与客户程序
虽然经典工厂方法模式通过Product、ConcreteProduct、Factory和ConcreteFactory这4个角色解决了客户程序对Product的获取问题,但没有介绍如何把Factory放到客户程序中,而且大部分介绍设计模式的书籍会给出大致如下示例代码:
Java
class Client{ public void someMethod(){ //获得了抽象Factory的同时与FactoryA产生依赖 Factory factory = new FactoryA(); Product product = factory.newInstance(); // 后续操作仅依赖抽象的Factory和Product … } }
这就产生问题了,如果让客户程序直接来new()某个Concrete Factory,由于Concrete Factory依赖于Concrete Product,因此还会形成客户程序对具体产品类型的间接依赖关系,背离了这个模式的初始意图。
采用前面提到的“依赖注入”办法,把客户程序需要的Factory给它注入进去。该方式的静态结构要在经典工厂方法模式上做一些修改,如图4-3所示。
图4-3 增加了装配对象的工厂方法静态结构
实际工程中很多时候不得不做这项工作——将Concrete Product与客户程序分离,其原因是我们不想因上游开发人员微小的修改就迫使我们重新检查代码,编译、重新单元测试后再记录,这种活动在项目开发的中、后期常常是非常令人厌恶的。
除了采用依赖注入的方式外,我们可以“递归套用”工厂模式的思路,先设计一个构造Factory接口的工厂,然后所有客户程序从这个“构造工厂的工厂”中获得满足Factory接口要求的某个工厂类型实例,然后再通过它获得所要的Product产品。
实际项目中,由于“构造工厂的工厂”的职责非常单一,而且面向系统全局,因此经常会将它设计为静态工厂的形式。
Java 负责构造某类产品的工厂类型的抽象定义
public interface Factory<T>{ T newInstance(); }
Java 构造工厂的工厂类型
/** * 创建工厂的工厂 * 此处定义为静态形式的工厂 */ public class Factories{ /*** 登记待构造产品类型及相应工厂类型*/ private static final Map<Class<?>,Class<?>> registry……;
/*** 封闭 Factories 类型的构造函数*/ private Factories(){}
/** * 配置待构造产品类型及相应工厂类型 * @param productType 待构造产品类型 * @param factoryType 相应工厂类型 */ public static void config( Class<?> productType,Class<?> factoryType){ registry.put(productType,factoryType); }
/** * 构造实际负责生产某类产品的工厂类型实例 * @param <T> 工厂负责构造的抽象产品类型 * @param productType 抽象的产品类型 * 这个参数本可以省略 * 但因为Java泛型会在运行过程中执行类型擦除 * 所以需要增加该参数 * @return 构造实际负责生产某类产品的工厂类型实例 */ @SuppressWarnings("unchecked") public static <T> Factory<T> newFactory( Class<?> productType) throws ClassNotFoundException, InstantiationException, IllegalAccessException{
if(!registry.containsKey(productType)) throw new ClassNotFoundException(); return (Factory<T>) (registry.get(productType).newInstance()); } }
Java 定义需要加工各类产品类型
public interface X {} public class X1 implements X{} public class X2 implements X{} public class X3 implements X{} public interface Y {} public class Y1 implements Y{} public class Y2 implements Y{}
Java 定义加工各类产品的工厂类型
public class FactoryXA implements Factory<X>{ @Override public X newInstance() { return new X1(); } }
public class FactoryXB extends FactoryXA{}
public class FactoryYA implements Factory<Y>{ @Override public Y newInstance() { return new Y1(); } }
Unit Test
public class FactoryOfFactoryFixture { @BeforeClass public static void setUpClass(){ //登记抽象类型和工厂类型的对应关系 Factories.config(X.class,FactoryXA.class); Factories.config(Y.class,FactoryYA.class); }
@Test public void testNewInstanceViaFactoryOfFactory() throws ClassNotFoundException, InstantiationException, IllegalAccessException{
Factory<X> factoryOfProductX = Factories.newFactory(X.class); assertTrue(factoryOfProductX.newInstance() instanceof X); assertTrue(factoryOfProductX.newInstance() instanceof X1);
Factory<Y> factoryOfProductY = Factories.newFactory(Y.class); assertTrue(factoryOfProductY.newInstance() instanceof Y); assertTrue(factoryOfProductY.newInstance() instanceof Y1); } }
上例中,客户程序并不用自己主动寻找具体工厂类型,因此把自己和具体的工厂类型划清了界限。其实这个思路很简单,就是当我们发现工厂类型本身(如FactoryXA、FactoryXB、FactoryYA)因为与“具体实现”产生依赖,进而导致客户程序与“具体实现”产生依赖的时候,就再用另一个工厂(如这里的Factories类)来构造工厂,延续这个思路就形成了后面要介绍的抽象工厂模式。
需要注意的是,因为类型擦除,所以newFactory()方法定义时需要同时传递泛型参数和抽象产品类型,两者其实基本应该是一个类型,但因为运行时类型参数被擦除了,所以不能通过T.class直接查找所需的工厂类型。当然,同样因为类型擦除,newFactory()方法可以定义非泛型类型的Factory返回结果,但这等于放弃本可以严谨定义的方法签名。
4.5 基于配置文件的工厂
4.5.1 基于配置文件解耦工厂接口和具体工厂类型
上面的方法虽然已经解决了很多问题,但工程中我们往往会要求Factories类知道更多的Factory/Concrete Factory配置关系,而且为了不用反复引用并编译代码,作为开发人员我们希望可以把这些涉及运行维护的工作交到系统管理员或配置人员手中,一个常见的选择就是把需要加载的Factory/Concrete Factory对应关系列表保存到配置文件中。
这种情况其实在生活中经常遇到:假设你是班里的活跃分子,毕业几年后同学们想找到其他人时总会给你发个消息,A找B要找你,D找F也找你。对你而言如果要找的人有手机还好办,有些人只是IM工具上有个账号,而大家平时用的IM工具又总是“土洋结合”,每个人偏好不同,所以就麻烦了,可能经常要找不同的人索要联系方式。不妨变通一下——在校友录上留个帖子,大家谁变动都自己在里面登记,就好像我们用配置文件一样,一段时间后如果A再要G的联系方式,你只要根据帖子给他一个联系方式即可,而不用和G直接打交道。
下面我们将上例中需要硬编码调用Factories.config()方法配置的Factory/Concrete Factory信息登记在配置文件中,看一下是否能让我们设计的工厂更加易于维护:
marvellousworks.practicalpattern.xml
<?xml version="1.0" encoding="UTF-8"?> <practicalpatterns>
<configSections> <add name="factorymethod" type="…….FactoryMethodConfigSection"/> </configSections>
<!--工厂方法模式一章使用的配置节--> <factorymethod> <!--映射关系的配置元素集合--> <factories> <!--登记抽象类型和构造其工厂类型映射关系的配置元素--> <add producttype="…….X" factorytype="…….FactoryXA"/> <add producttype="…….Y" factorytype="…….FactoryYA"/> </factories> </factorymethod>
</practicalpatterns>
Unit Test
/***通知Factories读取配置文件并更新产品类型与工厂类型间的映射关系*/ @BeforeClass public static void setUpClass() throws XPathExpressionException, DOMException, ClassNotFoundException{ Factories.refreshConfig(); }
@Test public void testNewInstanceViaFactoryOfFactory() throws ClassNotFoundException, InstantiationException, IllegalAccessException{ Factory<X> factoryOfProductX = Factories.newFactory(X.class); assertTrue(factoryOfProductX.newInstance() instanceof X); assertTrue(factoryOfProductX.newInstance() instanceof X1);
Factory<Y> factoryOfProductY = Factories.newFactory(Y.class); assertTrue(factoryOfProductY.newInstance() instanceof Y); assertTrue(factoryOfProductY.newInstance() instanceof Y1); }
通过这个改造,即便在系统上线后,如果需要修改Factory的具体类型,一样可以通过修改配置文件的方式将新开发的工厂类型注册进去。这样,客户程序成为一个基于配置定义的可以动态加载的框架。
不仅如此,Factories的开发人员也不用经常编译、重新提交代码了,开发人员只要写好一个比较稳定的框架,后面由其他人具体实现Factory类型,至于部署上线,那是下游开发人员的事情,相互间集成的边界很明确。
4.5.2 基于配置文件解耦工厂类型和具体工作产品
上面只是让客户程序脱离了具体工厂类型,但使用本模式的关键目的是构造Product而不是Factory,如果一味依据该模式的经典方式定义FactoryXA构造X、FactoryYA构造Y,这就需要反复创建很多工厂类型,实际项目中我们更希望能通过传递参数的方法让工厂类型更具普适性。
同样是这个问题,如果我们站在客户角度看上面的示例,也有一点遗憾——客户程序在运行过程中自主性选择太少,对于每类产品(无论是X还是Y),只能配置一个工厂,也就是说,只能产生唯一不变的一个具体产品实例(X1/X2/X3或Y1/Y2)。
为此,我们还需更进一步的扩展,为客户程序提供更多的运行态支持。扩展从哪里入手好呢?
●调整客户程序?我们要做的就是隔绝客户程序与具体new()的对应关系,如果要调整客户程序才能实现还不如不做。
●扩充配置项?这可以,它对于客户程序而言是透明的。
●扩展Factories类型?这样做实需要一些微调,以增加客户程序自主性,使其能够指定生产特定产品(X或Y)工厂的参数。
●调整Concrete Factory(FactoryXA、FactoryXB、FactoryYA)?它们应该就不需要调整了。
下面我们继续扩展上面的示例,选择Factories和配置文件作为切入点,为了不破坏现有的客户程序,我们在扩展时要保留对原有接口的兼容。
怎么能既保证客户程序可以灵活选择工厂类型,允许其传递参数,同时又要兼容以前的接口呢?那就是重载,然后对于每类产品定义一个“默认的”工厂类型。
下面我们综合上面的思路,形成如下示例:
Java 调整后的Factories
/** * 创建工厂的工厂 * 此处定义为静态形式的工厂 */ public class Factories{
/**登记待构造产品类型及相应工厂类型*/ private static Map<SimpleEntry<Class<?>,String>,Class<?>> registry …;
/**封闭 Factories 类型的构造函数*/ private Factories(){}
/**兼容原有的配置接口*/ public static void config(Class<?> productType, Class<? extends Factory<?>> factoryType)…
/**新增的配置接口*/ public static void config(Class<?> productType, String name,Class<? extends Factory<?>> factoryType)…
/**从配置文件直接加载配置信息*/ public static void refreshConfig()…
/**返回负责生产某类产品的工厂类型实例*/ @SuppressWarnings("unchecked") public static <T> Factory<T> newFactory( Class<?> productType,String name)…
/**兼容原有的构造工厂类型实例接口*/ public static <T> Factory<T> newFactory( Class<?> productType)… return newFactory(productType,DEFAULT_NAME); } }
marvellousworks.practicalpattern.xml
<?xml version="1.0" encoding="UTF-8"?> <practicalpatterns>
<configSections> <add name="factorymethod" type="…….FactoryMethodConfigSection"/> </configSections>
<!--工厂方法模式一章使用的配置节--> <factorymethod> <!--映射关系的配置元素集合--> <factories> <!--登记抽象类型和构造其工厂类型映射关系的配置元素--> <!-- 'default'属性表示是否为该类产品默认映射的工厂类型 --> <add name="a" producttype="…….X" factorytype="…….FactoryXA" default="true"/> <add name="b" producttype="…….X" factorytype="…….FactoryXB"/> <add name="x" producttype="…….X" factorytype="…….FactoryXB" default="false"/> <add name="c" producttype="…….Y" factorytype="…….FactoryYA" default="true"/> </factories> </factorymethod> </practicalpatterns>
Unit Test
public class FactoryWithConfigurationFixture {
/*** * 通知Factories读取配置文件并更新产品类型与工厂类型间的映射关系 */ @BeforeClass public static void setUpClass() throws XPathExpressionException, DOMException, ClassNotFoundException{ Factories.refreshConfig(); }
@Test public void testNewInstanceViaFactoryOfFactoryByName() throws ClassNotFoundException, InstantiationException, IllegalAccessException{
Factory<X> factoryOfProductX = Factories.newFactory(X.class,"a"); assertTrue(factoryOfProductX.newInstance() instanceof X); assertTrue(factoryOfProductX.newInstance() instanceof X1); } }
由于查找工厂和产品类型的映射关系需要两项数据——名称、产品类型,因此示例采用Java SE内置的java.util.AbstractMap.SimpleEntry<Class<?>,String> (<类型,名称>)作为键值。
示例验证工厂类型可以按照逻辑名称加工指定类型的产品,这点对于程序没有太大的意义,但它对于系统的设计人员和运维人员很有意义,因为我们已经习惯于用自己熟悉的“逻辑”名称指代和管理各类资源,需求分析时也是,我们一般都习惯于“财务库”、“人员库”之类的逻辑名称,至于它们具体是Oracle、SQL Server,还是Database名称先暂且不管。我们管理信息系统同样也喜欢用逻辑名称称呼,好说也好记。
所以,这个改造虽然简单,却把握住了需求、设计、实现、运行和维护(以下简称运维)主线,是面向人而不是面向机器管理信息系统的考虑。
4.6 批量工厂
此外,实际项目中经常需要加工一批对象,如果按部就班地采用一件接一件的模式来生成,效率上相对较低,最好专门设计独立的批量工厂。其实,“工厂”这个名称体现的也是这个思想,现实中除了造卫星、航空母舰之类的工厂是单件生产外,大多数工厂都是批量生产产品的,如果有人骑自行车批发啤酒的时候,要等24次newInstance()才能凑足一件,这也太累了。
本着OCP原则,我们定义一个新的接口,在Factory<T>接口的基础上增加批量指示,这样扩展后的接口定义如下:
Java 具有“批发”功能的工厂类型抽象定义
public interface BatchFactory<T> extends Factory<T> { /** * 构造产品实例 * @param quantity 待构造实例数量 * @return 一批目标实例 */ Collection<T> newInstance(int quantity); }
4.7 典型工程化实现
上述两个基于配置的实现看上去已经解决很多问题,现在再回顾一下其中的经验:
●从客户程序的角度看,工厂方法的方法签名已经很一致、很简洁。
●客户程序与工厂接口(Factory)、客户程序与产品接口(Product)、工厂接口(Factory)与具体工厂(FactoryX)类型、产品接口与具体产品类型(ProductX)这4个依赖关系从代码中剥离,变成完全基于外部配置文件的依赖。
●新的产品类型和工厂类型即便在系统上线后仍可以通过修改配置文件的方式不断补充。
●对于那些需要长期运维的项目,尤其是自己开发、自己运维的项目而言,它更适于部署和更新。
但同时它也有不足:
最明显的就是“多头管理”问题,也就是要为每“类”抽象产品定制特别的工厂接口并实现之。
其次,某些场合下使用配置文件定义产品类型接口与工厂接口的工作,不用兴师动众地使用配置文件,换言之有时候还要提供运行时动态配置的功能。
为此,我们能否把上面两节完成的工厂类型进一步整理,设计一个更为通用的工厂类型,由它全权承担项目中工厂的典型职责,尽量避免如上例中反复构建工厂类型(FactoryXA、FactoryXB、FactoryYA)的工作。
综合本节的分析,权衡各类功能性要求后,我们设计了一个更符合生产环境要求的工厂类型,如图4-4所示。
图4-4 更符合生产环境要求的工厂类型
Java 更适合生产环境的工厂类型抽象定义
/** * 一个更符合生产环境要求的通用工厂类型 * * 因为Java 5/6/7泛型运行态的类型擦除,无法直接 new T()或者访问T.class * 所以在相关方法需要重复定义类型参数<T>和Class<?> abstractClass参数 * 使用中,两个参数一般应保持一致 */ public interface Factory{ /** * 构造目标实例 * @param <T> 目标产品类型,一般采用抽象的产品类型 * @param abstractClass 抽象产品类型 * @return 目标实例 * @throws InstantiationException * @throws IllegalAccessException */ <T> T newInstance(Class<?> abstractClass) throws InstantiationException, IllegalAccessException; /** * 构造目标实例 * @param <T> 目标产品类型,一般采用抽象的产品类型 * @param abstractClass 抽象产品类型 * @param name 逻辑名称 * @return 目标实例 * @throws InstantiationException * @throws IllegalAccessException */ <T> T newInstance(Class<?> abstractClass,String name) throws InstantiationException, IllegalAccessException;
/** * 配置抽象产品类型与具体产品类型的映射关系 * 项目中,也可以通过这些重载的接口,注入从配置文件提取的配置信息 * @param abstractClass 抽象的产品类型 * @param implClass 具体产品类型 * @param name 指定目标产品的逻辑名称 * @return 工厂类型自身,定义上采用连贯接口的方式 */ Factory config(Class<?> abstractClass,Class<?> implClass,String name);
Factory config(Class<?> abstractClass,Class<?> implClass); Factory config(Map<SimpleEntry<Class<?>,String>,Class<?>> configSettings); }
Java Unit Test
public class FactoryFixture {
Factory factory;
@Before public void setUp(){ factory = new FactoryImpl() .config(X.class,X1.class) // default .config(X.class,X1.class,"1") .config(X.class,X2.class,"2") .config(X.class,X3.class,"3") .config(Y.class,Y1.class) // default .config(Y.class,Y1.class,"1") .config(Y.class,Y2.class,"2"); } @Test public void testNewInstance() throws InstantiationException, IllegalAccessException{ assertTrue(factory.newInstance(X.class) instanceof X); assertTrue(factory.newInstance(X.class) instanceof X1); assertTrue(factory.newInstance(X.class,"1") instanceof X1); assertTrue(factory.newInstance(X.class,"2") instanceof X2); assertTrue(factory.newInstance(X.class,"3") instanceof X3);
assertTrue(factory.newInstance(Y.class) instanceof Y); assertTrue(factory.newInstance(Y.class) instanceof Y1); assertTrue(factory.newInstance(Y.class,"1") instanceof Y1); assertTrue(factory.newInstance(Y.class,"2") instanceof Y2); } }
上面的示例表明,新的工厂类型不仅可以完成经典工厂方法模式所希望实现的各项要求,同时它可以作为整个项目中一个独立的而且是唯一的工厂入口,供项目中各子系统访问和使用。原因在于它的底层将工厂接口与抽象产品类型的依赖关系变成基于JDK“万能工厂”类型java.lang.Class基于参数类型的构造过程。此外,具体项目中可能还要进一步修改这个工厂类型,提供更多newInstance()方法的重载,如具有构造参数的newInstance()方法或者具有批量构造指示的newInstance()方法。如果为了便于部署和后续运维需要,还可以参考前面的介绍,借助config()方法将配置文件中定义的类型映射关系加载到新的具体工厂类型中。
4.8 小结
工厂方法模式重新诠释了如何去new()的问题,通过引入新的对象,在很大程度上解放了客户程序对具体类型的依赖,其方法就是延迟构造到子类,但依赖关系是始终存在的,解放一个对象的时候等于把麻烦转嫁到另一个对象上。为此,项目中往往最后把推不掉的依赖关系推到配置文件或“万能工厂”——java.lang.Class上,也就是推给了JDK环境,这样经典的工厂方法模式往往在项目中被演绎成“工厂方法 + 依赖注入 + 配置文件访问”的组合方式。
考虑到性能因素,或者干脆为了省去多次调用的麻烦,项目中常要求生成一批对象实例,这时候就会用到批量工厂方法。
而且,如前面章节所讨论的,写Demo和完成一个项目是两码事,其中一个关键的因素就是人,为了让整个实施过程中工厂的实现“中规中矩”,有时候要统一一些内容,泛型工厂能从根本上约束整个工厂体系中工厂方法的命名。
不过,本章也留下两个需要进一步完善的问题,其在实际项目中常常遇到,将在“自我检验”的参考答案和后续章节中介绍:
●处理带各种参数的构造函数。
●对象的构造过程本身涉及很多依赖对象的构造过程,如何“更优雅”地把它们注入到待构造的实例中。
项目中,我们还需要注意一个不好的倾向——工厂模式的滥用,包括本章的工厂方法和后面要介绍的抽象工厂。有些类型虽然既不是接口,也不是抽象类,但它就是稳定,甚至比某个项目的生命周期还要长(例如:计算农历、公历转换的类),尽管客户程序依赖的是具体而不是抽象,但大部分情况下没必要在中间建一个工厂类。
本章最后完成的工厂类型将作为本系列Java版本后续各章节普遍使用的内容,它会随着模式的介绍不断丰富。
4.9 Java 中的典型实现
下面的类型、方法采用了工厂方法模式,感兴趣的读者可以通过反编译参考。
●java.lang.Class#newInstance()。
●java.lang.Integer#valueOf(String)。
●java.lang.Class#forName()。
●java.lang.reflect.Array#newInstance()。
4.10 自我检验
请修改本章最后完成的那个比较适于工程应用的工厂类型,使其支持含参数的构造函数。
完成后的静态结构如图4-5所示。
图4-5 进一步面向工程使用需要扩展的工厂类型