SOLID Design Principles For Object Oriented Software Development


Writing good software is not just about having it working with all tests passing. This is not enough. We also need to ensure that what we write can be easily changed.

Many systems fail because they can’t be changed. I know of a company that acquired a system to complement its offering. Although the system worked fine, it was so badly written that they had to do a complete rebuild that took years so we could add new features.

In year two thousand, Robert C. Martin identified the solution to this problem in what are now known as the SOLID Design Principles. When followed, these principles help us write object-oriented code that can be maintained and extended over time.

In the next section, I will explain each principle in practical terms. Finally, I’ll discuss why we’re not using them enough, and what we can do to change that.

The SOLID Principles

The five principles that form the acronym are:

1. Single Responsibility Principle (SRP)

2. Open-Closed Principle (OCP)

3. Liskov Substitution Principle (LSP)

4. Interface Segregation Principle (ISP)

5. Dependency Inversion Principle (DIP)

Below I provide a brief practical description of each principle, along with code examples when I have found them useful.

For an in-depth explanation of the SOLID principles, I suggest reading the book Agile Software Development, Principles, Patterns, and Practices by Robert C. Martin.

Single Responsibility Principle (SRP)

A class or module should change for one business reason only.

For example, if you have a class that can either display or email a report, then it has two business reasons to change: the display format, and the email display or attachment format.

It is tempting to keep everything in the same class, as some logic may be shared across both features, and the code may not change often. If you decide to do this, I’d advise that you split the code the first time that you need to change it, as the cost of solving this increases as you add more code.

Open-Closed Principle (OCP)

This principle says that to add new features we should just add new code, without changing existing code.

For example, instead of changing a class to add a new feature, create a new class that extends the original class or implement the original interface, and use the new class where the new feature is needed.

When we change code we can introduce bugs, and even automated tests can’t fully protect us against this. If instead we just add new code, we limit the parts of the code that could be impacted by a potential bug.

Liskov Substitution Principle (LSP)

The idea behind the Liskov Substitution Principle is to safely support polymorphism. Imagine a base class, and some derived classes: the principle is followed if the instances of those classes can be used interchangeably, without changing the base functionality.

// Don't do this
class Bird {
  fly() {
    console.log("Flying");
  }
}

class Penguin extends Bird {
  fly() {
    throw new Error("Penguins can't fly");
  }
}

In this example, an instance of the Penguin class cannot replace an instance of the Bird class. If it did, the program would raise an error when calling the fly method. This breaks the expected behaviour, as we would no longer be able to use Penguin in the same way as Bird in places where polymorphism is used.

The goal of this principle is to write code that doesn’t require handling such exceptions, as each interchangeable class should maintain the same behaviour.

Interface Segregation Principle (ISP)

To adhere to the Interface Segregation principle, a class must only depend on interfaces it needs.

// Don't do this
interface Animal { fly(): void; swim(): void; }

class Eagle implements Animal {  
  fly() { }  
  swim() { throw new Error("Can't swim"); }  
}

// Do this instead
interface Flyer { fly(): void; }
interface Swimmer { swim(): void; }

class Eagle implements Flyer { fly() { } }
class Duck implements Flyer,Swimmer { fly(){} swim(){} }

The aim of this principle is to reduce unnecessary dependencies by having clean interfaces. When in violation of this principle, classes depend on things they don’t need. This makes the code rigid and difficult to change.

Dependency Inversion Principle (DIP)

In practice, the Dependency Inversion Principle suggests that we should not instantiate an object inside a class, but pass a previously created instance as argument to the class (this is called dependency injection).

# Don't do this
class House:
  def __init__(self):
    self.window = Window()

House()

# Do this instead
class House:
  def __init__(self, window):
    self.window = window

window = Window()
House(window)

What the principle actually says is that classes should depend on abstractions (ideally an interface), rather than a concrete class.

This approach allows us to change the behaviour of the class (change the window) without having to change the class itself. Similarly, writing tests becomes extremely easy! Instead of mocking classes, we can write our own WindowTest class and pass it as an argument.

Instances of classes must be created somewhere. Usually this will be done at the top level of our program. Ideally, we can have some configuration that defines what classes to instantiate.

Using The SOLID Principles

Although the solution has been known for a few decades already, we still observe that many developers ignore these principles, which makes many systems difficult to maintain.

Unfortunately, some developers have never heard of these principles, so it makes sense that they don’t follow them. However, many developers who are aware of the SOLID design principles still choose to ignore them. From what I’ve observed over the years, the main reason is that they simply don’t see it as a real problem. In a sense, it’s like smoking: having one cigarette doesn’t seem like a big deal, but the damage adds up.

I believe that the best way to solve this is to spread awareness on these principles. If you have read this far, talk about the topic with a teammate. Or give a demo to your team on how to apply a principle in one of your tasks. Every action we take in this direction, big or small, will help us get software that is easy to maintain.

Happy coding!
José Miguel

Share if you find this content useful.
Follow me on LinkedIn to be notified of new articles.


Leave a Reply

Your email address will not be published. Required fields are marked *