paint-brush
Implementation of the Strategy Pattern in Kotlin and Springby@KonstantinGlumov
104 reads

Implementation of the Strategy Pattern in Kotlin and Spring

by Konstantin GlumovAugust 15th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The Strategy pattern is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern promotes the Open/Closed Principle by allowing new strategies to be added without modifying existing code. This approach allows us to create easily extensible and maintainable applications.
featured image - Implementation of the Strategy Pattern in Kotlin and Spring
Konstantin Glumov HackerNoon profile picture

In the world of software development, there often arises a need to handle business logic depending on the values of input parameters. At first glance, one could address this requirement using simple if-else or when constructs, but such implementations can lead to code that is difficult to read and maintain. It conflicts with the principles of object-oriented programming, and extending this logic often requires modifying existing code. In this article, we will explore how to effectively implement the Strategy design pattern in the Kotlin programming language using the Spring framework. This approach allows us to create easily extensible and maintainable applications while adhering to SOLID principles.


The Strategy pattern is a behavioral design pattern that enables selecting an algorithm’s behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern promotes the Open/Closed Principle by allowing new strategies to be added without modifying existing code, which aligns perfectly with the goals of clean and maintainable software architecture.

The Problem with Using if-else

Consider the following code, which is an example of an anti-pattern that uses when to handle different screens:

fun getScreen(viewId: String): ScreenResponse {
   when (viewId) {
       "cardScreen" -> TODO("Implement logic for cardScreen")
       "infoScreen" -> TODO("Implement logic for infoScreen")
       "accountScreen" -> TODO("Implement logic for accountScreen")
       else -> throw RuntimeException("Filler for $viewId not found")
   }
}

As we can see, this code can quickly grow in complexity, making it difficult to manage and expand. When a new screen needs to be added, the developer is forced to modify existing code, which violates the Open/Closed Principle (OCP) from the SOLID design principles—software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.

Solution with the Strategy Pattern

A more elegant solution is to use the Strategy pattern. We can create several services, each responsible for a specific screen. This allows us to add new screens by simply creating new services while leaving the existing code unchanged.

Application Architecture

Let’s consider a microservice that returns an SDUI (Server-Driven User Interface) screen to the front end. The client requests the necessary screen by its identifier, and the server returns the corresponding business data.

@RestController
class ScreenController(
   private val screenService: ScreenService,
) {
   @GetMapping
   fun getScreen(@RequestParam viewId: String): ScreenResponse = screenService.getScreen(viewId)
}

Defining the Interface

To implement the Strategy pattern, we begin by defining an interface Filler, which will contain a method for filling the screen:

interface Filler {
   fun fill(): ScreenResponse
}

Implementing Concrete Strategies

Next, we create classes that implement this interface for each screen:

@Service
class CardFiller : Filler {
   override fun fill(): ScreenResponse {
       return ScreenResponse("Data for the card screen") // Implement logic for the card screen
   }
}

@Service
class AccountFiller : Filler {
   override fun fill(): ScreenResponse {
       return ScreenResponse("Data for the account screen") // Implement logic for the account screen
   }
}

@Service
class InfoFiller : Filler {
   override fun fill(): ScreenResponse {
       return ScreenResponse("Data for the owner information screen") // Implement logic for the info screen
   }
}

Creating the Main Service

The main service ScreenService, which will use our concrete strategies, looks as follows. We will store them in a Map, where the key is the screen identifier and the value is the strategy object:

@Service
class ScreenService(
   private val fillerMap: Map<String, Filler> // Injection of all implementations of the Filler interface
) {
   fun getScreen(viewId: String): ScreenResponse {
       val filler = fillerMap[viewIDToFiller[viewId]]
       return filler?.fill() ?: throw RuntimeException("Filler for $viewId not found")
   }

   private companion object {
       val viewIDToFiller = mapOf(
           "cardScreen" to "cardFiller",
           "infoScreen" to "infoFiller",
           "accountScreen" to "accountFiller"
       )
   }
}

How It Works

In this code, we inject all implementations of the Filler interface into the ScreenService. We use a Map for quick access to the desired strategy using the screen identifier as the key. This makes it very easy to add new strategies: just create a new class that implements the Filler interface and add a corresponding entry in viewIDToFiller.

Benefits of the Strategy Pattern

By employing the Strategy pattern, we achieve a high degree of modularity and separation of concerns, allowing for easier testing, maintenance, and future expansion of functionality. Each strategy is isolated in its own class, which makes it simpler to change or add functionality without affecting other parts of the application. This results in a more robust and adaptable codebase.

Conclusion

Utilizing the Strategy pattern in conjunction with powerful tools like Kotlin and Spring allows for a significant simplification of code while enhancing its flexibility and extensibility. Instead of manipulating complex if-else or when constructs, we organize business logic into separate services, each responsible for its part of the application.


Thus, when adding a new screen, we only need to create a new service and make minimal changes to the existing infrastructure. This practice aligns with object-oriented programming principles and promotes the development of cleaner, more maintainable code. Considering all the flexibility provided by Spring, the implementation of the Strategy pattern becomes not only straightforward but also an effective practice for creating high-quality software solutions.