1.1 Factory (Creational) Design Pattern

1 What is the Factory Design Pattern?

Imagine you're in a toy factory. You have different types of toys to make: dolls, cars, robots, and more. Now, every time you need to make a toy, you don't want to go through the hassle of figuring out how to make it from scratch. That's where the Factory Design Pattern comes in handy.

2 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. This not only simplifies our code but also makes it more flexible and easier to maintain.

3 Understanding the Factory Design Pattern

Let's break it down step by step:

  1. Toy Blueprint (Interface):
    1. 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):
    1. 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):
    1. 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):
    1. 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.