工厂模式是创建型模式的一种。通过提供创建对象的接口,而不是直接创建对象实例,使得代码更具扩展性和灵活性。根据对象创建的不同场景,工厂模式可以分为三类:简单工厂模式、工厂方法模式和抽象工厂模式。

简单工厂模式

简单工厂模式通过一个单一的工厂类,根据不同参数,创建并返回不同的实例对象。通过简单工厂模式,可以将对象的创建逻辑封装起来,提供统一的接口给客户端使用,简化了客户端的代码复杂度,提高了代码的可维护性。

适用场景

  • 需要创建的对象较少
    当需要创建的对象较少时,可以考虑使用简单工厂模式来管理对象的创建过程。
  • 对象的创建逻辑复杂
    将对象的创建逻辑集中在一个工厂类中,可以简化客户端的代码,提高代码的可维护性。
  • 不需要延迟创建对象
    对象创建的逻辑在一个具体的工厂类中实现,对象的创建过程不需要延迟到子类中。

UML图

以上图中各个角色的类说明如下表:

角色 功能说明
产品接口:IProduct 定义了具体产品需要实现的基本功能与规则约束
抽象产品:AbstractProduct 实现了各个产品类需要实现的通用功能与约束
具体产品:ConcreteProductX 根据各个具体类自身的不同特点,扩展抽象产品并实现标准产品接口
工厂:Factory 提供生产具体产品的工厂接口
客户端:Client 使用工厂类提供的接口,根据不同的参数获取对应的实例对象。

1:在实际开发中,Product不一定会有以上UML图中抽象的层次那么深,需要根据实际场景进行考量。比如:直接实现具体产品类并没有AbstractProduct。
2:Factory中定义的接口返回值不要返回具体类,要是使用标准的产品接口,遵循LSP原则。

实例分析

下面通过mysql-connector-java:8.0.33中获取ConnectionUrl的源码对简单工厂模式的具体实例进行分析。

实例UML图

代码具体实现

为了实现与MySQL进行连接,NonRegisteringDriver通过connect方法获取java.sql.Connection实例。在该方法中,通过ConnectionUlr提供的getConnectionUrlInstance接口,获取到具体的ConnectionUrl实例。如:SingleConnectionUrl、FailoverConnectionUrl、LoadBlanceConnectionUrl等9个具体实例。客户端调用工厂类获取具体实例的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public java.sql.Connection connect(String url, Properties info) throws SQLException {
try {
if (!ConnectionUrl.acceptsUrl(url)) {
/*
* According to JDBC spec:
* The driver should return "null" if it realizes it is the wrong kind of driver to connect to the given URL. This will be common, as when the
* JDBC driver manager is asked to connect to a given URL it passes the URL to each loaded driver in turn.
*/
return null;
}
// 通过简单工厂模式,根据url,info获取具体ConnectionUrl实例
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
// 后续通过ConnectionUrl构建并获取不同的java.sql.Connection实例
// 其它内容省略
} catch (UnsupportedConnectionStringException e) {
// When Connector/J can't handle this connection string the Driver must return null.
return null;
} catch (CJException ex) {
throw ExceptionFactory.createException(UnableToConnectException.class,
Messages.getString("NonRegisteringDriver.17", new Object[] { ex.toString() }), ex);
}
}

com.mysql.cj.conf.ConnectionUrl#getConnectionUrlInstance方法中,实现了根据不同connString与info实例化ConnectionUrl的封装。具体实现中,根据不同的key将不同类型的ConnectionUrl缓存在本地。由于本地缓存使用非线程安全的LRUCache,为了提高性能实现了基于双检查锁的加锁逻辑。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static ConnectionUrl getConnectionUrlInstance(String connString, Properties info) {
if (connString == null) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0"));
}
String connStringCacheKey = buildConnectionStringCacheKey(connString, info);
ConnectionUrl connectionUrl;
rwLock.readLock().lock();
connectionUrl = connectionUrlCache.get(connStringCacheKey);
if (connectionUrl == null) {
rwLock.readLock().unlock();
rwLock.writeLock().lock();
try {
// Check again, in the meantime it could have been cached by another thread.
connectionUrl = connectionUrlCache.get(connStringCacheKey);
if (connectionUrl == null) {
ConnectionUrlParser connStrParser = ConnectionUrlParser.parseConnectionString(connString);
// 实例化并返回具体实例
connectionUrl = Type.getConnectionUrlInstance(connStrParser, info);
connectionUrlCache.put(connStringCacheKey, connectionUrl);
}
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
}
rwLock.readLock().unlock();
return connectionUrl;
}

工厂方法模式

工厂方法模式定义了一个用于创建对象的接口,但让子类决定实例化哪一个类,使一个类的实例化延迟到子类。父类决定实例的生成方式,但并不决定所要生成的具体类,具体的处理全部交给子类负责。这样就将实例生成框架和实际负责生成实例的类进行了解耦。

适用场景

  • 需要创建的对象具有复杂的创建过程
    如果一个对象的创建过程比较复杂,包含多个步骤,且这些步骤可以被定制或者扩展,那么使用工厂方法模式可以将复杂的创建过程封装在工厂方法中。该场景多数情况下,还会用到模板方法模式。
  • 代码需要与具体类解耦
    当代码中需要使用一些具体类的实例时,直接依赖这些具体类会使得代码变得脆弱,难以维护和扩展。工厂方法模式通过使用抽象类或者接口来定义创建对象的方法,可以将代码与具体类解耦。
  • 需要在实例化时进行一些额外的操作
    有时候在创建对象时需要进行一些额外的操作,比如设置默认值、进行依赖注入、执行一些初始化逻辑等。这些操作可以在工厂方法中进行,从而避免在每个使用对象的地方重复这些操作。
  • 需要通过子类来选择需要创建的对象
    如果系统中有多个子类,客户端需要根据不同的条件创建不同的子类实例,那么可以将创建对象的职责下放到子类中,通过子类来决定具体实例化哪个类。
  • 需要创建产品对象的家族,且这些对象之间具有一定的关系
    当需要创建一组相关的对象时,可以使用工厂方法模式来定义一个创建这些对象的接口,不同的子类实现该接口来创建具体的对象,从而保证这些对象之间的一致性和互操作性。

UML图

以上图中各角色的类说明如下表:

角色 功能说明
产品接口:IProduct 定义了具体产品需要实现的基本功能与规则约束
抽象产品:AbstractProduct 实现了各个产品类需要实现的通用功能与约束
具体产品:ConcreteProductX 根据各个具体类自身的不同特点,扩展抽象产品并实现标准产品接口
抽象工厂:AbstractFactory 定义了IProduct的生产流程,具体的实现细节由子类去实现
具体工厂:ConcreateFactoryX 具体工厂实现抽象工厂定义的具体实现细节,生产具体的产品
客户端:Client 根据不同参数获取到具体工厂类,并通过工厂类方法生产指定的具体产品的实例对象。

1:工厂方法模式可以在工厂实现时同时采用模版方法模式,目的是对复杂的产品创建过程进行约束。
2:尽管Java中接口中支持通过default定义方法的默认实现,但这里的抽象工厂采用抽象类而没有使用接口,是因为Java接口中定义的方法只能为public,对于需要子类去实现的方法不能进行访问权限的限制。在客户端也可以对相应的方法进行访问。
3:在不需要约束产品流程时,抽象工厂也可以直接使用接口定义。

实例分析

下面通过org.slf4j.LoggerFactory中获取Logger的源码对工厂方法模式的具体实例进行分析。

实例UML图

代码具体实现

客户端代码只依赖Slf4j包定义的Logger和LogFactory,避免了直接依赖具体的日志实现(如:LogBack)。这样在需要进行日志替换时,只需要变更相应配置即可,业务代码不需要进行改变。在ILoggerFactory接口中定义了日志生产的方法,具体的日志生产延迟到具体的日志工厂中。这些具体工厂在不同的日志框架中实现。在org.slf4j.LoggerFactory#getLogger(java.lang.String)方法中获取ILoggerFactory实例,根据具体的ILoggerFactory获取Logger实例。

1
2
3
4
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

ILoggerFactory的具体实例,在编译时进行绑定。如果发现有多个日志组件实现则抛出异常。根据绑定的具体日志组件通过StaticLoggerBinder.getSingleton().getLoggerFacotry得到具体的IFactory实例。在spring boot中的默认日志实现是通过spring-boot-starter-logging模块提供的。Spring Boot的自动配置机制(spring-boot-autoconfigure)会在运行时加载默认的日志配置。如果检测到多个日志实现,按优先级加载第一个可用的实现。默认情况下,Logback是第一个被检测到的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 加锁避免重复加载
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 日志组件加载、bind核心方法
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITIALIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITIALIZATION:
// support re-entrant behavior.
// See also http://jira.qos.ch/browse/SLF4J-97
return SUBST_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

Spring Boot的日志LoggingSystemFactory默认配置如下:

1
2
3
4
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

抽象工厂模式

抽象工厂提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。这种模式有助于提高代码的灵活性和可扩展性,同时也使代码更易于维护和测试。

适用场景

一组相关或依赖对象一起使用,同一个工厂可以同时创建多个产品族产品实例。举个例子:我们要生产一辆汽车,这就需要发动机、轮胎、车架、车机等产品。这些产品都是相关的一族产品。

UML图

以上图中各角色的类说明如下表:

角色 功能说明
产品接口:IProductA、IProductB 定义不同产品族需要实现的基本功能与规则约束
具体产品:IProductA-X、IProductB-X 不同产品的具体实现实例
工厂接口:IFactory 定义不同产品族的生产流程(IProductA、IProductB),具体细节由子类实现
具体工厂:ConcreteFactoryX 工厂接口的具体实现,生产具体的产品实例
客户端 调用具体的工厂,获取相应的产品实例

1:抽象工厂模式与工厂方法模式的区别在于,抽象工厂模式提供了不同产品族产品实例的生产,工厂方法模式用于同一产品族不同实例的生产。

实例分析

在实际应用中,抽象工厂模式比较典型的实现是java.sql包中的Connection等相关接口的定义和实现。在该包中定义了Statement、Blob、Clob、NClod、SQLX等一批接口。这一批接口可以理解为不同的抽象产品族。Connnection接口为抽象工厂类,在该接口中定义了生产Statement、Blob、Clob、NClod、SQLX等产品族产品的接口定义。不同的数据库驱动包分别定义了适用于自身数据库相关产品族实例的实现。

实例UML图

代码具体实现

代码实现相对比较简单,直接对着上面UML图看源码即可。

参考资料

【1】[结城浩]. 图解设计模式[M]. 杨文轩译. 北京: 人民邮电出版社, 2017.1