模式与实践-工厂方法

工厂方法在我们开发过程中经常被使用到,它有声明一个工厂类对外提供接口,由这个接口来负责实例化需要的类的实例,用户无需关心工厂创建对象的逻辑。

应用样例

我现在要为正在开发的新系统封装一个数据库操作库,系统有一部分业务使用到了MySQL数据库,另一部分则是Oracle数据库,这两个数据库虽然都是RDBMS,用法差不多,但是由于厂商的不同,双方的SQL语句存在部分差异,我需要专门为这两个数据库各自提供一个操作类。

而且系统只是用这两套数据库是不确定的,可能在未来发展中添加另一其它厂商的数据库,由于一般的数据库数据操作层的行为都是差不多,可以把这套统一的行为封装成接口,以后每使用到不同的数据库,只需要把实现了这套接口的操作类拿来使用即可。

1
2
3
4
5
6
7
//切换到mysql数据库
$db = new MySQLDAO();
//切换到oracle数据库
$db = new OracleDAO();
//切换到其它数据库
$db = new OtherDAO();
//...

定义统一的数据库操作接口

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
/**
* 数据库数据访问接口
* Interface DAOInterface
*/
interface DAOInterface
{
/**
* 获取单个记录
* @param int $id 记录id
* @return mixed
*/
public function get($id);

/**
* 获取全部记录
* @return mixed
*/
public function findAll();

/**
* 根据主键删除记录
* @param int $id 记录id
* @return mixed
*/
public function delete($id);

/**
* 插入记录
* @param array $data 记录信息
* @return mixed
*/
public function insert($data);

/**
* 更新记录
* @param int $id 记录id
* @param array $data 记录数据
* @return mixed
*/
public function update($id, $data);
}

query方法代表者数据库的查询操作,而update方法则是增删改等更新操作。

实现接口

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
/**
* MySQL数据访问实现
* Class MySQLDAO
*/
class MySQLDAO implements DAOInterface
{
public function get($id)
{
echo __CLASS__ . ' get: ' . $id . '<br>';
}

public function findAll()
{
echo __CLASS__ . ' findAll' . '<br>';
}

public function delete($id)
{
echo __CLASS__ . ' delete: ' . $id . '<br>';
}

public function insert($data)
{
echo __CLASS__ . ' insert: <br>';
print_r($data);
}

public function update($id, $data)
{
echo __CLASS__ . ' update:' . $id . '<br>';
print_r($data);
}
}

/**
* Oracle数据访问实现
* Class OracleDAO
*/
class OracleDAO implements DAOInterface
{
public function get($id)
{
echo __CLASS__ . ' get: ' . $id . '<br>';
}

public function findAll()
{
echo __CLASS__ . ' findAll' . '<br>';
}

public function delete($id)
{
echo __CLASS__ . ' delete: ' . $id . '<br>';
}

public function insert($data)
{
echo __CLASS__ . ' insert: <br>';
print_r($data);
}

public function update($id, $data)
{
echo __CLASS__ . ' update:' . $id . '<br>';
print_r($data);
}
}

这时候我们可以在业务代码中使用:

1
2
3
4
5
$dao = new MySQLDAO();
$dao->findAll();

$dao = new OracleDAO();
$dao->update(10, ['value' => 'on']);

运行之后没问题:

1
2
3
MySQLDAO findAll
OracleDAO update:10
Array ( [value] => on )

但经过一段时间,以上代码的业务需要调整,查找操作选用Oralce数据库,更新操作使用MySQL数据库,这时,不得不把代码改成:

1
2
3
4
5
$dao = new OracleDAO();
$dao->findAll();

$dao = new MySQLDAO();
$dao->update(10, ['value' => 'on']);

运行之后也没问题:

1
2
3
OracleDAO findAll
MySQLDAO update:10
Array ( [value] => on )

然而又过一阵子,项目发生调整,需要把原来的改回来,查找使用MySQL数据库,更新使用Oracle数据库,你还是得去把代码改回来。看上面的代码,也许你会想这改一两下也没什么问题,但是如果项目的所用到MySQLDAOOracleDAO类不止这里,由几十处,甚至上百处,你可能要疯了,每一处都要手工修改。

是什么造成了这个需求如此麻烦?

关联到具体业务的DAOInterface具体子类MySQLDAOOracleDAO被大量地参杂到代码中,造成了高耦合,业务代码引用大量的具体实现类,当业务发生了变化,得手工去修改相关的实现类才能完成需求。

如果把具体的实现类抽离出业务层,把业务变化牵扯到的数据库操作具体实现类分离出来,系统的耦合程度大大降低。

此时我们可以通过一个专门的类来负责生成具体的数据库实现类,我们只需要传入我们想要的类就能得到具体的实例。

定义一个工厂类,其职责就是提供DAO的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* DAO工厂类
* Class DAOFactory
*/
class DAOFactory
{
public function getDAO($id)
{
switch ($id) {
case "logic1":
return new MySQLDAO();
break;
case "logic2":
return new OracleDAO();
break;
default:
throw new Exception("The {$id} does NOT be implemented");
}
}
}

修改业务代码,所有数据库操作对象的实例化行为都要通过DAO工厂类来获取:

1
2
3
4
5
6
$factory = new DAOFactory();
$dao = $factory->getDAO('logic2');
$dao->findAll();

$dao = $factory->getDAO('logic1');
$dao->update(10, ['value' => 'on']);

运行代码:

1
2
3
OracleDAO findAll
MySQLDAO update:10
Array ( [value] => on )

是没有问题的,假设现在发生了调整,我们需要把两个操作使用到的数据库调换,其实不用修改上面的业务代码,只需要修改工厂类的创建逻辑即可:

1
2
3
4
5
6
7
8
9
10
switch ($id) {
case "logic2":
return new MySQLDAO();
break;
case "logic1":
return new OracleDAO();
break;
default:
throw new Exception("The {$id} does NOT be implemented");
}

switch语句中的case条件调整,就可以实现这个需求。
加入以后添加了另一个不同的数据库,如SQLServer,,要求使用SQLServer来做查询操作,我们只需要为SQLServer实现一个SQLServerDAO数据操作子类:

1
2
3
4
class SQLServerDAO implements DAOInterface
{
//具体实现代码省略...
}

然后修改工厂类的代码:

1
2
3
4
5
6
7
8
9
switch ($id) {
case "logic1":
return new SQLServerDAO();
break;
case "logic2":
return new OracleDAO();
break;
default:
throw new Exception("The {$id} does NOT be implemented");

这样又可以满足新的需求了。

以上用到的代码解决思路就是使用了工厂方法模式,解决了数据操作对象接口的实现类创建问题。

其实在PHP中使用工厂方法,我们可以脱离对具体子类的依赖,得益于PHP是脚本语言的特性,类的是实例化可以是动态的,无需声明具体的类型。在其它语言如JAVA中,我们可以使用反射的方式来实例化具体的子类,避免了工厂类依赖其它类。

UML图

工厂方法

总结

以上就是工厂方法设计模式,相信这个模式被很多人使用过,它比较简单,而且轻松解决了简单的对象创建问题。但是工厂方法也有缺点,就是当子类的数量越来越多或者创建对象的逻辑越来越臃肿,工厂类就会变得很庞大。