设计模式笔记(二十一)– 工厂方法模式

​工厂方法模式(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 中无法覆盖静态方法。

因此,再重复一遍:使用工厂方法模式,我们可以将对象的创建提交给子类完成。


小结

使用工厂方法模式,我们可以将对象的创建交给子类完成。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注