设计模式-工厂方法模式(FactoryMethod)-Java

设计模式-工厂方法模式-Java


目录




内容

1、示例案例

  简单工厂模式虽然简单,但存在一个很严重的问题。当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,者必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?工厂方法模式应运而生,本文将介绍第二种工厂模式-工厂方法模式。

1.1、日志记录器的设计

  Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置由严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公共开发人员面临的要给难题。

  公司的开发人员通过对该需求进行分析,发现该日志记录器由两个设计要点:

  1. 需要封装日志记录器的初始化过程,这些初始化工作较为复杂,例如需要初始化其他相关的类,还有可能需要读取配置文件(例如连接数据库或者创建文件),导致代码较长,如果将它们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护。
  2. 用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或者增加日志记录方式。

 &emsp公司开发人员最初使用简单工厂模式对日志记录器进行了设计,初始结构如图1所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yO6ahhim-1591265368987)(./images/logger_simple.png)]
日记记录器,Logger是抽象日志记录器接口,其子类为具体日志记录器。其中,工厂类LoggerFactory代码片段如下所示:

	// 日志记录器工厂
	class LoggerFactory {
		// 静态工厂方法
		public static Logger createLogger(String args) {
			if(args.equalsIgnoreCase("db")) {
				// 连接数据库,代码省略
				// 创建数据库日志记录器,代码省略
				Logger logger = new DatabaseLogger();
				// 初始化数据库日志记录器,代码省略
				return logger;
			}else if(args.equalsIgnoreCase("file")) {
				// 创建日志文件
				// 创建日志文件记录器对象
				Logger logger = new FileLogger();
				// 初始化文件日志记录器,代码省略
				return logger;
			}else {
				return null;
			}
		}
	}

  为了突出设计重点,我们对上述代码进行了简化,省略了具体日志记录器类的初始化代码。在LoggerFactory类中提供了静态工厂方法creatLogger(),用于根据所传入参数创建各种不同类型的日记记录器。通过使用简单工厂模式,我们将日记记录器对象的创建和使用分离,客户端只需要使用由工厂类创建的日志记录器对象即可,无须关心对象的创建过程,但是我们发现,虽然简单工厂模式实现了对象的创建和使用分离,但是仍然存在如下两个问题:
1.工厂类过于庞大,包含了大量的if…else…代码,导致维护和测试难度增大;
2.系统扩展不灵活,如果增加新类型的日志记录器,必须修改静态公共方法的业务逻辑,违反了“开闭原则”。

  如何解决这两个问题,提供一种简单工厂模式的改进方案?这就是本文所介绍的工厂方法吗模式的动机之一。

2、工厂方法模式概述

  在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它需要知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。简单工厂模式最大缺点是当由新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。

  在工厂方法模式中,我们不再提供一个统一的工厂类来创建索引的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对于的工厂等级结构。工厂方法模式定义如下:

2.1、工厂方法模式定义

定义一个用于创建对象的接口,让子类决定哪一个类实例化。工厂方法模式(Factory Method Pattern)让一个类的实例化延迟到其子类。工厂方法模式又简称工厂模式,又可称作虚拟构造器模式或多态工厂模式。工厂方法模式是一种类创建型模式。

2.2、工厂方法模式要点

  工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,差个年级具体的产品对象。工厂方法模式结构如图2所示:

图2工厂方法模式结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ustzfJlo-1591265368991)(./images/model_factoryMethod.png)]

2.3、工厂方法模式结构图中角色

  • Product(抽象产品):它是定义产品的接口,是工厂方法模式所场景对象的超模型,也就是产品对象的公共父类(接口)。
  • ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
  • Factory(抽象工厂):在抽象工厂中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,索引创建对象的工厂类都必须实现该接口。
  • ConcerteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

2.4、工厂方法模式使用

&与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码吗如下所示:

	interface Factory {
		public Product factoryMethod();
	}

  在抽象工厂中声明了工厂方法但是并未实现工厂方法,具体产品对象的创建由其子类负责,客户端针对抽象工厂编程,可在运行时在指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示:

	class ConcreteFactory implements Factory {
		public Product factoryMethod() {
			return new ConcreteProduct();
		}
	}

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

  在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示:

	...
	Factory factory;
	factory = new ConcreteFactory();
	Product product;
	product = factory.factoryMethod();
	...

  可以通过配置文件来存储具体工厂类ConcreteFactory的类名,更新新的具体工厂时无须修改源代码,系统扩展更为方便。

2.5、思考

  工厂方法模式中的工厂方法能否为静态方法?为什么?

3、工厂方法模式完整解决方案

  Sunny公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图3-1所示:

图3-1 日志记录器结构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mE4beA6X-1591265368993)(./images/logger_factory.png)]
在图3-1中,Logger接口充当抽象产品,其子类FileLogger和DatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactory和DatabaseLoggerFactory充当具体工厂。完整代码如下所示:

// 日志记录器接口:抽象产品
	
	package factoryMethod.first;

	// 日志记录器接口
	public interface Logger {
		void writeLog();
	}

// 数据库日志记录器:具体产品
	package factoryMethod.first;

	public class DatabaseLogger implements Logger{

		@Override
		public void writeLog() {
			System.out.println("数据库日志记录");	
		}

	}

	
// 文件日志记录器:具体产品
	
	package factoryMethod.first;

	public class FileLogger implements Logger{

		@Override
		public void writeLog() {
			System.out.println("文件日志记录");
		}

	}

// 日志记录器工厂接口:抽象工厂
	package factoryMethod.first;

	public class FileLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 连接文件,代码省略
			// 创建文件日志记录器
			Logger logger = new FileLogger();
			// 文件日志记录器初始化,代码省略
			return logger;
		}

	}

// 数据库日志记录器工厂:具体工厂

	package factoryMethod.first;

	public class DatabaseLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 连接数据库,代码省略
			// 创建数据库日志记录器
			Logger logger = new DatabaseLogger();
			// 数据库日志记录器初始化,代码省略
			return logger;
		}

	}

// 文件日志记录器工厂:具体工厂

	package factoryMethod.first;

	public class FileLoggerFactory implements LoggerFactory{

		@Override
		public Logger creatLogger() {
			// 连接文件,代码省略
			// 创建文件日志记录器
			Logger logger = new FileLogger();
			// 文件日志记录器初始化,代码省略
			return logger;
		}

	}

// 客户端
	package factoryMethod.first;

	public class Client {
		public static void main(String[] args) {
			LoggerFactory lf = new DatabaseLoggerFactory();
			Logger logger = lf.creatLogger();
			logger.writeLog();
		}
	}
// 测试结果
数据库日志记录

4、方案的改进

  为了让系统具有更好的灵活性和可扩展性,Sunny公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。

&在客户端代码中将不再使用new关键字来创建工程对象,而是将具体工厂类的类名存储在配置文件中,通过读取配置文件获取文件类名字符串,在使用反射机制,根据类名字符串生产对象。在整个实现过程中需要用到两个技术:Java反射和配置文件读取。

4.1、反射与配置文件

  • 配置文件logger.properties内容

      className=factoryMethod.first.FileLoggerFactory
    
  • 创建工具类生成具体的日志记录器工厂

      // 生成具体日志记录器工厂对象方法类
      
      	package factoryMethod.first;
    
      import java.util.Properties;
    
      public class Utils {
      	public static LoggerFactory getLoggerFactory() {
      		try {
      		LoggerFactory factory = null;
      		Properties prop = new Properties();
      		prop.load(Utils.class.getClassLoader().getResourceAsStream("logger.properties"));
      		String className = prop.getProperty("className");
      		factory = (LoggerFactory)(Class.forName(className).newInstance());
      		return factory;
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
      }
    

4.2、修改客户端代码

// 客户端代码
	package factoryMethod.first;

	public class Client {
		public static void main(String[] args) {
			LoggerFactory lf = Utils.getLoggerFactory();
			Logger logger = lf.creatLogger();
			logger.writeLog();
		}
	}

// 测试结果:
文件日志记录

4.3、新增日志记录方式

  • 步骤:
    1. 新的日志记录方式需要实现(继承)日志记录器Logger
    2. 对应增加新的具体日志记录器工厂,继承抽象日志记录工厂LoggerFactory,并实现其他的供方法createLogger(),设置好初始化参数和环境变量,返回体贴的日志记录器对象;
    3. 修改配置文件logger.properties,将新增的日志记录器工厂类名字符串替换原有工厂类类名记录字符串;

4.4、重载的工厂方法

  Sunny公司开发人员通过进一步分析,发现可以通过多种方式来初始化日志记录器,例如可以为各种日志记录器提供默认实现;还可以为数据库日志记录器提供数据库连接字符串,为文件日志记录器提供文件路径;也可以提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个具体工厂而已,无论使用哪个工厂方法,创建的产品类型均哟啊相同。如图4.4-1所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IC88nxwC-1591265368994)(./images/overload_factoryMethod.png)]
  引入重载方法后,

  • 抽象工厂LoggerFactory的代码4.4-1修改如下:

      package factoryMethod.first;
    
      // 日志记录器工厂接口
      public interface LoggerFactory {
      	Logger createLogger();
      	Logger createLogger(String args);
      	Logger createLogger(Object obj);
      }
    
  • 具体工厂类DatabaseLoggerFactory代码4.4-2:

      package factoryMethod.first;
    
      public class DatabaseLoggerFactory implements LoggerFactory{
    
      	@Override
      	public Logger createLogger() {
      		// 使用默认方式连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(String args) {
      		// 使用参数args作为连接字符串连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(Object obj) {
      		// 使用封装在参数obj中连接字符串连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
    
      }
    

其他具体工厂类省略。

4.5、工厂方法的隐藏

  有时候,为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法差个年级产品,直接通过工厂即可使用所创建的对象中的业务方法。

  如果对客户端隐藏工厂方法,日志记录器的结构图修改为图4.5-1:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEYoItGI-1591265368995)(./images/factoryMethod_yincang.png)]

  • 抽象工厂类LoggerFactory代码修改4.5-1:

      package factoryMethod.second;
    
      // 抽象日志记录器工厂类
      public abstract class LoggerFactory {
      	public abstract Logger createLogger();
      	public abstract Logger createLogger(String args);
      	public abstract Logger createLogger(Object obj);
      	public void writeLog() {
      		Logger logger = this.createLogger();
      		logger.writeLog();
      	}
      }
    
  • 数据库日志记录工厂DatabaseLoggerFactory代码4.5-2:

      package factoryMethod.second;
    
      public class DatabaseLoggerFactory extends LoggerFactory{
    
    
      	public Logger createLogger() {
      		// 使用默认方式连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
    
    
      	public Logger createLogger(String args) {
      		// 使用参数args作为连接字符串连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
    
    
      	public Logger createLogger(Object obj) {
      		// 使用封装在参数obj中连接字符串连接数据库,代码省略
      		// 创建数据库日志记录器
      		Logger logger = new DatabaseLogger();
      		// 数据库日志记录器初始化,代码省略
      		return logger;
      	}
      }
    
  • 文件日志记录器工厂FileLoggerFactoryd代码4.5-3:

      package factoryMethod.second;
    
      public class FileLoggerFactory extends LoggerFactory{
    
      	public Logger createLogger() {
      		// 使用默认方式连接文件,代码省略
      		// 创建文件日志记录器
      		Logger logger = new FileLogger();
      		// 文件日志记录器初始化,代码省略
      		return logger;
      	}
    
      	public Logger createLogger(String args) {
      		// 使用字符串参数args连接文件,代码省略
      		// 创建文件日志记录器
      		Logger logger = new FileLogger();
      		// 文件日志记录器初始化,代码省略
      		return logger;
      	}
    
      	@Override
      	public Logger createLogger(Object obj) {
      		// 使用封装在obj中的参数连接文件,代码省略
      		// 创建文件日志记录器
      		Logger logger = new FileLogger();
      		// 文件日志记录器初始化,代码省略
      		return logger;
      	}
      }
    
  • 客户端Client代码4.5-4:

      package factoryMethod.second;
    
      public class Client {
      	public static void main(String[] args) {
      		LoggerFactory factory = Utils.getLoggerFactory();
      		factory.writeLog();
      	}
      }
    
  • 配置文件logger2.properties内容:

      className=factoryMethod.second.FileLoggerFactory
    
  • 测试结果:

      文件日志记录
    

5、总结

  工厂方法模式是简单工厂方法模式的延伸,它继承了简单工厂模式的优点,同时还秘弥补了简单工厂方法模式的不足。工厂方法模式是使用频率最高的设计模式之一,是很多开源框架和API类库的和核心模式。

5.1、优缺点

  • 主要优点:

    1. 工厂方法用来创建客户所需的产品,同时还向客户隐藏了那种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须指定具体产品类的类名。
    2. 基于工厂角色和产品角色的多态性设计是工厂方法模式的关机。它能够让工厂可以自主决定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
    3. 在系统中加入新产品是,无须修改抽象工厂和抽象产品提供的接口,无需修改客户端,无无须修改其他的具体工厂和具体产品,而只需添加一个具体工厂和具体产品就可以了,这一,系统的可扩展性也就变得非常好,完全符合“开闭原则”
  • 主要缺点:

    1. 在添加新产品是,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中的类的个数将成对增加,在一定程度上增加了系统的复杂都,又更多的类需要编译和运行,这一会给系统带来一些额外的开销。
    2. 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增增加了系统的抽象性和理解难度,且在实现时需要配置文件加载、反射等技术,增加了系统的实现难度。

5.2、适用场景

  1. 在客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类差个年级,可将具体工厂类的类名存储在配置文件或者数据库中。
  2. 抽象工厂类通过其子类来指定差个年级那个对象。在工厂方法模式中,对应抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和李氏代换原则在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

后记

  参考文献:Java设计模式(刘伟).pdf。持续更新,欢迎交流,本人QQ:806797785

前端项目源代码地址:https://gitee.com/gaogzhen/vue-leyou
后端JAVA源代码地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章