<< Home

Chapter 6: Modern Variations and Hybrid Patterns

Modern codebases blend classical patterns with pragmatic techniques. The goal is to keep systems flexible, testable, and easy to evolve. These variations focus on wiring, decoupling, and shaping behavior through explicit composition rather than inheritance heavy hierarchies.

Dependency Injection

Dependency Injection supplies required collaborators from the outside, instead of letting an object construct them. This keeps creation separate from use, which improves testability and swapability of implementations.

Intent

Invert control of construction so that classes declare what they need and a composer provides instances at the boundary using __init__(deps) or decorators such as @Inject in languages that support them.

Structure

Classes expose dependencies as constructor parameters or properties. A composition root builds the object graph. You can pass dependencies directly, use a factory, or ask a container to resolve them through resolve(type).

class Emailer:
  def send(self, to, body): print(f"email to {to}: {body}")

class SignupService:
  def __init__(self, emailer): self.emailer = emailer
  def signup(self, user):
    # create user …
    self.emailer.send(user, "Welcome")

emailer = Emailer()
service = SignupService(emailer)
service.signup("robin@example.com")
💡 Keep a single composition root near application start. All wiring lives there, which simplifies tests and minimizes surprises.

Considerations

Prefer constructor injection for required collaborators; reserve setter injection for optional ones. Avoid hiding dependencies behind global singletons; explicit parameters communicate intent better.

Service Locator

Service Locator provides a registry to look up services at runtime using get(name). It centralizes access to shared services such as logging, configuration, or transport clients.

Intent

Offer a simple way to retrieve services without passing them through every call. This can reduce parameter noise when code is already deeply nested.

Example

class Services:
  _map = {}
  @classmethod
  def register(cls, name, svc): cls._map[name] = svc
  @classmethod
  def get(cls, name): return cls._map[name]

class Logger:
  def log(self, msg): print("LOG:", msg)

Services.register("logger", Logger())

def process(item):
  Services.get("logger").log(f"processing {item}")
  # work …

process("order-123")
⚠️ Service Locator can hide dependencies. Excessive use makes code harder to reason about and to test. Prefer Dependency Injection when you can wire dependencies explicitly.

Variations

Use typed keys or generics instead of raw strings for safer lookups. Scope the locator per module to avoid a single global registry that grows without control.

Event Bus

An Event Bus decouples senders and receivers with asynchronous notifications. Publishers emit events through publish(event); subscribers register callbacks through subscribe(type, handler). No participant needs to know who sits on the other side.

Intent

Promote loose coupling and horizontal scaling by turning direct calls into events. This fits analytics, audit trails, cache invalidation, and background workflows.

Example

class EventBus:
  def __init__(self): self._subs = {}
  def subscribe(self, etype, fn): self._subs.setdefault(etype, []).append(fn)
  def publish(self, etype, event):
    for fn in self._subs.get(etype, []):
      fn(event)

bus = EventBus()

def on_user_signed_up(evt): print("welcome flow for", evt["user"])
bus.subscribe("UserSignedUp", on_user_signed_up)

bus.publish("UserSignedUp", {"user": "robin", "meta": {"ip": "…"}})
💡 Model events as immutable records such as {type: "UserSignedUp", id: …, occurred_at: …, data: …}. This helps with replay and audit.

Considerations

Event flow can be hard to trace. Add correlation identifiers to link related events. When delivery matters, use durable queues; in-memory buses are best for simple apps or local plugins.

Composition Over Inheritance

Composition builds behavior by assembling objects that collaborate, rather than by stacking classes in a deep hierarchy. This keeps parts small and swappable, and avoids the fragility that can come from inheritance chains.

Intent

Prefer a has_a relationship over is_a when you want to vary behavior at runtime or only share a small slice of functionality.

Example

class CacheLayer:
  def get(self, k): return None
  def set(self, k, v): pass

class RetryLayer:
  def __init__(self, inner, attempts=3): self.inner, self.attempts = inner, attempts
  def request(self, op):
    for i in range(self.attempts):
      try: return self.inner.request(op)
      except Exception: 
        if i + 1 == self.attempts: raise

class HttpClient:
  def request(self, op): 
    # perform HTTP …
    return "ok"

client = RetryLayer(HttpClient())
# Later swap in a caching decorator without touching callers:
client = RetryLayer(HttpClient())  # then wrap with CacheLayer if it supported request()
⚠️ Composition still needs clear interfaces. Define small protocols such as request(op) or process(item) so layers remain compatible.

Patterns Together

These ideas mix well. Use Dependency Injection to assemble objects; expose a Service Locator sparingly for legacy seams; route side effects through an Event Bus; prefer composition to extend behavior without subclassing. This combination supports testing, change, and clarity.