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
| Word | What it means |
|---|---|
| Design pattern | a named solution to a common design problem |
| Strategy | a swappable object that holds one way of doing a job |
| Factory | a function that chooses which object to make |
| Decorator | a wrapper that adds behaviour to another object |
| Interface | the 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
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 resultMake a folder for this lesson
mkdir -p ~/oop5Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Go into it
cd ~/oop5Shown 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
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.pyShown 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
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
| Strategy | Try 1 | Try 2 | Try 3 | Try 4 |
|---|---|---|---|---|
| FixedDelay(5) | 5 | 5 | 5 | 5 |
| DoublingDelay(2) | 2 | 4 | 8 | 16 |
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
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
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.pyShown 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
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
python3 -c "from factory import make_delay; make_delay('random', 1)" 2>&1 | tail -n 1Shown 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
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 waitWrite it to capped.py
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.pyShown 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
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.