SOLID Principles – one by one with examples in DDD context

SOLID Principles – one by one with examples in DDD context

I suppose some of you currently know what the SOLID is and how to practice that pool of principles but some of you are seeing that acronym (SOLID) for the first time. This post is prepared for all of you and for me as well (everytime when I write about something, I’m learning with higher understanding) as a short notes with benefits in DDD convention context.

Software Development is not a Jenga game

S.O.L.I.D. – what is this ??

SOLID is a acronym of five listed below principles, originaly defined by Robert C. Martin formally known as uncle Bob in the 1990s, these principles provide way to create better code and better us as a programmers.

  1. S – Single Responsibility Principle (SRP)
  2. O – Open-Closed Principle (OCP)
  3. L – Liskov Substitution Principle (LSP)
  4. I – Interface Segregation Principle (ISP)
  5. D – Dependency Inversion Principle (DIP)

Let’s try to understand how to play with principles one by one with explained benefits depend on project in DDD convention.

Idea:

For example, say we are building system which gonna be responsible for managing kitchen in our restaurant (managing ingredients, how many dishes and what we served and so on), first what we should do is design our system in order to increase transparency and properly understand bussines logic (I recommend DDD; about DDD I will prepare few addittional posts in future) but in that post we gonna focus on S.O.L.I.D. in DDD context let me clarify these principles on that example without using any formally known design strategies just using from Polish nomenclature “Na Oko” strategy which can be direclty translated to english language “On Eye” strategy (“Na oko” – means that we have feeling that something should looks like that without any analysis or any measurment maked).

Design:

“Na Oko” DDD design: I think we need some Dish aggregate and that Dish should be built upon some Ingredients (List of ingredients used into Dish in that case as value objects) and as well we need store/search information about Dishes somewhere so we also need some Repositories and as well I can say that we gonna have Domain event (DishCooked) – when the dish is ready and can be taken of from kitchen in order to inform waiter about it, we can add additional Domain event DishCookingAccepted which gonna inform waiter that cooking of that dish has been accepted (as long as we have enough ingredients etc.).

Kitchen manager has been pushed into Gitlab link: https://gitlab.com/Bobak/kitchen-manager-ddd


S – Single Responsibility Principle (SRP)

A class should only have one responsibility. Furthermore it should only have one reason to change.

Let say our system should doing daily calculation on our dishes (daily take). So we are preparing some Class responsible for that:

public class DishCalculatorService {
    
    private final DishRepository dishRepo;

    public DishCalculatorService() {
       dishRepo = new InMemoryDishRepository();
    }
    
    private BigDecimal summarize(List<Dish> dishes) {
        return dishes.stream().map(x -> x.getTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public float calculateDaily(LocalDate date) {
        return summarize(dishRepo.getDishesByDay(date)).floatValue();
    }
    
}

As far as good (not perfectly becuase DIP has been violated but about DIP principle later) but we have SRP achieved after some time we decided extend our class by additional calculation capabiltities get report per day and per specified dish, in order to not broke anything we had before we just decided to prepare additional method calculateDailyPerDish with dish from menu as param.

public class DishCalculatorService {
    
    private final DishRepository dishRepo;

    public DishCalculatorService() {
       dishRepo = new InMemoryDishRepository();
    }
    
    private BigDecimal summarize(List<Dish> dishes) {
        return dishes.stream().map(x -> x.getTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public float calculateDaily(LocalDate date) {
        return summarize(dishRepo.getDishesByDay(date)).floatValue();
    }
    
    public float calculateDailyPerDish(LocalDate date, MenuDish dish) {
        return summarize(dishRepo.getDishesByDay(date).stream().filter(x -> x.getDish() == dish).collect(Collectors.toList())).floatValue();
    }
    
} 

And now SRP has ben violated !!! Why ?? Look on calculateDailyPerDish method, first of all we are reading too much data from repository and another problem is we are doing more than calculation (becuase we are doing filtering !), this means that we have too many responsibilities.

To fix that we should extend Repository by additional method (getDishesByDayAndDish)

public interface DishRepository extends Repository {

    public void addDish(Dish dish);

    public void updateDish(Dish dish);

    public Dish getDish(String dishId);

    public List<Dish> getDishesByDay(LocalDate day);

    public List<Dish> getDishesByDayAndDish(LocalDate day, MenuDish dish);

    public String nextIdentity();

}

And now our DishCalculatorService could look like that:

public class DishCalculatorService {
    
    private final DishRepository dishRepo;

    public DishCalculatorService() {
       dishRepo = new InMemoryDishRepository();
    }
    
    private BigDecimal summarize(List<Dish> dishes) {
        return dishes.stream().map(x -> x.getTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public float calculateDaily(LocalDate date) {
        return summarize(dishRepo.getDishesByDay(date)).floatValue();
    }
    
    public float calculateDailyPerDish(LocalDate date, MenuDish dish) {
        return summarize(dishRepo.getDishesByDayAndDish(date, dish)).floatValue();
    }
    
}

So now again our class looks good and we have again SRP.

Benefits:

  • Testing – A class with one responsibility will have far fewer test cases
  • Lower coupling – Less functionality in a single class will have fewer dependencies
  • Organization – Smaller, well-organized classes are easier to search than monolithic ones

O – Open-Closed Principle (OCP)

Software entities (classes, modules, functions, …) should be open for extension, but closed for modification.

Of course exist exceptions, the one exception to the rule is when fixing bugs in existing code and another exception is when new implemenatation of functioanlity has been changed in language API (Some functions have been deprecated etc.)

In order to achieve OCP we should concern on implementation/interface(prefered) inheritance

Let’s have a look DomainEvent interface

public interface DomainEvent extends DomainObject {
}

Now let’s create concrete DomainEvent upon DomainEvent interface

public final class DishCooked implements DomainEvent {

    private final LocalDateTime occurredOn;
    private final Dish dish;

    public DishCooked(LocalDateTime occurredOn, Dish dish) {
        this.occurredOn = occurredOn;
        this.dish = dish;
    }

    public LocalDateTime occurredOn() {
        return this.occurredOn;
    }

    public Dish getDish() {
        return dish;
    }

} 

Looks as well correct, we have two fields occurredOn (when the event has been rised) and dish (what has been cooked). Now let’s imagine case that you want to capture all events from your application and send somewhere (Event Store) or just only print for logging capabilities. So we have to create Event application Service (which listening for DomainEvent objects)

public class EventAppService {

    public EventAppService() {
        listen();
    }

    private static void listen() {
        DomainEventPublisher.instance().reset();

        DomainEventPublisher.instance()
                .subscribe(new DomainEventSubscriber<DomainEvent>() {

                    public void handleEvent(DomainEvent aDomainEvent) {
                        //handle event
                    }

                    public Class<DomainEvent> subscribedToEventType() {
                        return DomainEvent.class;
                    }
                });
    }
}

In order to be able capture and print time of every event you have to in method handleEvent make matching by instanceof and make casting to that class and get occurredOn field which could be nasty and every new Event created in our application have to be handled separatelly (instanceof … casting etc…)

To fix this we shoudl create occurredOn method into DomainEvent interface.

public interface DomainEvent extends DomainObject {

  public LocalDateTime occurredOn();
  
}

Now every class implmenting DomainEvent interface gonna have that method (@Override)

public final class DishCooked implements DomainEvent {

    private final LocalDateTime occurredOn;
    private final Dish dish;

    public DishCooked(LocalDateTime occurredOn, Dish dish) {
        this.occurredOn = occurredOn;
        this.dish = dish;
    }

    @Override
    public LocalDateTime occurredOn() {
        return this.occurredOn;
    }

    public Dish getDish() {
        return dish;
    }
}

And our Event Application Servcie listener looks simple and every new DomainEvent gonna be captured without additional work.

public class EventAppService {

    public EventAppService() {
        listen();
    }

    private static void listen() {
        DomainEventPublisher.instance().reset();

        DomainEventPublisher.instance()
                .subscribe(new DomainEventSubscriber<DomainEvent>() {

                    public void handleEvent(DomainEvent aDomainEvent) {
                       // now we have access to aDomainEvent.occurredOn() method directly from DomainEvent
                    }

                    public Class<DomainEvent> subscribedToEventType() {
                        return DomainEvent.class;
                    }
                });
    }
}

So we don’t have to modify our Event Application Service just extending by adding additionals DomainEvents

Benefits:

  • Current implementation isn’t touched – it means that we avoid causing potential new bugs in implementation which has working before

L – Liskov Substitution Principle (LSP)

If class A is a subtype of class B, then we should be able to replace B with A without disrupting the behavior of our program.

Unfortunetelly I was not able get correct example with that principle within my project example, because DDD favor composition over inheritance (which is the strongest level of coupling between classes).

But when you are using inheritance you should follow below rule.

A a = new A();
A b1 = new B();
B b2 = new B();
a.MyMethod(); == b1.MyMethod(); == b2.MyMethod();

But in order to show at least violated example I tooked from this post about LSP: https://medium.com/@gabriellamedas/lsp-the-liskov-substitution-principle-e43910b638bc

class Shape {
  private int height;
  private int width;
  public void setHeight(int newHeight) {
     height = newHeight;
  }
  public void setWidth(int newWidth) {
    width = newWidth;
  }
  public int getArea() {
    return width * height;
  end
}

class Square extends Shape {
  private int height;
  private int width;
  public void setHeight(int newHeight) {
    height = height;
    width = height;
  }
  public void setWidth(int newWidth) {
    width = newWidth;
    height = newWidth;
  }
  public int getArea() {
    return width * height;
  end
}

class Calculator() {
  public int area(Shape shape) {
    int width = shape.setWidth(2);
    int height = shape.setHeight(5);
    shape.getArea();
  }
}
Calculator calculator = new Calculator();
Shape newSquare = new Square();
calculator.area(newSquare)         

The problem here is that Square class have height == width and we expect that we gonna have 5 * 5 but used area from Base class have different behavior (2 * 5) (example doesn’t return 25 as expected but 10.)

Benefits:

  • code re-usability
  • reduced coupling
  • easier maintenance

I – Interface Segregation Principle

Clients should not be forced to implement unnecessary methods which they will not use

This principle sounds simple and in fact is simple.

All in that principle is about favor smallest interfaces over big interfaces (interfaces of our Domain Services should be small – ideally should have one method)

Benefits:

  • smaller interface is a lot easier to implement due to not having to implement methods that our class doesn’t need
  • another benefit is that the Interface Segregation Principle increases readability and maintainability of our code. We are reducing our class implementation only to required actions without any additional or unnecessary code

D – Dependency Inversion Principle

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.

Let’s go back to our DishCalculatorService (at SRP principle explanation we noticed that DIP also has been broken there)

public class DishCalculatorService {
    
    private final DishRepository dishRepo;

    public DishCalculatorService() {
       dishRepo = new InMemoryDishRepository();
    }
    
    private BigDecimal summarize(List<Dish> dishes) {
        return dishes.stream().map(x -> x.getTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public float calculateDaily(LocalDate date) {
        return summarize(dishRepo.getDishesByDay(date)).floatValue();
    }
    
    public float calculateDailyPerDish(LocalDate date, MenuDish dish) {
        return summarize(dishRepo.getDishesByDayAndDish(date, dish)).floatValue();
    }
    
}

Look on the constructor we can see that we are working on detailed implementation (InMemoryDishRepository) in case of implementation of SQLDishRepository or MongoDishRepository we are forced to change here as well. To fix this we can simply inject DishRepository interface (like we did at below example) and allow control what implentation is injected by higher modules/classes (with using DI or not).

public class DishCalculatorService {
    
    private final DishRepository dishRepo;
    
    public DishCalculatorService(DishRepository dishRepo) {
        this.dishRepo = dishRepo;
    }
    
    private BigDecimal summarize(List<Dish> dishes) {
        return dishes.stream().map(x -> x.getTotalPrice()).reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    public float calculateDaily(LocalDate date) {
        return summarize(dishRepo.getDishesByDay(date)).floatValue();
    }
    
    public float calculateDailyPerDish(LocalDate date, MenuDish dish) {
        return summarize(dishRepo.getDishesByDayAndDish(date, dish)).floatValue();
    }
    
}

Benefits:

  • The main reason why DIP is so important is the modularity and reusability of the application modules.
  • worth mention that changing already implemented modules is risky. By depending on abstraction and not on a concrete implementation, we can reduce that risk by not having to change high-level modules in our project.
  • DIP when applied correctly gives us the flexibility and stability at the level of the entire architecture of our application. Our application will be able to evolve more securely and become stable and robust

As we have seen S.O.L.I.D. principles and DDD convention likes each other, with proper architecture (Layered/Hexagonal) make to keep S.O.L.I.D. principles simple to achive.

References:

Leave a Comment

Your email address will not be published. Required fields are marked *