一起学习设计模式--03.工厂方法模式
简单工厂模式虽然简单,但是存在一个很严重的问题:由于静态工厂方法是根据传入的参数不同来创建不同的产品的,所以当系统中需要引入新产品时,就需要修改工厂类的源代码,这将违背开闭原则。为了实现增加新产品而不修改原有代码,工厂方法模式应运而生。
一、日志记录器的设计
A科技公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,例如通过文件或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,A科技公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是A科技公司开发人员面临的一个难题。
开发人员对需求进行分析,发现该日志记录器有如下两个设计要点:
- 需要封装日志记录器的初始化过程,这些初始化工作比较复杂。比如:需要初始化其它相关的类,还有可能需要配置工作环境(如连接数据库或创建文件),导致代码较长,如果将他们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护。
- 用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或增加日志记录方式。
开发人员最开始使用简单工厂模式对日志记录器进行了设计,结构图如下:
LoggerFactory 充当创建日志记录器的工厂,CreateLogger() 负责创建日志记录日,ILogger 是抽象日志记录器的接口,FileLogger 和 DatabaseLogger 是具体的日志记录器。工厂类 LoggerFactory 的代码如下:
public class LoggerFactory
{
public static ILogger CreateLogger(string args)
{
ILogger logger = null;
if (args.Equals("db", StringComparison.OrdinalIgnoreCase))
{
//连接数据库,代码省略
//创建数据库日志记录器对象
logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
}
else if (args.Equals("file", StringComparison.OrdinalIgnoreCase))
{
//创建日志文件,代码省略
//创建文件日志记录器队形
logger = new FileLogger();
//初始化文件日志记录器,代码省略
}
return logger;
}
}
虽然从上边的代码可以看出简单工厂模式实现了对象的创建和使用分离,但是仍然存在两个问题:
- 工厂类过于庞大,而且包含了大量的 if...else... 代码,导致维护和测试难度增大。
- 系统的扩展不灵活,如果要增加新的日志记录器,必须修改静态工厂方法的业务逻辑,违反了开闭原则。
工厂方法模式的动机之一就是为了解决以上两个问题。
二、工厂方法模式概述
在简单工厂模式中只提供一个工厂类,这个工厂类处于实例化产品类的中心位置,他需要知道每个产品类的创建细节,并决定在何时实例化哪一个产品类。简单工厂最大的缺点就是每当有新的产品要加入系统的时候,就必须修改工厂类,在静态工厂方法中添加新产品的业务逻辑,这就违反了开闭原则。另外,简单工厂模式中,所有产品的创建都由同一个工厂类负责,工厂类的职责过于繁重,业务逻辑较为复杂,具体产品和工厂类之间的耦合度较高,严重影响了系统的扩展性和灵活性。工厂方法模式刚好就解决了这一点。
工厂方法模式,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。定义如下:
工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Patter),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。
工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。结构图如下:
从上图可以看出,工厂方法模式包含以下4个角色:
- IProduct(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
- ConreteProduct(具体产品):它实现了抽象产品的接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
- IFactory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
- ConreteFactory(具体工厂):它是抽象工厂的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
与简单工厂模式相比,工厂方法模式的主要区别就是引入了抽象工厂角色。抽象工厂可以是接口、抽象类或具体类。典型代码如下:
public interface IFactory{
IProduct FactoryMethod();
}
抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责。客户端针对抽象工厂编程,可以在运行时再指定具体工厂类。不同的具体工厂类可以创建不同的具体产品。
public class ConcreteFactory : IFactory{
public IProduct FactoryMethod(){
return new ConcreteProduct
}
}
具体工厂类除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工,比如:连接数据库、创建文件等。
三、完整的解决方案
开发人员决定是使用工厂方法模式来设计日志记录器,基本结构如图:
ILogger 充当抽象产品,其子类 FileLogger 、DatabaseLogger 是具体产品。ILoggerFactory 充当抽象工厂,FileLoggerFactory、DatabaseLoggerFactory 充当具体工厂。
完整代码如下:
/// <summary>
/// 日志记录器接口:抽象产品
/// </summary>
public interface ILogger
{
void WriteLog();
}
/// <summary>
/// 文件日志记录器:具体产品
/// </summary>
public class FileLogger : ILogger
{
public void WriteLog()
{
Console.WriteLine("文件日志记录!");
}
}
/// <summary>
/// 数据库日志记录器:具体产品
/// </summary>
public class DatabaseLogger : ILogger
{
public void WriteLog()
{
Console.WriteLine("数据库日志记录!");
}
}
/// <summary>
/// 日志记录器工厂接口:抽象工厂
/// </summary>
public interface ILoggerFactory
{
ILogger CreateLogger();
}
/// <summary>
/// 文件日志记录器工厂类:具体工厂
/// </summary>
public class FileLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger()
{
//创建文件日志记录器对象
var logger = new FileLogger();
//创建文件,省略代码
return logger;
}
}
/// <summary>
/// 数据库日志记录器工厂类:具体工厂
/// </summary>
public class DatabaseLoggerFactory : ILoggerFactory
{
public ILogger CreateLogger()
{
//连接数据库,代码省略
//创建数据库日志记录器对象
var logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}
客户端测试代码:
class Program
{
static void Main(string[] args)
{
var factory = new FileLoggerFactory();//可引入配置文件实现
var logger = factory.CreateLogger();
logger.WriteLog();
}
}
输出:
四、反射与配置文件
目前来说代码还存在一些问题,就是如果客户端要更换具体的日志记录器,就需要修改客户端的具体日志记录器工厂类的创建,这一点上来说违背了开闭原则。
为了让系统具有更好的灵活性和可扩展性,开发人员决定对日志记录器客户端代码进行重构,希望最终可以达到在不修改客户端任何代码的情况下更换或增加新的日志记录方式。
在客户端代码中将不再使用 new 关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件中(比如XML中),通过读取配置文件获取工厂类的类名字符串,然后再借助 .NET 的反射机制,根据类名字符串生成对象。
1.反射
有这么一句话 反射反射,程序员的快乐
,由此可见反射在开发中有着举足轻重的地位,在很多框架中都可以看到它的身影。
反射的定义:
反射是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断等。
2.实现
创建配置文件 App.config:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<!--value为具体工厂类的完全限定名(命名空间+类名)-->
<add key="LoggerFactory" value="LXP.DesignPattern.FactoryMethod.v2.FileLoggerFactory"/>
</appSettings>
</configuration>
创建一个配置文件的帮助类 AppConfigHelper 代码如下:
/// <summary>
/// 配置文件帮助类
/// </summary>
public class AppConfigHelper
{
/// <summary>
/// 获取具体日志工厂方法
/// </summary>
/// <returns></returns>
public static object GetLoggerFactory()
{
try
{
var loggerFactoryName = ConfigurationManager.AppSettings["LoggerFactory"];
var type = Type.GetType(loggerFactoryName);
return type == null ? null : Activator.CreateInstance(type);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return null;
}
}
客户端测试代码:
class Program
{
static void Main(string[] args)
{
//var factory = new FileLoggerFactory();//可引入配置文件实现
var factory = (ILoggerFactory) AppConfigHelper.GetLoggerFactory();
var logger = factory.CreateLogger();
logger.WriteLog();
}
}
后期如果要新增一个日志记录器,就创建一个具体的日志记录器(需要实现ILogger接口),然后新增一个具体的日志记录器的工厂类,然后将该工厂类的完全限定名(命名空间+类名)替换配置文件中原有工厂类类名字符串即可。原有类库代码和客户端无需做任何修改,完全符合开闭原则。
3.补充
.NET 中反射有多种方法:
1.如果要反射一个 DLL 中的类,并且程序并没有引用该 DLL(即对该程序来说,这个DLL中的类是一个未知的类型),可通过以下方法:
Assembly assembly = Assembly.LoadFile("程序集路径,不能是相对路径"); // 加载程序集(EXE 或 DLL)
object obj = assembly.CreateInstance("类的完全限定名(即包括命名空间)"); // 创建类的实例
2.如果要反射当前项目中的类
//方法1
Assembly assembly = Assembly.GetExecutingAssembly(); // 获取当前程序集
object obj = assembly.CreateInstance("类的完全限定名(命名空间 + 类名)"); // 创建类的实例,返回为 object 类型,需要强制类型转换
//方法2
Type type = Type.GetType("类的完全限定名(命名空间 + 类名)");
object obj = type.Assembly.CreateInstance(type);
//方法3
Type type = Type.GetType("类的完全限定名(命名空间 + 类名)");
object obj = Activator.CreateInstance(type);
详情的使用这里就不展开了,大家可以自行搜索。
五、工厂方法的隐藏
有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法。此时,在工厂类中将直接调用产品类的业务方法,客户端无需调用工厂方法创建具体的产品,直接通过工厂即可使用所创建的对象中的业务方法。
这时,需要需要将原来的工厂接口改为抽象工厂类,在抽象类中添加一个方法,在该方法中创建了具体的产品,并调用产品的业务方法。具体代码如下:
/// <summary>
/// 将工厂接口改为抽象类
/// </summary>
public abstract class LoggerFactory
{
/// <summary>
/// 在工厂类中直接调用日志记录器类的业务方法 WriteLog()
/// </summary>
public void WriteLog()
{
var logger = this.CreateLogger();
logger.WriteLog();
}
public abstract ILogger CreateLogger();
}
具体的工厂类需要将实现 ILoggerFactory
修改为继承抽象类 LoggerFactory
。
客户端代码修改:
class Program
{
static void Main(string[] args)
{
var factory = (ILoggerFactory) AppConfigHelper.GetLoggerFactory();
factory.WriteLog();//直接使用工厂对象来调用产品对象的业务方法
}
}
六、工厂方法模式总结
既继承了简单工厂模式的优点,又弥补了简单工厂模式的不足。
工厂方法模式是使用频率最高的设计模式之一。
1.主要优点
- 工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节。用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
- 在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其它的具体工厂和具体产品,只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变的非常好,完全符合开闭原则。
2.主要缺点
- 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中的类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
- 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
3.适用场景
- 客户端不知道其所需要的对象的类。在工厂方法模式中,客户端不需要知道具体的产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件中或数据库中。
- 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏替换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
如果您觉得这篇文章有帮助到你,欢迎推荐,也欢迎关注我的公众号。
示例代码:
https://github.com/crazyliuxp/DesignPattern.Simples.CSharp