Skip to content

Behavioral Patterns

Strategy Pattern

Definition

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

Design Principle

  • Encapsulate what varies: identify the aspects of your application that vary and seperate them from what stays the same.
  • Program to an interface, not an implementation (really means "Program to a supertype").
  • Favor composition over inheritance.

Original Diagram

Example codes

Info

We are using the Strategy Pattern to implement the various behaviours of the ducks. This means that the duck behaviour has been encapsulated into its own set of classes that can be easily expanded and changed, even at runtime if needed.

from abc import ABC, abstractmethod


class FlyBehaviour(ABC):
    @abstractmethod
    def fly(self):
        pass


class FlyWithWings(FlyBehaviour):
    def fly(self):
        print("Flying with wings")


class FlyNoWay(FlyBehaviour):
    def fly(self):
        print("Cannot fly")


class QuackBehaviour(ABC):
    @abstractmethod
    def quack(self):
        pass


class Quack(QuackBehaviour):
    def quack(self):
        print("Quack")


class Squeak(QuackBehaviour):
    def quack(self):
        print("Squeak")


class MuteQuack(QuackBehaviour):
    def quack(self):
        pass


class Duck:
    def __init__(self, fly_behaviour, quack_behaviour):
        self.fly_behaviour = fly_behaviour
        self.quack_behaviour = quack_behaviour

    def set_fly_behaviour(self, fly_behaviour):
        self.fly_behaviour = fly_behaviour

    def set_quack_behaviour(self, quack_behaviour):
        self.quack_behaviour = quack_behaviour

    def perform_quack(self):
        self.quack_behaviour.quack()

    def perform_fly(self):
        self.fly_behaviour.fly()

    @staticmethod
    def swim(self):
        print("Swimming")

    def display(self):
        pass


class MallardDuck(Duck):
    def __init__(self):
        super().__init__(FlyWithWings(), Quack())

    def display(self):
        print("I'm a Mallard Duck")


class RedheadDuck(Duck):
    def __init__(self):
        super().__init__(FlyWithWings(), Quack())

    def display(self):
        print("I'm a Redhead Duck")


class RubberDuck(Duck):
    def __init__(self):
        super().__init__(FlyNoWay(), Squeak())

    def display(self):
        print("I'm a Rubber Duck")


class DecoyDuck(Duck):
    def __init__(self):
        super().__init__(FlyNoWay(), MuteQuack())

    def display(self):
        print("I'm a Decoy Duck")


def main():
    mallard = MallardDuck()
    mallard.display()  # Output: I'm a Mallard Duck
    mallard.perform_quack()  # Output: Quack
    mallard.perform_fly()  # Output: Flying with wings

    # Change the fly behavior to FlyNoWay
    mallard.set_fly_behaviour(FlyNoWay())
    mallard.perform_fly()  # Output: Cannot fly

    # Change the quack behavior to Squeak
    mallard.set_quack_behaviour(Squeak())
    mallard.perform_quack()


if __name__ == '__main__':
    main()
package main

import "fmt"

// FlyBehavior is an interface that defines the fly behavior
type FlyBehavior interface {
    fly()
}

// FlyWithWings implements the FlyBehavior interface
type FlyWithWings struct{}

func (f *FlyWithWings) fly() {
    fmt.Println("Flying with wings")
}

// FlyNoWay implements the FlyBehavior interface
type FlyNoWay struct{}

func (f *FlyNoWay) fly() {
    fmt.Println("Cannot fly")
}

// QuackBehavior is an interface that defines the quack behavior
type QuackBehavior interface {
    quack()
}

// Quack implements the QuackBehavior interface
type Quack struct{}

func (q *Quack) quack() {
    fmt.Println("Quack")
}

// Squeak implements the QuackBehavior interface
type Squeak struct{}

func (s *Squeak) quack() {
    fmt.Println("Squeak")
}

// MuteQuack implements the QuackBehavior interface
type MuteQuack struct{}

func (m *MuteQuack) quack() {
    // No output
}

// Duck is the base struct that holds the fly and quack behaviors
type Duck struct {
    flyBehavior   FlyBehavior
    quackBehavior QuackBehavior
}

func (d *Duck) setFlyBehavior(fb FlyBehavior) {
    d.flyBehavior = fb
}

func (d *Duck) setQuackBehavior(qb QuackBehavior) {
    d.quackBehavior = qb
}

func (d *Duck) performQuack() {
    d.quackBehavior.quack()
}

func (d *Duck) performFly() {
    d.flyBehavior.fly()
}

func (d *Duck) swim() {
    fmt.Println("Swimming")
}

func (d *Duck) display() {
    // Base implementation does nothing
}

// MallardDuck is a concrete implementation of Duck
type MallardDuck struct {
    Duck
}

func (m *MallardDuck) display() {
    fmt.Println("I'm a Mallard Duck")
}

// RedheadDuck is a concrete implementation of Duck
type RedheadDuck struct {
    Duck
}

func (r *RedheadDuck) display() {
    fmt.Println("I'm a Redhead Duck")
}

// RubberDuck is a concrete implementation of Duck
type RubberDuck struct {
    Duck
}

func (r *RubberDuck) display() {
    fmt.Println("I'm a Rubber Duck")
}

// DecoyDuck is a concrete implementation of Duck
type DecoyDuck struct {
    Duck
}

func (d *DecoyDuck) display() {
    fmt.Println("I'm a Decoy Duck")
}

func main() {
    mallard := &MallardDuck{Duck{&FlyWithWings{}, &Quack{}}}
    mallard.display()      // Output: I'm a Mallard Duck
    mallard.performQuack() // Output: Quack
    mallard.performFly()   // Output: Flying with wings

    // Change the fly behavior to FlyNoWay
    mallard.setFlyBehavior(&FlyNoWay{})
    mallard.performFly() // Output: Cannot fly

    // Change the quack behavior to Squeak
    mallard.setQuackBehavior(&Squeak{})
    mallard.performQuack() // Output: Squeak
}
  • Running Go code on Go Playground here.

Observer Pattern

Definition

The Observer Pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Design Principle

  • Strive for loosely coupled designs betwwen objects that interacts.
  • Subjects update Observers using a common interface. Observers of any concrete type can participate in the pattern as long as they implement the Observer interace.
  • Observers are loosely coupled in that the Subject knows nothing about them, other thant that they implement the Observer interface.

Original Diagram

Example codes

from contextlib import suppress
from typing import Protocol


class Observer(Protocol):
    def update(self, subject) -> None:
        pass


class Subject:
    def __init__(self):
        self._observers = []

    def register_observer(self, observer: Observer) -> None:
        if observer not in self._observers:
            self._observers.append(observer)

    def remove_observer(self, observer: Observer) -> None:
        with suppress(ValueError):
            self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)


class WeatherData(Subject):
    def __init__(self):
        super().__init__()
        self._temperature = 0
        self._humidity = 0

    @property
    def temperature(self) -> int:
        return self._temperature

    @temperature.setter
    def temperature(self, value: int) -> None:
        self._temperature = value
        self.notify()

    @property
    def humidity(self) -> int:
        return self._humidity

    @humidity.setter
    def humidity(self, value: int) -> None:
        self._humidity = value
        self.notify()


class DisplayElement(Protocol):
    def display(self):
        pass


class CurrentConditionsDisplay(Observer, DisplayElement):
    def __init__(self, weather_data: WeatherData):
        self._weather_data = weather_data
        self._weather_data.register_observer(self)
        self._temperature = 0
        self._humidity = 0

    def update(self, subject: Subject) -> None:
        self._temperature = self._weather_data.temperature
        self._humidity = self._weather_data.humidity
        self.display()

    def display(self):
        print(f'Current conditions: {self._temperature}°C, {self._humidity}% humidity')


class StatisticsDisplay(Observer, DisplayElement):
    def __init__(self, weather_data: WeatherData):
        self._weather_data = weather_data
        self._weather_data.register_observer(self)
        self._temperature = 0
        self._temp_sum = 0
        self._num_readings = 0

    def update(self, subject: Subject) -> None:
        self._temperature = self._weather_data.temperature
        self._temp_sum += self._temperature
        self._num_readings += 1
        self.display()

    def display(self):
        print(f'Statistics: Average temp: {self._temp_sum / self._num_readings}')


def main():
    weather_data = WeatherData()
    current_condition_display = CurrentConditionsDisplay(weather_data)
    StatisticsDisplay(weather_data)
    weather_data.temperature = 10  # Current conditions: 10°C, 0% humidity  Statistics: Average temp: 10.0
    weather_data.temperature = 20  # Current conditions: 20°C, 0% humidity  Statistics: Average temp: 15.0
    weather_data.temperature = 30  # Current conditions: 30°C, 0% humidity  Statistics: Average temp: 20.0
    weather_data.humidity = 40     # Current conditions: 30°C, 40% humidity  Statistics: Average temp: 22.5
    weather_data.remove_observer(current_condition_display)
    weather_data.temperature = 50  # Statistics: Average temp: 28.0


if __name__ == '__main__':
    main()