Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding the Abstract Factory Design Pattern in C++

Tech Aug 10 19

The Abstract Factory pattern falls within the creasional category of software design patterns. While simpler factory variants excel at isolating the instantiation of a single product type, production systems frequently demand the coordinated creation of multiple interconnected objects. This architectural scenario requires a higher-level abstraction capable of generating complete families of related items while hiding implementation specifics from client code.

Evolving Requirements and Factory Scalability

Consider an internal tool managing personnel records across configurable storage layers. A straightforward Factory Method setup cleanly segregates logic between MySQL and SQLite connectors. Business needs typically expand over time, however. Suppose the system must now also persist team hierarchies and access roles using the identical storage engine.

Scaling a traditional Factory Method approach under these conditions becomes inefficient. Maintaining separate creator instances for users and teams multiplies initialization points across the codebase. Changing the underlying data layer forces developers to update numerous disjointed factory references, increasing coupling and maintenance overhead. The Abstract Factory pattern resolves this fragmentation by consolidating product creation behind a unified interface.

Defining Product Abstractions

The foundation begins with declaring repository contracts for each domain entity:

class UserRepository {
public:
    virtual ~UserRepository() = default;
    virtual bool persistUser(const std::string& username, int recordId) = 0;
    virtual std::string queryUser(int recordId) = 0;
};

class TeamRepository {
public:
    virtual ~TeamRepository() = default;
    virtual bool persistTeam(const std::string& teamName, int leadId) = 0;
    virtual std::string queryTeam(int teamId) = 0;
};

Concrete implementations mirror these interfaces, binding operations to specific database engines:

class PostgresUserRepo : public UserRepository {
public:
    bool persistUser(const std::string& username, int recordId) override {
        std::cout << "[Postgres] Inserting user: " << username << "\n";
        return true;
    }
    std::string queryUser(int recordId) override {
        std::cout << "[Postgres] Fetching user ID: " << recordId << "\n";
        return "jdoe";
    }
};

class PostgresTeamRepo : public TeamRepository {
public:
    bool persistTeam(const std::string& teamName, int leadId) override {
        std::cout << "[Postgres] Registering team: " << teamName << "\n";
        return true;
    }
    std::string queryTeam(int teamId) override {
        std::cout << "[Postgres] Retrieving team ID: " << teamId << "\n";
        return "Backend-Core";
    }
};

class SqliteUserRepo : public UserRepository {
public:
    bool persistUser(const std::string& username, int recordId) override {
        std::cout << "[SQLite] Inserting user: " << username << "\n";
        return true;
    }
    std::string queryUser(int recordId) override {
        std::cout << "[SQLite] Fetching user ID: " << recordId << "\n";
        return "asmith";
    }
};

class SqliteTeamRepo : public TeamRepository {
public:
    bool persistTeam(const std::string& teamName, int leadId) override {
        std::cout << "[SQLite] Registering team: " << teamName << "\n";
        return true;
    }
    std::string queryTeam(int teamId) override {
        std::cout << "[SQLite] Retrieving team ID: " << teamId << "\n";
        return "Frontend-UI";
    }
};

Consolidating Creation Logic

The Abstract Factory pattern introduces a creator interface that bundles multiple construction methods. Each method corresponds to a product family member, guaranteeing that returned objects share the same concrete lineage:

class DataProviderFactory {
public:
    virtual ~DataProviderFactory() = default;
    virtual std::unique_ptr<userrepository> createUserRepository() = 0;
    virtual std::unique_ptr<teamrepository> createTeamRepository() = 0;
};

class PostgresProvider : public DataProviderFactory {
public:
    std::unique_ptr<userrepository> createUserRepository() override {
        return std::make_unique<PostgresUserRepo>();
    }
    std::unique_ptr<TeamRepository> createTeamRepository() override {
        return std::make_unique<PostgresTeamRepo>();
    }
};

class SqliteProvider : public DataProviderFactory {
public:
    std::unique_ptr<UserRepository> createUserRepository() override {
        return std::make_unique<SqliteUserRepo>();
    }
    std::unique_ptr<TeamRepository> createTeamRepository() override {
        return std::make_unique<SqliteTeamRepo>();
    }
};</userrepository></teamrepository></userrepository>

By adopting smart pointers, automatic resource cleanup replaces manual deallocation, reducing leakage risks while preserving the structural integrity of the pattern.

Client Integration

Consumer modules depend exclusively on the abstract contracts. Instantiating a single factory object yields a fully interoperable object graph:

void configureApplication(DataProviderFactory& factory) {
    auto userStore = factory.createUserRepository();
    auto teamStore = factory.createTeamRepository();

    if (userStore && teamStore) {
        userStore->persistUser("manager_x", 9981);
        teamStore->persistTeam("Operations", 9981);
        
        std::cout << "Active user: " << userStore->queryUser(9981) << "\n";
        std::cout << "Active team: " << teamStore->queryTeam(442) << "\n";
    }
}

int main() {
    PostgresProvider pgSqlEngine;
    configureApplication(pgSqlEngine);

    std::cout << "----------------------\n";

    SqliteProvider localDb;
    configureApplication(localDb);

    return 0;
}

Modifying the runtime environment simply involves passing a different factory instance to the configuration routine. All downstream components automatically adopt the corresponding persistence strategy without touching business logic.

Architectural Implications

Enforcing family compatibility prevents subtle integration failures where mismatched implementations might expose incompatible serialization formats, transaction scopes, or connection lifecycles. Dependencies invert successfully because high-level modules never instantiate concrete types directly.

The primary limitation surfaces when extending the product catalog. Introducing a new entity, such as ComplianceLogs, mandates modifying the base factory interface and updating every concrete subclass. This structural rigidity conflicts with open-closed principles, as existing providers require recompilation to support additional creation signatures. Large-scale ecosystems frequently address this constraint through composition-heavy architectures, plugin registries, or runtime metadata discovery, yet the fundamental tension between cross-object consistency and hierarchical extensibility remains a core consideration in pattern selection.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.