设计模式之抽象工厂模式及在源码中的应用

抽象工厂

为创建一组相关或者是相互依赖的对象提供一个接口,而不需要指定他们的具体实现类。

一个对象族(或是一组没有任何关系的对象)都有相同的约束,则可以使用抽象工厂模式。例如一个文本编辑器和一个图片处理器,都是软件实体,但是Linix下的文本编辑器和WINDOWS下的文本编辑器虽然功能和界面都相同,但是代码实现是不同的,图片处理器也是类似情况,也就是具有了共同的约束条件:操作系统类型,于是我们可以使用抽象工厂模式,产生不同操作系统下的编辑器和图片处理器。

栗子:
以车厂生产汽车零部件为例,A、B两家车厂分别生产不同的轮胎、发动机、制动系统。虽然生产的零件不同,型号不同。但是根本上都有共同的约束,就是轮胎、发动机、制动系统。

轮胎相关类:

1
2
3
4
5
6
public interface ITire {
/**
* 轮胎
*/
void tire();
}

1
2
3
4
5
6
public class NormalTire implements ITire{
@Override
public void tire() {
System.out.println("普通轮胎");
}
}
1
2
3
4
5
6
public class SUVTire implements ITire{
@Override
public void tire() {
System.out.println("越野轮胎");
}
}

发动机相关类:

1
2
3
4
5
6
public interface IEngine {
/**
*发动机
*/
void engine();
}

1
2
3
4
5
6
public class DomesticEngine implements IEngine{
@Override
public void engine() {
System.out.println("国产发动机");
}
}
1
2
3
4
5
6
public class ImportEngine implements IEngine{
@Override
public void engine() {
System.out.println("进口发动机");
}
}

制动系统相关类:

1
2
3
4
5
6
public interface IBrake {
/**
*制动系统
*/
void brake();
}

1
2
3
4
5
6
public class NormalBrake implements IBrake{
@Override
public void brake() {
System.out.println("普通制动");
}
}
1
2
3
4
5
6
public class SeniorBrake implements IBrake{
@Override
public void brake() {
System.out.println("高级制动");
}
}

抽象车厂类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public abstract class CarFactory {
/**
* 生产轮胎
*
* @return 轮胎
* */
public abstract ITire createTire();
/**
* 生产发动机
*
* @return 发动机
* */
public abstract IEngine createEngine();
/**
* 生产制动系统
*
* @return 制动系统
* */
public abstract IBrake createBrake();
}

A车厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AFactory extends CarFactory{
@Override
public ITire createTire() {
return new NormalTire();
}
@Override
public IEngine createEngine() {
return new DomesticEngine();
}
@Override
public IBrake createBrake() {
return new NormalBrake();
}
}

B车厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BFactory extends CarFactory{
@Override
public ITire createTire() {
return new SUVTire();
}
@Override
public IEngine createEngine() {
return new ImportEngine();
}
@Override
public IBrake createBrake() {
return new SeniorBrake();
}
}

客户类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
//A车厂
CarFactory factoryA = new AFactory();
factoryA.createTire().tire();
factoryA.createEngine().engine();
factoryA.createBrake().brake();
System.out.println("---------------");
//B车厂
CarFactory factoryB = new BFactory();
factoryB.createTire().tire();
factoryB.createEngine().engine();
factoryB.createBrake().brake();
}
}

结果:

1
2
3
4
5
6
7
普通轮胎
国产发动机
普通制动
------------------
越野轮胎
进口发动机
高级制动

可以看出上面模拟了两个车厂,如果有了C厂、D厂,各自厂家生产的零部件型号种类又不相同,那么我们创建的类文件就会翻倍。这也是抽象工厂模式的一个弊端,所以实际开发中要权衡使用。

抽象工厂模式是工厂方法模式的升级版本。对比如下:

工厂方法模式 抽象工厂模式
只有一个抽象产品类 有多个抽象产品类
具体工厂类只能创建一个具体产品类的实例 具体工厂类能创建多个具体产品类的实例

源码中的实现

在源码中, 比较典型的抽象工厂模式的例子是Java.sql包中的Connection类,在刚学习Java时我们都会学习使用JDBC链接数据库,代码大致是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try {
Connection con = null; // 定义一个MYSQL链接对象
Class.forName("com.mysql.jdbc.Driver").newInstance(); // MYSQL驱动
con = DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:3306/test", "root", "root"); // 链接本地MYSQL
Statement stmt; // 创建声明
stmt = con.createStatement();
// 新增一条数据
stmt.executeUpdate("INSERT INTO user (username, password) VALUES ('init', '123456')");
ResultSet res = stmt.executeQuery("select LAST_INSERT_ID()");
// 代码省略
} catch (Exception e) {
e.printStackTrace();
}

上面我们是以MYSQL驱动为例,设置JDBC驱动以后,使用DriverManager.getConnection来获取具体的链接实现,然后通过这个Connection来创建一个Statement来提交SQL语句,Connection还可以创建clob, blob, sqlxml等对象,即Connection就是抽象工厂,而具体的工厂实现则在不同的数据库驱动包种。

首先我们看DriverManager中的getConnection方法 :

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

我们看到getConnection(String, String, String)函数调用了getConnection(Stringurl, java.util.Propertiesinfo,Class<?>caller)函数,在该函数中遍历以注册到DriverManager中的驱动,即registeredDrivers, 获取相应的驱动之后,链接到数据库,最后将该链接返回, 这样就获取到了具体的Connection, 代码为 :

1
2
3
4
5
6
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}

那么MYSQL JDBC驱动是什么时候注册到DriverManager的呢 ?
我们看到在使用DriverManager之前,调用了以下这句代码 :

1
Class.forName("com.mysql.jdbc.Driver").newInstance(); // MYSQL驱动

这句代码的作用就是通过反射来创建com.mysql.jdbc.Driver对象, 我们看看mysql jdbc驱动中该类的实现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver()throws SQLException{
}
static{
try{
DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
}

可以看到,上文中有一个静态语句块, 该语句块会在虚拟机第一次加载该类时首先执行, 该语句块的作用就是将Driver类的对象注册到DriverManager中,驱动的具体实现类为 NonRegisteringDriver。获取数据库驱动对象以后,我们需要调用驱动对象的connect(String, Properties)函数才能获取到Connection对象,我们看看 NonRegisteringDriver的connect(String, Properties)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public java.sql.Connection connect(String url, Properties info)throws SQLException{
if (url != null) {
if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:loadbalance://"))
return connectLoadBalanced(url, info);
if (StringUtils.startsWithIgnoreCase(url, "jdbc:mysql:replication://")){
return connectReplicationConnection(url, info);
}
}
Properties props = null;
if ((props = parseURL(url, info)) == null) {
return null;
}
try{
return new Connection(host(props), port(props), props, database(props), url);
}
catch (SQLException sqlEx){
throw sqlEx;
} catch (Exception ex) {
throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.17") + ex.toString() + Messages.getString("NonRegisteringDriver.18"), "08001");
}
}

通过分析代码,返回的是com.mysql.jdbc.Connection类的对象, 即 return new Connection(host(props), port(props), props, database(props), url)这句, 该Connection实现了java.sql.Connection声明的接口,这就是mysql数据库连接的具体实现。

现在我们来理一下思路, java.sql包中的Statement, Clob, Blob, SQLXML都是扮演了抽象产品类族中的一员, 而java.sql.Connection则代表了抽象工厂类,里面有创建各个产品类的函数,具体的产品实现类、具体Connection工厂都封装在各个数据库驱动包中,通过Connection我们就可以创建Statement, Clob等同一类族中的对象。抽象与实现想分离,工厂可以创建一组相关的对象,客户代码使用较为简单。

优点 :

  1. 抽象工厂模式隔离了具体类的生产,使得客户并不需要知道什么被创建;
  2. 容易改变产品的系列;
  3. 增加新的具体工厂和产品族很方便,无须修改已有系统,符合“开闭原则”。
  4. 将一个系列的产品族统一到一起创建,客户代码易于使用。

缺点 :
抽象工厂模式的最大缺点就是产品族扩展非常困难,为什么这么说呢?我们以通用代码为例,如果要增加一个产品 C, 也就是说产品家族由原来的 2 个增加到 3 个,看看我们的程序有多大改动吧!抽象类 AbstractCreator 要增加一个方法 createProductC(),然后两个实现类都要修改,想想看,这严重违反了开闭原则,而且我们一直说明抽象类和接口是一个契约。