Refactoring a Simple Factory with the Table-Driven Method in C++

The classic simple factory pattern often relies on a switch or if-else chain to instantiate concrete objects. This approach violates the open/closed principle because adding a new type forces modification of the factory class. Table-driven design eliminates hard-coded branching by mapping identifiers directly to object creation functions.

1. Define the Abstract Interface and Creator Signature

First, we declare an abstract base class and a function pointer type representing a factory method.

class ArithmeticOp {
public:
    ArithmeticOp() : left_(0), right_(0) {}
    virtual ~ArithmeticOp() {}

    virtual void SetOperands(int left, int right) {
        left_ = left;
        right_ = right;
    }

    virtual int Compute() const = 0;

    using CreatorFunc = ArithmeticOp* (*)();

protected:
    int left_, right_;
};

The CreatorFunc alias will be used in the lookup table that replaces the switch statement.

2. Concrete Operation Classes

Each concrete class implements the computation and provides a static method that acts as a factory function.

class Addition : public ArithmeticOp {
public:
    int Compute() const override {
        return left_ + right_;
    }

    static ArithmeticOp* MakeInstance() {
        return new Addition();
    }
};

class Subtraction : public ArithmeticOp {
public:
    int Compute() const override {
        return left_ - right_;
    }

    static ArithmeticOp* MakeInstance() {
        return new Subtraction();
    }
};

The static MakeInstance methods are compatible with CreatorFunc and avoid exposing the concrete types to the factory internals.

3. Table-Driven Factory

The factory stores a mapping between operation symbols and creator functions. No conditional branching is needed for object creation.

#include <map>

class OperatorFactory {
public:
    bool RegisterOp(char symbol, ArithmeticOp::CreatorFunc creator) {
        if (!creator) return false;
        ops_[symbol] = creator;
        return true;
    }

    ArithmeticOp* Create(char symbol) const {
        auto it = ops_.find(symbol);
        if (it != ops_.end()) {
            return it->second();
        }
        return nullptr;
    }

private:
    std::map<char, ArithmeticOp::CreatorFunc> ops_;
};

alen The registration method RegisterOp allows dynamic extension with out altering the factory’s source code.

4. Client Usage

Before the factory can produce objects, the required operation creators must be registered.

void demo() {
    OperatorFactory factory;
    factory.RegisterOp('+', Addition::MakeInstance);
    factory.RegisterOp('-', Subtraction::MakeInstance);

    ArithmeticOp* op = factory.Create('+');
    if (op) {
        op->SetOperands(4, 7);
        printf("4 + 7 = %d\n", op->Compute());
        delete op;
    }

    op = factory.Create('-');
    if (op) {
        op->SetOperands(10, 3);
        printf("10 - 3 = %d\n", op->Compute());
        delete op;
    }
}

ternWhile flexible, forcing the client to explicitly register every operation reduces encapsulation and may increase coupling.

5. Improving Encapsulation with a Default Configuration

A specialized factory subclass provides built-in registration through a virtual initialization method. This restores convenience while preserving extensibility.

class DefaultOperatorFactory : public OperatorFactory {
public:
    virtual void SetupDefaults() {
        RegisterOp('+', Addition::MakeInstance);
        RegisterOp('-', Subtraction::MakeInstance);
    }
};

The client calls SetupDefaults() to populate the factory with standard operations. To add new operations without modifying this class, two strategies remain available:

  • Call RegisterOp directly on the factory instance before use.
  • Derive from DefaultOperatorFactory and overrride SetupDefaults, chaining the base class call if desired.

This design respects the open/closed principle: the original factory and its default configuration stay untouched, while new behavior is injected either through composition (direct registration) or inheritance (overriding SetupDefaults).

The table-driven approach is not limited to arithmetic operations; it can be applied wherever a conditional structure would otherwise decide which concrete object to instantiate.

Tags: C++ design-patterns table-driven simple-factory refactoring

Posted on Thu, 07 May 2026 16:46:05 +0000 by nodehopper