The article discusses the application of the Liskov Substitution Principle (LSP) within a Spring Boot application, emphasizing the importance of proper inheritance to maintain consistent behavior in subtypes.
Abstract
This article is the third part of a series exploring SOLID software design principles in the context of a Spring Boot application. It focuses on the Liskov Substitution Principle (LSP), which dictates that subtypes should be interchangeable with their parent types without altering the correctness of the program. The author illustrates the principle with the classic "square/rectangle" problem and provides guidelines for maintaining LSP compliance, such as keeping superclasses abstract and ensuring that child classes are mutually substitutable. The article also includes a practical example by refactoring a cash flow management application to adhere to LSP, demonstrating the separation of concerns by reorganizing classes and interfaces into distinct packages and refining the inheritance structure to prevent LSP violations. The refactoring process aims to ensure that the application's architecture is robust, maintainable, and aligned with the principles of object-oriented design.
Opinions
The author emphasizes the importance of abstract superclasses to facilitate extendable and flexible software design.
Inheritance should be approached with caution to avoid creating messy hierarchies that violate LSP.
The "4 Eyes" principle is advocated for as a means to critically evaluate and design software systems to prevent confusion in object-oriented design.
The article suggests that semantically different interfaces, such as those for CRUD operations versus mathematical calculations, should not be forced into a single inheritance hierarchy to comply with LSP.
The use of Java generics is recommended for creating interfaces that can accommodate similar types of data, ensuring subtype substitutability.
The author values the segregation of interfaces based on functionality and encourages the use of master interfaces with specific extensions to maintain LSP compliance.
How to apply SOLID Software Design Principles to Spring Boot Application (Part 3)
(skip the first paragraph if you have already read other parts of this blog series)
This article is the third part of the blog series, dedicated to well-known software design principles, that evolved over time and were finally summarised by Robert C. Martin with initials of the corresponding principles. These principles guide us how to build well-designed software systems, giving best practices for arranging classes, functions, building blocks. We will look at each principle in depth and apply it to the Spring Boot Application. The idea is refactoring existing software based on these principles from architectural point of view.
The word S.O.L.I.D. stands for:
SRP: Single Responsibility Principle
OCP: Open-Closed Principle
LSP: Liskov Substitution Principle
ISP: Interface Segregation Principle
DIP: Dependency Inversion Principle
This third part is about LSP. Before jumping to concrete example, let’s have some theory.
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 behaviour of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
This definition from Barbara Liskov might sound pretty confusing but in essence, it is a simple and easy to understand principle. If we restate the above given definition, the principle’s motto is: when using inheritance, hierarchy of inheritance should be from functional and business logic aspect consistent. The subtypes should be mutually substitutable and not alter behavior of parent class. As a simple example we can take “infamous square/rectangle” problem. Where square should not be subtype of rectangle, because definition of height and length of these two geometric forms are different (square’s height and weight are equal whereas for rectangle they will vary).
Some important points to take into account when building inheritance to stay in compliance with LSP are:
Keep super classes as simple and abstract as you can, so that you can later extend the functionality without altering the behavior of parent object.
Think twice when building parent child relations not to have messy hierarchies and whether child classes are mutually substitutable.
“4 Eyes” principle is vital for designing the structure of software system, since object oriented design might be from one side powerful but also very confusing.
So let us have a throwback to our cash flow management application, and try to extend the functionality of application while keeping source code within LSP borders.
For an LSP example we are going to alter our cash flow management app initially to adapt it for further LSP refactoring. As a first step, we are going to refactor the implementation classes of CalculatorService. We are assuming that CalculatorService should only be responsible for mathematical calculation. And all CRUD operations are done through Income- and ExpenseServices. So with that in mind, it is unnecessary that Implementation classes of CalculatorService interface are injecting Repositories and having indirect connection with database through JPA specifications.
We will remove this Repository Interfaces and deliver those information through Income-, ExpenseServices, which are only eligible for connecting to database. On the other hand we are isolating different classes into different packages according to their area of responsibility under three main packages: api, core and database. So cashflow.api.* package is responsible for connection to front view. Accordingly, there will be our Controllers with Rest Endpoints. The second package cashflow.core.* is the main part of cash flow management application as you may know from previous blogs of these series, where we have got the main business logic. The third package is cashflow.database.* and it includes the classes and interfaces that are linked to database activities. The “Big Picture” of Class Diagram for cashflow management application would look like this:
Class Diagram refactored initially for SRP
If you want to check the source code and architectural captures please see the branch feature/lsp-refactored-step-1 and corresponding .puml files.
We are moving now to structure our interfaces in a manner that it complies to LSP. All Interfaces in cashflow.core.* package will be gathered under one hood. On the one hand, there are IncomeService, ExpenseService, which are injected by Controllers and responsible for CRUD Operations. On the other hand, we have a CalculatorService which is only for mathematical calculations there. So semantically these two Interface groups are different whether they are eligible to do CRUD operations or not. Remember we removed JPA Repository Interfaces from CalculatorService Implementations.
To bring all these interfaces under one abstract interface would be violation of LSP. So what we can do here is to have master interface which is Service, another RepositoryInterface should extend this Service, and IncomeService, ExpenseService also extend RepositoryService. CalculationService will directly extend Service interface. Since methods in Income- ExpenseServices are identical beside income and outcome data types, we can use java generics to accumulate them all in RepositoryService. As you can see, the subtypes of RepositoryService are mutually substitutable and semantically same. But CalculatorService is assigned to do completely other tasks, so it would not be correct sibling for Income- and ExpenseServices.
Class Diagram refactored with SRP
To have a look at final architectural design of the application, please check the lsp_refactored_step2_arch.puml file and master branch for source code. Remember to install puml extension for visualising the captures for eclipse or intellij.
The fourth part is dedicated to ISP, Interface Segregation Principle. You can find that blog here.