S.O.L.I.D principles: what are they and why projects should use them

Mariana Azevedo
8 min readMar 21, 2019

--

Creating quality code throughout the development phase is undoubtedly the mission of any developer who cares about your software product. Best practices tend to reduce code complexity, the coupling between classes, separating responsibilities, and defining their relations. These are simple ways to improve code internal quality.

This series’s first two articles list some best practices tips and tools to evaluate and evolve software quality. However, we don’t go deep into the theories of a SOLID code.

And what are these fundamental principles that help us to keep the code organized without code-smells and ̶s̶h̶a̶r̶e̶a̶b̶l̶e̶ ̶o̶n̶ ̶G̶i̶t̶h̶u̶b̶/̶B̶i̶t̶b̶u̶c̶k̶e̶t̶ ̶w̶i̶t̶h̶o̶u̶t̶ ̶f̶e̶a̶r̶ ̶o̶f̶ ̶b̶e̶i̶n̶g̶ ̶j̶u̶d̶g̶e̶d̶ clean? They are the S.O.L.I.D principles, and we’ll talk about them in the next sections.

What is S.O.L.I.D?

Concepts and principles…

S.O.L.I.D is an acronym that represents five principles of object-oriented programming and code design. It was theorized by our beloved Uncle Bob (Robert C. Martin) by the year 2000. The author Michael Feathers was responsible for creating the acronym:

[S]ingle Responsibility Principle
[O]pen/Closed Principle
[L]iskov Substitution Principle
[I]nterface Segregation Principle
[D]ependency Inversion Principle

The five pillars of solid code

We’ll talk in detail about these five principles in the sections below.

Single Responsibility Principle (SRP)

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

This first principle says that “a class must have only one reason to change,” that is, it must have a single responsibility. This principle deals specifically with cohesion. Cohesion is defined as the functional affinity of a module’s elements. It refers to the relationship that the members of this module have if they have a more direct and essential relationship. That way, the more clearly defined your class does, the more cohesive it is.

See the example of the Employee class. It contains two methods: calculatesSalary(), which calculates the employee’s salary, with a tax discount, and save() which is the persistence control method, to open a connection to the database and saves an employee in the database.

Example of class Employee that injuring SRP

Notice that this class does many things that aren’t necessarily tasks of it. For example, in evolving this system, if we want to persist another entity in the “Company” model, such as a Product, an Order, we will have to keep replicating the save() code snippet.

Suppose we want to calculate the salary of other employees from other positions, with different tax discounts. In that case, we should create different calculation methods. All these solutions, of course, aren’t feasible.

However, how to apply the SRP minimally in this scenario? We can break these responsibilities of the Employee class into more classes: ConnectionDAO (to manage the database connections), EmployeeDAO (to implement all employee-specific persistence methods), the Position enum (to save calculation rules per position), CalculationRule (which is an interface that has the calculation method), and for each tax discount scenario, we can define a class to save this information.

ConnectionDAO class example
EmployeeDAO class example
CalculationRule interface example
Position enum example, who knows the calculation rules
Example of a class that implements the 22.5% rule
Example of a class that implements the 11% rule

Lastly, we can change the Employee class behavior so that every employee has a Position. In the calculatesSalary() method, the calculation rule, which is encapsulated in the position, can be accessed without any damage.

Employee class refactored example

With this refactoring, the Employee class has a single responsibility. When a class has only one responsibility, they tend to be more reusable, simpler, and propagate fewer system changes. Commonly, we use FACADE and DAO patterns to avoid architecture with strong coupling and low cohesion.

Open/Closed Principle (OCP)

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

It says that “software entities (classes, modules, functions, etc.) must be opened for an extension, but closed for modification.” In detail, it says that we can extend the behavior of a class, when necessary, through inheritance, interface, and composition. Still, we cannot allow the opening of this class to make minor modifications.

To illustrate this principle’s understanding, let’s observe the PriceCalculator class, which belongs to a fictitious e-commerce system.

PriceCalculator class example

This class has a calculates() method which checks the discount and freight rules from the product payment method. Suppose the product is purchased by cash or single credit card payment. In that case, you have a specific discount in the PriceTableSimplePayment class.

PriceTableSimplePayment class example

If the product is purchased on a credit card with installments, it has a particular discount on the PriceTablePaymentInInstallments class. Besides, the discount rules also vary according to the product’s price. And in freight, the value also varies by geographical region.

PriceTablePaymentInInstallments class example
Freight class example

The problem with this implementation is the complexity. The more rules you create, the more maintenance of this class will be unfeasible. Also, the coupling of the PriceCalculator the class will increase because it will increasingly depend on more classes.

Now, how to use OCP to solve this problem? Simple! Let’s create the PriceTable interface to represent the abstraction discountCalculation(), regardless of the product payment method. Also, let’s create the freightCalculation() abstraction in the FreightService interface.

PriceTable interface example
FreightService interface example

After these changes, the PriceTableSimplePayment and PriceTablePaymentInInstallments classes will begin to implement the PriceTable interface, and the Freight class starts to implement the FreightService interface.

PriceTablePaymentInInstallments class refactored example
PriceTableSimplePayment class refactored example
Freight class refactored example

With this, we could wipe the PriceTable class, no longer need to know the various tables’ behavior. That is, we will CLOSE the PriceCalculator, Freight, and PriceTable classes for changes. In this case, if other rules arise to be used in the PriceCalculator, we are implementing new classes to represent them and receiving them by the constructor.

PriceCalculator class refactored example

In summary, this principle talks about maintaining useful abstractions. Besides, when using the STRATEGY pattern, we are obeying the OCP.

Liskov Substitution Principle (LSP)

Derived classes must be substitutable for their base classes.

It says, “subtypes should be replaceable by their base types,” pondering the care to use inheritance in your software project. Despite inheritance is a powerful mechanism, it must be used contextualized and moderating, avoiding classes being extended only by having something in common.

This principle was described by the researcher Barbara Liskov in her 1988 paper, which explains that we need to think about class’s preconditions and postconditions before choosing to inherit.

To be clearer, let’s observe the example of the CommonBankAccount and CheckingAccount classes. CommonBankAccount represents any bank account within our simplified context. It has the methods deposit(), getBalance(), cashOut(), and income().

CommonBankAccount class example

A CheckingAccount is identical to the CommonBankAccount class, except for the income() method. A checking account has no income; it is only for receiving a salary.

CheckingAccount class example

That way, we can solve this problem by extending the CommonBankAccount class, as shown above, and making the income() method throws an exception, right?

As expressed by our dear boss Michael Scott, this is not a good idea! If we were to try to access the income() method of all bank accounts in a loop, for example, and one of them is a CheckingAccount. Our application doesn’t work because, for any checking account, an exception is thrown.

Bank class example

In this scenario, we should refactor and use composition. Let’s create an AccountManager class, and this class will control the account’s financial movements.

AccountManager class example

The CommonBankAccount and CheckingAccount classes now have an AccountManager, removing the unnecessary parent-child relationship between them.

CommonBankAccount class refactored example
CheckingAccount class refactored example

Notice that in the refactored version of the CheckingAccount class, we don’t need to implement the income() method. We only use the manager in the behaviors that the class has. Uncle Bob explains that LSP is the enabling principle of the Open/Closed Principle since the possibility of substituting subtypes allows a module to be extensible without modifications.

Interface Segregation Principle (ISP)

Make fine grained interfaces that are client specific.

It says, “many specific interfaces are better than a general interface.” This principle deals with cohesion in interfaces, the construction of lean modules, and few behaviors. Interfaces that have many behaviors are challenging to maintain and evolve. So, it should be avoided.

For a better understanding, let’s go back to the Employee example and turn this class into an abstract class, with two other abstract methods: getSalary() and getCommission().

Example of Employee as an abstract class

Then, in our example, we have two positions, which will extend this class Employee: the Seller and the Developer.

Seller class example
Developer class example

However, the Employee class has a behavior that doesn’t make sense for the Developer position: getCommission(). The developer’s salary is calculated based on hours worked and contracted, having no relation to total sales in a period.

Hence, how to solve this problem? We are going to refactoring the code to break these behaviors into two interfaces: Commissionable and Conventional.

Conventional interface example
Commissionable interface example

Thus, the Employee starts to implement the Conventional interface.

Employee class refactored example

The Developer class doesn’t even need to exist since the Developer is an Employee with a Conventional payment regime. In the same way, the Seller class starts to implement the Commissionable interface, with getCommission() method, which is specific to this type of Employee.

Seller class refactored example

The ISP alerts us to the “fat” classes, which cause unusual and damaging couplings to business rules. We have to be careful, and this tip is good for the other principles: do not overdo it! Thoroughly check for cases where segregation is necessary, preventing hundreds of different interfaces from being created improperly.

Dependency Inversion Principle (DIP)

Depend on abstractions, not on concretions.

It says that we must “depend on abstractions and not on concrete classes.” Uncle Bob breaks the definition of this principle into two sub-items:

  • “High-level modules should not depend on low-level modules. Both should depend on abstractions.”
  • “Abstractions should not depend on details. Details should depend on abstractions.”

The option of abstractions is because they vary less and make it easier to change behaviors and future code evolutions.

When we speak of OCP, we also saw an example of DIP but don’t talk exclusively. When we do the PriceCalculator class’s refactoring, instead of having direct dependence of PriceTableSimplePayment and PriceTablePaymentInInstallments, we inverted the relationship on two interfaces: PriceTable and Freight. Thus, we continue to evolve and maintain only concrete classes.

The idea of applying the principles in software projects is to take advantage of the benefits of using the object-oriented paradigm correctly. Furthermore, we can avoid problems such as lack of code standardization, duplication (remember “Do not repeat yourself”?), and lack of maintenance.

You can follow all these tips to have an easy code to maintain, test, reuse, extend, and evolve, without a doubt. A piece of important information: besides reading the main bibliographies on the subject, such as the books Agile Principles, Patterns, and Practices in C# and Principles of Ood, you should start practicing on personal, small, and more straightforward projects.

Besides, you can start by making changes in specific classes, avoiding code smells. The faster you begin to practice, and the more mature your way of thinking will be when faced with more complex development situations.

Full examples that have been used in the article can be found in the artigo-solid-medium project on Github.

I hope you enjoyed the post!

References

  1. Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin, 2011)
  2. Agile Principles, Patterns, and Practices in C# (Robert C. Martin; Micah Martin, 2011)
  3. Alura — Cursos Online de Tecnologia — Design Patterns Java I: Boas práticas de programação
  4. Alura — Cursos Online de Tecnologia — Design Patterns Java II: Boas práticas de programação
  5. Alura — Cursos Online de Tecnologia — SOLID com Java: Orientação a Objetos com Java

--

--

Mariana Azevedo
Mariana Azevedo

Written by Mariana Azevedo

Senior Software Developer/Tech Lead, master in Computer Science/Software Engineering, Java, open source, and software quality enthusiast.