Skip to content

SOLID

Introduction

SOLID is a set of software design principles that every good programmer should know and follow when creating code.

SOLID is an acronym and each letter stands for:

  • Single Responsibility Principle
  • Open Closed Principle
  • Liskov's Substitution Principle
  • Interface Segregation Principle
  • Dependency Inversion Principle

Single Responsibility Principle

The first rule is that each class should have one and only one responsibility in the system. For example, if a class describes a database model, then it should not additionally define methods that will allow you to retrieve data from the database. This limitation of class responsibilities makes the code more flexible. Changing a class that does "little" is relatively straightforward and reduces the need for changes in other parts of the system. A change to a class or method, that has a few hundred lines of code, requires more tests, more changes, and more time to understand the code.

Example

Without applying the Single Responsibility principle:

public class Book {
    private Long bookId;
    private String title;
    private String author;
    private Long pageNumber;

    public List<Book> searchBookByTitle(String title);
    public List<Book> searchBookByAuthor(String author);
}

Apart from describing the book model, the above class also tries to give the search functionality. For this purpose, it is better to create a separate class, for example:

public class Book {
    private Long bookId;
    private String title;
    private String author;
    private Long pageNumber;
}
public class SearchEngine {
    public List<Book> searchBookByTitle(String title) { /* implementation */ }
    public List<Book> searchBookByAuthor(String author) { /* implementation */ }
}

The limitation of responsibility does not mean that a class or interface should only define one method. The important thing is not to confuse class responsibilities. For example, a search engine may have separate methods that search for records by a specific key, but we should separately implement a filtering or sorting class.

Open - Closed Principle

Classes should be open for extension but closed for modification. This means that when creating the code, we should write it in such a way that no changes are required to classes not related to the added functionality.

An example of use can be the example for the Strategy pattern. Imagine you want to extend the functionality of your program so that the end-user in the input String can replace spaces with the - character. To do this, just add another implementation of the SpacesModificationStrategy interface. Program structure, i.e. the implementation of the StrategyUsage class does not require any changes.

Liskov's Substitution Principle

The Liskov substitution principle was first formulated by Barbara Liskov and says that by using a base type, you can substitute it by any type of a derived class (i.e., inheriting from it) and keep the functionality working. This principle was used during the implementation of the Observer pattern, where in the ObserverUsage class we assign instances of the classes that extend BaseObserver to the reference of the base class BaseObserver.

Interface Segregation Principle

The interface-segregation principle is about not creating interfaces that have too many methods, in the sense that their implementations only overwrite some of these methods (and undefined are never used). To solve such a problem (if we have already encountered one), it is better to divide the interface into several smaller ones, in such a way that each of them performs a specific function. Thanks to this, the dependency tree between objects in the application will be simplified.

Dependency Inversion Principle

The dependency inversion principle is one of the most important rules when creating code. According to this principle, a given class, that wants to use certain dependent objects, should receive references to these objects "from the outside" and should not be responsible for their creation. With this approach, the class can run on an abstraction (i.e., abstract class, base class, or interface) instead of a specific implementation (i.e., additionally making use of Liskov's Substitution Principle).

This principle has been used in many examples, e.g .:

  • in the description of Chain of responsibility, in the class ChainAuthenticationElement, which in the constructor takes the dependencies AuthenticationHandler and ChainAuthenticationElement (instead of creating their implementations directly)
  • in this example class UserService, uses abstractions coming from outside (UserRepository and UserValidator).