Imagine a bacon-wrapped Ferrari. Still not better than our free technical reports.
See all our reports

Object-oriented design principles and the 5 ways of creating SOLID applications


Lately I’ve been hearing a lot about Functional Programming (FP), so I felt inspired to give some love to object-oriented programming (OOP) and object-oriented design (OOD) as well, since they are still very relevant when we design our systems.

With your permission, I’d like to start our journey with SOLID principles, which are class-level, object-oriented design concepts that, in conjunction with an extensive test suite, helps you avoid and combat code rot. Coined by every coder’s favorite uncle, Robert C. Martin (Uncle Bob), SOLID is actually a conglomerate of five other acronyms–SRP, OCP, LSP, ISP, DIP–that I’ll go into more deeply below; among other things, SOLID principles help you to keep the primary value of your software high.

Ew, this code smells rotten…


Code rot | kōd rät |
1. When an application becomes a festering mass of code that the developers find increasingly hard to maintain.
Gross. So how can we identify future code rot? These signs probably indicate code rot to come

  • Rigidity – small changes causes the entire system to rebuild.
  • Fragility – changes to one module causes other unrelated modules to misbehave. Imagine a car system in which changing the radio station affects windows.
  • Immobility – a module’s internal components cannot be extracted and reused in new environments. For example, if an application’s login module cannot be used in entirely different system then this module is immobile, caused by couplings and dependencies between different modules. The strategy is to decouple central abstractions from low-level details, like a particular database schema or UI implementation (web, desktop) or specific frameworks.
  • Viscosity – when building and testing are difficult to perform and take a long time to execute. When even a simple change is costly to make, and requires you to make changes in multiple places/levels.

Users expect to get some value out of the software they use. An application’s value is determined by whether it helps users do something better, increasing productivity or time or money and saving on “waste”. With high-value software, people often pay money in exchange for it.

But there is a secondary value that users get from great software, and I’d like to talk about that value because this is often the first thing people think of when talking of software values: behaviour.

If software does what its users need without bug crashes and delays, then the secondary value of the software is high. Secondary value is achieved when the software meets the current needs of the user. But user needs change, and frequently; the behaviour that the software provides and what the user needs can easily get out of sync, leading to lower value. Your software must be able to keep up with the changing needs of your customer in order to keep the secondary value high. So here we come to the primary value of software, it has to be capable to tolerate and facilitate the ongoing change.

Imagine that your software currently meets the needs of users, but is really hard and costly to change. Here, users get unhappier due to the app’s inflexibility, and profitability is likely to decrease.

And now imagine other software that has low secondary value at first, but is easy and inexpensive to change. Profitability can go only up and users get happier and happier.

So what are the SOLID principles?

Single Responsibility Principle


The Single Responsibility Principle (SRP) states that there should never be more than one reason for a class to change. This means that every class, or similar structure, in your code should have only one job to do.

Everything in the class should be related to that single purpose, i.e. be cohesive. It does not mean that your classes should only contain one method or property.

There can be a lot of members as long as they relate to the single responsibility. It may be that when the one reason to change occurs, multiple members of the class may need modification. It may also be that multiple classes will require updates.

How many responsibilities?

class Employee {
  public Pay calculatePay() {...}
  public void save() {...}
  public String describeEmployee() {...}

The correct answer is three. ;-)

Here we have pay 1) calculation logic with 2) database logic and 3) reporting logic all mixed up within one class. If you have multiple responsibilities combined into one class, it might be difficult to change one part without breaking others. Mixing responsibilities also makes the class harder to understand and harder to test, decreasing cohesion. The easiest way to fix this is to split the class into three different classes, with each having only one responsibility: database access, calculating pay and reporting, all separated.

Open-Closed Principle


The Open-Closed Principle (OCP) states that classes should be open for extension but closed for modification. “Open to extension” means that you should design your classes so that new functionality can be added as new requirements are generated. “Closed for modification” means that once you have developed a class you should never modify it, except to correct bugs.

These two parts of the principle appear to be contradictory. However, if you correctly structure your classes and their dependencies, you can add functionality without editing existing source code.

Generally you achieve this by referring to abstractions for dependencies, such as interfaces or abstract classes, rather than using concrete classes. Functionality can be added by creating new classes that implement the interfaces.

Applying OCP to your projects limits the need to change source code once it has been written, tested and debugged. This reduces the risk of introducing new bugs to existing code, leading to more robust software.

Another side effect of the use of interfaces for dependencies is reduced coupling and increased flexibility.

void checkOut(Receipt receipt) {
  Money total =;
  for (item : items) {
    total += item.getPrice();
  Payment p = acceptCash(total);

So how do we add credit card support? You *could* add an “if” statement like this, but then that would be violation of OCP.

Payment p;
if (credit)
  p = acceptCredit(total);
  p = acceptCash(total);

Here is a better solution:

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

void checkOut(Receipt receipt, PaymentMethod pm) {
  Money total =;
  for (item : items) {
    total += item.getPrice();
  Payment p = pm.acceptPayment(total);

And here’s a dirty little secret: OCP helps only if the changes that are going to come are predictable, so you should apply it only if a similar change has already happened. So, first do the simplest thing and then see what changes are requested so you can more accurately predict the future changes.

This means waiting for a customer to make a change and then invent the abstractions that will protect yourself from a similar change in the future.

Liskov Substitution Principle


The Liskov Substitution Principle (LSP) applies to inheritance hierarchies, specifying that you should design your classes so that client dependencies can be substituted with subclasses without the client knowing about the change.

All subclasses must, therefore, operate in the same manner as their base classes. The specific functionality of the subclass may be different but must conform to the expected behaviour of the base class. To be a true behavioural subtype, the subclass must not only implement the base class’s methods and properties, but also conform to its implied behaviour.

In general, if a subtype of the supertype does something that the client of the supertype does not expect, then this is in violation of LSP. Imagine a derived class throwing an exception that the superclass does not throw, or if a derived class has some unexpected side effects. Basically, derived classes should never do less than their base class.

A typical example that violates LSP is a Square class that derives from a Rectangle class. The Square class always assumes that the width is equal with the height. If a Square object is used in a context where a Rectangle is expected, unexpected behaviour may occur because the dimensions of a Square cannot (or rather should not) be modified independently.

This problem cannot be easily fixed: if we can modify the setter methods in the Square class so that they preserve the Square invariant (i.e., keep the dimensions equal), then these methods will weaken (violate) the post-conditions for the Rectangle setters, which state that dimensions can be modified independently.

public class Rectangle {
  private double height;
  private double width;

  public double area();

  public void setHeight(double height);
  public void setWidth(double width);

What you see above violates LSP.

public class Square extends Rectangle {  
  public void setHeight(double height) {

  public void setWidth(double width) {

Violations of LSP cause undefined behaviour. Undefined behaviour means that it works okay during development but blows up in production, or that you spend weeks debugging something that only occurs once per day, or that you have to go through hundreds of megabytes of logs to figure out what went wrong.

Interface Segregation Principle


The Interface Segregation Principle (ISP) states that clients should not be forced to depend upon interface members they do not use. When we have non-cohesive interfaces, the ISP guides us to create multiple, smaller, cohesive interfaces.

When you apply ISP, classes and their dependencies communicate using tightly-focused interfaces, minimizing dependencies on unused members and reducing coupling accordingly. Smaller interfaces are easier to implement, improving flexibility and the possibility of reuse. As fewer classes share these interfaces, the number of changes that are required in response to an interface modification is lowered, which increases robustness.

Basically, the lesson here is “Don’t depend on things you don’t need”. Here is an example:

Picture an ATM machine (aka Bankomat), which has a screen where we wish to display different messages. How would you solve the problem of displaying different messages? We apply SRP, OCP and LSP and come up with a solution–but still, this system would be hard to maintain. Why is that?

Imagine the ATM’s owner wants to add a message that appears only for withdrawal functionality, they want to display the message that says “This ATM will charge you some fee for withdrawals, do you agree?” How would you solve it?

Perhaps you would add a method to the Messenger interface and be done with it. But this causes you to recompile all the users of this interface and almost all the system needs to be redeployed, which is in direct violation of OCP. Let the code rot begin!

What happened here was that changing the withdrawal functionality caused changes to other totally unrelated functionalities as well, which is something, as we now know, we don’t want. How did this happen?

Actually, here is backwards dependency at play, where each functionality that uses this Messengers interface depends on methods it does not need but are needed by other functionalities. Here is what we want to avoid:

public interface Messenger {

Instead, split the Messenger interface up so that different ATM functionality depend on separate Messengers.

public interface LoginMessenger {

public interface WithdrawalMessenger {

publc class EnglishMessenger implements LoginMessenger, WithdrawalMessenger {

Dependency Inversion Principle


The Dependency Inversion Principle (DIP) states that high-level modules should not depend upon low-level modules; they should depend on abstractions. Secondly, abstractions should not depend upon details; details should depend upon abstractions. The idea is that we isolate our class behind a boundary formed by the abstractions it depends on. If all the details behind those abstractions change, then our class is still safe. This helps keep coupling low and makes our design easier to change. DIP also allows us to test things in isolation, details like database are plugins to our system.

Here is an example: A program depends on Reader and Writer interfaces that are abstractions, and Keyboard and Printer are details that depend on those abstractions by implementing those interfaces. Here CharCopier is oblivious to the low-level details of Reader and Writer implementations and thus you can pass in any Device that implements the Reader and Writer interface and CharCopier would still work correctly.

public interface Reader { char getchar(); }
public interface Writer { void putchar(char c)}

class CharCopier {

  void copy(Reader reader, Writer writer) {
    int c;
    while ((c = reader.getchar()) != EOF) {

public Keyboard implements Reader {...}
public Printer implements Writer {…}

Final points – use SOLID

I guess my main point here is that SOLID principles are valuable tools in your toolbox, and that you should keep them in back of your mind when designing your next feature or application. As Uncle Bob reminds us in his timeless post:


The Single Responsibility Principle

A class should have one, and only one, reason to change.


The Open Closed Principle

You should be able to extend a classes behavior, without modifying it.


The Liskov Substitution Principle

Derived classes must be substitutable for their base classes.


The Interface Segregation Principle

Make fine grained interfaces that are client specific.


The Dependency Inversion Principle

Depend on abstractions, not on concretions.

And, as always, be pragmatic. See you soon, when I hope to talk about coupling and cohesion, but in the meantime, please leave comments, share with your friends and reach out to me @ZeroTurnaround.

  • TG

    Great article and a good description (love the ‘motivational-style’ images) of SOLID.

    Well Done.

  • David Leppik

    The one thing I have a beef with is the example of LSP. In my mind, squares are a type of rectangle, so Square should subclass from Rectangle. However, neither squares nor rectangles in the “real” (Euclidean) world change their size or shape. That should be a clue that the abstraction is wrong, and that geometric shapes should be represented as immutable.

    Consider the following Java.

    interface Rectangle {
    double getWidth();
    double getHeight();
    Rectangle withHeight(double height);
    Rectangle withWidth(double width);

    interface Square extends Rectangle { // No new methods needed

    Whenever you call withHeight() or withWidth() you get a new object, which could be a Square if the width and height are equal.

  • Coder

    Wow nice explanation . Can you explain using python syntax ?

  • magooly

    nice post, perhaps in your OCP example

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

    should read

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

  • jrbirdman

    I want these posters.

    Excellent article, btw. Nicely done!