Skip to content

๐Ÿ’ง Strategy Pattern: Dynamic Sprinkler Scheduler

๐Ÿ“ Overview

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. It allows the behavior of an object (the Context) to be swapped dynamically without changing the object itself, favoring composition over inheritance.

Core Concepts

  • Context: The object that uses a strategy (e.g., the SprinklerController).
  • Strategy Interface: A common contract that all algorithms must follow (e.g., WateringStrategy).
  • Interchangeable Behaviors: Different "plays" (strategies) can be swapped in and out based on the situation (e.g., weather).

๐Ÿญ The Engineering Story & Problem

๐Ÿ˜ก The Villain (The Problem)

You're building a "Smart Sprinkler Controller." It needs to handle many rules:
- If it's raining, don't water.
- If it's a heatwave, water for 60 minutes.
- If there's a drought restriction, only water on Tuesdays for 10 minutes.
The "Conditional Nightmare" version is a 500-line activate() method filled with nested if/else statements. Every time the city changes its water restrictions, you have to rewrite and re-deploy the entire controller's core logic. The code is brittle, hard to test, and violates the Open/Closed principle.

๐Ÿฆธ The Hero (The Solution)

The Strategy Pattern introduces the "Playbook." Instead of one giant method, we extract each watering rule into its own class: RainyDayStrategy, HeatwaveStrategy, and DroughtStrategy.
The SprinklerController (Context) is now just a shell. It holds a reference to a WateringStrategy.
1. At 5:00 AM, the controller checks the weather.
2. It picks the right "play" (e.g., controller.set_strategy(RainyDayStrategy())).
3. When it's time to water, it just calls strategy.get_duration().
The controller doesn't care why it's watering for 0 or 60 minutes; it just follows the strategy it was given. You can add a HolidayStrategy next month without touching the controller's code.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): Support multiple watering schedules (Standard, Drought, Rainy).
  2. (Technical): Schedules must be swappable at runtime without restarting the controller.
  3. (Technical): All schedules must implement a common interface for duration calculation.

๐Ÿ—๏ธ Structure & Blueprint

Class Diagram

classDiagram
    direction TB
    class WateringStrategy {
        <<interface>>
        +get_duration(sensor_data) int
    }
    class StandardStrategy {
        +get_duration(sensor_data)
    }
    class DroughtStrategy {
        +get_duration(sensor_data)
    }
    class RainyDayStrategy {
        +get_duration(sensor_data)
    }
    class SprinklerController {
        -strategy: WateringStrategy
        +set_strategy(strategy)
        +run()
    }

    WateringStrategy <|.. StandardStrategy
    WateringStrategy <|.. DroughtStrategy
    WateringStrategy <|.. RainyDayStrategy
    SprinklerController o-- WateringStrategy : uses

Runtime Context (Sequence)

sequenceDiagram
    participant App
    participant Controller
    participant Strategy

    App->>Controller: set_strategy(DroughtStrategy)
    App->>Controller: run()
    Controller->>Strategy: get_duration(sensors)
    Strategy-->>Controller: 10 mins
    Controller->>Valve: open(10 mins)

๐Ÿ’ป Implementation & Code

๐Ÿง  SOLID Principles Applied

  • Open/Closed: Add a new EcoModeStrategy without modifying the SprinklerController.
  • Single Responsibility: Each strategy class handles exactly one set of watering rules.

๐Ÿ The Code

The Villain's Code (Without Pattern)
class SprinklerController:
    def run(self, weather, day):
        # ๐Ÿ˜ก Brittle, hardcoded logic
        if weather == "RAIN":
            duration = 0
        elif weather == "HEATWAVE":
            duration = 60
        elif day == "TUESDAY" and restriction == True:
            duration = 10
        # Adding a new rule requires editing this method
The Hero's Code (With Pattern)
from typing import Optional, Protocol


class IStrategy(Protocol):
    def water(self):
        ...


class EcoStrategy(IStrategy):
    def water(self):
        print("Running in Eco Mode: 10 minutes at 50% power")


class HeatWaveStrategy(IStrategy):
    def water(self):
        print("Running in Booster Mode: 30 minutes at 100% power")


class RainyStrategy(IStrategy):
    def water(self):
        print("Running on Rainy Mode: 0 minutes")


class Sprinkler:
    def __init__(self) -> None:
        self.strategy: Optional[IStrategy] = None

    def set_strategy(self, mode: IStrategy) -> None:
        self.strategy = mode

    def water_the_garden(self):
        if not self.strategy:
            print("No watering strategy set")
            return

        self.strategy.water()



sprinkler= Sprinkler()

sprinkler.water_the_garden()

sprinkler.set_strategy(RainyStrategy())

sprinkler.water_the_garden()

sprinkler.set_strategy(EcoStrategy())

sprinkler.water_the_garden()

โš–๏ธ Trade-offs & Testing

Pros (Why it works) Cons (The Twist / Pitfalls)
Clean Logic: No giant if/else blocks. Overhead: Creating many small classes for simple rules.
Runtime Swapping: Change behavior on the fly. Client Awareness: The code that sets the strategy must know which one to pick.
Isolated Testing: Test each watering rule in a tiny unit test. Complexity: Can be overkill for a system with only one set of rules.

๐Ÿงช Testing Strategy

  1. Unit Test Strategies: Verify RainyDayStrategy always returns 0. Verify HeatwaveStrategy returns 60 when temp > 35.
  2. Test Controller: Mock the strategy, call run(), and verify the controller calls get_duration() on the mock and opens the valve for that exact time.

๐ŸŽค Interview Toolkit

  • Interview Signal: mastery of composition over inheritance and algorithm encapsulation.
  • When to Use:
    • "Implement different payment methods (Credit Card, PayPal, Crypto)..."
    • "Support multiple compression formats (Zip, Gzip, Tar)..."
    • "Switch sorting algorithms based on data size..."
  • Scalability Probe: "How to handle a controller with 1,000 different zones?" (Answer: Use a Strategy Registry or share strategy instances as Flyweights to save memory.)
  • Design Alternatives:
    • Template Method: Uses inheritance to change parts of an algorithm; Strategy uses composition to change the whole algorithm.
  • State โ€” Like Strategy, but the "strategies" (states) can trigger transitions to each other.
  • Template Method โ€” Defines the skeleton of an algorithm but lets subclasses fill in the steps.
  • Flyweight โ€” Strategies are often stateless and can be shared.