工厂方法模式(Factory Method Pattern)可以将对象的创建时机延迟到子类。
问题提出
工厂方法模式可能是最容易被误解的模式,传统上对它的介绍是:它能将对象的创建延迟到子类。
听起来非常枯燥无味和理论化。我们付诸实践介绍。
目前有很多的WEB框架可以使用,比如JavaScript可用的框架有Express.js,C#的框架有ASP.net MVC或Core,Java的框架有Spring或SpringBoot,Python的框架有Django等。
假设我们也要创建一个WEB框架,命名为Matcha。大部分的框架都有模板(template)或视图(view)的概念,它是一个带有逻辑代码的HTML文件。
如上图Template.html是一个模板文件(或叫view),<h1>、<p>标签是html的原生标记,{% 、%}、{{、}}则是该框架定义的符号,框架利用某个引擎获取这个view文件,然后解释框架定义的符号及其逻辑,输出纯html文件。
我们来实现该引擎。
创建类MatchaViewEngine。它的render方法获取viewName和上下文对象context,返回解释后的html文本。
在我们这个例子里(Template.html),上下文对象就是 products,使用MatchaViewEngine的render方法,读取Template.html。这个文件的逻辑是遍历products,并将每个product的title用<p> 包起来。这里我们不关心这些细节,只简单地返回“View rendered by Matcha”。
我们还要实现控制器Controller类,用于响应用户的操作。当用户浏览网页,并点击某个按钮,相应的控制器将启动,接收用户的请求并返回响应。
Controller类是所有控制器的基本类。
用户使用我们的Matcha框架创建一个显示产品列表(products)的页面,就在应用程序里创建一个扩展于Controller的新类ProductsController。
为了清晰起见,我们将Matcha框架相关的代码整理到一个命名为matcha的Package里。
现在我们来编写ProductsController。
添加listProducts方法,用于获取产品列表products并渲染在页面上,真实的场景可能是从数据库获取products并形成context,这里我们为了不分心只模拟不实现细节。
在基本Controller类的实现里,它与MatchaViewEngine的耦合很紧密。用户无法使用他们自己的引擎来支持更好的模板语法。
一种解决方法是从MatchaViewEngine中抽象出接口(ViewEngine),然后在Controller类的render方法传递实际的引擎(MatchaViewEngine)。
但这又带来了新问题,它使别人使用我们的框架变得困难。
比如,用户应用ProductsController调用render方法时都必须传递相关的引擎(ViewEngine)对象作为第三个参数。
显然,这种解决方法不够好。
利用工厂方法模式可以优雅地解决这个问题。
解决方案
当前我们的结构是这样的。
在render方法里,首先创建MatchaViewEngine对象engine,再调用engine的render方法。前面说了,这种结构Controller与MatchaViewEngine紧密耦合,不灵活。
可以添加方法createViewEngine,用于创建引擎对象。render方法里调用createViewEngine获取引擎对象,再调用该引擎对象的render方法。
createViewEngine方法的实现类似下面这样,返回某个引擎对象。
这样做的好处是什么?
使用这种结构,使用我们框架的其他开发者可以扩展Controller并重写CreateViewEngine方法,它们可以返各种ViewEngine,比如SharpViewEngine。
这就是工厂方法模式。CreateViewEngine方法扮演工厂角色,它返回新的对象。
现在回头看工厂方法模式的经典定义:延迟对象的创建到子类。
我们来看看这到底是什么意思。
基本的Controller类中,我们提供了一个默认的CreateViewEngine实现,它返回MatchaViewEngine对象。我们也可以定义CreateViewEngine为抽象方法,强制它的子类比如SharpController重写这个方法。
不管是否有默认实现,CreateViewEngine都相当于一个工厂方法,利用该模式我们能延迟对象的创建到子类。
经典的四人帮图是这样的。
Creator相当于我们的Controller,operation方法首先调用factoryMethod方法获取某个产品(product),然后对product进行操作(operation)。ConcreteCreator继承Creator,它实现的factoryMethod方法可以生成某种特定的产品(ConcreteProduct)。
代码实现
修改Controller类。添加 createViewEngine方法,使用protected修饰,这样外部不能访问,而子类可以重写。render方法里,使用这个方法(createViewEngine)获取引擎对象。
我们也可以定义createViewEngine为抽象方法,这样就会强制每一个子类实现它。这里就保持返回MatchaViewEngine对象的默认实现。
现在我们假设要使用另外一个引擎SharpViewEngine。创建SharpViewEngine类和SharpController类。
我们到Main类中进行测试。
创建ProductsController对象并调用listProducts(),分别让ProductsController继承于Controller和SharpController,能看到使用的引擎分别为MatchaViewEngine和SharpViewEngine。
这是默认使用MatchaViewEngine引擎的。
下面则是使用SharpViewEngine的。
使用这个模式,我们允许子类创建各种不同的ViewEngine。
前面说过,工厂方法模式可能是最容易被误解的模式。很多人没有意识到,工厂方法模式依赖于继承,因此使用继承和多态性为这种设计增加了灵活性,我们允许其他人更改 ViewEngine 的类型。
实现工厂方法模式的糟糕方法是使用静态方法。许多人会创建一个类似 ViewEngineFactory 的类,然后为其提供一个createViewEngine的静态方法,这不是实现工厂方法模式的正确做法,因为这样我们将无法更改 createViewEngine 方法的实现,因为在 Java 中无法覆盖静态方法。
因此,再重复一遍:使用工厂方法模式,我们可以将对象的创建提交给子类完成。
小结
使用工厂方法模式,我们可以将对象的创建交给子类完成。