This article complements the subject of cleaning code by explaining connections between the Refactoring Pyramid and Uncle Bob’s SOLID principles. We will see how each of SOLID rules acts as basement for each level in the pyramid. We will end up at the top level of architectures.
In my previous post How to clean code according to refactoring pyramid? I explained levels of Refactoring Pyramid while refactoring some java code. We took the simplest case from “Refactoring to Patterns” book and explained the first four levels of refactoring pyramid. If you haven’t gone through that article – read it before as this article is a continuation.
SINGLE RESPONSIBILITY PRINCIPLE
First of all let’s have a closer look what single responsibility principle means. One reason for a change? This is very common & vague theory to me… When I do explain it, I always talk about SRP in context of abstraction levels. Single responsibility means a lot of things expressed / contained within single word from higher level of abstraction, and refers to a single small thing from low level of abstraction.
In the first case a kind of manager / controller class is a the top entry that delegates to subclasses to perform details. In the second case small action is being performed itself.
Methods should do one thing, as Uncle Bob writes in Clean Code. When I join it with my above explanation the case makes sense to me.
– A method should refer to single level of abstraction
– It should not contain code that delegates logic to lower level class and deal with low level details at the same time
– It should not deal with two levels of abstrations. In such a case it should delegate both parts to lower level class
This leads us towards things like
- split loop into two parts
- replace loop with stream
- extract smaller methods
- extract composed methods
- create smaller classes
Let’s play with the above transformations. Permission for experiments and drawing conclusions are the the first steps to go forward!
private boolean checkMaxPrice(float price, Product product) {
return product.getPrice() < price;
}
private boolean checkMinAge(float age, Product product) {
return product.getMinAge() < age;
}
Continue your experiments towards having smaller classes.
public class BelowPriceSpec {
private float price;
public BelowPriceSpec(float price) {
this.price = price;
}
public boolean isSatisfiedBy(Product product) {
return product.getPrice() < price;
}
}
INTERFACE SEGREGATION PRINCIPLE
Once we have a few similar classes, we can think about common way we interact with then. This leads to hiding them behind an interface that refers to a kind of abstraction.
When following the refactoring approach in my previous post we would end up a set of classes like MinAgeSpec, DeliveryTimeSpec, AndSpec and so on.
Finally all of them can implement the Spec interface
public interface Spec {
boolean isSatisfiedBy(Product product);
}
[FM_form id=”1″]
OPEN CLOSED PRINCIPLE
Once we have a setup of interfaces then our common logic is based on these contracts. When a logic is based on contracts or uses such contracts that the logic is not likely to change often. Even when we provide new implementation of such a contract as a parameter of class’s constructor, or method’s parameter – the logic stays the same!
Here is how Open Closed Principle reveals as a next level in Pyramid of Refactoring. Look below at “bySpec” method that is not likely to be changed in the future.
public class ProductFinder {
private List<Product> repository;
public ProductFinder(List<Product> repository) {
this.repository = repository;
}
public List<Product> bySpec(Spec spec) {
return repository.stream()
.filter(spec::isSatisfiedBy)
.collect(Collectors.toList());
}
@Deprecated
public List<Product> byBelowPrice(float price) {
return bySpec(new BelowPriceSpec(price));
}
}
At the same time I expect new implementations of Spec interface to come. For example what happens when we need to consider delivery time when choosing an item to buy?
public class DeliveryDeadlineSpec implements Spec{
private Datetime deadline;
public DeliveryDeadlineSpec(Datetime dealine) {
this.deadline = deadline;
}
public boolean isSatisfiedBy(Product product) {
Datetime latestDelivery =
timeProvider.getTime().plusDays((product.getDeliveryTime())
return deadline.before(latestDelivery);
}
}
DEPENDENCY INVERSION PRINCIPLE
Finally it’s time to outline the emerging architecture. Architectures emerge at level of
- classes
- sets of APIs
- modules
- libraries
- microservices
- domains
Going forward for some time the concept will be clear and therefore the idea will be copy-pasted (ugh…). While the codebase grows the concept might become vague and foggy so it wioll be time to refactor it again in order to come back to readability.
Let’s come back to ProductFinder and see how Dependency Inversion Principle is expressed.
package pl.refactoring.ProductFinder
package pl.refactoring.Spec
package pl.refactoring.specs.BelowPriceSpec
package pl.refactoring.specs.MinAgeSpec
package pl.refactoring.spesc.DeliveryDeadlineSpec
Please note that higher level of abstraction (ProductFinder) defined its contract interface Spec. Then the implementations of the interfaces might be delivered in different packages, different modules or even different libraries.
LISKOV SUBSTITUTION PRINCIPLE
There was no clear case for usage of LSP here, but it is placed at level of classes. This is because usually usage of this principle ends up with replacing relation between 2 classes from inheritance with delegation.