Factory design pattern is one of the common design patterns seen in code. It is a simple structure, but it can be written a variety of ways in C++, with the conceptual intentions behind it. Let us see several solutions and consider their potential.
Motivation to use Factory Design Pattern
Encapsulation of Object Creation
The design pattern encapsulates the initialization of objects, which helps separate it from the code that uses the objects and improves readability and maintainability.
While it may overlap with the role of constructors, consider a scenario where a third-party library lacks sufficient object initialization in its constructors. This would require additional code for construction and configurations. By centralizing such code in factory classes and methods, you can easily manage and reuse configurations. If the third-party library evolves over time, you'll only need to maintain the code in the factory, avoiding the need to update multiple instances across your codebase.
Decoupling Code
The Factory Design Pattern plays a crucial role in decoupling client code from concrete classes. Instead of instantiating classes directly, the client code relies on the factory to create instances, promoting loose coupling. This decoupling allows for polymorphic behavior at runtime, and the ability to implement caching mechanisms or singleton instances. It isolates the code that utilizes the created instances from these concerns.
Extending the design pattern a bit, the abstraction of factories will help substitute concrete implementations in testing. It is also a benefit of decoupling.
Implementation
We will examine two implementations of the factory design pattern. They create a SQLite database with a table and write and read data from it. Although they may be overcomplicated as examples of the design pattern's usage, the code here focuses on providing something practical in a small amount of code.
Implementation A
The first implementation shown in the following code snippets is in a legacy way. The factory class holds the database's identification, and its factory method creates a DB connection to it.
#include <string>
#include <sqlite3.h>
namespace sample {
class DbConnFactory {
private:
std::string mDbName;
public:
DbConnFactory(const std::string& dbName);
sqlite3 *open();
};
}
#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;
}
}
The main
function below uses the factory class to create a DB connection twice: once for database initialization and once to read records from it.
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;
}
}
One concern with this implementation is that the factory method returns a pointer. As a result, the callers are responsible for correctly destructing the returned instance. In the case of SQLite DB connections, we need to call the sqlite3_close
function, which appears multiple times in the code. Depending on callers for object disposal is a bit risky. In practice, ensuring it in all caller codes also costs.
Implementation B
The following implementation improves the code by shifting the object disposal responsibility into the factory method using RAII and Smart Pointer.
The factory method creates a DB connection and wraps it with a class before returning it as a unique pointer. The Smart Pointer ensures that the returning instance is to be destructed, and the wrapper class is responsible for destructing the DB connection instance correctly.
#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();
};
}
#endif
#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;
}
}
}
Now, see how to use the factory method in the code below. It does the same thing as the first implementation, including error handling.
#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;
}
}
Insight
The code in the main
function becomes simpler in Implementation B than in Implementation A, while it still handles the exceptions. Implementing the Factory Design Pattern with RAII and Smart Pointer reduces memory management risk and improves code readability. It will be a big help in the maintenance phase of software development.