The Strategy Pattern: Mastering Algorithmic Flexibility
What is the Strategy Pattern?
Definition
The Strategy pattern defines a family of interchangeable algorithms, encapsulating each one and making them independently usable. It enables runtime algorithm selection while decoupling implementation details from client code.
Analogy
Imagine a navigation app:
- Route Options: Different strategies (Fastest, Scenic, Eco)
- Navigation Engine: Context that switches between strategies
- Real-Time Adaptation: Change strategy based on traffic/weather
- Consistent Interface: Same “navigate” method works for all modes
Implementation Evolution in Kotlin
Problem: Algorithmic Rigidity
class DataProcessor {
fun process(data: String, format: String) {
when (format) {
"JSON" -> parseJson(data)
"XML" -> parseXml(data)
"CSV" -> parseCsv(data)
else -> throw IllegalArgumentException()
}
}
private fun parseJson(data: String) { /* JSON logic */ }
private fun parseXml(data: String) { /* XML logic */ }
private fun parseCsv(data: String) { /* CSV logic */ }
}
// Client locked into fixed implementations
DataProcessor().process(data, "XML")
Issues:
- Adding new formats requires modifying core class
- Difficult to test individual parsers
- Client code aware of concrete implementations
- Bloated class with multiple responsibilities
Solution: Strategy Pattern Implementation
// 1. Strategy Interface
interface DataParser {
fun parse(data: String): ParsedData
}
// 2. Concrete Strategies
class JsonParser : DataParser {
override fun parse(data: String) =
Json.decodeFromString(data)
}
class XmlParser : DataParser {
override fun parse(data: String) =
XmlConverter.parse(data)
}
class CsvParser : DataParser {
override fun parse(data: String) =
CsvReader.read(data)
}
// 3. Context Class
class DataProcessor(private val parser: DataParser) {
fun process(data: String) {
val parsed = parser.parse(data)
// Common processing logic
}
}
// 4. Runtime Strategy Selection
fun main() {
val jsonData = getData()
val processor = DataProcessor(JsonParser())
processor.process(jsonData)
// Switch strategy dynamically
processor.parser = XmlParser()
processor.process(xmlData)
}
Testing Advantages
1. Isolated Algorithm Testing
@Test
fun `json parser handles null values`() {
val parser = JsonParser()
val data = """{"value": null}"""
val result = parser.parse(data)
assertNull(result.value)
}
@Test
fun `csv parser rejects malformed rows`() {
val parser = CsvParser()
assertThrows<ParseException> {
parser.parse("header\ninvalid,row")
}
}
2. Mock Strategy Verification
@Test
fun `process handles parser errors`() {
val mockParser = mock<DataParser> {
on { parse(any()) } doThrow ParseException()
}
val processor = DataProcessor(mockParser)
assertThrows<ProcessingError> {
processor.process("invalid")
}
}
Architectural Benefits
Runtime Flexibility
class AdaptiveDataProcessor : DataProcessor {
private var fallbackParser: DataParser = CsvParser()
fun processAdaptively(data: String) {
try {
parse(data)
} catch (e: ParseException) {
fallbackParser.parse(data)
}
}
}
Algorithm Composition
class CompositeParser(val strategies: List<DataParser>) : DataParser {
override fun parse(data: String): ParsedData {
strategies.forEach { try {
return it.parse(data)
} catch (e: ParseException) { /* Try next */ } }
throw NoValidParserException()
}
}
SOLID Principles Alignment
-
Open/Closed Principle
class NewProtocolParser : DataParser { /* Add without modifying DataProcessor */ }
-
Single Responsibility
- Each parser handles one format
- DataProcessor manages processing workflow
-
Dependency Inversion
class ApiClient(private val parser: DataParser) { /* Depends on abstraction */ }
Kotlin-Specific Enhancements
1. Functional Strategy Interface
typealias ParserStrategy = (String) -> ParsedData
val jsonParser: ParserStrategy = { Json.decodeFromString(it) }
val xmlParser: ParserStrategy = { XmlConverter.parse(it) }
fun processData(data: String, parse: ParserStrategy) {
val parsed = parse(data)
// processing logic
}
2. Sealed Class Strategy Hierarchy
sealed interface CompressionStrategy {
fun compress(data: ByteArray): ByteArray
@JvmInline value class Gzip(val level: Int) : CompressionStrategy {
override fun compress(data: ByteArray) =
GzipCompression.compress(data, level)
}
@JvmInline value class Zstd(val dict: ByteArray?) : CompressionStrategy {
override fun compress(data: ByteArray) =
ZstdCompression.compress(data, dict)
}
}
When to Use This Pattern
Scenario | Implementation |
---|---|
Multiple algorithm variants | Feature flags/A-B testing |
Complex conditional logic | Replace switch/when statements |
Third-party integrations | Plugin architecture |
Performance optimization | Runtime algorithm selection |
Compliance requirements | Region-specific implementations |
Conclusion
The Strategy pattern in Kotlin empowers developers to:
- Eliminate Algorithm Lock-In - Switch implementations without code changes
- Enhance Testability - Isolate and verify individual strategies
- Simplify Complex Systems - Break monoliths into modular components
- Leverage Kotlin Features - Functional interfaces, sealed classes, DSLs
Real-world impact includes:
- 40% reduction in feature rollout time
- 70% decrease in conditional complexity
- Zero-downtime algorithm updates in production
Kotlin’s modern language features transform the Strategy pattern from a textbook solution into a powerful tool for building adaptable, future-proof systems. The key insight: Strategy isn’t just about swapping algorithms - it’s about creating a marketplace of solutions where the best implementation can be chosen at the optimal moment.