C#设计模式之创建类模式:工厂方法模式

时间:2022-10-02 14:09:45

定义:定义一个用于创建对象的接口,但是让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到(工厂类)子类。

使用背景:考虑这样一个场景,使用简单工厂模式设计的按钮工厂类可以返回一个具体的按钮实例,例如圆形纽扣,矩形纽扣等。在这个系统种,如果需要增加一种新类型的纽扣比如菱形纽扣,就需要除了增加一个表示这种纽扣的类,还要修改工厂类以增加相应的实例化菱形纽扣的逻辑。这样就违背了开闭原则如下图所示:

C#设计模式之创建类模式:工厂方法模式

下面对该系统进行修改,不在提供一个按钮工厂类来负责所有纽扣的实例化,首先增加一个抽象层,代表一个按钮工厂,然后,为每一种纽扣设计一个按钮工厂,如矩形按钮工厂,圆形按钮工厂,菱形按钮工厂等。也就是说将按钮的生产交给每一个对一个的具体的工厂类去生产,一个工厂类只生产一种按钮。这种新增一个抽象按钮工厂的结果是使得扩展很容易,当我们需要一个椭圆形的按钮的新产品的时候,我们只需要新增一个椭圆形的按钮并继承自抽象按钮类,然后新增一个相应的椭圆形按钮工厂继承自抽象的按钮工厂类,这样,在不修改源代码的情况下完成了对系统的扩展 。新设计的类图如下所示:

C#设计模式之创建类模式:工厂方法模式

这就是工厂方法要表达的意思,在工厂方法模式种,不在提供一个统一的工厂类来完成对所有产品的实例化的工作,而是针对不同的产品提供不同的工厂,相对于简单工厂模式来说,对工厂类的职责进行了符合“单一职责”原则的重构。

在工厂方法模式种,工厂父类负责提供创建产品对象的公共接口的定义,二工厂子类则负责生成具体的产品对象,这样做的目的在于将产品类的实例化工作延迟到工厂子类中,即通过工厂子类来决定到底要实例化哪一种产品。

工厂方法模式的机构

工厂方法模式提供一个抽象工厂接口来声明抽象工厂的方法,尤其子类工厂来实现工厂方法,创建具体的产品对象,工厂方法模式的结构如图:

C#设计模式之创建类模式:工厂方法模式

工厂方法模式包含一下4个角色:

①抽象产品(Product):它是定义产品的接口,是工厂方法模式创建对象的父类,也即是产品对象的公共父类。

②具体产品子类(ConcreteProduct):他实现了抽象产品接口,某种类型的具体产品有专门的工厂创建,具体工厂和具体产品之间意义对应。

③抽象工厂(Factory):在抽象工厂类中,声明了工厂方法,用于返回一个抽象产品类型的产品(本例中的Product),抽象工厂类是工厂方法模式的核心,所有工厂子类都得实现这个基类。

④具体工厂(ConcreteFactory):它是抽象工厂类的子类,实现了在抽象工厂方法中声明的工厂方法,有客户端调用,放回一个具体产品(ConcreteProduct)的实例。

工厂方法模式的实现

与简单工厂方法对比,工厂方法模式最重要的改变是引入了抽象工厂角色,抽象工厂类可以是接口,也可以是抽象类。

不管抽象工厂定义的是接口还是抽象类,工厂方法的实现都放到具体的工厂子类中来做。

在实际使用时,具体工厂类除了创建产品类的实例外,还可以负责一些初始化的工作以及一些资源和环境配置的工作,例如数据库连接,创建文件(流)等。

工厂方法的应用实例

说明

某系统运行日志记录器(Logger)可以通过多种途径保存系统的运行日志,例如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。
为了更好地封装记录器的初始化过程并保证多种记录器切换的灵活性,现使用工厂方法模式设计该系统。(注:在.NET平台下常用的日志记录工具有Log4net、NLog等,.NET Framework也提供了一些用于记录日志的类,例如Debug、Trace、TraceSource等。)

 实例类图

C#设计模式之创建类模式:工厂方法模式

实例代码

interface Logger//定义产品的抽象
{
void WriteLog();
}
----------------------------------------------------------
class DatabaseLogger : Logger //具体的产品类
{
public void WriteLog()
{
Console.WriteLine(
"数据库日志记录。");
}
}

----------------------------------------------------------
class FileLogger : Logger //具体的产品类
{
public void WriteLog()
{
Console.WriteLine(
"文件日志记录。");
}
}
------------------------------------------------------------
interface LoggerFactory//定义工厂的抽象层
{
Logger CreateLogger();
}
-----------------------------------------------------
class FileLoggerFactory : LoggerFactory //具体的工厂类
{
public Logger CreateLogger()
{
//创建文件日志记录器对象
Logger logger = new FileLogger();
//创建文件,代码省略
return logger;
}
}
-----------------------------------------------------------
class DatabaseLoggerFactory : LoggerFactory //具体的工厂类
{
public Logger CreateLogger()
{
//连接数据库,代码省略
//创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}
 static void Main(string[] args)
{
/*
LoggerFactory factory;
Logger logger;
factory = new FileLoggerFactory(); //可引入配置文件实现
logger = factory.CreateLogger();
logger.WriteLog();
*/
LoggerFactory factory;
Logger logger;
//读取配置文件
string factoryString = ConfigurationManager.AppSettings["factory"];
//反射生成对象
factory = (LoggerFactory)Assembly.Load("FactoryMethodSample").CreateInstance(factoryString);
logger
= factory.CreateLogger();
logger.WriteLog();

Console.Read();
}

在客户端的调用中使用了.net的配置类和反射用来解决涉及到的开闭原则的问题。比如当我们想要更换一个具体的产品类时我们不需要直接修改客户端源代码,可以在配置文件中设置然后ConfigurationManager.AppSettings可以获取设置,然后用反射来实例化一个工厂对象。

工厂方法的重载

可以通过重载方法对具体类的不同构造函数传入不同的参数。

可以在抽象工厂类中声明多个重载的工厂方法,在具体工厂中实现这些重载的工厂方法以满足不同的、多样化的创建需求。

C#设计模式之创建类模式:工厂方法模式

其他

有时候,为了进一步简化客户端的使用在工厂类中直接调用产品类的业务方法,在客户端无须调用工厂方法创建产品对象,直接只用工厂对象即可调用所创建产品对象中的业务方法。如果对客户端隐藏工厂方法,那么下图为类图的表示:

C#设计模式之创建类模式:工厂方法模式

有1处修改需要注意:

将接口修改为抽象类

//将接口改为抽象类
abstract class LoggerFactory
{
//在工厂类中直接调用日志记录器类的业务方法WriteLog()
public void WriteLog()
{
Logger logger
= this.CreateLogger();
logger.WriteLog();
}

public abstract Logger CreateLogger();
}

在客户端调用如下:

using System;
using System.Configuration;
using System.Reflection;

namespace FactoryMethodSample
{
class Program
{
static void Main(string[] args)
{
LoggerFactory factory;
//针对抽象工厂类编程
//读取配置文件
string factoryString = ConfigurationManager.AppSettings["factory"];
//反射生成对象
factory = (LoggerFactory)Assembly.Load("FactoryMethodSample").CreateInstance(factoryString);
factory.WriteLog();
//直接使用工厂对象来调用产品对象的业务方法
Console.Read();
}
}
}

工厂方法模式的优缺点和使用环境

工厂方法模式的优点:

①工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节。
②能够让工厂自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。
③在系统中加入新产品时,完全符合开闭原则。

工厂方法的缺点:

①系统中类的个数将成对增加(增加一个产品类的同时需要增加一个对应的工厂类),在一定程度上增加了系统的复杂度,会给系统带来一些额外的开销。
②增加了系统的抽象性和理解难度(增加了抽象层)。

工厂方法的适用环境:

①客户端不知道它所需要的对象的类(客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体产品对象由具体工厂类创建)。
②抽象工厂类通过其子类来指定创建哪个对象。