C++ 中的工厂设计模式
在本文中,我们将研究工厂设计模式的两种实现方式。
工厂设计模式是代码中常见的设计模式之一。它结构简单,但在 C++ 中可以用多种方式编写,并带有其背后的概念意图。让我们看看几个解决方案,并考虑它们的潜力。
使用工厂设计模式的动机
对象创建的封装
该设计模式封装了对象的初始化,这有助于将其与使用对象的代码分离,并提高可读性和可维护性。
虽然它可能与构造函数的作用重叠,但请考虑一种场景,其中第三方库的构造函数在其构造函数中缺乏足够的对象初始化。这将需要额外的代码用于构造和配置。通过将此类代码集中在工厂类和方法中,您可以轻松管理和重用配置。如果第三方库随着时间的推移而演进,您只需要维护工厂中的代码,而无需更新整个代码库中的多个实例。
代码解耦
工厂设计模式在将客户端代码与具体类解耦方面起着至关重要的作用。客户端代码不直接实例化类,而是依靠工厂来创建实例,从而促进松耦合。这种解耦允许在运行时进行多态行为,以及实现缓存机制或单例实例的能力。它将使用已创建实例的代码与这些关注点隔离开来。
稍微扩展一下设计模式,工厂的抽象将有助于在测试中替换具体实现。这也是解耦的一个好处。
实现
我们将研究工厂设计模式的两种实现方式。它们使用表创建 SQLite 数据库,并从中读写数据。虽然它们作为设计模式用法的示例可能过于复杂,但这里的代码侧重于在少量代码中提供一些实用的东西。
实现 A
以下代码片段中显示的第一种实现方式是传统方式。工厂类保存数据库的标识,其工厂方法创建与其的数据库连接。
/* include/DbConnFactory.hpp */
#include <string>
#include <sqlite3.h>
namespace sample {
class DbConnFactory {
private:
std::string mDbName;
public:
DbConnFactory(const std::string& dbName);
sqlite3 *open();
};
} // namespace sample
/* src/DbConnFactory.cpp */
#include <stdexcept>
#include "DbConnFactory.hpp"
namespace sample {
DbConnFactory::DbConnFactory(const std::string& dbName) : mDbName(dbName) {}
sqlite3 *DbConnFactory::open() {
sqlite3 *db;
int rc = sqlite3_open(mDbName.c_str(), &db);
if (rc != SQLITE_OK)
throw std::runtime_error("SQL error: " + std::string(sqlite3_errmsg(db)));
return db;
}
} // namespace sample
下面的 main
函数使用工厂类创建两次数据库连接:一次用于数据库初始化,一次用于从数据库中读取记录。
/* main-1.cpp */
include <iostream>
#include <string>
#include <stdexcept>
#include <sqlite3.h>
#include "DbConnFactory.hpp"
static const int LINE_MAX_LENGTH = 30;
void execIn(sqlite3* db, const std::string &sql, int (*callback)(void *notUsed, int argc, char **argv, char **colName) = nullptr) {
std::cout << "[SQL] " << sql << std::endl;
char *err = nullptr;
int rc = sqlite3_exec(db, sql.c_str(), callback, 0, &err);
if (rc != SQLITE_OK) {
std::string errMsg = "SQL Error (" + std::to_string(rc) + "): " + std::string(err);
sqlite3_free(err);
throw std::runtime_error(errMsg);
}
}
void createDb(sample::DbConnFactory& factory) {
sqlite3 *db = factory.open();
try {
execIn(db, "CREATE TABLE IF NOT EXISTS ORDER_ENTRY ("
"ID INTEGER PRIMARY KEY AUTOINCREMENT,"
"CUSTOMER TEXT NOT NULL );");
execIn(db, "INSERT INTO ORDER_ENTRY ( CUSTOMER ) VALUES ( 'Paul' ); "
"INSERT INTO ORDER_ENTRY ( CUSTOMER ) VALUES ( 'Mark' );");
} catch (std::exception const &ex) {
sqlite3_close(db);
throw;
}
sqlite3_close(db);
}
void readDb(sample::DbConnFactory factory) {
sqlite3 *db = factory.open();
try {
std::cout << std::string(20, '=') << std::endl;
execIn(db, "SELECT * FROM ORDER_ENTRY;",
[](void *notUsed, int argc, char **argv, char **colName) {
std::cout << std::string(20, '-') << std::endl;
for (int i = 0; i < argc; i++)
std::cout << colName[i] << ": " << (argv[i] ? argv[i] : "NULL") << std::endl;
return 0;
});
std::cout << std::string(20, '=') << std::endl;
} catch (std::exception const &ex) {
sqlite3_close(db);
throw;
}
sqlite3_close(db);
}
int main(int argc, char *arg[]) {
sample::DbConnFactory factory("sample.db");
try {
createDb(factory);
readDb(factory);
} catch (std::exception const &ex) {
std::cerr << "[ERROR] " << ex.what() << std::endl;
}
}
这种实现方式的一个问题是工厂方法返回一个指针。因此,调用者有责任正确销毁返回的实例。对于 SQLite 数据库连接,我们需要调用 sqlite3_close
函数,该函数在代码中多次出现。依赖调用者进行对象处理有点冒险。实际上,在所有调用者代码中确保这一点也需要成本。
实现 B
以下实现通过使用 RAII 和智能指针将对象处理责任转移到工厂方法中来改进代码。
工厂方法创建一个数据库连接,并将其包装在一个类中,然后将其作为唯一指针返回。智能指针确保返回的实例将被销毁,并且包装类负责正确销毁数据库连接实例。
/* include/SmartDbConnFactory.hpp */
#ifndef H_SMART_DB_CONN_FACTORY
#define H_SMART_DB_CONN_FACTORY
#include <string>
#include <memory>
#include <sqlite3.h>
namespace sample {
class SmartDbConn {
private:
sqlite3* mDb;
public:
SmartDbConn(sqlite3* db);
~SmartDbConn();
sqlite3* db() { return mDb; }
};
class SmartDbConnFactory {
private:
std::string mDbName;
public:
SmartDbConnFactory(const std::string& dbName);
std::unique_ptr<SmartDbConn> open();
};
} // namespace sample
#endif
/* src/SmartDbConnFactory.cpp */
#include <iostream>
#include <memory>
#include <stdexcept>
#include "SmartDbConnFactory.hpp"
namespace sample {
SmartDbConnFactory::SmartDbConnFactory(const std::string& dbName) : mDbName(dbName) {}
std::unique_ptr<SmartDbConn> SmartDbConnFactory::open() {
sqlite3 *db;
int rc = sqlite3_open(mDbName.c_str(), &db);
if (rc != SQLITE_OK)
throw std::runtime_error("SQL error: " + std::string(sqlite3_errmsg(db)));
return std::make_unique<SmartDbConn>(db);
}
SmartDbConn::SmartDbConn(sqlite3* db) : mDb(db) {}
SmartDbConn::~SmartDbConn() {
if (mDb) {
sqlite3_close(mDb);
std::cout << "A DB connection has been closed." << std::endl;
}
}
} // namespace sample
现在,请参阅如何在下面的代码中使用工厂方法。它与第一个实现方式做的事情相同,包括错误处理。
/* src/SmartDbConnFactory.cpp */
#include <iostream>
#include <string>
#include <stdexcept>
#include <sqlite3.h>
#include "SmartDbConnFactory.hpp"
static const int LINE_MAX_LENGTH = 30;
void execIn(sample::SmartDbConn &db, const std::string &sql, int (*callback)(void *notUsed, int argc, char **argv, char **colName) = nullptr) {
std::cout << "[SQL] " << sql << std::endl;
char *err = nullptr;
int rc = sqlite3_exec(db.db(), sql.c_str(), callback, 0, &err);
if (rc != SQLITE_OK) {
std::string errMsg = "SQL Error (" + std::to_string(rc) + "): " + std::string(err);
sqlite3_free(err);
throw std::runtime_error(errMsg);
}
}
void createDb(sample::SmartDbConnFactory& factory) {
auto db = factory.open();
execIn(*db, "CREATE TABLE IF NOT EXISTS ORDER_ENTRY ("
"ID INTEGER PRIMARY KEY AUTOINCREMENT,"
"CUSTOMER TEXT NOT NULL );");
execIn(*db, "INSERT INTO ORDER_ENTRY ( CUSTOMER ) VALUES ( 'Paul' ); "
"INSERT INTO ORDER_ENTRY ( CUSTOMER ) VALUES ( 'Mark' );");
}
void readDb(sample::SmartDbConnFactory factory) {
auto db = factory.open();
std::cout << std::string(20, '=') << std::endl;
execIn(*db, "SELECT * FROM ORDER_ENTRY;",
[](void *notUsed, int argc, char **argv, char **colName) {
std::cout << std::string(20, '-') << std::endl;
for (int i = 0; i < argc; i++)
std::cout << colName[i] << ": " << (argv[i] ? argv[i] : "NULL") << std::endl;
return 0;
});
std::cout << std::string(20, '=') << std::endl;
}
int main(int argc, char *arg[]) {
sample::SmartDbConnFactory factory("sample.db");
try {
createDb(factory);
readDb(factory);
} catch (std::exception const &ex) {
std::cerr << "[ERROR] " << ex.what() << std::endl;
}
}
洞察
在实现 B 中,main
函数中的代码变得比实现 A 简单,同时仍然处理异常。使用 RAII 和智能指针实现工厂设计模式降低了内存管理风险,并提高了代码可读性。这将对软件开发的维护阶段有很大帮助。