Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Factory, strategy and decorator patterns

After this lesson: I can apply a factory, a strategy and a decorator.

60 min lesson9 commandsEveryday Linux commands lab: the thirty commands engineers type from memory

A design pattern is a named solution

A design pattern is a named, proven solution to a problem that keeps coming up in object-oriented code. Three common ones are the strategy, the factory and the decorator. Knowing them lets you pick a pattern for the problem in front of you, instead of inventing a new design each time.

By the end of this lesson

Note

I can apply a factory, a strategy and a decorator.

Key words

WordWhat it means
Design patterna named solution to a common design problem
Strategya swappable object that holds one way of doing a job
Factorya function that chooses which object to make
Decoratora wrapper that adds behaviour to another object
Interfacethe methods other code is allowed to call

The cells students fill in are left blank in this preview.

Start with the problem and then the pattern

A program that calls a web service sometimes fails. The usual answer is to wait and try again. How long to wait is a choice: the same wait every time, or a wait that doubles.

That choice is a good fit for the strategy pattern. Put each way of waiting in its own object, and let the retrying code hold whichever one it is given.

Two delay strategies and a Retrier

Code: python
class FixedDelay:
    def __init__(self, seconds):
        self.seconds = seconds

    def delay(self, attempt):
        return self.seconds

class DoublingDelay:
    def __init__(self, first):
        self.first = first

    def delay(self, attempt):
        wait = self.first
        for step in range(attempt - 1):
            wait = wait * 2
        return wait

class Retrier:
    def __init__(self, strategy):
        self.strategy = strategy

    def waits(self, tries):
        result = []
        for attempt in range(1, tries + 1):
            result.append(self.strategy.delay(attempt))
        return result

Make a folder for this lesson

Command
mkdir -p ~/oop5

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Go into it

Command
cd ~/oop5

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the classes to retry.py

Command
printf 'class FixedDelay:\n    def __init__(self, seconds):\n        self.seconds = seconds\n\n    def delay(self, attempt):\n        return self.seconds\n\nclass DoublingDelay:\n    def __init__(self, first):\n        self.first = first\n\n    def delay(self, attempt):\n        wait = self.first\n        for step in range(attempt - 1):\n            wait = wait * 2\n        return wait\n\nclass Retrier:\n    def __init__(self, strategy):\n        self.strategy = strategy\n\n    def waits(self, tries):\n        result = []\n        for attempt in range(1, tries + 1):\n            result.append(self.strategy.delay(attempt))\n        return result\n' > retry.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Print the waits for four tries

Command
python3 -c "from retry import Retrier, FixedDelay, DoublingDelay; print(Retrier(FixedDelay(5)).waits(4)); print(Retrier(DoublingDelay(2)).waits(4))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Worked example: seconds to wait before each try

StrategyTry 1Try 2Try 3Try 4
FixedDelay(5)5555
DoublingDelay(2)24816

The cells students fill in are left blank in this preview.

A factory chooses which object to make

Where does the strategy come from? Often from a setting, such as the word "doubling" in a configuration file.

A factory is a function that turns that word into the right object. The code that needs a delay calls the factory and never names a class. An unknown word is refused with a clear error.

A factory function for delays

Code: python
from retry import FixedDelay, DoublingDelay

def make_delay(kind, seconds):
    if kind == "fixed":
        return FixedDelay(seconds)
    if kind == "doubling":
        return DoublingDelay(seconds)
    raise ValueError("unknown delay: " + kind)

Write it to factory.py

Command
printf 'from retry import FixedDelay, DoublingDelay\n\ndef make_delay(kind, seconds):\n    if kind == "fixed":\n        return FixedDelay(seconds)\n    if kind == "doubling":\n        return DoublingDelay(seconds)\n    raise ValueError("unknown delay: " + kind)\n' > factory.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Ask the factory for a doubling delay

Command
python3 -c "from factory import make_delay; print(make_delay('doubling', 2).delay(3))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Ask for a kind that does not exist

Command
python3 -c "from factory import make_delay; make_delay('random', 1)" 2>&1 | tail -n 1

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A decorator wraps an object to add behaviour

Suppose waits must never pass ten seconds. You could edit both strategies. A decorator does it without editing either.

Capped has a delay method like a strategy, so it can stand where a strategy stands. It holds another strategy inside, asks it, and trims the answer.

A decorator that caps any delay

Code: python
class Capped:
    def __init__(self, inner, most):
        self.inner = inner
        self.most = most

    def delay(self, attempt):
        wait = self.inner.delay(attempt)
        if wait > self.most:
            return self.most
        return wait

Write it to capped.py

Command
printf 'class Capped:\n    def __init__(self, inner, most):\n        self.inner = inner\n        self.most = most\n\n    def delay(self, attempt):\n        wait = self.inner.delay(attempt)\n        if wait > self.most:\n            return self.most\n        return wait\n' > capped.py

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Cap a doubling delay at ten

Command
python3 -c "from retry import Retrier, DoublingDelay; from capped import Capped; print(Retrier(Capped(DoublingDelay(2), 10)).waits(5))"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A common mistake

Warning

Many people learn a pattern and then look for somewhere to use it. The problem should come first. A pattern adds classes, so use one only when you can name the change it makes easier, such as swapping a delay.

About this lab and its source: Everyday Linux commands lab: the thirty commands engineers type from memory

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.