SOLID Principles

In this first article of the blog I will talk about SOLID Principles. These are principles for software development consolidated by Bob Martin. Material about them you can find it everywhere, so I’ll keep mine short.

My initial idea was to talk about some patterns I use (or thought of) to overcome design issues. To show their value, it would be nice to relate them to the SOLID principles. That’s why this first article’s subject.

Motivation - or why not following principles is crazy

Before we go through each of them, let’s list why you should consider these principles and what can happen if you ignore them.

Why SOLID?

They are not silver bullets, of course. Together with other good practices your code will be:

  • understandable: people develop software. So people need to understand it.
  • maintainable: easier to modify and to correct bugs.
  • extensible: easier to add new functionality.

Following SOLID can be challenging even for experienced programmers. Even as a SOLID advocate, not always you can come up with the best solution that strictly follow all the principles.

I have participated of heated discussions about whether we should follow them or not.

Discussion Examples About SOLID Usage

It’s already done this way, and we’ll deliver like it is. We can fix it in the future.

Never fixed.

SOLID principles are OK, but we need to evaluate each case and define if it is relevant to use them or not.

This seemed to be an excuse for not following them ever.

It’s ridiculous those kids saying you need fancy code and not listening to senior people. We need to deliver, not refactor again and again to beautify code.

Writing maintainable code is not about being fancy. The more you deliver things without quality, the bigger is the cost to correct them.

In books, everything is possible. Real life is different, and we may come up with different solutions.

I agree. At the same time I think best practices should be tried before you try to reinvent the wheel.

You’re a just complicating stuff.

Being a developer is more than knowing where to add an if statement to conform to requirements. Just throwing code in classes to make it work is unprofessional.

Outcomes of Not Following SOLID

At the end, choosing not to follow SOLID wasn’t an informed decision. The principles were simply ignored and, each time, technical debt was piling up.

The symptoms I suppose everyone can notice:

  • huge classes;
  • difficult maintenance and
  • a hard time developing new features.

The issue behind these symptoms is code lacking SOLID. Let’s take a look on these principles and you’ll, hopefully, understand why.

SOLID Principles

As said before, they were consolidated by Bob Martin. He began to do so in late 80’s and the list stabilized in early 2000’s. The name SOLID was a suggestion of Michael Feathers, just modifying the order of the principles as Bob Martin was presenting them. They are:

  • Single Responsibility Principle (SRP)
  • Open/Closed Principle (OCP)
  • Liskov Substitution Principle (LSP)
  • Interface Segregation Principle (ISP)
  • Dependency Inversion Principle (DIP)

These are design principles that tell us how to arrange and interconnect groups of functions and data structures. Although they nicely fit Object-Oriented Programming, they can be used in any kind of programming paradigm.

Single Responsibility Principle (SRP)

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

You can consider the word module as a class, source file, software artifact etc.

What are the reasons to change? Users or stakeholders.

Consider you have a class to create a report with some business rules, and the code to access the database as in the diagram below:

Bad Example of SRP

  • What if the customer requests new information on the report to conform to legislation?
  • What if an infrastructure stakeholder decides to change the database to reduce costs?

Two reasons to change for the same module. Just splitting the classes may not be the best solution (considering the other principles). For the sake of this example, let just do this:

Good Example of SRP

This way, the request for new information made by the customer will only cause ReportGenerator to change. Changes caused by the infrastructure stakeholder will affect only ReportDb.

SRP doesn’t say so, but one of the best ways to conform to it is to create small classes, that do “one” thing. If you do just one thing, it’s less possible you’ll have more than one reason to change.

Separate GUI code from business rules. Separate business rules from persistence access. And so on. For each of these, think if it makes sense to hold functionality together or if you should separate in different modules.

As Bob Martin acknowledged the reasons to change are users and stakeholders. As they can form groups (which he calls actors), his “final version” of the SRP is:

A module should be responsible to one, and only one, actor.

Open-Closed Principle (OCP)

A software artifact should be open for extension but closed for modification.

Imagine the same example of before where the database will be replaced. Modifying the existing code for the database to access this new database will certainly lead you to either SRP violation, or a harder to maintain code (generally, both).

If the abstractions for database access were carefully crafted, you should use them to create new realizations for the new database. Then little to none code modification should occur outside the new realizations.

Let’s say we have a good interface for defining the database abstraction (ReportDb) and an implementation for MySQL (ReportMySql):

Open-Closed Principle Example, Part 1

Adding code for an PostgreSQL should be as easy as creating a new class:

Open-Closed Principle Example, Part 2

Consider the difference between this approach and rewriting parts of the existing class to allow the usage of the new database. In case one more database is to be supported, more code needs to be included in the same class for the peculiarities of each database. The code of one class will grow every once a new database is to be supported.

Liskov Substitution Principle (LSP)

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

The definition of Barbara Liskov for the LSP is too formal for the intent of this article. Let’s simplify it by saying:

A subtype cannot change the expected behavior of it’s supertype.

Think of a class that creates a list, wisely named ListCreator. Another class extends it and it’s called AnotherListCreator.

  public class ListCreator {
  
    public List createList() {
      new LinkedList();
    }
  
  }
  public class AnotherListCreator extends ListCreator {
  
    public List createList() {
      return Collections.unmodifiableList(new LinkedList());
    }
  }

You may notice that the interface of the subclass conforms to the superclass. However, the subclass creates an unmodifiableList when the superclass doesn’t. Anyone expecting the same behavior of the superclass will have a surprise in form of an exception when using the subclass.

LSP is not about conforming to the interface, which is already enforced by polymorphism. LSP is about expected behavior.

As another example, we can think on one of the Bad Smells in Code described by Martin Fowler in his book Refactoring: Refused Bequest. This is when a subclass only uses part of the parent’s behavior or, even worse, don’t stick to the parent’s interface.

Interface Segregation Principle (ISP)

Clients should not be forced to depend upon interfaces that they do not use.

Consider an audio system with multiple channels. Some of them have only gain controls (volume), others have an equalizer (bass, middle, treble). Both implement the interface AudioChannel:

  public interface AudioChannel {
  	
    void setGain(int gain);
    int getGain();
    void setEqualizer(AudioEqualizer equalizer);
    AudioEqualizer getEqualizer();
  
  }
  public class SimpleChannel {
  
    private int gain = 0;
    
    public void setGain(int gain) {
      this.gain = gain;
    }
    
    public int getGain() {
      return gain;
    }
    
    public void setEqualizer(AudioEqualizer equalizer) {
  
    }
    
    public AudioEqualizer getEqualizer() {
      return null;
    } 
  
  }
  public class EqualizedChannel extends SimpleChannel {
  
    private AudioEqualizer equalizer = null;
  
    @Override
    public void setEqualizer(AudioEqualizer equalizer) {
      this.equalizer = equalizer;	
    }
  
    @Override
    public AudioEqualizer getEqualizer() {
      return equalizer;
    }
  
  }

As we can see, SimpleChannel depends on stuff of AudioChannel interface that it does not use. Returning null or methods only throwing exceptions may be a clear ISP violation.

A better solution will be to separate the interfaces like the following:

  public interface AudioChannel {
  	
    void setGain(int gain);
    int getGain();
  
  }
  public interface EqualizedAudioChannel {
  	
    void setEqualizer(AudioEqualizer equalizer);
    AudioEqualizer getEqualizer();
  
  }
  public class SimpleChannel {
  
    private int gain = 0;
    
    public void setGain(int gain) {
      this.gain = gain;
    }
    
    public int getGain() {
      return gain;
    }
  
  }
  public class EqualizedChannel
          extends SimpleChannel
          implements EqualizedAudioChannel {
  
    private AudioEqualizer equalizer = null;
    
    public void setEqualizer(AudioEqualizer equalizer) {
      this.equalizer = equalizer;	
    }
    
    public AudioEqualizer getEqualizer() {
      return equalizer;
    }
  
  }

Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions.

Make the non-important things depend on the important ones by creating an abstraction between them.

In the past, a database would be a core part of the system. The business rules would depend on them to retrieve information. However, the database is just a detail. It doesn’t matter what is the concrete class implementing database access. The business rules should depend on an abstraction of persistence. The realizations of database access will also depend on these abstractions of persistence. Done: you inverted dependencies.

Consider always:

  • use of abstractions (abstract classes, interfaces etc.)
  • important parts (business rules, data objects) should not depend on details (implementation of database, GUI etc.)

Remember the example on the OCP section:

Example from Open-Closed Principle Section

The ReportGenerator class uses the abstraction of ReportDb. It does not depend on ReportDbMySql or ReportDbPostgreSQL. These classes may be instantiated by a Factory or another pattern like that, but it should make no difference to ReportGenerator using one concrete class or the other.

Dependency Inversion Principle is also key to create testable code. If your tests depend on a real database instance, you may have issues if the test data is modified. With dependency inversion you can create test doubles to get rid of database when testing: you create new realizations of the persistence Abstraction and inject them in your test code.

By the way, this is my favorite principle. Understanding it for sure made me a better programmer.

Conclusion

Not following SOLID principles (having a good reason to do so or not) will probably lead to technical debts. If you do not pay your technical debts, your software can turn into a mess. After a while, you may decide rewriting everything from scratch and not following SOLID again. As you have more experience, as a programmer and about the business, you may create a better design. It can take longer, but it will turn into a mess once more.

It is this way because even the most brilliant design can turn into chaos after more and more functionality is added. Having a clear vision on how following SOLID principles is good for code health (also business health, your own mental health etc.) is key to work professionally. When I say professionally it is not only getting paid to develop software, but making this money worth to whoever pays you.

To understand those principles, it requires study, time and, effort. In this sense, knowledge is far superior to experience. Experience mostly relates to the time you’ve been doing something - it may happen you’ve been doing the same stuff again and again and not evolving. One thing I’m sure is I’ll never learn enough. And I feel uncomfortable when looking into code I wrote some time ago and not finding deficiencies. Not because I didn’t do the best I could at the moment, but because I didn’t evolve since then.

If you weren’t familiar with SOLID principles, I hope this article was a good introduction. If you already knew them, I hope you could find useful information or refresh some concepts.

Changelog

  • 2021-01-09: Minor fixes. Adding categories and tags.