12、工厂模式实战(一):数据库连接工厂(支持MySQL、PostgreSQL、SQLite)
好,咱们今天来聊点实在的。
工厂模式,说白了就是帮你“生产对象”的。你想想看,写代码的时候,是不是经常要 new 这个、new 那个?如果项目小还好,一旦系统复杂起来,到处都是 new,后期想换个实现,那叫一个痛苦。
我个人习惯,遇到这种“创建对象逻辑可能变化”的场景,第一反应就是上工厂模式。今天咱们就拿数据库连接来练手,做一个支持 MySQL、PostgreSQL、SQLite 的工厂。
为什么需要数据库连接工厂?
先问个问题:你的项目里,数据库连接代码是怎么写的?
很多新手会这样写:
// 直接硬编码
$conn = new mysqli($host, $user, $pass, $db);
// 或者
$pdo = new PDO("mysql:host=$host;dbname=$db", $user, $pass);
嗯,看起来没问题。但你想过没有——如果有一天,客户要求从 MySQL 换成 PostgreSQL,你要改多少处?
我在项目中遇到过这种惨痛教训。一个老系统,数据库连接散落在 50 多个文件里,改了一个通宵,还漏了两个地方没改,第二天线上直接报错。从那以后,我给自己定了个规矩:所有对象的创建,必须集中管理。
核心思想:工厂模式把“创建对象”这件事封装起来。调用方只需要告诉工厂“我要什么”,不需要关心“怎么创建”。
先画个图:工厂模式的核心结构
动手写代码之前,咱们先看看结构。我画了一张图,帮你理清思路:
你看,客户端只跟工厂打交道。工厂内部根据参数,决定返回哪个数据库连接。这样客户端代码就干净了——它不需要知道 MySQL 怎么连、PostgreSQL 怎么连,它只需要说“给我一个数据库连接”。
第一步:定义产品接口
既然是生产“数据库连接”,那咱们先定义一下“连接”长什么样。所有数据库连接,都得实现这个接口:
// 数据库连接接口
interface DBConnectionInterface {
public function connect();
public function query($sql);
public function close();
}
这个接口很简洁,就三个方法:连接、查询、关闭。你想想看,不管什么数据库,最终不都是这三件事吗?
小提示:接口定义得越简单,扩展起来越容易。我见过有人把接口搞了十几个方法,结果每个实现类里一半都是空方法。没必要。
第二步:实现具体产品类
接口有了,咱们来实现三个具体的数据库连接类。
MySQL 连接:
class MySQLConnection implements DBConnectionInterface {
private $host;
private $dbname;
private $username;
private $password;
private $connection;
public function __construct($host, $dbname, $username, $password) {
$this->host = $host;
$this->dbname = $dbname;
$this->username = $username;
$this->password = $password;
}
public function connect() {
// 这里用 PDO 做底层,更安全
$dsn = "mysql:host={$this->host};dbname={$this->dbname}";
$this->connection = new PDO($dsn, $this->username, $this->password);
echo "MySQL 连接成功\n";
}
public function query($sql) {
return $this->connection->query($sql);
}
public function close() {
$this->connection = null;
echo "MySQL 连接已关闭\n";
}
}
PostgreSQL 连接:
class PostgreSQLConnection implements DBConnectionInterface {
private $host;
private $dbname;
private $username;
private $password;
private $connection;
public function __construct($host, $dbname, $username, $password) {
$this->host = $host;
$this->dbname = $dbname;
$this->username = $username;
$this->password = $password;
}
public function connect() {
$dsn = "pgsql:host={$this->host};dbname={$this->dbname}";
$this->connection = new PDO($dsn, $this->username, $this->password);
echo "PostgreSQL 连接成功\n";
}
public function query($sql) {
return $this->connection->query($sql);
}
public function close() {
$this->connection = null;
echo "PostgreSQL 连接已关闭\n";
}
}
SQLite 连接:
class SQLiteConnection implements DBConnectionInterface {
private $dbPath;
private $connection;
public function __construct($dbPath) {
$this->dbPath = $dbPath;
}
public function connect() {
$dsn = "sqlite:{$this->dbPath}";
$this->connection = new PDO($dsn);
echo "SQLite 连接成功\n";
}
public function query($sql) {
return $this->connection->query($sql);
}
public function close() {
$this->connection = null;
echo "SQLite 连接已关闭\n";
}
}
你有没有发现,这三个类的结构几乎一样?只是 DSN 字符串不同。这就是接口的好处——统一了行为,细节各自实现。
第三步:创建工厂类
重头戏来了。工厂类负责根据参数,决定实例化哪个数据库连接:
class DBConnectionFactory {
// 用一个数组来存储配置
private static $config = [];
// 设置配置
public static function setConfig($config) {
self::$config = $config;
}
// 核心方法:创建连接
public static function getConnection($type = 'mysql') {
$type = strtolower($type);
switch ($type) {
case 'mysql':
return new MySQLConnection(
self::$config['mysql']['host'],
self::$config['mysql']['dbname'],
self::$config['mysql']['username'],
self::$config['mysql']['password']
);
case 'pgsql':
case 'postgresql':
return new PostgreSQLConnection(
self::$config['pgsql']['host'],
self::$config['pgsql']['dbname'],
self::$config['pgsql']['username'],
self::$config['pgsql']['password']
);
case 'sqlite':
return new SQLiteConnection(
self::$config['sqlite']['db_path']
);
default:
throw new Exception("不支持的数据库类型: {$type}");
}
}
}
注意:这里的 switch-case 虽然简单,但如果你要支持几十种数据库,代码会变得臃肿。我曾经在一个项目中维护过 20 多种数据库的工厂,后来改用注册表模式来优化。不过对于咱们这个场景,switch 完全够用。
第四步:使用工厂
好了,现在看看客户端代码有多清爽:
// 配置数据库信息
DBConnectionFactory::setConfig([
'mysql' => [
'host' => 'localhost',
'dbname' => 'test_db',
'username' => 'root',
'password' => '123456'
],
'pgsql' => [
'host' => 'localhost',
'dbname' => 'test_db',
'username' => 'postgres',
'password' => '654321'
],
'sqlite' => [
'db_path' => '/data/test.db'
]
]);
// 获取 MySQL 连接
$mysqlConn = DBConnectionFactory::getConnection('mysql');
$mysqlConn->connect();
$mysqlConn->query("SELECT * FROM users");
$mysqlConn->close();
// 获取 PostgreSQL 连接
$pgsqlConn = DBConnectionFactory::getConnection('pgsql');
$pgsqlConn->connect();
$pgsqlConn->query("SELECT * FROM users");
$pgsqlConn->close();
// 获取 SQLite 连接
$sqliteConn = DBConnectionFactory::getConnection('sqlite');
$sqliteConn->connect();
$sqliteConn->query("SELECT * FROM users");
$sqliteConn->close();
你看,客户端只需要改一个参数,就能切换数据库。如果以后要加 MongoDB、Redis,只需要新增一个类,然后在工厂的 switch 里加一行。调用方完全不用改。
避坑指南:我曾经踩过的坑
讲几个实际项目中容易遇到的问题:
- 连接泄漏:我曾经在一个项目中,工厂只负责创建连接,不负责管理生命周期。结果调用方忘了 close,数据库连接池被耗尽。后来我加了一个析构函数自动关闭,或者在工厂里做引用计数。
- 配置硬编码:早期我把数据库配置写在工厂类里,结果测试环境和生产环境要改代码。后来我改成从配置文件读取,或者用环境变量注入。
- 单例滥用:有人喜欢把工厂做成单例,然后全局共享。但如果你有多个数据库实例(比如读写分离),单例就不合适了。我建议工厂本身保持无状态,只负责创建。
总结一下
数据库连接工厂,说白了就是把“创建连接”这件事集中管理。它的好处很明显:
- 解耦:调用方不依赖具体数据库实现
- 易扩展:加新数据库类型,只需要加一个类,改一行工厂代码
- 易维护:所有连接配置集中管理,改一处就行
嗯,今天的内容就到这。代码不多,但思路很重要。你可以在自己的项目里试试,把散落的数据库连接代码收拢到工厂里,相信我,你会爱上这种清爽的感觉。
一句话记住:工厂模式不是炫技,是让你少加班。