Ease Your Life With Strategy Pattern - Spring Boot & Kotlin

OzBenSimhon
Fiverr Tech
Published in
3 min readApr 19, 2022

Whenever a product manager says, “We want this to do ‘x’, but ‘x’ is not fully defined and may change in future”, panic no more, strategy to the rescue!

Strategy is a behavioral design pattern that lets you define one interface with many concrete implementations (i.e. algorithms), with each encapsulated in a separate class. By using composition over inheritance, strategy clients couple themselves to an interface and know nothing about the concrete algorithm, thus allowing them to be used interchangeably and easily be replaced at runtime.

Alright, having these fancy words said, let’s dive into short code examples to understand what these words mean! We’ll begin with naive implementation, and we’ll then see some of the strategy magic by applying it to a Service bean.

Order DeliveryService Validation

In our example, we will have four different order types: TypeA, TypeB, TypeC, and TypeD. Each has a different delivery validation concern.
Now, imagine that we have a simple order DeliveryService that has two methods:

  • deliver - Logic is shared among all order types.
  • validateDelivery - Logic depends on order type.

First Solution: Switch Case

Pretty simple, right? Let’s examine the pros and cons.
Pros:

  • No Overhead - If a change in logic is not expected, then this solution is pretty straightforward.

Cons:

  • Code Readability - Imagine that we have twenty different order types, the when(order) {...} statement will involve a significant amount of logic.
  • Hard to Maintain - Each new order type will force a change in DeliveryService logic and tests. The same applies for a change in validation algorithm.

Second Solution: Abstract Class & Inheritance

Let’s define the abstract DeliveryService class:

  • deliver Logic is shared among all order types, thus the implementation will be in the abstract class.
  • validateDelivery Logic depends on order type, thus it will be declared as abstract.

Each order type will have a corresponding delivery service with concrete validateDelivery implementation.

Pros:

  • Open to Expansion - On new order type, we simply need to implement new delivery service.
  • Better Code Readability - There is no when(order) {...} statement.

Cons:

  • Code Duplication - TypeA and TypeB delivery services share the same validation logic.
  • Hard to Maintain - If the validation logic changes, we need to change the logic in all delivery services that share it.
  • When Statement - We probably just moved the when(order) {...} statement to DeliveryService clients, as they should now choose which DeliveryService to use.

Third Solution: Composition Over Inheritance!

Let’s encapsulate what varies and define a new DeliveryValidator interface with two methods: shouldValidate and validate.

Each validation algorithm will have a dedicated class that implements DeliveryValidator interface and encapsulate its logic.

Inject all DeliveryValidator interface implementations into DeliveryService.

Pros:

  • Easy to Maintain - Each validation logic is centralized to a single class, and is easy to modify.
  • Easy to Expand - To introduce a new validation we just need to create a new class that implements the DeliveryValidator interface.
  • Easy to Tests - Validation tests are agnostic to each other and to DeliveryService tests.
  • No Coupling - Clients are coupled to the interface and know nothing about the concrete validation algorithm.
  • Easy to Reuse - Validations can be used by other clients in the system.
  • Easy to Change Behavior - Clients can replace the validation algorithm at runtime.
  • Better Code Readability - There is no when(order) { ... } statement.

Cons:

  • Can Add Overhead - If a logic change is not expected, using strategy may be redundant.

--

--

OzBenSimhon
Fiverr Tech

Software engineer at Fiverr. Tel Aviv University BSc computer science graduate.