设计模式的征途—3.工厂方法(Factory Method)模式
上一篇的简单工厂模式虽然简单,但是存在一个很严重的问题:当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背开闭原则。如何实现新增新产品而不影响已有代码?工厂方法模式为此应运而生。
工厂方法模式(Factory Method) | 学习难度:★★☆☆☆ | 使用频率:★★★★★ |
一、简单工厂版的日志记录器
1.1 软件需求说明
Requirement:M公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,例如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,M公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的摄制过程比较复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是M公司开发人员面临的一个难题。
M公司开发人员学习了简单工厂模式对日志记录器进行了设计,初始结构如下图所示。
1.2 基于简单工厂的代码实现
M公司的程序猿按照结构图,写下了核心代码LoggerFactory的CreateLogger方法:
// 简单工厂方法 public static ILogger CreateLogger(string args) { if (args.Equals("db", StringComparison.OrdinalIgnoreCase)) { // 连接数据库,代码省略 // 创建数据库日志记录器对象 ILogger logger = new DatabaseLogger(); // 初始化数据库日志记录器,代码省略 return logger; } else if(args.Equals("file", StringComparison.OrdinalIgnoreCase)) { // 创建日志文件,代码省略 // 创建文件日志记录器对象 ILogger logger = new FileLogger(); // 初始化文件日志记录器,代码省略 return logger; } else { return null; } }
上述代码省略了具体日志记录器类的初始化代码,在LoggerFactory中提供了静态工厂方法CreateLogger(),用于根据所传入的参数创建各种不同类型的日志记录器。通过使用简单工厂模式,将日志记录器对象的创建和使用分离,客户端只需要使用由工厂类创建的日志记录器对象即可,无须关心对象的创建过程。
But,虽然简单工厂模式实现了对象的创建和使用分离,仍然存在以下两个问题:
(1)工厂类过于庞大!包含了大量的if-else代码,维护和测试的难度增大不少。
(2)系统扩展不灵活,如果新增类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反了开闭原则。
如何解决这两个问题,M公司程序猿苦思冥想,想要改进简单工厂模式,于是开始学习工厂方法模式。
二、工厂方法模式介绍
2.1 工厂方法模式概述
在简单工厂模式中只提供一个工厂类,该工厂类需要知道每一个产品对象的创建细节,并决定合适实例化哪一个产品类。其最大的缺点就是当有新产品加入时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了开闭原则。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度较高,严重影响了系统的灵活性和扩展性。
在工厂方法模式中,不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
工厂方法(Factory Method)模式:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式,也可称为多态工厂模式,它是一种创建型模式。
2.2 工厂方法模式结构图
工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法并创建具体的产品对象。
从图中可以看出,在工厂方法模式结构图中包含以下4个角色:
(1)Product(抽象产品):定义产品的接口,是工厂方法模式所创建的对象的超类,也就是产品对象的公共父类。
(2)ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
(3)Factory(抽象工厂):抽象工厂类,声明了工厂方法,用于返回一个产品。
(4)ConcreteFactory(具体工厂):抽象工厂的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
三、工厂方法版的日志记录器
3.1 解决方案
M公司的程序猿学习了工厂方法之后,决定使用工厂方法模式来重构设计,其基本结构图如下图所示:
其中, Logger接口充当抽象产品角色,而FileLogger和DatabaseLogger则充当具体产品角色。LoggerFactory接口充当抽象工厂角色,而FileLoggerFactory和DatabaseLoggerFactory则充当具体工厂角色。
3.2 重构代码
(1)抽象产品:ILogger接口
public interface ILogger { void WriteLog(); }
(2)具体产品:FileLogger和DatabaseLogger类
public class FileLogger : ILogger { public void WriteLog() { Console.WriteLine("文件日志记录..."); } } public class DatabaseLogger : ILogger { public void WriteLog() { Console.WriteLine("数据库日志记录..."); } }
(3)抽象工厂:ILoggerFactory接口
public interface ILoggerFactory { ILogger CreateLogger(); }
(4)具体工厂:FileLoggerFactory和DatabaseLoggerFactory类
public class FileLoggerFactory : ILoggerFactory { public ILogger CreateLogger() { // 创建文件日志记录器 ILogger logger = new FileLogger(); // 创建文件,代码省略 return logger; } } public class DatabaseLoggerFactory : ILoggerFactory { public ILogger CreateLogger() { // 连接数据库,代码省略 // 创建数据库日志记录器对象 ILogger logger = new DatabaseLogger(); // 初始化数据库日志记录器,代码省略 return logger; } }
(5)客户端调用
public static void Main() { ILoggerFactory factory = new FileLoggerFactory(); // 可通过引入配置文件实现 if (factory == null) { return; } ILogger logger = factory.CreateLogger(); logger.WriteLog(); }
运行结果如下图:
四、借助反射的重构版本
4.1 逃离修改客户端的折磨
为了让系统具有更好的灵活性和可扩展性,M公司程序猿决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础之上更换或是增加新的日志记录方式。
在客户端代码中将不再使用new关键字来创建工厂对象,而是将具体工厂类的类名存在配置文件(例如XML文件)中,通过读取配置文件来获取类名,再借助.NET反射机制来动态地创建对象实例。
4.2 撸起袖子开始重构
(1)创建配置文件
<?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="LoggerFactory" value="Manulife.ChengDu.DesignPattern.FactoryMethod.v2.DatabaseLoggerFactory, Manulife.ChengDu.DesignPattern.FactoryMethod" /> </appSettings> </configuration>
(2)封装一个简单的AppConfigHelper类
public class AppConfigHelper { public static string GetLoggerFactoryName() { string factoryName = null; try { factoryName = System.Configuration.ConfigurationManager.AppSettings["LoggerFactory"]; } catch (Exception ex) { Console.WriteLine(ex.Message); } return factoryName; } public static object GetLoggerFactoryInstance() { string assemblyName = AppConfigHelper.GetLoggerFactoryName(); Type type = Type.GetType(assemblyName); var instance = Activator.CreateInstance(type); return instance; } }
(2)重构客户端代码
public static void Main() { ILoggerFactory factory = (ILoggerFactory)AppConfigHelper.GetLoggerFactoryInstance(); if (factory == null) { return; } ILogger logger = factory.CreateLogger(); logger.WriteLog(); }
运行结果如下图所示:
五、工厂方法的隐藏
有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。
(1)修改抽象工厂
public abstract class LoggerFactory { // 在工厂类中直接调用日志记录器的业务方法WriteLog() public void WriteLog() { ILogger logger = this.CreateLogger(); logger.WriteLog(); } public abstract ILogger CreateLogger(); }
(2)修改具体工厂
public class DatabaseLoggerFactory : LoggerFactory { public override ILogger CreateLogger() { // 连接数据库,代码省略 // 创建数据库日志记录器对象 ILogger logger = new DatabaseLogger(); // 初始化数据库日志记录器,代码省略 return logger; } }
(3)简化的客户端调用
public static void Main() { LoggerFactory factory = (LoggerFactory)AppConfigHelper.GetLoggerFactoryInstance(); if (factory == null) { return; } factory.WriteLog(); }
六、工厂方法模式总结
5.1 主要优点
- 工厂方法用于创建客户所需要的产品,还向客户隐藏了哪种具体产品类将被实例化这一细节。因此,用户只需要关心所需产品对应的工厂,无须关心创建细节。
- 在系统中加入新产品时,无需修改抽象工厂和抽象产品提供的接口,也无须修改客户端,还无须修改其他的具体工厂和具体产品,而只要加入一个具体工厂和具体产品就可以了。因此,系统的可扩展性得到了保证,符合开闭原则。
5.2 主要缺点
- 在添加新产品时,需要编写新的具体产品类,还要提供与之对应的具体工厂类,系统中类的个数将成对增加,一定程度上增加了系统的复杂度。
- 由于考虑到系统的可扩展性,需要引入抽象层,且在实现时可能需要用到反射等技术,增加了系统的实现难度。
5.3 适用场景
- 客户端不知道其所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的的工厂即可,具体的产品对象由具体工厂创建,可将具体工厂的类名存储到配置文件或数据库中。
- 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏替换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统易于扩展。
参考资料
刘伟,《设计模式的艺术—软件开发人员内功修炼之道》