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

Mariana Azevedo
Mar 21 · 8 min read

Creating a quality code throughout the development phase is undoubtedly the mission of any developer who really cares about your software product. The use of best practices tends to reduce code complexity, the coupling between classes, separating responsibilities and well defining the relations between them. These are simple ways to improve code internal quality.

In the first two texts of this series, we list some best practices tips and tools we can use to help us evaluate and evolve software quality. However, we don’t go deep into the theories of a SOLID code.

And what are these basic 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?

S.O.L.I.D is an acronym that represents five principles of object-oriented programming and code design 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 to follow.

1. Single Responsibility Principle (SRP)

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

This first principle says that , that is, it must have a single responsibility. Basically, 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 important relationship. That way, the more clearly defined your class does, the more cohesive it is.

See the example of the class. It contains two methods: , which calculates the employee’s salary, with tax discount; and , 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 , an , we will have to keep replicating the code snippet. Or even, if we want to calculate the salary of other employees from other positions, who have different tax discounts, we would be required to create different calculation methods within the same class. All these solutions, of course, aren’t viable!!!

And how to apply minimally the SRP in this scenario? We can break these responsibilities of the class into more classes: (to manage the database connections), (to implement all employee-specific persistence methods), the enum (to save calculation rules per position), (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 class behavior, so that every employee has a and in the method, the calculation rule, which is encapsulated in the position, can be accessed without any damage.

Employee class refactored example

With this refactoring, the class has a single responsibility, which is to know the business rules of an employee. If a class has only one responsibility, they tend to be more reusable, simpler, and propagate fewer changes to the system. Commonly, we use FACADE and DAO patterns to avoid architecture with strong coupling and low cohesion.

2. Open/Closed Principle (OCP)

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

It says that . In detail, it says that we can extend the behavior of a class, when necessary, through inheritance, interface, and composition, but we cannot allow the opening of this class to make minor modifications.

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

PriceCalculator class example

This class has a method which checks the discount and freight rules from the product payment method. If the product is purchased by cash or single credit card payment, you have a specific discount in the class. If the product is purchased on credit card with installments, it has a specific discount on the class. In addition, the discount rules also vary according to the products price. And in freight, the value also varies by geographical region.

PriceTableSimplePayment class example
PriceTablePaymentInInstallments class example
Freight class example

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

So, how to use OCP to solve this problem? Simple! Let’s create the interface to represent the abstraction , regardless of the product payment method. Also, let’s create the abstraction in the interface. After these changes, the and classes will begin to implement the interface and the class starts to implement the interface.

PriceTable interface example
FreightService interface example
PriceTablePaymentInInstallments class refactored example
PriceTableSimplePayment class refactored example
Freight class refactored example

With this, we were able to wipe the class, no longer needing to know the behavior of the various tables. That is, we will CLOSE the , and classes for changes and if other rules arise to be used in the , 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 good abstractions. In addition, when using the STRATEGY pattern, we are obeying the OCP.

3. Liskov Substitution Principle (LSP)

Derived classes must be substitutable for their base classes.

It says pondering the care to use inheritance in your software project. Despite inheritance is a powerful mechanism, it must be used contextualized and moderating, avoiding cases of 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 before choosing to inherit, we need to think about the preconditions and postconditions of her class.

To be clearer, let’s observe the example of the and classes. represents any bank account within our simplified context. It has the methods , , and .

CommonBankAccount class example

A is identical to the class, except for the method. A payroll account has no income, it is only for receiving a salary.

PayrollAccount class example

That way, we can solve this problem by extending the class, as shown above, and making the method throw 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 method of all bank accounts in a loop, for example, and one of them is a , our application doesn’t work, because for any payroll account an exception is thrown.

Bank class example

In this scenario, we should refactor and use composition. Let’s create an classand this class will control the accounts financial movements. The and classes now have an , removing the unnecessary parent-child relationship between them.

AccountManager class example
CommonBankAccount class refactored example
PayrollAccount class refactored example

Notice that in the refactored version of the class, we don’t need to implement the method. We only use the manager in the behaviors that the class has. 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.

4. Interface Segregation Principle (ISP)

Make fine grained interfaces that are client specific.

It says . This principle deals with cohesion in interfaces, the construction of lean modules, that is, with few behaviors. Interfaces that have many behaviors are difficult to maintain and evolve. So, should be avoided.

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

Example of Employee as an abstract class

And in our company, we have two positions, which will extend this class : the and the .

Seller class example
Developer class example

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

So, how to solve this problem? Refactoring the code to break these behaviors into two interfaces: and . Thus, the starts to implement the interface, so that the class doesn’t even need to exist since the is an with a payment regime. In the same way, the class starts to implement the interface, with method, which is specific to this type of .

Conventional interface example
Commissionable interface example
Employee class refactored example
Seller class refactored example

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

5. Dependency Inversion Principle (DIP)

Depend on abstractions, not on concretions.

It says that we must Uncle Bob breaks the definition of this principle into two sub-items:

And this is because abstractions change 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 speak exclusively of it. When we do the refactoring of the class, instead of having direct dependence of and concrete classes to calculate the discount of the product and the freight, we inverted the relationship to depend on two interfaces: and . 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 object-oriented paradigm correctly, avoiding problems such as: lack of code standardization, duplication (remember “), to isolate features that may be common to various code snippets and maintenance (very fragile code, where a modification can break an already tested functionality).

And if we can follow all these tips, we will have an easy code to maintain, test, reuse, extend and evolve, without a doubt. An important tip, besides reading the main bibliographies on the subject, such as the books is to start training on personal, small and simpler projects. You can start by making changes at specific classes, avoiding code smells. So you begin to train your brain to think more maturely 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. (Robert C. Martin, 2011)
  2. (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

Written by

Full-stack developer, Master in Computer Science - with emphasis on software engineering, and agile development enthusiast

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade