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")
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")
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": "…"}})
{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()
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.