Software and development · By Sina Esfahani

Design patterns in Python: let’s build a game and see what breaks

Our archers were doing fine until it started raining. Then the players wanted nearby chat, and the prince needed a hat. Let’s see what breaks—and how to fix it.

By Sina Esfahani · Founder, OmniTechs · Updated .

Let’s build a game.

We need archers, knights, a battlefield, and some equipment that makes our hero look considerably more important than he actually is. Nothing too complicated.

At first, everything will work beautifully. Then somebody will ask for one small change.

You already know where this is going.

Along the way, we will discover three Python design patterns: Strategy, Observer, and Decorator. Not by memorising definitions, but by giving our game new requirements and watching our first ideas become uncomfortable.

This story revisits my original Persian explanations and Python exercises in the head-first-design-pattern learning repository. You need a little familiarity with Python classes and methods; the more interesting vocabulary will arrive when we actually need it.

The examples use the standard library and are written for Python 3.10 or newer. Treat each chapter as a separate small script, running its code blocks in order. Later versions replace earlier class definitions. We are building three focused prototypes, not quietly assembling a complete game engine.

Source code: every example below is adapted from sadeghesfahani/head-first-design-pattern on GitHub — clone it, run it, break it yourself.

1. Everything was fine until somebody invented weather

Our first character carries a bow. Our second carries a sword. Both have a name and a level, but they attack and move differently.

That sounds like a perfectly reasonable excuse to write some classes.

class Character:
    def __init__(self, name: str, level: int) -> None:
        self.name = name
        self.level = level


class Archer(Character):
    def attack(self) -> str:
        return f"{self.name} attacks with a bow."

    def move(self) -> str:
        return f"{self.name} moves slowly."


class Knight(Character):
    def attack(self) -> str:
        return f"{self.name} attacks with a sword."

    def move(self) -> str:
        return f"{self.name} moves quickly."
A knight in blue-and-steel armour carrying a sword and shield
One of our early melee knights.
An archer in blue-and-gold armour carrying a bow and a quiver of arrows
One of our early archers.

Look at that. The archer shoots, the knight swings a sword, and both know how to move. We are practically a game studio already.

Since things are going so well, let’s add a royal archer. A more impressive one. Probably better paid.

class RoyalArcher(Character):
    def attack(self) -> str:
        return f"{self.name} attacks with a bow."

    def move(self) -> str:
        return f"{self.name} moves slowly."

Wait a second.

This is the same archer with a more expensive job title. We have copied the attack and movement methods without changing what they do.

Fine. What actually makes the royal archer different? Perhaps a normal archer shoots over a shorter distance and misses more often. The royal archer has better range and accuracy.

Easy. We add attack_range and accuracy to our base character.

Then we look at the knight.

Why does a man carrying a sword have a bow’s accuracy setting? Who gave him that? What happens when another developer assumes it means something?

All right, new plan. We separate ranged characters from melee characters. Archers inherit from one branch, knights from another. Everyone gets the right properties. Order has been restored.

Feeling rather pleased with ourselves, we create 48 different archers.

Then it starts raining.

“It’s only a small weather feature”

We decide that bad weather should reduce an archer’s accuracy. A perfectly reasonable feature for our game.

“No problem,” we say. “I’ll add a weather factor to the attack calculation.”

Then we remember where we put the attack calculation.

Inside the characters.

Inside lots of characters.

Inside 48 different archers whose attack methods we have been copying and adjusting.

I wanted to build a game. Apparently, I have built a full-time job maintaining archers.

The immediate problem is not that inheritance is forbidden. It is that, in our design, the same changing behaviour is scattered across character classes. We keep organising the code around who the character is, even when the thing we need to change is how the character attacks.

Could a shared method in a parent class remove the duplication too? Yes. Inheritance did not sneak into our office and copy those methods. We did.

But what happens when attack, movement, and character identity need to vary independently? An archer might ride a horse. A knight might pick up a bow. Building a family tree for every combination starts to feel like organising a wedding where everyone keeps changing families.

What happens if we separate those two ideas?

Forget the archer for a moment. What does a bow attack do?

A character has a name and a level. An attack has its own rules.

Those rules do not need to live inside every character that uses them. We can give the attack its own object and let the character use it.

First, we agree on one thing: every attack behaviour must provide an attack() method. Then we write the different behaviours behind that common contract.

from abc import ABC, abstractmethod


class AttackBehavior(ABC):
    @abstractmethod
    def attack(self) -> str:
        pass


class RangedAttack(AttackBehavior):
    def __init__(
        self,
        attack_range: int,
        accuracy: float,
        weather_factor: float = 1.0,
    ) -> None:
        if attack_range <= 0:
            raise ValueError("Attack range must be positive.")
        if not 0 <= accuracy <= 1 or not 0 <= weather_factor <= 1:
            raise ValueError("Accuracy and weather factor must be between 0 and 1.")
        self.attack_range = attack_range
        self.accuracy = accuracy
        self.weather_factor = weather_factor

    def effective_accuracy(self) -> float:
        return self.accuracy * self.weather_factor

    def attack(self) -> str:
        return (
            f"Bow attack: range {self.attack_range} m, "
            f"accuracy {self.effective_accuracy():.0%}."
        )


class MeleeAttack(AttackBehavior):
    def __init__(self, damage: int) -> None:
        if damage < 0:
            raise ValueError("Damage cannot be negative.")
        self.damage = damage

    def attack(self) -> str:
        return f"Sword attack: damage {self.damage}."

AttackBehavior is our contract: “Whatever kind of attack you are, provide this operation.” ABC and @abstractmethod prevent us from instantiating this base class, or a subclass that has not implemented its abstract method. They do not check whether a sword attack makes sense; that remains our responsibility.

The -> str annotation describes the expected result. Python does not automatically enforce type annotations at runtime, which is why the constructor also checks the numerical limits we care about.

Our weather model is deliberately simple: take the usual accuracy and multiply it by a factor. An accuracy of 0.9 with a weather factor of 0.5 becomes 45%. We are demonstrating the design, not applying for a job in meteorology.

These methods calculate and describe an attack. They do not roll a random hit, choose a target, or subtract anybody’s health. The calculation is separate from the wording so we can test the number without inspecting a sentence. weather_factor is supplied configuration here, not a live weather service.

What does the character do now?

Almost nothing. Which, in this case, is excellent news.

class Character:
    def __init__(
        self, name: str, level: int, attack_behavior: AttackBehavior
    ) -> None:
        self.name = name
        self.level = level
        self.attack_behavior = attack_behavior

    def attack(self) -> str:
        return f"{self.name}: {self.attack_behavior.attack()}"

The character knows its name, its level, and which attack behaviour it has been given. When we ask it to attack, it passes the work to that behaviour.

Let’s try it. In this version, Character replaces our earlier character hierarchy for the attack demonstration.

archer = Character("Sina", 1, RangedAttack(30, 0.9, 0.5))
print(archer.attack())

archer.attack_behavior = MeleeAttack(25)
print(archer.attack())

Output:

Sina: Bow attack: range 30 m, accuracy 45%.
Sina: Sword attack: damage 25.

Hold on. Our archer is using a sword now?

Yes. His bow broke. Or he ran out of arrows. Or the player pressed the wrong button. Games are full of opportunities for poor decisions.

The important part is that we changed his attack without creating a new character or rewriting the character’s attack() method.

We have just used the Strategy pattern.

Instead of making the character permanently responsible for one attack implementation, we give it an interchangeable attack behaviour. This is composition: the character has an attack behaviour and delegates to it.

Now, when we improve the ranged-attack calculation, there is one shared implementation to update rather than 48 copied versions to hunt down. Different archers can still have different range and accuracy values.

“Fine. Now add magic.”

Of course. We have barely solved the weather, and somebody wants fireballs.

This time, we add another implementation of the same contract:

class MagicAttack(AttackBehavior):
    def attack(self) -> str:
        return "Cast a fireball. Stand somewhere else."


archer.attack_behavior = MagicAttack()
print(archer.attack())

No edit to Character.attack(). No new if weapon == "magic" branch inside the character. We give it a different object, and the same call reaches a different implementation. That interchangeable behaviour is the polymorphism we are using here.

In pattern terminology, Character is the context, AttackBehavior is the strategy interface, and the bow, sword, and magic implementations are concrete strategies. The names describe the roles we have already built; they are not three more things to install.

Someone still has to choose the strategy. That choice belongs in our setup or weapon-selection logic. Strategy relocates that decision; it does not make decisions disappear.

Before the archers celebrate, check the numbers

from math import isclose


assert isclose(RangedAttack(30, 0.9, 0.5).effective_accuracy(), 0.45)
assert isclose(RangedAttack(30, 0.9).effective_accuracy(), 0.9)

sara = Character("Sara", 1, RangedAttack(30, 0.9))
sara.attack_behavior = MeleeAttack(25)
assert sara.attack() == "Sara: Sword attack: damage 25."
assert archer.attack() == "Sina: Cast a fireball. Stand somewhere else."

The last assertion checks something easy to overlook: changing Sara’s selected behaviour does not change Sina’s.

There is a different trap, though. Give two characters the same mutable RangedAttack object, then change that object’s accuracy, and both characters will see the change. Sharing a class is not the same as sharing one instance. Decide whether the configuration belongs to a character, a weapon, or the whole match.

Could we use a function for a simpler attack? Absolutely. Do we need a class for every tiny setting? Please don’t. Two archers with different accuracy numbers can use the same strategy class with different data. A different number is not automatically a different algorithm.

The useful boundary is between behaviours that change independently—not between every two lines of code.

Good. Our characters can fight.

Unfortunately, they are standing in a rather lonely game. Let’s invite some people.

2. Maryam says hello. Who actually hears her?

Twenty players enter the battlefield. We divide them into teams, let them run around, and immediately discover that they want to talk to each other.

An army of soldiers charging across a snowy mountain battlefield toward a fortified camp
Twenty players enter the battlefield — give or take a few hundred extras.

Naturally. We gave them swords, but they would like a chat box.

Team chat looks straightforward. Find the sender’s team, loop through the teammates, and deliver the message.

Then someone asks for nearby chat.

Maryam wants to say something to the players standing beside her. Sara should hear it. Samira should hear it. Sina, who has wandered off to the other end of the map, probably should not.

Now we have a different problem: the list of nearby players changes whenever somebody moves.

We could put all that work inside Player.move(). Update the position, recalculate everyone’s neighbours, update the minimap, refresh a sound system, inform another interface…

Our player only wanted to take three steps. We have made walking responsible for half the game.

What if the battlefield simply announced that something had changed, and the interested parts of the game reacted for themselves?

Put your name down, and we’ll tell you when something changes

Here is the arrangement: anything interested in movement updates registers a function with the battlefield. When a player moves, the battlefield calls those registered functions.

It does not need to know how to draw a minimap or decide whether Sara is close enough to hear Maryam. Those are the listeners’ jobs.

from collections.abc import Callable
from math import dist, isfinite


class Battlefield:
    def __init__(self) -> None:
        self.players: list["Player"] = []
        self._listeners: list[Callable[[], None]] = []

    def subscribe(self, listener: Callable[[], None]) -> None:
        if listener not in self._listeners:
            self._listeners.append(listener)

    def unsubscribe(self, listener: Callable[[], None]) -> None:
        if listener in self._listeners:
            self._listeners.remove(listener)

    def notify(self) -> None:
        for listener in tuple(self._listeners):
            listener()

subscribe() means “call this function when you have an update.” unsubscribe() means “stop calling it.” Each battlefield has its own list, so starting another match does not mix its listeners with this one.

Those lists are created inside __init__ deliberately. Put a mutable roster on the class instead, and instances can end up sharing that same list. Suddenly, someone from Tuesday’s battle appears in Wednesday’s match. That is not time travel; it is shared state.

The tuple(...) gives one notification a snapshot of its listeners. Registering or removing a callback during notification will not change this iteration. A listener removed midway through may still run if it was already in that snapshot; future notifications use the updated list.

Now let’s give every player a little notebook called nearby. Whenever the battlefield announces a change, the player checks the current positions and replaces the notebook’s contents.

class Player:
    def __init__(self, name: str, battlefield: Battlefield) -> None:
        self.name = name
        self.battlefield = battlefield
        self.position = (0.0, 0.0)
        self.nearby: list["Player"] = []
        self._active = True

        battlefield.players.append(self)
        battlefield.subscribe(self.refresh_nearby)
        battlefield.notify()

    def move(self, x: float, y: float) -> None:
        if not self._active:
            raise RuntimeError("This player has left the battlefield.")
        if not isfinite(x) or not isfinite(y):
            raise ValueError("Coordinates must be finite numbers.")
        self.position = (x, y)
        self.battlefield.notify()

    def refresh_nearby(self) -> None:
        if not self._active:
            return
        self.nearby = [
            player
            for player in self.battlefield.players
            if player is not self
            and dist(self.position, player.position) < 5
        ]

    def send_nearby_message(self, message: str) -> None:
        if not self._active:
            raise RuntimeError("This player has left the battlefield.")
        for recipient in [self, *self.nearby]:
            print(f"{recipient.name}'s chat | {self.name}: {message}")

    def leave(self) -> None:
        if not self._active:
            return
        self._active = False
        self.battlefield.unsubscribe(self.refresh_nearby)
        self.battlefield.players.remove(self)
        self.nearby.clear()
        self.battlefield.notify()

When a player joins, they register refresh_nearby as their reaction to an update. When somebody moves, the battlefield notifies its listeners, and each player recalculates who is nearby.

Notice subscribe(self.refresh_nearby), without parentheses after the method name. We hand over something to call later. Writing self.refresh_nearby() would call it immediately and hand over its result instead.

Notice that the moving player receives the update too. Maryam’s own view of her neighbours needs to change when she walks away—not just everybody else’s view of Maryam.

We are using a radius of less than five game units. The player is excluded from their own nearby list, but we display their outgoing message in their own chat box separately.

Otherwise, Maryam types “hello,” sees nothing, and immediately files our first bug report.

Let’s put four people on the map

battlefield = Battlefield()

sina = Player("Sina", battlefield)
sara = Player("Sara", battlefield)
samira = Player("Samira", battlefield)
maryam = Player("Maryam", battlefield)

sina.move(100, 100)
sara.move(5, 5)
samira.move(7, 7)
maryam.move(8, 8)

maryam.send_nearby_message("Hello to everyone nearby!")

Output:

Maryam's chat | Maryam: Hello to everyone nearby!
Sara's chat | Maryam: Hello to everyone nearby!
Samira's chat | Maryam: Hello to everyone nearby!

No message for Sina. He is too far away.

Nothing personal, Sina. Come back to the group.

Now Maryam can move somewhere else, and the next notification rebuilds the nearby lists from the new positions. We are not repeatedly adding names to an old list and hoping nobody notices the duplicates.

That is the Observer pattern at work: a subject lets observers subscribe and notifies them when a relevant change occurs.

The battlefield is the source of the notification. The registered callbacks are the observers’ reactions. Here, the announcement says only that something changed; each player reads the current positions to work out what that means for them.

Maryam, a character in dark leather and hooded armour, aiming a rifle-like weapon
Maryam — the player whose message we just traced through the nearby list.

A minimap could register its own refresh function without making the battlefield learn how to draw. When that screen closes, its listener can unsubscribe without removing a player from the match. Those are different responsibilities.

Maryam walks away. Samira logs out.

Let’s check that “nearby” actually means nearby, rather than “someone I met earlier and never removed.”

assert {player.name for player in maryam.nearby} == {"Sara", "Samira"}

maryam.move(50, 50)
assert maryam.nearby == []
assert maryam not in sara.nearby

maryam.move(8, 8)
samira.leave()
assert {player.name for player in maryam.nearby} == {"Sara"}

That leave() method has two jobs: remove the player from the match and remove their subscription. Then it announces the change so everyone else updates their nearby list.

Only unsubscribe, and Samira remains in the roster. Only remove her from the roster, and the battlefield still holds her callback. We need to clean up both relationships. Calling leave() twice is harmless in this version; moving or messaging after leaving raises an error. Samira has logged out, not acquired supernatural powers.

Twenty players are fine. What about twenty thousand?

Here is the bill for our simple implementation: one movement asks every player to scan the roster. With n players, that is roughly distance checks per movement. Observer organised the communication; it did not make the work inside each callback cheaper.

For a larger version, I would investigate tracking only affected neighbours or grouping positions into spatial cells. I would not add that machinery to this lesson before measuring the actual need.

We have not secretly built a multiplayer server, either. This prototype runs callbacks immediately, one after another. A slow callback delays the next one; an exception stops this notification loop. There is no message queue, persistence, retry policy, or thread-safety mechanism hidden in notify().

For a UI refresh, skipping a failed listener and reporting the error might be acceptable. For a required game-state update, silently skipping it might leave the world inconsistent. That is a decision to make explicitly, not something the pattern chooses for us.

Our notification says only “something changed,” and the players read current state. An alternative would pass a movement event containing the player and old and new positions. That could reduce unnecessary work, but the listeners would then depend on that event’s structure. We have separated responsibilities, not eliminated every dependency.

Use Observer for reactions that can stand on their own. When steps must happen in a required sequence, make that workflow explicit instead of relying on a lucky subscription order.

Right. Our players can fight, move, and talk.

There is just one embarrassing problem left.

3. A prince cannot go into battle dressed like that

Our hero is called LovelyPrince.

He starts with 100 health, 10 power, and absolutely no armour. I admire his confidence. I am less impressed by his preparation.

An unequipped hero in a plain shirt and trousers, before any equipment is added
LovelyPrince, before we give him any equipment.
class Hero:
    def __init__(self, name: str) -> None:
        self.name = name

    def get_health(self) -> int:
        return 100

    def get_power(self) -> int:
        return 10

    def get_armor(self) -> int:
        return 0

    def get_equipments(self) -> str:
        return "No equipment"

As he progresses through the game, he finds better equipment. Let’s start with a Prince Hat and some Prince Gloves.

The hat adds 300 health, 100 power, and 5 armour. The gloves add 100 health, 3 power, and 1 armour.

Yes, the hat is absurdly powerful. We are learning software design, not balancing the game economy.

from dataclasses import dataclass


@dataclass(frozen=True)
class Equipment:
    name: str
    health: int
    power: int
    armor: int


hat = Equipment("Prince Hat", health=300, power=100, armor=5)
gloves = Equipment("Prince Gloves", health=100, power=3, armor=1)

@dataclass saves us writing the equipment constructor ourselves. frozen=True blocks ordinary reassignment of those fields after creation; it is not a promise that every possible object inside a frozen dataclass becomes deeply immutable.

How should we put them on him?

We could create a HeroWithHat class. Then a HeroWithGloves class. Then a HeroWithHatAndGloves class.

Then somebody adds boots.

I have already spent one chapter maintaining archers. I refuse to spend the rest of my life naming every possible outfit.

What if we leave the original hero alone and put something around him instead?

The hat does not replace the hero. It wraps him.

Imagine asking the hat-wearing hero for his power.

The hat wrapper asks the hero underneath, “How much power do you have?” The hero answers, “Ten.” The hat adds its 100 and returns 110.

Now put the gloves around that result.

The gloves ask the object underneath for its power. That object is already the hat-wearing hero, so the answer is 110. The gloves add 3 and return 113.

Each layer does its own little job. Nobody needs to know the entire wardrobe.

Let’s write down the questions any wrapped hero must answer:

from typing import Protocol


class HeroView(Protocol):
    name: str

    def get_health(self) -> int: ...
    def get_power(self) -> int: ...
    def get_armor(self) -> int: ...
    def get_equipments(self) -> str: ...


class WearEquipment:
    def __init__(self, hero: HeroView, equipment: Equipment) -> None:
        self.hero = hero
        self.equipment = equipment
        self.name = hero.name

    def get_health(self) -> int:
        return self.hero.get_health() + self.equipment.health

    def get_power(self) -> int:
        return self.hero.get_power() + self.equipment.power

    def get_armor(self) -> int:
        return self.hero.get_armor() + self.equipment.armor

    def get_equipments(self) -> str:
        return f"{self.hero.get_equipments()} -> {self.equipment.name}"

The wrapper offers the same methods we use to ask the original hero about health, power, armour, and equipment. That is why another wrapper can sit around it: from the outside, we can ask the same questions.

For this Python example, that shared set of methods is what matters; the wrapper does not need to inherit from the concrete Hero class. HeroView is a protocol: it describes the structure a type checker expects, rather than requiring every compatible object to have the same parent class. It is not automatic runtime validation.

That is also why the wrapper accepts HeroView, rather than a list of every concrete wrapper we might eventually invent. Give it an object that satisfies the contract, and it can ask the same questions.

Let’s finally get our prince dressed.

A fully equipped hero, now visibly armoured and battle-ready
LovelyPrince, wrapped in a Prince Hat and Prince Gloves.
hero = Hero("LovelyPrince")
with_hat = WearEquipment(hero, hat)
fully_equipped = WearEquipment(with_hat, gloves)

print(fully_equipped.get_health())
print(fully_equipped.get_power())
print(fully_equipped.get_armor())
print(fully_equipped.get_equipments())

Output:

500
113
6
No equipment -> Prince Hat -> Prince Gloves

There we go. A reasonably threatening prince.

His health is 100 + 300 + 100 = 500. His power is 10 + 100 + 3 = 113. His armour is 0 + 5 + 1 = 6.

The original hero still returns its original statistics. We have not changed its methods or quietly overwritten its base values. We have built something around it.

That is the Decorator pattern.

A wrapper keeps the interface its callers expect while adding something to what the wrapped object does. Because the next wrapper can use that same interface, the layers can be combined.

The pattern here is WearEquipment wrapping another object. Python’s @decorator syntax applies a transformation to a function or class definition; that language feature and this object-wrapping pattern are related ideas, but not interchangeable terms. Here, @dataclass creates useful class methods, while WearEquipment is the object decorator.

One game rule also needs a name: get_health() here means total health capacity. It is not the prince’s current health after being hit. Otherwise, asking for his statistics could appear to heal him to 500 every time. Damage and recovery need separate state.

Does it matter which item goes on first?

With our flat bonuses, the numerical totals are the same: 10 + 100 + 3 equals 10 + 3 + 100. The equipment description still records the wrapping order.

Now imagine a blessing that doubles the power of whatever it wraps.

Put it outside the hat-wearing hero: (10 + 100) × 2 = 220.

Put the hat outside the blessed base hero: (10 × 2) + 100 = 120.

Same hero. Same two extras. Very different result.

The lesson is not “decorators always commute” or “order always changes the result.” It is know what each wrapper does before deciding whether its position matters. Our additive equipment happened to make the arithmetic forgiving.

“Lovely. Now take the hat off.”

Excellent question. I was hoping nobody would ask.

For this small example, rebuild the combination using the original hero and the gloves, without the hat:

without_hat = WearEquipment(hero, gloves)

assert without_hat.get_power() == 13
assert fully_equipped.get_power() == 113
assert hero.get_power() == 10

We made a new combination. We did not reach inside the old one and remove anything. These assertions check both the new result and the fact that the existing objects still answer as before.

A proper inventory would also need rules about equipment slots, repeated items, and saving the result. Our wrapper happily lets the prince wear two hats. A shared interface is not a fashion police department.

A wrapper chain is not automatically the best answer to all of those requirements. Sometimes an equipment list and an explicit statistics calculation would be easier to manage.

That does not make the example useless. It tells us what the pattern solves: adding behaviour through compatible layers. It does not promise to solve every future wardrobe emergency.

Wait. Aren’t Strategy and Decorator both “put an object inside another object”?

Yes. Similar building blocks, different jobs.

Our character delegates an operation to a selected Strategy. Choosing another attack changes how that operation is performed.

Our equipment Decorator keeps the hero’s interface and forwards calls through itself, adding a contribution to the object underneath. It changes what surrounds or extends the original behaviour.

Observer answers another question altogether: who should hear about a change?

What each pattern solved in our game
What happened in our game?What needed separating?Pattern we used
A character needed different attacks.The character and its selected attack behaviour.Strategy
Movement needed several independent reactions.The announcement and the listeners’ work.Observer
Equipment needed stackable bonuses.The base hero and the optional additions.Decorator

You can use all three in one application. You do not have to. Their value comes from the responsibilities they separate, not the number of pattern names in your README.

Your turn: the next three feature requests

The archer found poisoned arrows. Add a new attack behaviour without editing Character.attack(). Then ask whether poison is truly a different attack strategy or an effect added to an existing attack. Explain your choice before writing another class.

Maryam is standing exactly five units away. Should she hear the message? Our code uses < 5, so the answer is no. Write a test for that boundary, move her slightly closer, and check both players’ nearby lists. Also check that moving repeatedly does not duplicate a recipient.

LovelyPrince found boots and a belt. Add their bonuses, verify the totals, and remove the hat while keeping everything else. Then decide whether rebuilding wrappers or managing an equipment list makes the next requirement easier.

A hand-drawn-style illustration inviting the reader to try the next exercise themselves
Your turn — with the tools from the strategy chapter.

There is no prize for forcing every request into today’s pattern. “This is simpler as data” can be a very good answer.

We started with a game. We ended up with three patterns.

We did not start by drawing three impressive diagrams and looking for somewhere to use them.

We started with an archer. Then the attack rules changed, and we needed a way to separate the behaviour from the character. That led us to Strategy.

We put players on a battlefield. Then movement needed to update interested parts of the game without making one method responsible for everything. That led us to Observer.

We gave our hero equipment. Then the combinations started multiplying, and we needed a way to add bonuses without creating a subclass for every outfit. That led us to Decorator.

The names are useful. They let us discuss the ideas without retelling the entire adventure every time.

But memorising the names is not the interesting part. The interesting part is noticing when the code is fighting the change you need to make—and finding a better place for that change to live.

Outside the castle, imagine replacing the attack choices with genuinely different pricing calculations, movement updates with order-status notifications, and equipment wrappers with optional timing or logging around a report generator. Those are possible applications of the same ideas, not reasons to install all three patterns in every business tool.

That is the question I would bring to a software project at OmniTechs: when this requirement changes, what else should have to change with it?

Sometimes the answer is a strategy. Sometimes it is a small function. Sometimes the code is already clear and we should stop rearranging it.

Our job is not to make simple software look impressive. It is to keep useful software understandable as the requirements grow.

Now go and test those boots. LovelyPrince has survived a rainstorm, a chat-system rewrite, and three architecture lessons. It would be unfortunate to lose him to footwear.

Have a codebase that fights every change you need to make?

Tell us what keeps changing in your product — pricing rules, integrations, notification channels. We will tell you honestly whether that calls for a pattern, a small function, or nothing at all.