SOLID

Well Codeignition has added some new faces recently, and yeah I am one of them. I just passed out from IIT Jodhpur and got in here. And its been a huge learning experience since. Writing code is not the same now. Now I need to impose a great deal of thought on the structure of the code, and yes, SOLID has been a big help. So let’s get back to the basics and see what the fuss it is.

SOLID is an acronym for the five principles of software design, devised by Robert C. Martin or well known as Uncle Bob. What are these five principles, let’s find out.

1. Single Responsibility Priniciple -

A class should have only one reason to change, thus it should perform only one job.

Probably the most intuitive and easy to understand, but hard in practice. Its application in code does wonders regarding simplicity and readability.

Take a look at the code below, we have defined a class person, which has 3 attributes. One of them is email, which needs to be verified.

class Person{
  name: string ;
  address: string;
  email: string;
  validate email(){
    //validation logic
  }
}

One might want to verify the email in his own way, whether its syntax or domain or whatever. These changes do not affect the job of person class, since it is supposed to define a person object. If it is also validating the email, it is then violating SRP.

So, we take the validation step outside, create a new class. That will leave us with person class with only one job - defining a person object. That should be it.

class Email{
  email: string;
  validate email(){
    //validation logic
  }
}

class Person{
  name: string;
  address: string;
  email: Email;
}

SRP has following advantages

  • it makes refactoring really easy.
  • imposes a great deal of structure on the code.
  • naming things, which is actually tough, is made easier since classes now have one simple job, so one can name them accordingly which can make it clear what it does.

Let’s move on to the next one.

2. Open/Closed Principle

Modules should be open for extension, but closed for modification.

Let’s take a look at an example -

class Rectangle{
  width;
  height;
}

class AreaCalculator{
  calculateArea(){
    //calculate area
  }
}

Now suppose, you are instructed that AreaCalculator class should calculate area of a circle as well. You might think that’s not a big deal.

You make one circle class and edit the AreaCalculator accordingly.

class Circle{
  radius;
}

class AreaCalculator(shape){
  calculateArea(){
    if(shape == rectangle) return width * height;
    else return pie*radius*radius
  }
}

Yay, you did it. But now, you are instructed to add one for triangle as well. Well this is getting bad now, you will now edit the function, adding another if-else will result in complex code. You do know we don’t like complex code.

So, how do we get it right? Well, OCP wants us to write code, which are closed for modification but open for extension. It wants you to separate the behaviours of the system, such that utterly intrinsic behaviours are behind a wall, that can’t be touched and the remainings that are variable and might change a lot are on other side of the wall. The base class is behind the wall, untouchable and all the dependencies point towards the base class.

To resolve the above code, we will be using interfaces. Let’s see how that works out.

interface Area{
  calculateArea(){}
}

class Rectangle : Area{
  width;
  height;

  calculateArea(){
    return width*height;
  }
}

class Circle : Area{
  radius;

  calculateArea(){
    return pie*radius*radius;
  }
}

Now, even if you are told to find area of triangle, you will simply add another class, implement the Area interface and override the function, without modifying the existing code. Neat huh.

3. Liskov Substitution

Objects of base class can be replaced by objects of derived class.

Let’s talk about shapes. There are quadrilaterals, rectangles, squares etc. Let’s take up rectangles and squares. A rectangle class would posses width and height as its attributes and some functions like area, perimeter.

class Rectangle{
  width;
  height;

  setWidth(){};
  setHeight(){};
}

Now, a square is a rectangle, right. So, a square can inherit from a rectangle.

class Square extends Rectangle{
}

Good enough? No. We just violated the Liskov Substitution principle.

Think about it, a rectangle has two attributes: width and height, while a square needs just one: side. Square needs only one variable, but it will inherit two. A rectangle would have methods for set height and set width but a square won’t need those both.

Now, there is a design issue at stake.

Understand that IsA is not inheritance. Square is a rectangle, can’t be more true. But it’s in terms of geometry, the same can’t be said about those two while programming. It is an example of bad design. A better design would be to keep rectangle and square different and inheriting from a shape class.

Liskov Substitution principle saves us from such bad software designs, apply this and you won’t make such mistakes again.

One more example is of circle and ellipse. I am leaving it on you to think about it.

4. Interface Segregation Principle

A client should never be forced to implement an interface that it does not use or clients shouldn’t be forced to depend on methods they do not use.

Let’s say we have a class, a big fat class, call it fat, and it has got a whole bunch of methods and there is a group of clients.

The population of clients that does not care about some specific methods are still forced to, to change because fat class changed. In general, if a source file changes, you have to recompile everybody who depends on that source file, even if nothing they really cared about changed.

An example should make it more clear:

interface ShapeInterface {
  area();
  volume();
}

class square : ShapeInterface{
}

Now, we know that square is 2D object, and it does not have any volume but still it has acquired that function. The function when called upon will give errors and we surely don’t want that.

The way we can avoid this from happening is by taking each of the clients and creating an interface that contains only the methods that they care about.

So, our example can be solved like this

interface ShapeInterfaceTwoD{
  area();
}

class square : ShapeInterfaceTwoD{
}

The square is now not taking any unnecessary methods, and we have achieved interface segregation. Woohoo!!

5. Dependency Inversion

High level modules should not depend upon low level modules. Both should depend upon abstractions & abstractions should not depend upon details. Details should depend upon abstractions.

DI is a special case of OCP, which is basically used to reduce coupling between two modules.

An example would be a better way to understand this.

class PasswordReminder {
  dbConnection;

  __construct(MySQLConnection dbConnection) {
    this->dbConnection = dbConnection;
  }
}

First, the MySQLConnection is the low level module while the PasswordReminder is high level, but according to DI, this snippet above violates this principle as the PasswordReminder class is being forced to depend on the MySQLConnection class.

Later if we were to change the database engine, we would also have to edit the PasswordReminder class and thus violates OCP.

To fix this again we code to an interface, since high level and low level modules should depend on abstraction, we can create an interface:

interface DBConnectionInterface {
  public function connect();
}

class MySQLConnection implements DBConnectionInterface {
  public function connect() {
    return "Database connection";
  }
}

class PasswordReminder {
  private $dbConnection;

  public function __construct(DBConnectionInterface $dbConnection) {
    $this->dbConnection = $dbConnection;
  }
}

Now, even if we are to change the db engine, we can do that without changing the Password Reminder class. See, we achieved reduced coupling and we respected our OCP too.

Further Reading -