SOLID: 5 Principles of Object Oriented Design
Alex Sikorski

Alex Sikorski

Sep 02, 2022

SOLID: 5 Principles of Object Oriented Design

#swe

#java

SOLID stands for:

  1. Single Responsibility
  2. Open for Extension/Closed for Modification Principle
  3. Liskov's Substitution Principle
  4. Interface Segregation Principle
  5. Dependency Inversion Principle

Single Responsibility

This principle is self-explanatory. One class should have one and only one responsibility. In practice, the benefits are; less test cases per class, lower coupling and therefore less functionality in one class which leads to fewer dependencies and better organization, easier to navigate behaviours in a specified class.

For example, if we have a Book class with the state name, author, content and with the method (behaviour) printBook. This class violates this principle, as a Book should not have to print itself. In this case, a new class would be introduced, called Printer. This class would then be used to print the Book's contents.

class Book
{
    String name;
    String author;
    String content;
    
    Book(String name, String author, String content){
        this.name = name;
        this.author = author;
        this.content = content;
    }
    
    private void printBook(){
        // violation! introduce new Printer class with printing responsibility.
    }
}

Open for Extension/Closed for Modification

This principle states that software components should be open for extension, but closed for modification. As a developer, if you wanted to edit behaviour, you would not change the existing code but extend a class and then override a method. This simply prevents the introduction of code that can lead to new bugs. ⁠ ⁠For example, if we have a Guitar class with the state make, model, and we wanted to give it some awesome flame decals, we would not edit the existing Guitar class, but we would extend Guitar with a new class GuitarWithFlames and add the state flameColour.

class Guitar
{
    String make = "Fender";
    String model = "Stratocaster";
}

class GuitarWithFlames extends Guitar
{
    String flameColour = "Red";
}

Liskov's Substitution

This principle states that any derived class should be able to substitute its parent class. So, the behaviour must be consistent in the parent and child class.

For example, let's say we have a class Bird with the method fly(). A Pigeon can extend this class since it can fly, however if an Ostrich extended it, it would fail the Liskov test since the Ostrich cannot fly.

A fix for this would require a new class called FlyingBirds that extends Birds. The result would look like:

public class Bird{}

public class FlyingBirds extends Bird{
    public void fly(){}
}

public class Duck extends FlyingBirds{}

public class Ostrich extends Bird{} 

Interface Segregation

This principle states that larger interfaces should be split up into smaller, specific ones. This effectively ensures that classes aren't forced to depend on methods that will not be used.

For example, let's say a BearKeeper interface has the following behaviours washBear(), feedBear(), petBear(). This interface can be split into a BearCleaner, BearFeeder and a BearPetter. In this example, only a CrazyPerson would implement the BearPetter whilst a BearCarer would implement BearCleaner and BearFeeder.

// from
interface BearKeeper
{
    void washBear();
    void feedBear();
    void petBear();
}

// to
interface BearCleaner
{
    void washBear();
}
interface BearFeeder
{
    void feedBear();
}
interface BearPetter
{
   void petBear(); 
}
// the implementation
class CrazyPerson implements BearPetter
{
    @Override
    void petBear(){
        // because I am crazy
    }
}
class BearKeeper implements BearCleaner, BearFeeder
{
    @Override
    void washBear(){}

    @Override
    void feedBear(){}
}

Dependency Inversion Principle

This principle states: depend on abstractions, not on concretions. High level modules should not depend on low level modules. Both should depend on abstractions.

For example, let's say we have a RemoteControl that has a Television instance and the RemoteControl itself can click(). The Television can turnOn() or turnOff(). The click() method has the logic for turning the Television on or off.

This works fine, but there is room for improvement. With abstraction, we can make sure that the RemoteControl can control anything with a turnOn() or turnOff() button (e.g. a light, a computer or speaker). An interface can be created, called OnOffDevice that contains methods turnOn() and turnOff(). Now, our RemoteControl example will depend on the new OnOffDevice abstraction as opposed to the concrete Television class. The Television class now also depends on the OnOffDevice with its method implementations for turning on and off.

// from
class RemoteControl
{
    Television tv; // instance
    
    void click(){
        tv.turnOn();
        tv.turnOff();
    }
}
// to
class RemoteControl
{
    OnOffDevice device; // instance
    
    void click(){
        device.turnOn();
        device.turnOff();
    }
}
// where
class Television implements OnOffDevice
{
    @Override
    turnOn(){
        // turn on
    }
    
    @Override
    turnOff(){
        // turn off
    }
}

Effectively, this frees high-level components from being dependent on details of low-level components, and it helps design software that is reusable and resilient to changes.

Alex Sikorski

Alex Sikorski

Currently working as a full stack Software Engineer and curiously seeking new knowledge in free time.

Leave a comment

Categories