In my previous post Pyramid of Refactoring – Example I was talking about 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.

This article complements the subject by explaining connections between the 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.

  • 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. A method should not contain code that delegates logic to lower level class and deal with low level details at the same time. A method should never contain 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
  • extract 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);
}
  • 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 new implementation of such a contract is provided 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 new implementations of Spec interface are likely – and moreover expected – to happen. For example what happens when we are constrained by 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
… and probably more

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.

Spread the word. Share this post!