JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

时间:2023-03-09 03:05:00
JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

简单工厂模式尽管简单,但存在一个非常严重的问题。当系统中须要引入新产品时,因为静态工厂方法通过所传入參数的不同来创建不同的产品,这必然要改动工厂类的源码,将违背“开闭原则”。怎样实现添加新产品而不影响已有代码?工厂方法模式应运而生,本文将介绍另外一种工厂模式——工厂方法模式。

1 日志记录器的设计

Sunny软件公司欲开发一个系统执行日志记录器(Logger)。该记录器能够通过多种途径保存系统的执行日志,如通过文件记录或数据库记录,用户能够通过改动配置文件灵活地更换日志记录方式。

在设计各类日志记录器时。Sunny公司的开发者发现须要对日志记录器进行一些初始化工作,初始化參数的设置过程较为复杂,并且某些參数的设置有严格的先后次序,否则可能会发生记录失败。怎样封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发者面临的一个难题。

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

(1) 须要封装日志记录器的初始化过程。这些初始化工作较为复杂,比如须要初始化其它相关的类,还有可能须要读取配置文件(比如连接数据库或创建文件),导致代码较长,假设将它们都写在构造函数中,会导致构造函数庞大。不利于代码的改动和维护;

(2) 用户可能须要更换日志记录方式,在client代码中须要提供一种灵活的方式来选择日志记录器,尽量在不改动源码的基础上更换或者添加日志记录方式。

Sunny公司开发者最初使用简单工厂模式对日志记录器进行了设计,初始结构如图1所看到的:

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTG92ZUxpb24=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="779" height="422" style="border:none; max-width:100%">

图1 基于简单工厂模式设计的日志记录器结构图

在图1中,LoggerFactory充当创建日志记录器的工厂。提供了工厂方法createLogger()用于创建日志记录器。Logger是抽象日志记录器接口。其子类为详细日志记录器。

当中,工厂类LoggerFactory代码片段例如以下所看到的:

[java] view
plain
copy
  1. //日志记录器工厂
  2. public class LoggerFactory {
  3. //静态工厂方法
  4. public static Logger createLogger(String args) {
  5. if(args.equalsIgnoreCase("db")) {
  6. //连接数据库,代码省略
  7. //创建数据库日志记录器对象
  8. Logger logger = new DatabaseLogger();
  9. //初始化数据库日志记录器,代码省略
  10. return logger;
  11. }
  12. else if(args.equalsIgnoreCase("file")) {
  13. //创建日志文件
  14. //创建文件日志记录器对象
  15. Logger logger = new FileLogger();
  16. //初始化文件日志记录器。代码省略
  17. return logger;
  18. }
  19. else {
  20. return null;
  21. }
  22. }
  23. }

为了突出设计重点,我们对上述代码进行了简化,省略了详细日志记录器类的初始化代码。在LoggerFactory类中提供了静态工厂方法createLogger()。用于依据所传入的參数创建各种不同类型的日志记录器。

通过使用简单工厂模式。我们将日志记录器对象的创建和使用分离。client仅仅需使用由工厂类创建的日志记录器对象就可以,无须关心对象的创建过程,可是我们发现。尽管简单工厂模式实现了对象的创建和使用分离,可是仍然存在例如以下两个问题:

(1) 工厂类过于庞大,包括了大量的if…else…代码,导致维护和測试难度增大;

(2) 系统扩展不灵活。假设添加新类型的日志记录器,必须改动静态工厂方法的业务逻辑。违反了“开闭原则”。

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

2 工厂方法模式概述

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

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

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

工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来详细实现工厂方法,创建详细的产品对象。

工厂方法模式结构如图2所看到的:

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTG92ZUxpb24=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="525" height="335" style="border:none; max-width:100%">

图2 工厂方法模式结构图

在工厂方法模式结构图中包括例如以下几个角色:

    ● Product(抽象产品):它是定义产品的接口。是工厂方法模式所创建对象的超类型。也就是产品对象的公共父类。

  ● ConcreteProduct(详细产品):它实现了抽象产品接口,某种类型的详细产品由专门的详细工厂创建,详细工厂和详细产品之间一一相应。

   ● Factory(抽象工厂):在抽象工厂类中。声明了工厂方法(Factory Method)。用于返回一个产品。

抽象工厂是工厂方法模式的核心,全部创建对象的工厂类都必须实现该接口。

    ● ConcreteFactory(详细工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法。并可由client调用,返回一个详细产品类的实例。

与简单工厂模式相比,工厂方法模式最重要的差别是引入了抽象工厂角色,抽象工厂能够是接口,也能够是抽象类或者详细类,其典型代码例如以下所看到的:

[java] view
plain
copy
  1. public interface Factory {
  2. public Product factoryMethod();
  3. }

在抽象工厂中声明了工厂方法但并未实现工厂方法,详细产品对象的创建由其子类负责,client针对抽象工厂编程。可在执行时再指定详细工厂类,详细工厂类实现了工厂方法,不同的详细工厂能够创建不同的详细产品,其典型代码例如以下所看到的:

[java] view
plain
copy
  1. public class ConcreteFactory implements Factory {
  2. public Product factoryMethod() {
  3. return new ConcreteProduct();
  4. }
  5. }

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

在client代码中。仅仅需关心工厂类就可以,不同的详细工厂能够创建不同的产品。典型的client类代码片段例如以下所看到的:

[java] view
plain
copy
  1. ……
  2. Factory factory;
  3. factory = new ConcreteFactory(); //可通过配置文件实现
  4. Product product;
  5. product = factory.factoryMethod();
  6. ……

能够通过配置文件来存储详细工厂类ConcreteFactory的类名,更换新的详细工厂时无须改动源码,系统扩展更为方便。

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

思考

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


3 完整解决方式

Sunny公司开发者决定使用工厂方法模式来设计日志记录器。其基本结构如图3所看到的:

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

图3 日志记录器结构图

在图3中,Logger接口充当抽象产品。其子类FileLogger和DatabaseLogger充当详细产品,LoggerFactory接口充当抽象工厂。其子类FileLoggerFactory和DatabaseLoggerFactory充当详细工厂。

完整代码例如以下所看到的:

[java] view
plain
copy
  1. //日志记录器接口:抽象产品
  2. public interface Logger {
  3. public void writeLog();
  4. }
  5. //数据库日志记录器:详细产品
  6. class DatabaseLogger implements Logger {
  7. public void writeLog() {
  8. System.out.println("数据库日志记录。");
  9. }
  10. }
  11. //文件日志记录器:详细产品
  12. class FileLogger implements Logger {
  13. public void writeLog() {
  14. System.out.println("文件日志记录。

    ");

  15. }
  16. }
  17. //日志记录器工厂接口:抽象工厂
  18. public interface LoggerFactory {
  19. public Logger createLogger();
  20. }
  21. //数据库日志记录器工厂类:详细工厂
  22. class DatabaseLoggerFactory implements LoggerFactory {
  23. public Logger createLogger() {
  24. //连接数据库。代码省略
  25. //创建数据库日志记录器对象
  26. Logger logger = new DatabaseLogger();
  27. //初始化数据库日志记录器,代码省略
  28. return logger;
  29. }
  30. }
  31. //文件日志记录器工厂类:详细工厂
  32. class FileLoggerFactory implements LoggerFactory {
  33. public Logger createLogger() {
  34. //创建文件日志记录器对象
  35. Logger logger = new FileLogger();
  36. //创建文件,代码省略
  37. return logger;
  38. }
  39. }

编写例如以下client測试代码:

[java] view
plain
copy
  1. public class Client {
  2. public static void main(String args[]) {
  3. LoggerFactory factory = new FileLoggerFactory(); //可引入配置文件实现
  4. Logger logger = factory.createLogger();
  5. logger.writeLog();
  6. }
  7. }

编译并执行程序,输出结果例如以下:

文件日志记录。

4 反射与配置文件

为了让系统具有更好的灵活性和可扩展性,Sunny公司开发者决定对日志记录器client代码进行重构。使得能够在不改动不论什么client代码的基础上更换或添加新的日志记录方式。

在client代码中将不再使用newkeyword来创建工厂对象,而是将详细工厂类的类名存储在配置文件(如XML文件)中,通过读取配置文件获取类名字符串。再使用Java的反射机制。依据类名字符串生成对象。

在整个实现过程中须要用到两个技术:Java反射机制与配置文件读取。软件系统的配置文件通常为XML文件。我们能够使用DOM
(Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技术来处理XML文件。

关于DOM、SAX、StAX等技术的具体学习大家能够參考其它相关资料。在此不予扩展。

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

扩展

关于Java与XML的相关资料,大家能够阅读Tom Myers和Alexander
Nakhimovsky所著的《Java XML编程指南》一书或訪问developer Works中国中的“Java XML 技术专题”。參考链接:

http://www.ibm.com/developerworks/cn/xml/theme/x-java.html

       Java反射(Java Reflection)是指在程序执行时获取已知名称的类或已有对象的相关信息的一种机制,包含类的方法、属性、父类等信息,还包含实例的创建和实例类型的推断等。

在反射中使用最多的类是Class。Class类的实例表示正在执行的Java应用程序中的类和接口,其forName(String
className)方法能够返回与带有给定字符串名的类或接口相关联的Class对象,再通过Class对象的newInstance()方法创建此对象所表示的类的一个新实例。即通过一个类名字符串得到类的实例。如创建一个字符串类型的对象,其代码例如以下:

[java] view
plain
copy
  1. //通过类名生成实例对象并将其返回
  2. Class c=Class.forName("String");
  3. Object obj=c.newInstance();
  4. return obj;

此外。在JDK中还提供了java.lang.reflect包,封装了其它与反射相关的类。此处仅仅用到上述简单的反射代码。在此不予扩展。

Sunny公司开发者创建了例如以下XML格式的配置文件config.xml用于存储详细日志记录器工厂类类名:

[html] view
plain
copy
  1. <!— config.xml -->
  2. <?xml version="1.0"?>
  3. <config>
  4. <className>FileLoggerFactory</className>
  5. </config>

为了读取该配置文件并通过存储在当中的类名字符串反射生成对象,Sunny公司开发者开发了一个名为XMLUtil的工具类,其具体代码例如以下所看到的:

import java.io.File;
import org.dom4j.Document;
import org.dom4j.io.SAXReader;
public class XMLUtil {
//该方法用于从XML配置文件里提取图表类型,并返回类型名
public static Object getBean() throws Exception {
SAXReader reader = new SAXReader();
String path = XMLUtil.class.getClassLoader().
getResource("com/somnus/designPatterns/factoryMethod/config.xml").getPath();
Document document = reader.read(new File(path));
String cName = document.selectSingleNode("/config/className").getText();
//通过类名生成实例对象并将其返回
Class<?> c = Class.forName(cName);
Object obj = c.newInstance();
return obj;
}
}

有了XMLUtil类后。能够对日志记录器的client代码进行改动。不再直接使用newkeyword来创建详细的工厂类,而是将详细工厂类的类名存储在XML文件里。再通过XMLUtil类的静态工厂方法getBean()方法进行对象的实例化。代码改动例如以下:

public class Client {
public static void main(String[] args) throws Exception {
//getBean()的返回类型为Object。须要进行强制类型转换
LoggerFactory factory = (LoggerFactory)XMLUtil.getBean();
Logger logger = factory.createLogger();
logger.writeLog();
}
}

引入XMLUtil类和XML配置文件后,假设要添加新的日志记录方式,仅仅须要运行例如以下几个步骤:

(1) 新的日志记录器须要继承抽象日志记录器Logger;

(2) 相应添加一个新的详细日志记录器工厂,继承抽象日志记录器工厂LoggerFactory,并实现当中的工厂方法createLogger()。设置好初始化參数和环境变量,返回详细日志记录器对象。

(3) 改动配置文件config.xml。将新增的详细日志记录器工厂类的类名字符串替换原有工厂类类名字符串。

(4) 编译新增的详细日志记录器类和详细日志记录器工厂类,执行client測试类就可以使用新的日志记录方式,而原有类库代码无须做不论什么改动,全然符合“开闭原则”。

通过上述重构能够使得系统更加灵活。因为非常多设计模式都关注系统的可扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

思考

有人说:能够在client代码中直接通过反射机制来生成产品对象,在定义产品对象时使用抽象类型。相同能够确保系统的灵活性和可扩展性,添加新的详细产品类无须改动源码,仅仅须要将其作为抽象产品类的子类再改动配置文件就可以。根本不须要抽象工厂类和详细工厂类。

试思考这样的做法的可行性?假设可行。这样的做法是否存在问题?为什么?

5 重载的工厂方法

Sunny公司开发者通过进一步分析。发现能够通过多种方式来初始化日志记录器,比如能够为各种日志记录器提供默认实现;还能够为数据库日志记录器提供数据库连接字符串。为文件日志记录器提供文件路径;也能够将參数封装在一个Object类型的对象中。通过Object对象将配置參数传入工厂类。此时,能够提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个详细工厂而言。不管使用哪个工厂方法,创建的产品类型均要同样。

如图4所看到的:

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvTG92ZUxpb24=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" width="646" height="288" style="border:none; max-width:100%">

图4 重载的工厂方法结构图

引入重载方法后,抽象工厂LoggerFactory的代码改动例如以下:

[java] view
plain
copy
  1. interface LoggerFactory {
  2. public Logger createLogger();
  3. public Logger createLogger(String args);
  4. public Logger createLogger(Object obj);
  5. }

详细工厂类DatabaseLoggerFactory代码改动例如以下:

[java] view
plain
copy
  1. class DatabaseLoggerFactory implements LoggerFactory {
  2. public Logger createLogger() {
  3. //使用默认方式连接数据库,代码省略
  4. Logger logger = new DatabaseLogger();
  5. //初始化数据库日志记录器,代码省略
  6. return logger;
  7. }
  8. public Logger createLogger(String args) {
  9. //使用參数args作为连接字符串来连接数据库,代码省略
  10. Logger logger = new DatabaseLogger();
  11. //初始化数据库日志记录器,代码省略
  12. return logger;
  13. }
  14. public Logger createLogger(Object obj) {
  15. //使用封装在參数obj中的连接字符串来连接数据库,代码省略
  16. Logger logger = new DatabaseLogger();
  17. //使用封装在參数obj中的数据来初始化数据库日志记录器。代码省略
  18. return logger;
  19. }
  20. }
  21. //其它详细工厂类代码省略

在抽象工厂中定义多个重载的工厂方法,在详细工厂中实现了这些工厂方法。这些方法能够包括不同的业务逻辑。以满足对不同产品对象的需求。

6 工厂方法的隐藏

有时候。为了进一步简化client的使用。还能够对client隐藏工厂方法,此时。在工厂类中将直接调用产品类的业务方法,client无须调用工厂方法创建产品,直接通过工厂就可以使用所创建的对象中的业务方法。

假设对client隐藏工厂方法,日志记录器的结构图将改动为图5所看到的:

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

图5 隐藏工厂方法后的日志记录器结构图

在图5中。抽象工厂类LoggerFactory的代码改动例如以下:

[java] view
plain
copy
  1. //改为抽象类
  2. abstract class LoggerFactory {
  3. //在工厂类中直接调用日志记录器类的业务方法writeLog()
  4. public void writeLog() {
  5. Logger logger = this.createLogger();
  6. logger.writeLog();
  7. }
  8. public abstract Logger createLogger();
  9. }

client代码改动例如以下:

[java] view
plain
copy
  1. class Client {
  2. public static void main(String args[]) {
  3. LoggerFactory factory;
  4. factory = (LoggerFactory)XMLUtil.getBean();
  5. factory.writeLog(); //直接使用工厂对象来调用产品对象的业务方法
  6. }
  7. }

通过将业务方法的调用移入工厂类。能够直接使用工厂对象来调用产品对象的业务方法,client无须直接使用工厂方法。在某些情况下我们也能够使用这样的设计方案。

7
工厂方法模式总结

工厂方法模式是简单工厂模式的延伸。它继承了简单工厂模式的长处。同一时候还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之中的一个。是非常多开源框架和API类库的核心模式。

        1. 主要长处

工厂方法模式的主要长处例如以下:

(1) 在工厂方法模式中,工厂方法用来创建客户所须要的产品,同一时候还向客户隐藏了哪种详细产品类将被实例化这一细节,用户仅仅须要关心所需产品相应的工厂,无须关心创建细节,甚至无须知道详细产品类的类名。

(2) 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它可以让工厂可以自主确定创建何种产品对象。而怎样创建这个对象的细节则全然封装在详细工厂内部。工厂方法模式之所以又被称为多态工厂模式。就正是由于全部的详细工厂类都具有同一抽象父类。

(3) 使用工厂方法模式的还有一个长处是在系统中加入新产品时,无须改动抽象工厂和抽象产品提供的接口,无须改动client,也无须改动其它的详细工厂和详细产品,而仅仅要加入一个详细工厂和详细产品就能够了,这样,系统的可扩展性也就变得很好,全然符合“开闭原则”。

      2. 主要缺点

工厂方法模式的主要缺点例如以下:

(1) 在加入新产品时,须要编写新的详细产品类。并且还要提供与之相应的详细工厂类。系统中类的个数将成对添加。在一定程度上添加了系统的复杂度,有很多其它的类须要编译和执行,会给系统带来一些额外的开销。

(2) 因为考虑到系统的可扩展性。须要引入抽象层。在client代码中均使用抽象层进行定义,添加了系统的抽象性和理解难度。且在实现时可能须要用到DOM、反射等技术,添加了系统的实现难度。

       3. 适用场景

在下面情况下能够考虑使用工厂方法模式:

(1) client不知道它所须要的对象的类。在工厂方法模式中。client不须要知道详细产品类的类名。仅仅须要知道所相应的工厂就可以,详细的产品对象由详细工厂类创建。可将详细工厂类的类名存储在配置文件或数据库中。

(2) 抽象工厂类通过其子类来指定创建哪个对象。

在工厂方法模式中。对于抽象工厂类仅仅须要提供一个创建产品的接口,而由其子类来确定详细要创建的对象,利用面向对象的多态性和里氏代换原则。在程序执行时。子类对象将覆盖父类对象。从而使得系统更easy扩展。

JAVA设计模式(01):创建型-工厂模式【工厂方法模式】(Factory Method)

练习

使用工厂方法模式设计一个程序来读取各种不同类型的图片格式,针对每一种图片格式都设计一个图片读取器,如GIF图片读取器用于读取GIF格式的图片、JPG图片读取器用于读取JPG格式的图片。

需充分考虑系统的灵活性和可扩展性。