yeasir007

Monday, October 01, 2018

The definitions of the SOLID principles in Software Design with examples



SOLID Principles

SOLID principles are the foundation of a good software design which helps developers to write a maintainable and extendable code that can easily adapt with the future requirement. It’s acronym of five principles that are Single responsibility, Open-closed, Liskov substitution, Interface segregation and Dependency inversion. This acronym was first introduced by Michael Feathers based on Uncle Bob’s (Robert Cecil Martin) paper “Design Principles and Design Patterns”. Here I will explain each of five principles with an example.
 
 



Single Responsibility Principle (Known as SRP)


  1. A class /method should have one and only one reason (purpose or responsibility) to change. That means if anything is changed in the class, it will affect only one particular behavior of the software.
  2. Focus on SOC
    •  Separation of Concern.
    • A method or class should do one thing at a time.
    • Write highly cohesive and loosely coupled code

Example:
Suppose we have to develop a simple calculator which will take input of 2 numbers and calculate the sum as output.
In general approach we do like bellow:


#include
using namespace std;

int main()
{
    double first, second, sum;
    cin >> first >> second;
    sum = first + second;
    cout << sum << endl;
}
Actually we are performing 3 different types of responsibly for this task.
  1. Take input
  2. Process input
  3. Display output

So according to SRP we should write our code like bellow. This approach does not violate the single responsibility principle by abstracting the task in different methods.

#include<iostream>
using namespace std;

class Calculator
{
public:
    double add(double first, double second);
    void display(double sum);
    void takeInput();
};

double g_first, g_second;
int main()
{
   double sum;
   Calculator myCalculator;
   myCalculator.takeInput();
   sum = myCalculator.add(g_first, g_second);
   myCalculator.display(sum);
}

double Calculator::add(double first, double second)
{
   double sum = 0;
   sum = first + second;
   return sum;
}

void Calculator::display(double sum)
{
  cout << sum << endl;
}

void Calculator::takeInput()
{
   g_first = g_second = 0;
   cin >> g_first >> g_second;
}



Open/Closed Principles


  1. Software entities (classes, modules, functions etc.) should be open for extension but close for modification. (Here modification defines by the developer’s response to the client’s changed requirement.)  Suppose a class CAR is developed by AAA and developer BBB wants some modification on BREAK method of this class. In that case developer BBB can do this easily by extending/inheriting this class not by modifying this class.
  2. Class behavior should be changed by inheritance and composition.

Example:
Let say we have to implement a class which will calculate the salary of an office and there are three types of employees:
  • Manager (Salary: 100000/=)
  • Officer (Salary: 50000/=)
  • Clerk (Salary: 20000/=)

In general code will be like bellow:
#include

using namespace std;

enum EmployeeTypes
{
   Manager,
   Officer,
   Clerk
};

class SalaryCalculator
{
public:
   double getSalary(EmployeeTypes type);
};

double SalaryCalculator::getSalary(EmployeeTypes types)
{
  switch (types)
  {
    case Manager:
        return 100000;
    case Officer:
        return 50000;
    case Clerk:
        return 20000;
    default:
        return 0;
  }
}


Now Clients changed the requirement:

  1. Put taxes for the employees who withdraw salary more than 50000/=
  2. Want to introduce Human Resource Manager into office who will get 35000/= per month.

What can we do for that change?

  1. Add another enum type
  2. Add another check in the switch
  3. Add logic to check whether the salary is more than 50000/= or not etc..

What will happen if we frequently changed our class?
  1. This may cost in functionality malfunctioning.
  2. Class is getting bigger and various type of code smell will arises.
  3. Maintenance of the code will be getting harder followed by requirement changes.
  4. Unit test case may fail for those random changes.

So what can we do?
Salary calculator is calculating salary for each type of employee which is not good. Salary calculator is going to be changed for multiple purposes like:
  • Tax rule
  • Introduction of new employee
  • Change of salary amount
  • Change of salary calculation logic

Let’s apply SRP here:
  • Decouple the responsibility of this class
  • Introduce inheritance and interface if needed
  • Use dynamic polymorphism etc..
  • Use abstraction

Solution Steps:
  1. Now declare an abstract class for Employee
class Empolyee
{
 public:
   virtual double getSalary();
};
     2. Create subclass by inheriting Employee class for different types of employee. 
class Officer : Empolyee
{
 public:
   double getSalary()
   {
      return 50000;
   }
};

class Manager : Empolyee
{
 public :
    double getSalary()
    {
       return 100000;
    }
};

class Clerk : Empolyee
{
 public:
  double getSalary()
  {
    return 20000;
  }
};

     3. Now Salary calculator class will be changed like bellow:

class SalaryCalculator
{
 public:
   double getSalary(Empolyee type);
};

double SalaryCalculator::getSalary(Empolyee type)
{
  return type.getSalary();
}

Now we can calculate the salary for different types of employees like bellow:

void display(double salary)
{
  cout << salary << endl;
}

int main()
{
  SalaryCalculator mySalaryCalculator;
  Manager myManager;
  Officer myOfficer;
  Clerk myClerk;

  display ( mySalaryCalculator.getSalary( myManager ) );
  display ( mySalaryCalculator.getSalary( myOfficer ) );
  display ( mySalaryCalculator.getSalary( myClerk ) );
}
Now we can deal with customer changed requirement easily.
  • Put 1% tax is applied for the salary more than 50000/=.

Take a look, no other class/subclasses will be affected for this changed requirement. Only getSalary () method of the SalaryCalculator class will be changed little bit like bellow.
 

class SalaryCalculator
{
 public:
   double getSalary(Empolyee type);
};

double SalaryCalculator::getSalary(Empolyee type)
{
   double salary = type.getSalary();
   if (salary > 50000) //Applied Tax deduction rule according to change requirement
   {
      double tax = salary * 0.1;
      return salary - tax;
   }

   return salary;
}
  • Introduced Human Resource Manager having salary 35000/=

We can easily add another subclass for these new Types of resources by inheriting Employee class. We don’t have to change our existing code for this changed requirement.
code here
 
class HRM : public Empolyee
{
 public:
   double getSalary()
   {
      return 35000;
   }
};


Liskov's Substitution Principles


  1. The parent class should be able to refer child class object seamlessly during runtime polymorphism. Assume we have a class Tesla which is a subclass of class Car. According to LSP an object of class Car will be replaceable by the object of class Tesla without changing the behavior of the program.
  2. The implementation of this principle can be a little bit tricky if we combine it with Open-Closed Principle. Due to extend the behavior of subclass we have to make sure that we can still exchange the base class with the derived class without breaking anything.
Example:
Let’s say we want to show the behavior of animals:
  1. The specific animal should apply the behavior according to them.
  2. Animal class might have the following behaves:
    •  Eat
    • Drink
    • Run
    • Fly

So we can implement the Animal class like bellow:
class Animal
{
public:
  virtual void Eat();
  virtual void Drink();
  virtual void Run();
  virtual void Fly();
};

Now we want to introduce Lion’s behavior: 
  1. Lion is an animal, so it will inherit Animal class.
  2. So it should be:
  • Eat
  • Drink
  • Run
 
But does it Fly?     
So we cannot consider Lion’s object as Animal’s object according to Liskov’s Substitution Principle.

So what can we do?
  •   According to GOF (Gang of Four): Favor object composition over class inheritance.
  •   We should figure out the common behavior for all of the Animals and put those in Animal class.
     
For specific functionalities we should use Interface instead of putting those in base class like bellow.

class Animal
{
 public:
   void Eat();
   void Drink();
};

void Animal::Eat()
{
  cout << "Can eat" << endl;
}

void Animal::Drink()
{
  cout << "Can drink" << endl;
}

 __interface IRunnable
{
 public:
   void Run();
};

 __interface IFlyable
{
 public:
    void Fly();
};

Now we can implement Lion’s class:

class Lion : public Animal, public IRunnable
{
 public:
   void Eat();
   void Drink();
   void Run();
};

void Lion::Eat()
{
  cout << "Eat flesh" << endl;
}

void Lion::Drink()
{
  cout << "Drink water" << endl;
}

void Lion::Run()
{
  cout << "Runs fast" << endl;
}


Now we can introduce Eagle class as Animal easily:

class Eagle : public Animal, public IFlyable
{
 public:
   void Eat();
   void Drink();
   void Fly();
};

void Eagle::Eat()
{
  cout << "Eat flesh" << endl;
}

void Eagle::Drink()
{
  cout << "Drinks water" << endl;
}

void Eagle::Fly()
{
  cout << "Fly high" << endl;
}

Now, wherever in our code we were using Animal class object we must be able to replace it with the Lion or Eagle without exploding our code. What do we mean here is the child class should not implement code such that if it is replaced by the parent class then the application will stop running.


Interface Segregation Principles


  1. A client should not be forced to use an interface, if it doesn’t need it.
  2. Make fine grained interfaces that are client specific, small and cohesive.

Classes should be as specialized as possible. We shouldn’t develop any god classes that contain the whole application logic. The source code should be modular and every class should contain only the minimum necessary logic to achieve the desired behavior. The same goes for interfaces. Make small and specific interfaces so the client who implements them does not depend on methods it does not need. Instead of one class that can handle three special cases it is better to have three classes, one for each special case.

Let’s say, we have to implement a Smart device that can perform some operation like Print, Scan and Fax. So interface can be like bellow:

__interface ISmartDevice
{
  void Print();
  void Fax();
  void Scan();
};

This interface states that a smart device is able to print, fax, and scan. If we want to implement a smart device class AllInOnePrinter which can perform all of this action then it will be like bellow:

class AllInOnePrinter : public ISmartDevice
{
 public:
   void Print();    
   void Fax();
   void Scan();
};

void AllInOnePrinter::Print()
{
   cout << "Printing code." << endl;
}

void AllInOnePrinter::Fax()
{
  cout << "Beep booop biiiiip." << endl;
}

void AllInOnePrinter::Scan()
{
  cout << "Scanning code." << endl;
}

Now suppose we need to implement another smart device class named EconomicPrinter which can only perform Print operation. In that case we have to force to implement the whole interface, which is not a good practice at all.

class EconomicPrinter : public ISmartDevice
{
 public:
   void Print();
   void Fax();
   void Scan();
};

void EconomicPrinter::Print() 
{
  cout << "Yes I can print." << endl;
}

void EconomicPrinter::Fax()
{
  throw new exception;
}

void EconomicPrinter::Scan()
{
  throw new exception();
}

So what can we do?
Here we can apply the ISP by separating the single ISmartDevice interface into three smaller interfaces: IPrinter, IFax, and IScanner.


__interface IPrinter 
{
   void Print();
};

__interface IFax 
{
   void Fax();
};

__interface IScanner 
{
   void Scan();
};


Now our code is more decoupled and easier to maintain. Let's re-implement our EconomicPrinter class with this architecture:

class EconomicPrinter :public IPrinter
{
 public:
   void Print();
};

void EconomicPrinter::Print() 
{
   cout << "Yes I can print." << endl;
}

In that case our AllInOnePrinter would be like bellow:

class AllInOnePrinter :public IPrinter,public IFax,public IScanner
{
 public:
   void Print();    
   void Fax();
   void Scan();
};

void AllInOnePrinter::Print()
{
  cout << "Printing code." << endl;
}

void AllInOnePrinter::Fax()
{
  cout << "Beep booop biiiiip." << endl;
}

void AllInOnePrinter::Scan()
{
   cout << "Scanning code." << endl;
}



Dependency Inversion Principles




  1. High level modules should not depend upon low level modules. Both should depend upon abstractions by using a technique like inheritance or interface.
  2. Abstraction should not depend on details. Details should depend on abstraction.

Example:
Let’s say, we have two classes named Manager and Worker where Manager is a higher level class and Worker is a lower level class.
  • Manager has two types of task like SetWorker and Manage
  • Worker  has one simple task like DoWork


class Worker
{
 public:
   void doWork();
};

void Worker::doWork()
{
  cout << "working..." << endl;
}


class Manager
{
 public:
   Worker worker;

   void setWork(Worker w);
   void manageWorker();     
};

void Manager::setWork(Worker worker)
{
   this->worker = worker;
}

void Manager::manageWorker()
{
   worker.doWork();
}

Now there is a requirement change, we have to introduce other types of employee called SuperWorker. What we have to do if we want to introduce this new SuperWorker?
  • We have to change the Manager class
  • Some of the current functionality from the Manager class might be affected.
  • Unit test case may fail or should be redone.




class SuperWorker
{
 public:
   void doWork();
};

void SuperWorker::doWork()
{
   cout << "Working much more" << endl;
}


class Manager
{
public:
  Worker worker;
  SuperWorker superWorker;

  void setWork(Worker w);
  void setWork(SuperWorker w);

  void manageWorker();
  void manageSupperWorker();        
};

void Manager::setWork(Worker worker)
{
   this->worker = worker;
}
void Manager::setWork(SuperWorker superWorker)
{
   this->superWorker = superWorker;
}

void Manager::manageWorker()
{
   worker.doWork();
}
void Manager::manageSupperWorker()
{
   superWorker.doWork();
}

Okay, take a break.
Let’s assume the Manager class is quit complex, containing very complex logic and lots of activities like WorkingTimeCalculation, DailyBasisSalaryGeneration etc..
  • Just think, if most the function of this class is dependent on Worker class, what will happened?
  • All those problems could take a lot of time to solve and might introduce new errors in the old perfect functionality.
  • What will happened if another type of Worker introduce again?

So what can we do?
We can solve this kind of problem by applying DIP( Dependency Inversion Principle ).

  • Now we will add a new abstraction layer through IWorker Interface followed by DIP.
         

__interface IWorker
{
 public:
   void doWork();
};
  •  Lower classes will implement this Interface.
class Worker: public IWorker
{
 public:
   void doWork();
};

void Worker::doWork()
{
   cout << "working..." << endl;
}

class SuperWorker: public IWorker
{
 public:
    void doWork();
};

void SuperWorker::doWork()
{
   cout << "Working much more" << endl;
}

  •        Supper class like Manager will work with IWorker. By this way we can also introduce different types of Worker without changing the Manager class.
class Manager
{
 public:
   IWorker *worker;

   void setWork(IWorker *worker);
   void manageWorker();
};

void Manager::setWork(IWorker *worker)
{
   this->worker = worker;
}

void Manager::manageWorker()
{
   worker->doWork();
}
So, if we applied DIP high level class will not work with low level class directly rather than it will use an abstract layer by using Interface.

Conclusions:
The SOLID principles are guidelines that can help to create maintainable and extendable code. As Uncle Bob says: “They are not laws. They are not perfect truths”. Nevertheless, this guideline will help to create clean code. Finally as a developer you should at least know these principles by which you can take decision where to apply them or not.

Thanks for your endurance.


2 comments:

Thanks for your comments