S.O.L.I.D. principles

Principles for the object-oriented working


12 May 2018 View Comments
#computer #working #SOLID #OOP #composition

S.O.L.I.D. is an acronym intended to make software designs more understandable, flexible and maintainable Object-Oriented Programming. These 5 principles are not intended to fix any code errors but they are designed to make it easy for developers to avoid any bad code style, easier code reuse, easier to identify problems, etc. Robert C. Martin promotes them beside a handful of others. These 5 are the important ones in the field of OOP to be recognized.

When you list the acronyms, they might seem a bit complicated. However, when you dive into it, they are pretty simple concepts to understand for OOP worker.

  • S - Single-responsiblity principle
  • O - Open-closed principle
  • L - Liskov substitution principle
  • I - Interface segregation principle
  • D - Dependency Inversion Principle

S — Single Responsibility Principle (known as SRP)

Robert C. Martin preaches this even in his book, Clean Code.

"We want our systems to be composed of many small classes, not a few large ones."

This means we should have single responsibility for each class, not multiple. Let’s take a look at an example:

class UserAccount {
  public Stub calculatePayCheck() {...}
  public void save() {...}
  public String showUserStatus() {...}
}

You have 3 distinct operations here: calculation of paycheck, saving user information to the storage(ex: database), showing the user information. These operations do not require to be coupled to each other. You should create a class for each of these operations so you have a class that does distinct operation such as the one that calculates, one handling the storage and one handling the user information.

O - Open-Closed Principle

The idea is simple.

"Classes should be built to open for extension but closed for modification"

Classes should be designed so you could extend the functionalities easily but the developed class should not be modified except to correct any bugs or issues. The focus sometimes is misinterpreted as “extending” to “inheriting” as the keyword “extend” refers to the “inherit” in many languages. However, this is not true. Principles are older than these modern languages and they do not necessarily mean you should subclass the actual class that needs the new behaviour. We should favour composition over inheritance where favouring the composition produces more malleable and favours polymorphic behaviour rather than using the inheritance where it is more tightly coupled and breaks encapsulation. Let me show an example of what I mean:

Receipt pay(List<Stuff> items) {
   Receipt r = new Receipt();
   BigInteger sum = BigInteger.valueOf(0);
   for (item : items) {
      r.addItem(item);
      sum = sum.add(BigInteger.valueOf(item.getPrice()));
   }
   Payment p = payCash(sum);
   return r;
}

Now let’s say we would like to extend this to support multiple different payment methods. We could pass in a boolean flag which controls the payment method like this: if (credit) payCredit(sum) else payCash(sum). However, this is kind of boolean flag argument (parameter) is a code smell and should be avoided because you are providing 2 different behaviours in one function in contrast to just one. This is probably a better way:

public interface PaymentMethod {void acceptPayment(Money total);}

Receipt pay(List<Stuff> items, PaymentMethod pm) {
   Receipt r = new Receipt();
   BigInteger sum = BigInteger.valueOf(0);
   for (item : items) {
      r.addItem(item);
      sum = sum.add(BigInteger.valueOf(item.getPrice()));
   }
   Payment p = pm.acceptPayment(sum);
   return r;
}

This does what OCP really wants. It provides the support for even more payments methods by leaving the implementation open-ended in a concise and clean manner.

L - Liskov Substitution Principle

Liskov Substitution Principle (LSP) starts out by saying,

"objects in a work should be replaceable with instances of their subtypes without altering the correctness of that work."

This statement really explains everything. In other words, It’s basically saying not to forcibly fit objects to become one of your models. Here is an example. Say you defined a “Car” in this fashion:

interface ICar {
    public void drive();
    ... bunch of other operations for ICar ...
    public void startBlueToothCall();
}

Well, most of the recent modern cars are equipped and comes with a Bluetooth function. What if a car has no Bluetooth functionality at all? like cars made before the year 2000? We can make our ways out and just throw an exception or something or ignore such functionality to make the program flow and define this old car to be our ICar interface. However, this is what’s breaking Liskov Substitution Principle because we end up altering the correctness of the work. A better way to design this would be to rename ICar to such as IModernCar and create another interface that describes older cars. In common sense, old cars definitely categorize into cars, but with the way we have designed the ICar, those old cars are not how the car should be defined and they shouldn’t be categorized as the ICar.

I - Interface Segregation Principle

The interface-segregation principle (ISP) states that,

"no client should be forced to depend on methods it does not use."

Within object-oriented design, interfaces provide layers of abstraction that facilitate conceptual explanation of the code and create a barrier preventing coupling to dependencies which means that interfaces should be constructed very specifically to its needs. Let’s describe a Worker who can work and eat at the same time.

interface IWorker {
    public void work();
    public void eat();
}

class Worker implements IWorker{
    public void work() {
        // ....working
    }
    public void eat() {
        // ...... eating in launch break
    }
}

class SuperWorker implements IWorker{
    public void work() {
        //.... working much more
    }
    public void eat() {
        //.... eating in launch break
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker p) {
        worker=p;
    }

    public void manage() {
        worker.work();
    }
}

We have a problem though. Not all workers require eating. Also, if we have more behaviours added to this “Worker”, the single interface will be cluttered and confusing. Instead, we can simplify this and design in this fashion by segregating interfaces:

interface IWorker {
    public void work();
}
interface IFeedable {
    public void eat();
}
interface IStudy {
   public void study();
}

class Worker implements IWorker, IFeedable {
    public void work() {
        // ....working
    }
    public void eat() {
        // ...... eating in launch break
    }
}

class Student implements IStudy, IFeedable {
   public void study() {
        // ....studying
    }
    public void eat() {
        // ....eating
    }
}

class Robot implements IWorker {
    public void work() {
        // ....working
    }
}

class Manager {
    IWorker worker;

    public void setWorker(IWorker p) {
        worker=p;
    }

    public void manage() {
        worker.work();
    }
}

You can see that by dividing the works necessary to the specific interfaces, you can manage the different classes according to the necessary action in a much more flexible way. Adapter design pattern usually helps to segregate these fat interfaces.

D - Dependency Inversion Principle

The principle states:

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

The way of thinking is “inverted” for OOP designers designing their dependencies. High-level classes are not working directly with low-level classes anymore, they are built through the abstractions. The DIP is about the level of the abstraction in the messages sent from your code to the thing it is calling. It is about the shape of the object upon which the code depends. Let’s start with an example. We have a reader which reads a book in Scala:

class BookReader(book: Book) {
    def read(): String = "reading a book";
}

Now requirement becomes that you need to be able to read more than just a physical book such as PDF book, physical book, Kindle book, etc. You could build multiple classes to handle each case, but can’t you abstract this? Yes, you can!

trait Book {
   val name: String
   def read(): String
}
class PDFBook(val name: String) extends Book {
    override def read(): String = "reading a PDF book."
}
class PhysicalBook(val name: String) extends Book {
    override def read(): String = "reading a physical book."
}
class KindleBook(val name: String) extends Book {
    override def read(): String = "reading a Kindle book."
}

You can then use it in a very flexible way. You can apply design patterns like Adapter or Decorator depending on the tasks you are to do. See below for basic usage on above design:

val pdfbook = new PDFBook("wonderful")
val kindlebook = new KindleBook("beautiful")
pdfbook.read() // prints reading a PDF book
kindlebook.read() // prints reading a Kindle book

//Also can gather and do operations on abstractions like this.
val books = ArrayBuffer.empty[Book]
books.append(pdfbook)
books.append(physicalbook)
books.foreach(book => println(book.name))
Share this post

Me

I am a passionate programmer working in Vancouver. I strongly believe in art of algorithms and together with it to write clean and efficient software to build awesome products. If you would like to connect with me, choose one from below options :) You can also send me an email at