Chapter 15: Refactoring Toward Patterns
Refactoring toward patterns is not about forcing shapes onto code. It is about revealing the design that was already trying to speak. In this chapter we take realistic code smells and reshape them into clear structures using Strategy, Observer, Facade and dependency injection DI. Each section shows how to move step by step, keeping tests green while the design grows simpler.
Evolving Conditionals Into Strategy
Large conditional blocks are like tall hedges that hide small pathways. The Strategy pattern trims those hedges so each path stands on its own. We replace nested conditionals with interchangeable behaviours that share a clean interface.
Recognise the Smell
You often see a tangle of if and elif statements around the same decision. Each branch tweaks similar values and returns related results. This repetition signals that a single concept is wearing many costumes.
Extract the Policy Interface
Define a small contract that captures what varies. The client should depend on a calculate or apply shaped method rather than many branches. The name of the method frames intent and guides the refactor.
Move Branch Logic Into Strategies
Create one class per former branch. Keep each class tiny. If the behaviours share helpers, pull those up into a common utility. Tests help confirm that each variant produces the same outputs as the original branch.
# Before
def shipping_cost(order, region):
if region == "domestic":
return order.weight * 1.0
elif region == "eu":
return order.weight * 1.3 + 5
elif region == "intl":
return order.weight * 1.8 + 12
return 0
# After: Strategy
class ShippingStrategy:
def cost(self, order): raise NotImplementedError
class Domestic(ShippingStrategy):
def cost(self, order): return order.weight * 1.0
class EU(ShippingStrategy):
def cost(self, order): return order.weight * 1.3 + 5
class International(ShippingStrategy):
def cost(self, order): return order.weight * 1.8 + 12
class Shipper:
def __init__(self, strategy: ShippingStrategy):
self.strategy = strategy
def cost(self, order): return self.strategy.cost(order)
Strategy is often enough; more methods suggest multiple strategies or a forgotten helper.
Wire With Factories or DI
Construct the right Strategy at the edges of the system. Use a small factory or supply the strategy via DI. The core stays branch free while composition chooses behaviour.
Growing a Notifier Into Observer
Ad hoc callbacks start simple then turn into vines. The Observer pattern trains those vines onto a trellis. We introduce a subject that manages subscribers, and observers that react to events without tight coupling.
Identify Implicit Subscribers
Look for code that calls a hard coded list of handlers or toggles flags for other components. These are observers in disguise. They want a formal subscribe and notify flow.
Introduce Subject and Subscription API
Create a subject that exposes subscribe, unsubscribe and notify. Move direct calls to go through the subject. Keep payloads small and well named so tests read cleanly.
# Before
def save(user):
db.write(user)
analytics.track("user_saved", {"id": user.id})
cache.invalidate(f"user:{user.id}")
# After: Observer
class UserSavedSubject:
def __init__(self): self._observers = []
def subscribe(self, obs): self._observers.append(obs)
def unsubscribe(self, obs): self._observers.remove(obs)
def notify(self, event):
for o in list(self._observers): o.update(event)
class AnalyticsObserver:
def update(self, event): analytics.track("user_saved", {"id": event["id"]})
class CacheObserver:
def update(self, event): cache.invalidate(f"user:{event['id']}")
def save(user, subject: UserSavedSubject):
db.write(user)
subject.notify({"id": user.id})
Test With Spies
Attach a spy observer that records calls and arguments. Verify order and count. This confirms that the subject only broadcasts the intended sequence and that unsubscribed observers no longer receive updates.
Breaking Down a God Object
The God Object hoards Knowledge, Orchestration and Storage. Untangling it is like moving from a cluttered garage to labelled shelves. We slice by responsibility, tease apart data and behaviour, then reconnect pieces through clear interfaces.
Map Responsibilities
List everything the object knows and does. Group by capability such as validation, persistence, calculation and coordination. This map becomes your cut plan.
Extract Cohesive Types
For each group, create a small type with a single focus. Move methods and the fields they truly need. Leave behind only what still belongs. Aim for crisp names that match the capability, not the old class.
# Before: one giant class UserService {...} handles …
# After: split into UserRepository, PasswordPolicy, ProfileComposer, UserService
Rewire Through DI and Facades
Let the high level service depend on interfaces for its new collaborators. Supply them via DI. If callers previously touched the giant surface, place a small Facade in front that routes to the new parts while clients slowly migrate.
Blending Facade, Strategy, and DI
Complex subsystems benefit from a welcoming entry point plus pluggable behaviours underneath. A Facade presents a tidy surface, Strategy provides variation, and DI composes the pieces without hidden construction.
Define the Facade Contract
Capture the most common tasks behind a simple API. The facade hides wiring and advanced options while still allowing access to the underlying parts for expert paths.
Insert Strategies Behind the Facade
Where behaviour varies, depend on a Strategy interface. The facade delegates to the selected strategy. This avoids new conditionals as features grow and keeps tests focused on single behaviours.
Compose With DI at the Edges
Construct concrete strategies and collaborators at composition time. Inject them into the facade. Tests can supply fakes and spies to verify orchestration without invoking heavy dependencies.
# Sketch
class ImageFacade:
def __init__(self, loader, saver, scaler_strategy):
self.loader = loader
self.saver = saver
self.scaler = scaler_strategy
def resize(self, path, out, size):
img = self.loader.load(path)
img2 = self.scaler.scale(img, size)
self.saver.save(img2, out)
Facade small. If it starts sprouting rarely used methods, split it by use case and expose multiple focused facades.