简单工厂模式

思考并回答以下问题:

  • 什么是简单工厂模式?为什么简单?和工厂模式相比有什么缺点?
  • 两个类A和B之间的关系应该仅仅是A创建B或者是A使用B,而不能两种关系都有。怎么理解?
  • 防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中。怎么理解?
  • 从一组工厂方法中选择一个意义明确的工厂方法,比从一组名称相同参数不同的构造函数中选择一个构造函数要方便很多。怎么理解?

本章导学

创建型模式关注对象的创建过程,是一类最常见的设计模式,在软件开发中的应用非常广泛。创建型模式描述如何将对象的创建和使用分离,让用户在使用对象时无须关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。

简单工厂模式是最简单的设计模式之一,它虽然不属于GoF的23种设计模式,但是应用也较为频繁,同时它也是学习其他创建型模式的基础。在简单工厂模式中,只需要记住一个简单的参数即可获得所需的对象实例,它提供专门的核心工厂类来负责对象的创建,实现对象的创建和使用分离。

本章将对6种创建型模式进行简要的介绍,并通过实例来学习简单工厂模式,理解简单工厂模式的结构及特点,学习如何在实际软件项目开发中合理地使用简单工厂模式。

本章知识点

  • 创建型模式
  • 简单工厂模式的定义
  • 简单工厂模式的结构
  • 简单工厂模式的实现
  • 简单工厂模式的应用
  • 简单工厂模式的优缺点
  • 简单工厂模式的适用环境

创建型模式

软件系统在运行时,类将实例化成对象,并由这些对象协作完成各项业务功能。创建型模式(Creational Pattern)关注对象的创建过程,是一类最常用的设计模式,在软件开发中的应用非常广泛。创建型模式对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离,对用户隐藏了类的实例创建细节。

创建型模式描述如何将对象的创建和使用分离,让用户在使用对象时无须关心对象的创建细节,从而降低系统的耦合度,让设计方案更易于修改和扩展。每一个创建型模式都通过采用不同的解决方案来回答3个问题:创建什么(What),由谁创建(Who)和何时创建(When)。

在GoF设计模式中,包含5种创建型模式,通常将一种非GoF设计模式——简单工厂模式作为学习其他工厂模式的基础,这5种设计模式的名称、定义、学习难度和使用频率如表1所示。

表1 创建型模式一览表

模式名称
定 义
学习难度
使用频率
简单工厂模式
(Simple Factory Pattern)
定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。 ★★☆☆☆ ★★★☆☆
工厂方法模式
(Factory Method Pattern)
定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。 ★★☆☆☆ ★★★★★
抽象工厂模式
(Abstract Factory Pattern)
提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。 ★★★★☆ ★★★★★
建造者模式
(Builder Pattern)
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 ★★★★☆ ★★☆☆☆
原型模式
(Prototype Pattern)
使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。 ★★★☆☆ ★★★☆☆
单例模式
(Singleton Pattern)
确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。 ★☆☆☆☆ ★★★★☆

简单工厂模式概述

简单工厂模式并不属于GoF的23种经典设计模式,但通常将它作为学习其他工厂模式的基础,下面通过一个简单实例来引出简单工厂模式。

考虑一个水果农场,当用户需要某一种水果时,该农场能够根据用户所提供的水果名称返回该水果。在此,水果农场被称为工厂(Factory),而生产出的水果被称为产品(Product),水果的名称则被称为参数,工厂可以根据参数的不同返回不同的产品,这就是简单工厂模式的动机。该过程的示意图如图1所示,用户无须知道苹果(Apple)、橙(Orange)、香蕉(Banana)如何创建,只需要知道水果的名称即可得到对应的水果。

图1 简单工厂模式示意图

作为最简单的设计模式之一,简单工厂模式的设计思想和实现过程都比较简单,其基本实现流程如下:

首先将需要创建的各种不同产品对象的相关代码封装到不同的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体产品类都是抽象产品类的子类;然后提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入参数的不同创建不同的具体产品对象;客户端只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象。

简单工厂模式(Simple Factory Pattern)的定义如下:

1
定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。

由于在简单工厂模式中用于创建实例的方法通常是静态(static)方法,所以简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它是一种类创建型模式。简单工厂模式的要点在于:如果需要什么,只需要传入一个正确的参数,就可以获取所需要的对象,而无须知道其创建细节。

简单工厂模式的结构与实现

简单工厂模式的结构

简单工厂模式的结构比较简单,其核心是工厂类的设计,其结构如图2所示。

图2 简单工厂模式结构图

由图2可知,简单工厂模式包含以下3个角色。

(1)Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法FactoryMethod(),它的返回类型为抽象产品类型Product。

(2)Product(抽象产品角色):它是工厂类所创建的所有对象的父类,封装了各种产品对象的公共方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。

(3)ConcreteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。

简单工厂模式的实现

在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用new关键字来创建对象,它是工厂模式家族中最简单的一员。

在使用简单工厂模式时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需要根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现。典型的抽象产品类代码如下:

1
2
3
4
5
6
7
8
9
10
11
abstract class Product
{
// 所有产品类的公共业务方法
public void MethodSame()
{
// 公共方法的实现
}

// 声明抽象业务方法
public abstract void MethodDiff();
}

在具体产品类中实现了抽象产品类中声明的抽象业务方法,不同的具体产品类可以提供不同的实现。典型的具体产品类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ConcreteProductA : Product
{
// 实现业务方法
public override void MethodDiff()
{
// 业务方法的实现
}
}

class ConcreteProductB : Product
{
// 实现业务方法
public override void MethodDiff()
{
// 业务方法的实现
}
}

简单工厂模式的核心是工厂类,在没有工厂类之前,客户端一般会使用new关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类来创建产品。在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入参数的不同可以创建不同的产品对象。典型的工厂类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Factory
{
// 静态工厂方法
public static Product GetProduct(string arg)
{
Product product = null;

if(arg.Equals("A"))
{
product = new ConcreteProductA();
// 初始化设置product
}
else if(arg.Equals("B"))
{
product = new ConcreteProductB();
// 初始化设置product
}
return product;
}
}

在客户端代码中,通过调用工厂类的工厂方法即可得到产品对象。其典型代码如下:

1
2
3
4
5
6
7
8
9
10
class Program
{
static void Main(string[] args)
{
Product product;
product = Factory.GetProduct("A"); // 通过工厂类创建产品对象
product.MethodSame();
product.MethodDiff();
}
}

简单工厂模式的应用实例

下面通过一个应用实例来进一步学习和理解简单工厂模式。

1.实例说明

某软件公司要基于C#语言开发一套图表库,该图表库可以为应用系统提供多种不同外观的图表,例如柱状图(HistogramChart)、饼状图(PieChart)、折线图(LineChart)等。该软件公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,通过设置不同的参数即可得到不同类型的图表,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。

现使用简单工厂模式来设计该图表库。

2.实例类图

通过分析,本实例的结构如图3所示。

图3 图表库结构图

在图3中,Chart接口充当抽象产品类,其子类HistogramChart、PieChart和LineChart充当具体产品类,ChartFactory充当工厂类。

3.实例代码

(1)Chart:抽象图标接口,充当抽象产品类。

1
2
3
4
5
6
7
namespace SimpleFactorySample
{
interface Chart
{
void Display();
}
}

(2)HistogramChart:柱状图类,充当具体产品类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace SimpleFactorySample
{
class HistogramChart : Chart
{
public HistogramChart()
{
Console.WriteLine("创建柱状图!");
}

public void Display()
{
Console.WriteLine("显示柱状图!");
}
}
}

(3)PieChart:饼状图类,充当具体产品类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace SimpleFactorySample
{
class PieChart : Chart
{
public PieChart()
{
Console.WriteLine("创建饼状图!");
}

public void Display()
{
Console.WriteLine("显示饼状图!");
}
}
}

(4)LineChart:折线图类,充当具体产品类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace SimpleFactorySample
{
class LineChart : Chart
{
public LineChart()
{
Console.WriteLine("创建折线图!");
}

public void Display()
{
Console.WriteLine("显示折线图!");
}
}
}

(5)ChartFactory:图表工厂类,充当工厂类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;

namespace SimpleFactorySample
{
class ChartFactory
{
// 静态工厂方法
public static Chart GetChart(string type)
{
Chart chart = null;

if (type.Equals("histogram"))
{
chart = new HistogramChart();
Console.WriteLine("初始化设置柱状图!");
}
else if (type.Equals("pie"))
{
chart = new PieChart();
Console.WriteLine("初始化设置饼状图!");
}
else if (type.Equals("line"))
{
chart = new LineChart();
Console.WriteLine("初始化设置折线图!");
}
return chart;
}
}
}

(6)Program:客户端测试类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

namespace SimpleFactorySample
{
class Program
{
static void Main(string[] args)
{
Chart chart;

chart = ChartFactory.GetChart("histogram"); // 通过静态工厂方法创建产品
chart.Display();

Console.Read();
}
}
}

4.结果及分析

编译并运行程序,输出结果如下:

1
2
3
创建柱状图!  
初始化设置柱状图!
显示柱状图

在客户端测试类中,使用工厂类ChartFactory的静态工厂方法创建产品对象,如果需要更换产品,只需修改静态工厂方法中的参数即可。例如将柱状图改为饼状图,只需将代码:

1
chart = ChartFactory.GetChart("histogram");

改为:

1
chart = ChartFactory.GetChart("pie");

编译并运行程序,输出结果如下:

1
2
3
创建饼状图!  
初始化设置饼状图!
显示饼状图!

不难发现,本实例在创建具体Chart对象时,必须通过修改客户端代码中静态工厂方法的参数来更换具体产品对象,客户端代码需要重新编译,这对于客户端而言,违背了开闭原则。

下面介绍一种常用的解决方案,可以实现在不修改客户端代码的前提下让客户端能够更换具体产品对象。

首先将静态工厂方法的参数存储在XML格式的配置文件App.config中,如下所示:

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="chartType" value="histogram"/>
</appSettings>
</configuration>

然后将客户端代码修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Configuration;

namespace SimpleFactorySample
{
class Program
{
static void Main(string[] args)
{
Chart chart;

// 读取配置文件
string chartStr = ConfigurationManager.AppSettings["chartType"];
chart = ChartFactory.GetChart(chartStr); // 通过静态工厂方法创建产品
chart.Display();

Console.Read();
}
}
}

编译并运行程序,输出结果如下:

1
2
3
创建柱状图!  
初始化设置柱状图!
显示柱状图!

在上述客户端代码中,通过使用ConfigurationManager类的AppSettings属性可以获取存储在配置文件(例如App.config)中的字符串,在C#源代码中不包含任何与具体图表对象相关的信息,如果需要更换具体图表对象,只需修改配置文件App.config即可,无须修改任何源代码,符合开闭原则。

需要注意的是,在某些版本的Visual Studio中需要手动引入组件“System.Configuration”,否则无法使用ConfigurationManager类。步骤如下:

在“解决方案资源管理器”项目树形结构中的“引用”结点上右击,在快捷菜单中选择“添加引用”命令,在弹出的对话框中选择“.NET”选项卡,选中名称为“System.Configuration”的组件,即可添加该组件,并使用其中的ConfigurationManager等类。

创建对象与使用对象

本节将讨论工厂类的作用以及如何通过工厂类来创建对象。在一个面向对象软件系统中,与一个对象相关的职责通常有3种:对象本身所具有的职责、创建对象的职责和使用对象的职责。对象本身的职责比较容易理解,就是对象自身所具有的一些数据和行为,可通过一些公开的(public)方法来实现。本节将重点讨论创建对象的职责和使用对象的职责。

在C#语言中,通常有以下几种创建对象的方式:

  • (1)使用new关键字直接创建对象。
  • (2)通过反射机制创建对象(工厂方法模式)将学习此方式。
  • (3)通过克隆方法创建对象(原型模式)将学习此方式。
  • (4)通过工厂类创建对象。

毫无疑问,在客户端代码中直接使用new关键字是最简单的创建对象的方式,但是它的灵活性较差,下面通过一个简单的实例来加以说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Login
{
private UserDAO udao;

public Login()
{
// 创建对象
udao = new OracleUserDAO();
}

public void Execute()
{
// 其他代码
// 使用对象
udao.FindUserById();
// 其他代码
}
}

以上代码中,在Login类中定义了一个UserDAO类型的对象udao,在Login的构造函数中创建了OracleUserDAO类型的udao对象,并在Execute()方法中调用了udao对象的FindUserById()方法。Login类负责创建一个UserDAO子类的对象并使用该对象的方法来完成相应的业务处理,也就是说,Login既负责udao的创建又负责udao的使用,创建对象和使用对象的职责耦合在一起,这样的设计会导致一个很严重的问题:如果在Login中希望能够使用UserDAO的另一个子类,例如SQLServerUserDAO类型的对象,必须修改Login类的源代码,这将违背开闭原则。

当遇到这种情况时,最常用的一种解决方法是将udao对象的创建职责从Login类中移除,在Login类之外创建对象,由专门的工厂类来负责udao对象的创建。通过引入工厂类,让客户类(例如Login)不涉及对象的创建,对象的创建者也不会涉及对象的使用。引入工厂类UserDAOFactory之后的结构如图4所示。

图4 引入工厂类之后的结构图

工厂类的引入将降低因为产品或工厂类改变所造成的维护工作量。如果UserDAO的某个子类的构造函数发生改变或者需要添加或移除不同的子类,只要维护UserDAOFactory的代码,不会影响到Login;如果UserDAO接口发生改变,例如添加、移除方法或改变方法名,只需要修改Login,不会给UserDAOFactory带来任何影响。

所有的工厂模式都强调一点:两个类A和B之间的关系应该仅仅是A创建B或者是A使用B,而不能两种关系都有。将对象的创建和使用分离,使得系统更加符合单一职责原则,有利于对功能的复用和系统的维护。

此外,将对象的创建和使用分离还有一个好处:防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中。因为有时创建一个对象不只是简单调用其构造函数,还需要设置一些参数,可能还需要配置环境,如果将这些代码散落在每一个创建对象的客户类中,势必会出现代码重复、创建蔓延的问题,而这些客户类其实无须承担对象的创建工作,它们只需使用已创建好的对象就可以了。此时,可以引入工厂类来封装对象的创建逻辑和客户代码的实例化配置选项。

使用工厂类还有一个优点,一个类可能拥有多个构造函数,而在C#、Java等语言中构造函数的名称都与类名相同,客户端只能通过传入不同的参数来调用不同的构造函数创建对象,从构造函数和参数列表中大家很难了解不同构造函数所构造的产品的差异。如果将对象的创建过程封装在工厂类中,可以提供一系列名称完全不同的工厂方法,每一个工厂方法对应一个构造函数,客户端就可以以一种更加可读、易懂的方式来创建对象,而且,从一组工厂方法中选择一个意义明确的工厂方法,比从一组名称相同参数不同的构造函数中选择一个构造函数要方便很多,如图5所示。

图5 矩形工厂与矩形类

在图5中,矩形工厂类RectangleFactory提供了CreateRectangle()和CreateSquare()两个工厂方法,一个用于创建长方形,一个用于创建正方形,这两个方法比直接通过构造函数来创建长方形或正方形意义更加明确,在一定程度上降低了客户端调用时出错的概率。

但是,并不需要为系统中的每一个类都配备一个工厂类,如果一个类很简单,而且不存在太多变化,其构造过程也很简单,此时就无须为其提供工厂类,直接在使用之前实例化即可,例如C#语言中的String类(或string),就无须为它专门提供一个StringFactory,这样做反而会导致工厂泛滥,增加系统的复杂度。

以上关于创建对象和使用对象的讨论也适用于工厂方法模式和抽象工厂模式。

简单工厂模式的简化

有时为了简化简单工厂模式,可以将抽象产品类和工厂类合并,将静态工厂方法移到抽象产品类中,如图6所示。

图6 简化的简单工厂模式

在图6中,客户端可以通过调用产品父类的静态工厂方法,根据不同参数创建不同类理的产品子类对象,这种方法在很多类库和框架中也广泛存在。

简单工厂模式的优缺点与适用环境

简单工厂模式提供了专门的工厂类用于创建对象,将对象的创建和对象的使用分离开,它作为一种最简单的工厂模式在软件开发中得到了较为广泛的应用。

简单工厂模式的优点

简单工厂模式的主要优点如下:

  • (1)工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象创建和使用的分离。
  • (2)客户端无须知道所创建的具体产品类的类名,只需知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以在一定程度上减少使用者的记忆量。
  • (3)通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

简单工厂模式的缺点

简单工厂模式的主要缺点如下:

  • (1)由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
  • (2)使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
  • (3)系统扩展困难,一旦添加新产品不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  • (4)简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。在C#语言中,不能通过类的实例对象来访问静态方法和静态变量,无法在客户端代码中针对工厂父类编程,而在运行时使用工厂子类对象来覆盖父类,因此,工厂类不能得到很好的扩展。

简单工厂模式的适用环境

在以下情况下可以考虑使用简单工厂模式:

  • (1)工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑过于复杂。
  • (2)客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

本章小结

(1)创建型模式关注对象的创建过程,它对类的实例化过程进行了抽象,能够将软件模块中对象的创建和对象的使用分离,对用户隐藏了类的实例创建细节。在GoF设计模式中一共包含5种创建型模式,通常将简单工厂模式作为学习其他工厂模式的基础,简单工厂模式不是GoF设计模式。

(2)在简单工厂模式中,定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。简单工厂模式是一种类创建型模式。

(3)简单工厂模式包含工厂角色、抽象产品角色和具体产品角色3个角色。其中,工厂角色是简单工厂模式的核心,负责实现创建产品实例的内部逻辑;抽象产品角色是工厂类所创建的所有对象的父类,封装了各种产品对象的公共方法;具体产品角色是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。

(4)简单工厂模式的主要优点在于实现了对象创建和使用的分离;客户端无须知道所创建的具体产品类的类名,只需知道具体产品类所对应的参数即可;通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。其主要缺点在于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响;增加了系统中类的个数且增加了系统的复杂度和理解难度系统扩展困难,一旦添加新产品不得不修改工厂逻辑,且工厂角色无法形成基于继承的等级结构。

(5)简单工厂模式适用的环境:工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂;客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

(6)将对象的创建和使用分离,使得系统更加符合单一职责原则,有利于对功能的复用和系统的维护。

0%