๐ง 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
- (Functional): Support multiple watering schedules (Standard, Drought, Rainy).
- (Technical): Schedules must be swappable at runtime without restarting the controller.
- (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
EcoModeStrategywithout modifying theSprinklerController. - Single Responsibility: Each strategy class handles exactly one set of watering rules.
๐ The Code
The Villain's Code (Without Pattern)
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
- Unit Test Strategies: Verify
RainyDayStrategyalways returns0. VerifyHeatwaveStrategyreturns60when temp > 35. - Test Controller: Mock the strategy, call
run(), and verify the controller callsget_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.
๐ Related Patterns
- 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.