Strategy Design Pattern in Kotlin

1. Definition

The Strategy Design Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy allows the algorithm to vary independently from the clients that use it.

2. Problem Statement

Suppose you're developing an e-commerce platform where various types of discounts can be applied to an order. If you hard-code each discount logic within the order processing, the system becomes rigid and hard to extend with new discount types.

3. Solution

Instead of hard-coding the discount logic, we can define a set of discount strategies. Each strategy encapsulates a specific discount logic. The order processing class then delegates the discount calculation to a strategy object. This allows us to easily introduce new discount types without modifying the order processing.

4. Real-World Use Cases

1. Different compression techniques in a file storage system (ZIP, RAR, TAR).

2. Various payment methods in an e-commerce application (Credit Card, PayPal, Cryptocurrency).

3. Different rendering algorithms in a graphics editor.

5. Implementation Steps

1. Define a strategy interface for the varying behavior.

2. Implement concrete strategy classes for each variant.

3. A context class that uses a strategy to perform an action.

6. Implementation in Kotlin

// Step 1: Strategy Interface
interface DiscountStrategy {
    fun applyDiscount(amount: Double): Double
}
// Step 2: Concrete Strategies
class NoDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double) = amount
}
class SeasonalDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double) = amount * 0.9  // 10% discount
}
class LoyaltyDiscount : DiscountStrategy {
    override fun applyDiscount(amount: Double) = amount * 0.85  // 15% discount
}
// Step 3: Context - Order
class Order(private var strategy: DiscountStrategy) {
    fun finalizeOrder(amount: Double): Double {
        return strategy.applyDiscount(amount)
    }
}
// Testing the implementation
fun main() {
    val order1 = Order(NoDiscount())
    println("Total Amount with No Discount: ${order1.finalizeOrder(100.0)}")
    val order2 = Order(SeasonalDiscount())
    println("Total Amount with Seasonal Discount: ${order2.finalizeOrder(100.0)}")
    val order3 = Order(LoyaltyDiscount())
    println("Total Amount with Loyalty Discount: ${order3.finalizeOrder(100.0)}")
}

Output:

Total Amount with No Discount: 100.0
Total Amount with Seasonal Discount: 90.0
Total Amount with Loyalty Discount: 85.0

Explanation:

1. We first define a DiscountStrategy interface which sets a contract for all discount types.

2. Concrete strategies (NoDiscount, SeasonalDiscount, and LoyaltyDiscount) provide the actual discount logic.

3. The Order class (context) uses a discount strategy to calculate the final amount. It delegates the discount calculation to the strategy object.

4. In our test scenario, three different orders are created with different discount strategies, showcasing the flexibility provided by the Strategy pattern.

7. When to use?

Use the Strategy Pattern when:

1. Many related classes differ only in their behavior. Strategies provide a way to configure a class with various behaviors.

2. You need different variants of an algorithm.

3. An algorithm uses data that clients shouldn't know about. Use the Strategy pattern to avoid exposing complex, algorithm-specific data structures.

4. A class defines many behaviors, and these appear as multiple conditional statements in its operations. Instead of many conditionals, move related conditional branches into their own Strategy class.


Comments