Mastering Factory Design Patterns through the Construction of a Banking System

Mastering Factory Design Patterns through the Construction of a Banking System

A Hands-on Approach to Understanding and Implementing the Factory Design Pattern in a Real-World Application

First of all, let's define the

Requirements of our problem

  • There can be multiple banks

    • All banks share some common attributes and behavior like

      • Behavior - showBalance() , withdraw() , addBalance() etc

      • Attributes - amount_left , num_of_users etc.

    • Banks can also implement their unique methods and attributes

  • Provide a simple interface to the user for creating bank objects of their choice (i.e., users should now be responsible for)

  • Our solution should follow S.O.L.I.D. Principle

Having Multiple Banks

Let's start by defining two banks classes - A1 and A2

class A1 {
    private:
        int amount_left , num_of_users; // .. other params
    public:
        int NumOfUsers(){...} 
        // Other attributes
};

class A2 {
    private:
        int amount_left , num_of_users; // .. other params
    public:
        int NumOfUsers(){...} 
        // other attributes
};

Notice something wrong.

  • First of all, there is a ton of code duplication. Since there are a lot of common attributes and methods we will be writing, the same code again and again.

  • Secondly, there is no guarantee that all classes will define the common attributes and methods.

To solve these issues, we can define a common interface. This common interface will have all common attributes and methods. All other classes that implement this common interface will have to define the methods.

  • It solves the code duplication part, as we don't need to redefine the attributes and methods in the subclasses. (classes that implement the interface)

  • Any class that implements the common interface needs to define the methods.

Let's have a look at the code now.

class Bank{
    protected:
        int amount_left , num_of_users; // .. other common attributes
    public:
        virtual int NumOfUsers(){ // Subclass can redefine this
            return this->num_of_users
        } 
        virtual int getBalance() const = 0; //Subclass have to redefine this
};

class A1 : public Bank {
    private:
        int special_params;
    public:
       int getBalance() const override{...}
};

class A2 : public Bank {
    private:
        int special_params;
    public:
       int getBalance() const override{...}
};

Okay, now that this is taken care of, let's focus on the second requirement.

Provide a simple interface to the user for creating a bank objects of their choice

In our existing code, if a user wants to create a bank object then

void client(){
    A1* a1 = new A1;
    std::cout<<a1->getBalance(); 
}

Well, this doesn't look that bad, but we are exposing too much of our backend logic to the client. Ideally, the client shouldn't know about every subclass. We should be providing a centralized code for bank selection. Let's define a new class for this.

class BankCreator{
    Bank* getBankInstance(string bankName){
        switch(bankName){
            case "a1":
                return new A1;
            case "a2":
                return new A2;
            default:
                return NULL;
        }
    }
};

void client(){
    BankCreator* backCreator = new BankCreator;
    Bank* a1 = backCreator->getBankInstance("a1");
    Bank* a2 = backCreator->getBankInstance("a2");
}

NOTE: All the objects created by the BankCreator class should have a common superclass.

Now, this looks great. But do you notice something wrong with our code?

The BankCreator class violates the Open For Extension and Closed for Modification Principle.

When we are adding a new bank (subclass) we need to modify the BankCreator class (add another case to the switch statement).

So, we need to define the creator class in such a way that whenever we add/remove a bank subclass, we do so without modifying the creator class. To do this

  • We first make the creator a class an interface

  •        class BankCreator{
            protected:
                virtual Bank* getBankInstance() const = 0;
        };
    
  • Now to add a new bank we simply implement this interface

  •     class A1Creator: public BankCreator{
            Bank* getBankInstance() const override{
                return new A1;
            }
        };
    
        class A2Creator: public BankCreator{
            Bank* getBankInstance() const override{
                return new A2;
            }
        };
    

    Since we can add new bank subclasses without modifying the creator class, we can say that we are following the Open For Extension and Closed for Modification Principle.

Now a client can create a bank object in the following way

void client(){
    A1Creator* a1creator = new A1Creator;
    A1* a1 = a1creator->getBankInstance();
    A1* a11 = a1creator->getBankInstance();
}

Also, it might look like we are exposing too much of our backend logic, but that's not the case. The client is not concerned about the implementation of the concrete subclasses (bank subclasses) but is only aware of creator classes.

Before moving forward, here's the entire

Pseudo code for Factory Design Pattern

//This is an Abstract class. But you can also make it an interface 
class Bank{
    protected:
        int amount_left , num_of_users; // .. other common attributes
    public:
        virtual int NumOfUsers(){ // Subclass can redefine this
            return this->num_of_users
        } 
        virtual int getBalance() const = 0; //Subclass have to redefine this
};

class A1 : public Bank {
    private:
        int special_params;
    public:
       int getBalance() const override{...}
};

class A2 : public Bank {
    private:
        int special_params;
    public:
       int getBalance() const override{...}
};

class BankCreator{
      protected:
          virtual Bank* getBankInstance() const = 0;
};

 class A1Creator: public BankCreator{
      Bank* getBankInstance() const override{
          return new A1;
      }
  };

  class A2Creator: public BankCreator{
      Bank* getBankInstance() const override{
          return new A2;
      }
  };

void client(){
    A1Creator* a1creator = new A1Creator;
    A1* a1 = a1creator->getBankInstance();
    A1* a11 = a1creator->getBankInstance();
}

Now, let's discuss the pros and cons of this pattern

✅ Pros

  • It follows the Single Responsibility Principle.

    The creator class and each concrete creator subclass have only one responsibility. Tldr, there is a segregation of responsibility.

  • It follows the Open For Extension and Closed for Modification Principle.

  • There is loose coupling

    Instead of adding the logic to decide the type of object in the creator class, we are assigning the responsibility to some other class

❌ Cons

  • Code can become too complex.

    Since we are adding a lot of subclasses.


Cover image credit: Image by storyset on Freepik