Unleash Flexibility: Mastering the Strategy Pattern in Modern Kotlin
Go Beyond Class Explosion: Elegant and Idiomatic Strategy Implementation with Kotlin Functions and Lambdas.
Have you ever found yourself staring at a growing codebase, adding new features, and feeling like you’re playing a never-ending game of “Whack-a-Mole” with your design? You add a new class here, another one there, and suddenly your elegant architecture starts to resemble a tangled mess of spaghetti. If so, you’re not alone.
One of the most powerful tools in a software developer’s arsenal for combating this complexity is the Strategy Pattern. It’s a classic design pattern, but with modern Kotlin, we can implement it in ways that are not just effective, but incredibly elegant and concise.
Let’s dive in and see how we can “extract what differs” to build more flexible, maintainable, and delightful software.
The Challenge: When Your Code Starts to Multiply
Imagine you’re building a content management system (CMS), and you have a core Notification requirement. Initially, you might have different types of notifications: Email, SMS, Push.
// Initial, simple approach
class EmailNotification {
fun send(recipient: String, message: String) {
println("Sending Email to $recipient: '$message'")
// Email specific logic
}
}
class SmsNotification {
fun send(recipient: String, message: String) {
println("Sending SMS to $recipient: '$message'")
// SMS specific logic
}
}
// ... and so on for PushNotificationThis seems fine, right? But what happens when your product manager says, “We also need to support priority notifications: High, Medium, and Low”?
Suddenly, you’re looking at a combinatorial explosion of classes! (3 notification types * 3 priority levels = 9 classes, and it only gets worse with more dimensions). This is a rigid design, hard to scale, and a nightmare to maintain.
Enter the Strategy Pattern: Encapsulate What Varies
The core idea of the Strategy Pattern is to define a family of algorithms, encapsulate each one, and make them interchangeable. This allows the algorithm (the “how-to”) to vary independently from clients that use it.
Let’s apply this to our notification system.
1. Identify What Differs: The commonality was send(recipient, message), but the implementation details of how the notification was sent were different. This varying implementation is our "strategy."
2. Extract the Strategy (Interface): We’ll create an interface to define the contract for our notification sending strategies.
// The Strategy Interface
interface NotificationStrategy {
fun send(recipient: String, message: String)
}3. Implement Concrete Strategies: Now, each specific notification type becomes a concrete implementation of our NotificationStrategy.
// Concrete Strategy for Email
class EmailStrategy : NotificationStrategy {
override fun send(recipient: String, message: String) {
println("Sending Email to $recipient: '$message'")
// Complex email sending logic, integration with email service, etc.
}
}
// Concrete Strategy for SMS
class SmsStrategy : NotificationStrategy {
override fun send(recipient: String, message: String) {
println("Sending SMS to $recipient: '$message'")
// SMS gateway integration
}
}4. The Context: Where the Strategy is Used: The NotificationService now becomes our Context. It holds a reference to a NotificationStrategy and delegates the send operation to it.
// The Context Class
class NotificationService(private val strategy: NotificationStrategy) {
fun notify(recipient: String, message: String) {
// The service doesn't care HOW it's sent, just that it IS sent.
println("NotificationService received request for $recipient.")
strategy.send(recipient, message)
}
}Benefits of the Traditional Strategy Pattern:
- Single Responsibility Principle (SRP): Each strategy class focuses solely on how to send a specific type of notification.
- Open/Closed Principle (OCP): You can add new notification types (strategies) without modifying the
NotificationService(the context). - Testability: Each strategy can be tested in isolation.
The Kotlin Twist: Functions as Strategies (The Modern Way)
Kotlin, with its first-class functions, allows us to make the Strategy Pattern incredibly elegant and less verbose, especially when our strategies are simple and stateless.
If our strategy interface only defines one method, we can leverage Kotlin’s capabilities to replace boilerplate classes with concise functional types.
1. Using fun interface (SAM Conversion):
By using fun interface, Kotlin allows us to instantiate the interface using a lambda (Single Abstract Method conversion).
// Our strategy is now a functional interface
fun interface NotificationStrategyKotlin {
fun send(recipient: String, message: String)
}
// The Context remains the same, accepting the functional interface
class NotificationServiceKotlin(private val strategy: NotificationStrategyKotlin) {
fun notify(recipient: String, message: String) {
strategy.send(recipient, message)
}
}Now, we create strategies without defining an entire class:
fun main() {
// Email strategy created using a lambda
val emailService = NotificationServiceKotlin { recipient, message ->
println("[Lambda Strategy] Sending email to $recipient: '$message'")
}
emailService.notify("charlie@example.com", "Your subscription is about to expire!")
// SMS strategy created using a lambda
val smsService = NotificationServiceKotlin { recipient, message ->
println("[Lambda Strategy] Sending SMS to $recipient: '$message'")
}
smsService.notify("+15551234567", "Your package has been delivered!")
}2. The Ultimate Kotlin Elegance: Type Aliases
For pure functional strategies, we can skip the interface entirely and use a typealias to define the function signature itself.
// Define a type alias for our strategy function
typealias NotificationFunction = (recipient: String, message: String) -> Unit
// Our context now directly accepts a function
class NotificationServiceFunc(private val sendFunction: NotificationFunction) {
fun notify(recipient: String, message: String) {
println("NotificationService (Functional) received request.")
sendFunction(recipient, message) // Directly call the function
}
}Using it is the most concise:
fun main() {
// Strategy as a lambda directly passed to the constructor
val loggerService = NotificationServiceFunc { recipient, message ->
println("LOGGING: Notification for $recipient: '$message'")
// No actual sending, just logging the intent
}
loggerService.notify("audit_log", "User 'Eve' performed action 'login'.")
}Advanced Enhancement: Dynamic Strategy Dispatcher
In real-world applications, you often need to select a strategy dynamically based on a key (e.g., user preference, configuration file).
// Reusing our typealias
typealias NotificationFunction = (recipient: String, message: String) -> Unit
// Concrete functions (strategies)
val emailSender: NotificationFunction = { recipient, message ->
println("--- EMAIL: Sending to $recipient: '$message'")
}
val smsSender: NotificationFunction = { recipient, message ->
println("--- SMS: Sending to $recipient: '$message'")
}
class DynamicNotificationDispatcher(
private val defaultStrategy: NotificationFunction = emailSender
) {
// In multithreaded environments, consider using an immutable map or thread-safe structure.
private val strategies = mutableMapOf<String, NotificationFunction>(
"email" to emailSender,
"sms" to smsSender
)
fun registerStrategy(key: String, strategy: NotificationFunction) {
strategies[key] = strategy
}
fun notify(recipient: String, message: String, notificationType: String? = null) {
val strategyToUse = notificationType?.let { strategies[it] } ?: defaultStrategy
strategyToUse(recipient, message)
}
}
fun main() {
val dispatcher = DynamicNotificationDispatcher()
dispatcher.notify("fred@example.com", "Your report is ready.", "email")
dispatcher.notify("+1122334455", "Emergency Alert!", "sms")
dispatcher.notify("harry@example.com", "Important update.") // Uses default (email)
}This dynamic dispatcher manages and switches between different behaviors efficiently, adapting to various scenarios without modifying the core dispatcher logic.
Frequently Asked Questions about the Strategy Pattern
What’s the difference between Strategy Pattern and State Pattern?
While both patterns use delegation and look structurally similar, their intent differs. The Strategy Pattern defines what algorithm to use to perform a specific action (e.g., how to sort an array). The State Pattern changes the behavior of an object based on its internal state (e.g., a traffic light object changing its behavior from “Red” to “Yellow”). Think of Strategy as “how to do something,” and State as “what I am, therefore how I act.”
When should I not use the Strategy Pattern?
If you only have one algorithm or your algorithms rarely change, the overhead of creating interfaces and context classes might be unnecessary. For very simple branching logic, a simple when statement is often perfectly adequate. The goal is maintainability, not pattern-adherence.
Can strategies have state?
Yes, concrete strategy implementations can have internal state, but it is generally discouraged. Strategies are ideally stateless and reusable (like our functions). If state is required, ensure it is necessary for the strategy’s function and managed correctly to avoid unexpected side effects.
Is this related to dependency injection (DI)?
Absolutely! The Strategy Pattern is one of the foundational reasons DI exists. You use a DI framework (like Koin or Hilt) to inject the desired NotificationStrategy implementation into your NotificationService (the Context), making configuration and testing seamless.
To the Readers: Your Turn!
We’ve covered how Kotlin features like fun interface, lambdas, and typealias beautifully streamline the Strategy Pattern.
- In what other areas of your Kotlin projects (like file processing, pricing calculations, or data serialization) could you replace rigid
whenblocks with the Strategy Pattern? - How might using Sealed Classes alongside the Strategy Pattern enhance type safety when defining a closed set of known strategies?
- What is your favorite way to manage and initialize these strategies using a dependency injection framework?
Share your thoughts in the comments below!
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments
Post a Comment