SOLID principles by example, step by step.



When we start talking about “good code”, quality or software design we always end up coming face to face with the so-called SOLID principles.

The purpose of this code is not to find out why these principles exist. I won’t try to explain the theory either. There are plenty of resources starting with the book Clean Code by Robert Martin. Recently, “Uncle Bob” updated the SOLID principles on his blog.

Although SOLID has become a commodity in our industry, these are concepts that are still difficult for many of us to grasp. Usually theory is not enough. Developers have that need to see code to understand.

The objective of this article is to start from a simple but coherent code and to make it evolve. Each step will be an opportunity to highlight a particular concept.

Does this code comply with the SOLID principles?

class Formatter
{
	public String format(String format, String text)
	{
		if (format == 'lower')
			return text.toLowerCase();
		if (format == 'html')
			return '<p>' + text + '</p>';
	}
}

Single Responsability Principle (SRP)

Formatter class has to format a raw text into a given format. My code has a triple responsability:

  • To transform the text into lower case.
  • Or to apply a HTML tag.
  • To decide which strategy to use.

Single Responsability Principle

Open for extension but Closed for modification Principle (OCP)

What happens if I need to format my text in JSON for example?

class Formatter
{
	public String format(String format, String text)
	{
		if (format == 'lower')
			return text.toLowerCase();
		if (format == 'html')
			return '<p>' + text + '</p>';
		if (format == 'json')
			return '{' + '"text":"' + text + '"' + '}';
	}
}

To extend the behavior of my class I had to modify it.

Open-Closed Principle

Liskov Substitution Principle (LSP)

Code does not depend on sub-type (yet).

Liskov Substitution Principle

Interface Segregation Principle (ISP)

Public API has only one method. This means that a user who might depend on it doesn’t end up with things he doesn’t need.

Interface Segregation Principle

Last but not least Dependency Inversion Principle (DIP)

Formatter class doesn’t have dependency (yet).

Dependency Inversion Principle

In summary

  • S ❌
  • O ❌
  • L ✅
  • I ✅
  • D ✅

Code remains very simple. I could very well leave it alone. SOLID is not an end in itself. However, if code tends to evolve, like we saw with JSON, it will be more and more difficult to maintain.

Let’s try to make our code SOLID compliant to see where it leads us.

Good fences make good neighbors

My first thought is to reduce my cognitive load: split the responsabilities.

class DelegatorFormatter
{
	private LowercaseFormatter lowercaseFormatter;
	private HtmlFormatter htmlFormatter;

	public DelegatorFormatter()
	{
		this.lowercaseFormatter = new LowercaseFormatter();
		this.htmlFormatter = new HtmlFormatter();
	}

	public String format(String format, String text)
	{
		if (format == 'text')
			return this.lowercaseFormatter.format(text);
		if (format == 'html')
			return this.htmlFormatter.format(text);
	}
}

class HtmlFormatter
{
	public String format(String text)
	{
		return '<p>' + text + '</p>';
	}
}

class LowercaseFormatter
{
	public String format(String text)
	{
		return text.toLowerCase();
	}
}

I have extracted the formatting logic into dedicated implementations. Now Formatter has only the responsibility to delegate the work to another contributor. That’s why I changed its name to DelegatorFormatter.

This code is now compliant with the single responsibility principle! 🙂

However, my class is still not extensible without changing its implementation.
Worse, I have “broken” the dependency inversion principle. DelegatorFormatter now depends on a level of detail that theoretically does not concern it. 😓

  • S ✅
  • O ❌
  • L ✅
  • I ✅
  • D ❌

DIP is fundamental for a modular architecture

I make sure that my class can receive, through injection, formatters without knowing their implementations.

class DelegatorFormatter
{
	private Map<String, Object> formatters;

	public DelegatorFormatter(Map<String, Object> formatters)
	{
		this.formatters = formatters;
	}

	public String format(String format, String text)
	{
		return this.formatters.get(format).format(text);
	}
}

My class no longer has any specific dependencies. It is isolated and is no longer aware of implementation details from a lower layer.

Better yet, the code has become OCP compliant too! 😀
It is now possible to extend the behavior of my class by injecting the necessary formatters into it.

But wait… What about the Liskov substitution now? 🤔
As a DelegatorFormatter nothing assures me that the formatters respect the expected behavior.

Let’s take an example:

class VendorXMLFormatter
{
	public String formatInXml(String text) {...} // The signature of the method is different.
}

If I don’t want my program to raise an arror at runtime I must decide according to whom I should delegate the work.

// DelegatorFormatter class
public String format(String format, String text)
{
	if (format == 'xml') {
		return this.formatters.get(format).formatInXml(text);
	}

	return this.formatters.get(format).format(text);
}

My code is no longer LSP compliant !

I also broke OCP ! 🤬

  • S ✅
  • O ❌
  • L ❌
  • I ✅
  • D ✅

It’s about trust

The idea of injecting the formatters is good. It is just a matter of making a contract between the parties.

interface Formatter
{
	public String format(String text);
}

class DelegatorFormatter
{
	private Map<String, Formatter> formatters;

	public DelegatorFormatter(Map<String, Formatter> formatters)
	{
		this.formatters = formatters;
	}

	public String format(String format, String text)
	{
		return this.formatters.get(format).format(text);
	}
}

By using Strategy pattern I no longer have to worry about how DelegatorFormatter should delegate work.

My code now conforms to Liskov’s substitution and back to the open-closed principle! 🥳

📣 There are lots of different ways to break Liskov substitution. Strong typing helps but it’s still possible to commit confusion.
Just keep in minds that this is about following a contract so consumers can act with confidence.

  • S ✅
  • O ✅
  • L ✅
  • I ✅
  • D ✅

A word about Interface Segregation

Until now, nothing pushed me to violate this principle. However, imagine for a moment that our application needs to validate a string according to the expected format.

interface Formatter
{
	public String format(String text);
	public boolean isValid(String text);
}

class LowercaseFormatter implements Formatter
{
	public String format(String text) {...}

	public boolean isValid(String text)
	{
		// Is the text in lower case?
	}
}

The problem with this solution is that for the DelegatorFormatter, the validation part is useless since it is not the goal. It doesn’t need it and yet each collaborator it inherits will have to implement a method that doesn’t make sense in this particular context.

  • S ✅
  • O ✅
  • L ✅
  • I ❌
  • D ✅

It could be better to separate formatting and validation and use one or the other of the interfaces, depending on the context! 👍

interface Formatter
{
	public String format(String text);
}

class LowercaseFormatter implements Formatter
{
	public String format(String text) {...}
}

interface Validator
{
	public boolean isValid(String text);
}

class LowercaseValidator implements Validator
{
	public boolean isValid(String text) {...}
}
  • S ✅
  • O ✅
  • L ✅
  • I ✅
  • D ✅

Conclusion

I always found the SOLID principles tricky to grasp as long as I focused only on the theory.

However, it is also a matter of automatism. With a bit of practice it is possible to produce solid code very naturally.

The implementation I proposed may seem too simplistic to be credible.
In reality there is no reason to write more complicated code than this.