Search code examples
pythondecoratorgetterstrategy-pattern

should this python implementation of strategy pattern use private variable in init method?


I'm learning about the strategy design pattern, as well as the property decorator in Python. I came across this example:

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Context():
    """
    The Context defines the interface of interest to clients.
    """

    def __init__(self, strategy: Strategy) -> None:
        """
        Usually, the Context accepts a strategy through the constructor, but
        also provides a setter to change it at runtime.
        """

        self._strategy = strategy

    @property
    def strategy(self) -> Strategy:
        """
        The Context maintains a reference to one of the Strategy objects. The
        Context does not know the concrete class of a strategy. It should work
        with all strategies via the Strategy interface.
        """

        return self._strategy

    @strategy.setter
    def strategy(self, strategy: Strategy) -> None:
        """
        Usually, the Context allows replacing a Strategy object at runtime.
        """

        self._strategy = strategy

    def do_some_business_logic(self) -> None:
        """
        The Context delegates some work to the Strategy object instead of
        implementing multiple versions of the algorithm on its own.
        """

        # ...

        print("Context: Sorting data using the strategy (not sure how it'll do it)")
        result = self._strategy.do_algorithm(["a", "b", "c", "d", "e"])
        print(",".join(result))

        # ...


class Strategy(ABC):
    """
    The Strategy interface declares operations common to all supported versions
    of some algorithm.

    The Context uses this interface to call the algorithm defined by Concrete
    Strategies.
    """

    @abstractmethod
    def do_algorithm(self, data: List):
        pass


"""
Concrete Strategies implement the algorithm while following the base Strategy
interface. The interface makes them interchangeable in the Context.
"""


class ConcreteStrategyA(Strategy):
    def do_algorithm(self, data: List) -> List:
        return sorted(data)


class ConcreteStrategyB(Strategy):
    def do_algorithm(self, data: List) -> List:
        return reversed(sorted(data))


if __name__ == "__main__":
    # The client code picks a concrete strategy and passes it to the context.
    # The client should be aware of the differences between strategies in order
    # to make the right choice.

    context = Context(ConcreteStrategyA())
    print("Client: Strategy is set to normal sorting.")
    context.do_some_business_logic()
    print()

    print("Client: Strategy is set to reverse sorting.")
    context.strategy = ConcreteStrategyB()
    context.do_some_business_logic()

The way I understand the property method/decorator is that @property provides an interface for setting the property (temperature) in this case. Naming the property _strategy in the __init__ method implies it should be a private variable. Is this wrong or redundant? I would think that this variable should be named strategy, but its interface should be implemented using private variables (i.e. in the getter/setter)

taken from https://refactoring.guru/design-patterns/strategy/python/example

Edit:

To clarify my reasoning: shouldn't it be possible to change the strategy at runtime like this:

a = Context()
a.strategy = somestrategy

Solution

  • The property isn't named _strategy, it's named strategy, but the internal variable that holds its value is named _strategy.

    This:

        def __init__(self, strategy: Strategy) -> None:
            self._strategy = strategy
    

    Allows:

    c = Context(some_strategy)
    

    But naming it _strategy causes warnings when you try c._strategy.

    And this:

        @property
        def strategy(self) -> Strategy:
            return self._strategy
    

    Then allows:

    my_strategy = c.strategy
    

    The getter returns the value of self._strategy when the property is accessed.

    And finally this:

         @strategy.setter
         def strategy(self, strategy: Strategy) -> None:
            self._strategy = strategy
    

    Allows:

    c.strategy = another_strategy
    

    Making strategy not just a read-only property but a read/write property.

    Note: the code below is not wrong, but it's doing something else:

    class Complex:
        def __init__(self, strategy: int) -> None:
            self.strategy = strategy
    
        @property
        def strategy(self) -> int:
            return self._strategy
    
        @strategy.setter
        def strategy(self, strategy: int) -> None:
            self._strategy = strategy
    
    
    c = Complex(1)
    print(c.strategy)
    

    The difference is that now, the constructor (__init__) is not setting the hidden attribute directly, but it is itself calling the setter for the property.

    If someone now overrides the class, it still uses that, compare:

    class MyClass:
        def __init__(self, a, b: int) -> None:
            self.a = a
            self._b = b
    
        @property
        def a(self) -> int:
            return self._a
    
        @a.setter
        def a(self, a: int) -> None:
            self._a = a
    
        @property
        def b(self) -> int:
            return self._b
    
        @b.setter
        def b(self, b: int) -> None:
            self._b = b
    
    
    class MySubClass(MyClass):
        @MyClass.a.setter
        def a(self, a: int) -> None:
            self._a = a + 10
    
        @MyClass.b.setter
        def b(self, b: int) -> None:
            self._b = b + 10
    
    
    c = MyClass(1, 2)
    print(c.a)
    print(c.b)
    
    s = MySubClass(1, 2)
    print(s.a)
    print(s.b)
    

    Result:

    1
    2
    11
    2
    

    So, it depends if you want someone that inherits your class to be able to change that behaviour.