1.1 Factory (Creational) Design Pattern

The Factory Design Pattern is a creational design pattern that provides an interface or method to create objects in a way that abstracts the instantiation logic from the client. It is used to encapsulate the process of object creation, making the code more maintainable and scalable.

The Factory Design Pattern is a way to create objects without specifying the exact class of the object being created.

Normally we create a object by writing new keyword in client code, however in this case we use a factory to handle the creation of objects.

Key Concepts

What Is It?

  • A factory is a class or method that encapsulates the object creation process.
  • The client doesn't need to know the specific class to instantiate; it simply requests an object from the factory and works with it through a common interface.

Why Use It?

  • To reduce tight coupling between the client and concrete classes.
  • To centralize and control object creation logic.
  • To follow SOLID principles, especially the Open/Closed Principle (open for extension, closed for modification).

Real-Life Analogy

  • Think of a pizza shop. Instead of making the pizza yourself, you place an order specifying the type of pizza you want, and the shop prepares it for you.
  • The pizza shop (factory) hides the details of how the pizza is made and gives you the final product.

Understanding with an Example 🤷‍♂️

Imagine you’re building a program for a pizza shop. There are different types of pizzas (CheesePizza, VeggiePizza, etc.), and the shop’s customer interface (client) should not worry about how these pizzas are created.

Without the Factory Pattern:

#include <iostream>
#include <string>

class CheesePizza {
public:
    void prepare() {
        std::cout << "Preparing Cheese Pizza\n";
    }
};

class VeggiePizza {
public:
    void prepare() {
        std::cout << "Preparing Veggie Pizza\n";
    }
};

int main() {
    std::string pizzaType = "cheese";

    if (pizzaType == "cheese") {
        CheesePizza pizza;
        pizza.prepare();
    } else if (pizzaType == "veggie") {
        VeggiePizza pizza;
        pizza.prepare();
    }

    return 0;
}

Problems:

  • Tight Coupling: The client knows the exact pizza classes (CheesePizza, VeggiePizza).
  • Code Duplication: If the pizza creation logic changes, it must be updated in multiple places.
  • Scalability Issues: Adding a new pizza type requires changes in the if-else structure, violating the Open/Closed Principle.

With the Factory Pattern:

#include <iostream>
#include <memory>
#include <string>

// Step 1: Abstract Product Interface
class Pizza {
public:
    virtual void prepare() const = 0; // Pure virtual function
    virtual ~Pizza() = default;      // Virtual destructor
};

// Step 2: Concrete Products
class CheesePizza : public Pizza {
public:
    void prepare() const override {
        std::cout << "Preparing Cheese Pizza\n";
    }
};

class VeggiePizza : public Pizza {
public:
    void prepare() const override {
        std::cout << "Preparing Veggie Pizza\n";
    }
};

// Step 3: Factory Class
class PizzaFactory {
public:
    static std::unique_ptr<Pizza> createPizza(const std::string& type) {
        if (type == "cheese") {
            return std::make_unique<CheesePizza>();
        } else if (type == "veggie") {
            return std::make_unique<VeggiePizza>();
        } else {
            throw std::invalid_argument("Invalid pizza type");
        }
    }
};

// Step 4: Client Code
int main() {
    try {
        // Request a Cheese Pizza
        std::unique_ptr<Pizza> pizza = PizzaFactory::createPizza("cheese");
        pizza->prepare();

        // Request a Veggie Pizza
        pizza = PizzaFactory::createPizza("veggie");
        pizza->prepare();
    } catch (const std::invalid_argument& e) {
        std::cerr << e.what() << '\n';
    }

    return 0;
}

How the Factory Pattern Improves Design:

  1. Encapsulation of Object Creation
    • The PizzaFactory handles the creation of CheesePizza and VeggiePizza, so the client doesn't need to know the specifics of object instantiation.
  2. Abstraction
    • The client only works with the abstract Pizza interface. It doesn’t interact directly with concrete implementations (CheesePizza, VeggiePizza).
  3. Open/Closed Principle
    • Adding a new pizza type (e.g., PepperoniPizza) only requires changes in the factory, not in the client code.
  4. Centralized Object Creation
    • All creation logic is in one place (PizzaFactory), making it easier to modify and maintain.

Advantages of the Factory Pattern:

  • Promotes loose coupling.
  • Centralizes creation logic.
  • Improves code readability and maintainability.
  • Makes the system easier to extend.

Disadvantages:

  • Can introduce complexity if overused or for very simple applications.
  • Requires careful management when scaling to avoid bloated factories.

Why do we need it ?

The Factory Pattern makes our lives easier by providing a way to create objects without specifying the exact class of object that will be created. It's like having a one-stop-shop for all your object creation needs.

Imagine you want to create different types of shapes like Circle or Rectangle. Instead of directly creating them in your main program, you ask the factory to give you the shape you need. The factory takes care of deciding which class to instantiate.

This makes your code:

  1. Simpler: You don't worry about the details of object creation.
  2. Flexible: If a new type of shape is added, you just update the factory without changing the rest of your code.

🤔 How It Works

  1. Define a common interface or parent class for the objects you want to create (e.g., Shape).
  2. Create different subclasses that implement or extend this interface/class (e.g., Circle, Rectangle).
  3. Use a factory class with a method that returns objects based on input.

🤔 Components of Factory Design Pattern

The Factory Design Pattern typically consists of several key components that work together to provide a flexible and extensible way to create objects.

1️⃣ Product Interface (or Abstract Class):

  • Definition: Defines a common interface or abstract class for all products that the factory creates.
  • Purpose: Ensures that the client code works with objects through a common abstraction, regardless of their concrete types.
  • Example:
class Pizza {
public:
    virtual void prepare() const = 0; // Pure virtual function
    virtual ~Pizza() = default;      // Virtual destructor
};

2️⃣ Concrete Products:

  • Definition: Specific implementations of the product interface. These are the objects created by the factory.
  • Purpose: Represents the actual instances that are returned to the client.
  • Example:
class CheesePizza : public Pizza {
public:
    void prepare() const override {
        std::cout << "Preparing Cheese Pizza\n";
    }
};

class VeggiePizza : public Pizza {
public:
    void prepare() const override {
        std::cout << "Preparing Veggie Pizza\n";
    }
};

3️⃣ Factory (or Creator):

  • Definition: The class that contains the logic for object creation. It often provides a method to create objects of different types based on input parameters.
  • Purpose: Encapsulates the object creation process and abstracts it from the client.
  • Example:
class PizzaFactory {
public:
    static std::unique_ptr<Pizza> createPizza(const std::string& type) {
        if (type == "cheese") {
            return std::make_unique<CheesePizza>();
        } else if (type == "veggie") {
            return std::make_unique<VeggiePizza>();
        } else {
            throw std::invalid_argument("Invalid pizza type");
        }
    }
};

4️⃣ Client:

  • Definition: The code that uses the factory to create objects. It works only with the product interface, not the concrete product classes.
  • Purpose: Ensures loose coupling between the client and the product implementations.
int main() {
    try {
        // Client requests a cheese pizza
        std::unique_ptr<Pizza> pizza = PizzaFactory::createPizza("cheese");
        pizza->prepare();

        // Client requests a veggie pizza
        pizza = PizzaFactory::createPizza("veggie");
        pizza->prepare();
    } catch (const std::invalid_argument& e) {
        std::cerr << e.what() << '\n';
    }

    return 0;
}

Relationship Between Components:

  1. The client interacts with the factory to request objects.
  2. The factory determines which concrete product to instantiate based on input parameters.
  3. The concrete product is returned as a pointer/reference to the product interface, ensuring the client remains decoupled from the actual implementation.

Diagram:

[Client] --> [Factory]
                |
                v
    [Abstract Product] <-- [Concrete Products]

Example Scenario

Imagine you're implementing a tool for creating different shapes in a drawing application:

  1. Abstract Product: class Shape { virtual void draw() = 0; };
  2. Concrete Products: class Circle : public Shape, class Square : public Shape
  3. Factory: ShapeFactory with a method createShape(std::string type)
  4. Client: Code that calls ShapeFactory::createShape("circle").

Example:

Let's say we need to create Circle and Rectangle objects.

Without Factory:

Circle* circle = new Circle();
Rectangle* rectangle = new Rectangle();
  • Here we directly instantiate the objects, which ties our code to specific classes.

With Factory:

Shape* shape = ShapeFactory::createShape("circle");
  • Here, the ShapeFactory creates the correct object for us. Our code doesn't know or care about the exact class.

Complete Code:

#include <iostream>
#include <string>

// Step 1: Common Interface
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

// Step 2: Concrete Implementations
class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Circle.\n";
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a Rectangle.\n";
    }
};

// Step 3: Factory Class
class ShapeFactory {
public:
    static Shape* createShape(const std::string& type) {
        if (type == "circle") {
            return new Circle();
        } else if (type == "rectangle") {
            return new Rectangle();
        }
        return nullptr;
    }
};

// Step 4: Client Code
int main() {
    Shape* shape1 = ShapeFactory::createShape("circle");
    if (shape1) shape1->draw(); // Output: Drawing a Circle.

    Shape* shape2 = ShapeFactory::createShape("rectangle");
    if (shape2) shape2->draw(); // Output: Drawing a Rectangle.

    delete shape1;
    delete shape2;

    return 0;
}

What's Happening?

  1. Client asks the ShapeFactory for a circle or rectangle.
  2. The factory decides which class to instantiate and gives the correct object.
  3. The client just uses the object without worrying about its exact class.

In Simple Words:

The Factory Design Pattern is like a "vending machine."
You press a button (give input), and the vending machine (factory) gives you the item (object) you want, without you needing to know how it was made or stored.

3 Understanding the Factory Design Pattern

Let's break it down step by step:

  1. Toy Blueprint (Interface):
    • Just like in our toy factory, we have a blueprint for every toy. This blueprint says, "Every toy must have these features." We call this the Toy Interface.
  2. Actual Toys (Concrete Classes):
    • Now, we have the actual toys, like Dolls, Cars, Robots, etc. Each toy follows the blueprint (Toy Interface), but they each have their own unique features.
  3. Toy-Making Process (Factory Interface):
    • Next up, we have a set way of making toys. It's like our secret recipe for making awesome toys, but we haven't decided which type of toy to make yet. We call this the Toy Factory Interface.
  4. Toy Factories (Concrete Factories):
    • Finally, we have specialized toy-making processes for each type of toy. These factories follow the toy-making recipe (Toy Factory Interface) but specialize in making a specific type of toy.

Example:

#include <iostream>

// Step 1: Toy Interface (Blueprint)
class Toy {
public:
    virtual void play() = 0;
    virtual ~Toy() {}
};

// Step 2: Actual Toys (Concrete Classes)
class Doll : public Toy {
public:
    void play() override {
        std::cout << "Playing with Doll\n";
    }
};

class Car : public Toy {
public:
    void play() override {
        std::cout << "Playing with Car\n";
    }
};

// Step 3: Toy Factory Interface (Toy-Making Process)
class ToyFactory {
public:
    virtual Toy* createToy() = 0;
    virtual ~ToyFactory() {}
};

// Step 4: Toy Factories (Concrete Factories)
class DollFactory : public ToyFactory {
public:
    Toy* createToy() override {
        return new Doll();
    }
};

class CarFactory : public ToyFactory {
public:
    Toy* createToy() override {
        return new Car();
    }
};

// Client Code
int main() {
    ToyFactory* factory = new DollFactory();
    Toy* toy = factory->createToy();
    toy->play();
    delete factory;
    delete toy;
    return 0;
}

4 Without Factory Method Design Pattern

#include <iostream>

// Library classes
class Vehicle {
public:
    virtual void printVehicle() = 0;
};

class TwoWheeler : public Vehicle {
public:
    void printVehicle() override {
        std::cout << "I am two wheeler\n";
    }
};

class FourWheeler : public Vehicle {
public:
    void printVehicle() override {
        std::cout << "I am four wheeler\n";
    }
};

// Client (or user) class
class Client {
private:
    Vehicle* pVehicle;

public:
    Client(int type) {
        if (type == 1) {
            pVehicle = new TwoWheeler();
        } else if (type == 2) {
            pVehicle = new FourWheeler();
        } else {
            pVehicle = nullptr;
        }
    }

    ~Client() {
        delete pVehicle;
    }

    Vehicle* getVehicle() {
        return pVehicle;
    }
};

// Driver program
int main() {
    Client* pClient = new Client(1);
    Vehicle* pVehicle = pClient->getVehicle();
    if (pVehicle != nullptr) {
        pVehicle->printVehicle();
    }
    delete pClient;
    return 0;
}

4.1 What are the problems with the above design?

  1. Violation of Single Responsibility Principle (SRP):
    The Client class is responsible for both creating and managing the lifetime of the Vehicle object. This violates the SRP, which states that a class should have only one reason to change. Separating concerns would improve the maintainability and readability of the code.
  2. Tight Coupling: The Client class is tightly coupled with the concrete implementations of TwoWheeler and FourWheeler. This makes it difficult to extend the codebase with new types of vehicles or to switch different implementations without modifying the Client class.
  3. Lack of Flexibility:
    Adding a new type of vehicle requires modifying the Client class, violating the Open-Closed Principle (OCP), which states that a class should be open for extension but closed for modification. This limits the flexibility and scalability of the code.

4.2 How do we avoid these problem?

1 Define Factory Interface:

Create a VehicleFactory interface with a method for creating vehicles.

// Factory Interface
class VehicleFactory {
public:
    virtual std::unique_ptr<Vehicle> createVehicle() = 0;
    virtual ~VehicleFactory() {}
};

2 Implement Concrete Factories:

Implement concrete factory classes (TwoWheelerFactory and FourWheelerFactory) that implement the VehicleFactory interface and provide methods to create instances of specific types of vehicles.

// Concrete Factory for TwoWheeler
class TwoWheelerFactory : public VehicleFactory {
public:
    std::unique_ptr<Vehicle> createVehicle() override {
        return std::make_unique<TwoWheeler>();
    }
};

// Concrete Factory for FourWheeler
class FourWheelerFactory : public VehicleFactory {
public:
    std::unique_ptr<Vehicle> createVehicle() override {
        return std::make_unique<FourWheeler>();
    }
};

3 Refactor Client:

Modify the Client class to accept a VehicleFactory instance instead of directly instantiating vehicles. The client will request a vehicle from the factory, eliminating the need for conditional logic based on vehicle types.

// Client class
class Client {
private:
    std::unique_ptr<Vehicle> pVehicle;

public:
    Client(std::unique_ptr<VehicleFactory> factory) {
        pVehicle = factory->createVehicle();
    }

    void printVehicle() {
        if (pVehicle)
            pVehicle->printVehicle();
    }
};

4 Enhanced Flexibility:

With this approach, adding new types of vehicles is as simple as creating a new factory class for the new vehicle type without modifying existing client code. For example, if we want to add a ThreeWheeler vehicle, we can create a new factory class ThreeWheelerFactory implementing VehicleFactory, without changing the Client class.

5 With Factory Method Design

#include <iostream>

// Library classes
class Vehicle {
public:
    virtual void printVehicle() = 0;
    virtual ~Vehicle() {}
};

class TwoWheeler : public Vehicle {
public:
    void printVehicle() override {
        std::cout << "I am two wheeler\n";
    }
};

class FourWheeler : public Vehicle {
public:
    void printVehicle() override {
        std::cout << "I am four wheeler\n";
    }
};

// Factory Interface
class VehicleFactory {
public:
    virtual Vehicle* createVehicle() = 0;
    virtual ~VehicleFactory() {}
};

// Concrete Factory for TwoWheeler
class TwoWheelerFactory : public VehicleFactory {
public:
    Vehicle* createVehicle() override {
        return new TwoWheeler();
    }
};

// Concrete Factory for FourWheeler
class FourWheelerFactory : public VehicleFactory {
public:
    Vehicle* createVehicle() override {
        return new FourWheeler();
    }
};

// Client class
class Client {
private:
    Vehicle* pVehicle;

public:
    Client(VehicleFactory* factory) {
        pVehicle = factory->createVehicle();
    }

    ~Client() {
        delete pVehicle;
    }

    Vehicle* getVehicle() {
        return pVehicle;
    }
};

// Driver program
int main() {
    VehicleFactory* twoWheelerFactory = new TwoWheelerFactory();
    Client* twoWheelerClient = new Client(twoWheelerFactory);
    Vehicle* twoWheeler = twoWheelerClient->getVehicle();
    twoWheeler->printVehicle();

    delete twoWheelerClient;

    VehicleFactory* fourWheelerFactory = new FourWheelerFactory();
    Client* fourWheelerClient = new Client(fourWheelerFactory);
    Vehicle* fourWheeler = fourWheelerClient->getVehicle();
    fourWheeler->printVehicle();

    delete fourWheelerClient;

    return 0;
}

In the above code:

  1. Vehicle serves as the Product interface, defining the common method printVehicle() that all concrete products must implement.
  2. TwoWheeler and FourWheeler are concrete product classes representing different types of vehicles, implementing the printVehicle() method.
  3. VehicleFactory acts as the Creator interface (Factory Interface) with a method createVehicle() representing the factory method.
  4. TwoWheelerFactory and FourWheelerFactory are concrete creator classes (Concrete Factories) implementing the VehicleFactory interface to create instances of specific types of vehicles.
#include <iostream>
#include <string>

// Base class representing a generic product
class Product {
public:
  // Constructor for Product
  Product() { std::cout << "Product" << std::endl; }
  
  // Virtual destructor for proper cleanup in derived classes
  virtual ~Product() {}

  // Pure virtual function that must be implemented by derived classes
  virtual void checkProduct() = 0;
};

// Concrete implementation of Product: ConcreateProductA
class ConcreateProductA : public Product {
public:
  // Constructor for ConcreateProductA
  ConcreateProductA() { std::cout << "ConcreateProductA" << std::endl; }
  
  // Destructor for ConcreateProductA
  ~ConcreateProductA() {}

  // Implementation of the checkProduct method
  void checkProduct() { std::cout << "ProductA has been created" << std::endl; }
};

// Concrete implementation of Product: ConcreateProductB
class ConcreateProductB : public Product {
public:
  // Constructor for ConcreateProductB
  ConcreateProductB() { std::cout << "ConcreateProductB" << std::endl; }
  
  // Destructor for ConcreateProductB
  ~ConcreateProductB() {}

  // Implementation of the checkProduct method
  void checkProduct() { std::cout << "ProductB has been created" << std::endl; }
};

// Abstract Creator class that declares factory methods
class Creater {
public:
  // Constructor for Creater
  Creater() { std::cout << "Creater" << std::endl; }
  
  // Virtual destructor for proper cleanup in derived classes
  virtual ~Creater() {}

  // Factory method for creating ProductA (pure virtual, must be implemented by derived classes)
  virtual Product* createProductA() = 0;

  // Factory method for creating ProductB (pure virtual, must be implemented by derived classes)
  virtual Product* createProductB() = 0;
};

// Concrete Creator that implements the factory methods
class ConcreateCreater : public Creater {
public:
  // Constructor for ConcreateCreater
  ConcreateCreater() { std::cout << "ConcreateCreater" << std::endl; }
  
  // Destructor for ConcreateCreater
  ~ConcreateCreater() {}

  // Factory method implementation for creating ProductA
  Product* createProductA() { return new ConcreateProductA; }

  // Factory method implementation for creating ProductB
  Product* createProductB() { return new ConcreateProductB; }
};

int main(int argc, char* argv[]) {
  // Create an instance of ConcreateCreater
  Creater *creater = new ConcreateCreater;

  // Use the factory method to create a ProductA instance
  Product *productA = creater->createProductA();
  productA->checkProduct(); // Call checkProduct on ProductA

  // Use the factory method to create a ProductB instance
  Product *productB = creater->createProductB();
  productB->checkProduct(); // Call checkProduct on ProductB

  // Clean up dynamically allocated memory
  delete productA;
  delete productB;
  delete creater;

  return 0;
}

Another Example:

// Abstract product
class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() = default;
};

// Concrete products
class Circle : public Shape {
public:
    void draw() override { std::cout << "Drawing Circle\n"; }
};

class Rectangle : public Shape {
public:
    void draw() override { std::cout << "Drawing Rectangle\n"; }
};

// Factory method
class ShapeFactory {
public:
    static Shape* createShape(const std::string& type) {
        if (type == "circle") return new Circle();
        if (type == "rectangle") return new Rectangle();
        return nullptr;
    }
};

// Client
int main() {
    Shape* shape = ShapeFactory::createShape("circle");
    shape->draw(); // Output: Drawing Circle
    delete shape;
    return 0;
}

Problems Before the Factory Design Pattern

1️⃣ Tight Coupling:

The client directly instantiates objects of concreate classes.

Example (without factory):

CheesePizza cheesePizza;
cheesePizza.prepare();
  • The client must know the exact class (cheese Pizza) to create, which ties the client code to specific implementations.
  • Adding new pizza types (e.g., PepperoniPizza) requires changes to the client code, violating the Open/Closed Principle (OCP).

2️⃣ Code Duplication:

Object Creation logic is repeated in multiple places.

Example:

if (type == "cheese") {
    CheesePizza cheesePizza;
    cheesePizza.prepare();
} else if (type == "veggie") {
    VeggiePizza veggiePizza;
    veggiePizza.prepare();
}
  • If the creation logic for CheesePizza or VeggiePizza changes (e.g., adding toppings), it must be updated everywhere the objects are instantiated.
  • This duplication increases maintenance overhead and risks inconsistencies.

3️⃣ Reduced Scalability:

Problem: Adding a new product (e.g., PepperoniPizza) requires changes throughout the codebase.

  • Each place where objects are created needs to be updated to include the new type.
  • As the number of products grows, this becomes unmanageable.

4️⃣ Lack of abstraction:

The client directly interacts with concrete classes instead of abstractions.

Example:

CheesePizza cheesePizza;
VeggiePizza veggiePizza;
  • The client doesn't rely on a common interface (Pizza) but directly uses the concrete implementations (CheesePizza, VeggiePizza).
  • This makes it harder to change or replace implementations without modifying the client code.

5️⃣ Lack of Centralized Control

  • Problem: Object creation logic is scattered across the codebase.
    • There’s no centralized place to enforce rules, constraints, or optimizations for object creation.
    • For example, ensuring only one instance of a particular product type (singleton) becomes challenging.