<< Home

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)
💡 Keep the interface tiny. A single method on the 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})
⚠️ Keep events stable. Changing event shapes breaks observers silently. Version the payload or add fields with defaults to protect consumers.

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.

💡 Move behaviour in thin slices with tests after each slice. Many small steps keep the code base calm and predictable.

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)
⚠️ Keep the Facade small. If it starts sprouting rarely used methods, split it by use case and expose multiple focused facades.