Design Patterns and Reusable SolutionsIntroductionSoftware systems rarely fail because programmers cannot write individual classes or functions. They fail because changing one part unexpectedly breaks another, because responsibilities are distributed poorly, or because similar problems are solved inconsistently. Design patterns provide named, tested approaches to recurring design problems. They are not libraries, frameworks, or rigid templates. A pattern is a documented relationship among a context, a problem, forces or trade-offs, and a reusable solution. This chapter examines patterns used in object-oriented Java and Python development. It focuses on nine patterns: Factory, Builder, Adapter, Decorator, Facade, Strategy, Observer, Command, and Dependency Injection. These patterns are grouped as creational, structural, and behavioral patterns. The grouping is useful, but the boundaries are conceptual rather than absolute. For example, dependency injection concerns object creation and collaboration, while also influencing the structure and testability of a system. Patterns should be used to reduce design friction, not to demonstrate sophistication. A two-line conditional should not become a hierarchy of factories and strategies merely because a pattern exists. The best design is usually the simplest one that keeps likely change isolated, makes responsibilities clear, and remains easy to test. Learning ObjectivesBy the end of this chapter, you should be able to: - explain the purpose and trade-offs of creational, structural, and behavioral patterns;
- implement Factory, Builder, Adapter, Decorator, Facade, Strategy, Observer, Command, and Dependency Injection in Java or Python;
- select a pattern based on a concrete design problem rather than its popularity;
- distinguish composition from inheritance and identify the effect on coupling;
- evaluate pattern implementations for extensibility, testability, readability, and performance;
- recognize pattern overuse and simplify designs when a pattern adds unnecessary complexity.
1. Patterns as Design KnowledgeA pattern generally contains four elements: - Name: a shared vocabulary for discussing a design.
- Context: the situation in which the problem occurs.
- Problem and forces: the competing requirements, such as flexibility versus simplicity.
- Solution and consequences: the structure that resolves the problem and its costs.
Patterns are related to several broader principles. The Single Responsibility Principle suggests that a class should have one primary reason to change. The Open–Closed Principle encourages designs that can be extended without repeatedly modifying stable code. The Dependency Inversion Principle recommends that high-level policy depend on abstractions rather than concrete implementation details. These principles are guides, not laws; they must be balanced against readability and project scale. A useful decision sequence is: - What change or variation is causing difficulty?
- Which object or module should own that decision?
- Can ordinary functions, parameters, or a small class solve it?
- If not, which pattern isolates the variation most clearly?
- What new classes, indirection, configuration, or failure modes will the pattern introduce?
2. Creational PatternsCreational patterns control how objects are created. They are valuable when construction is conditional, complex, expensive, or likely to change. 2.1 FactoryA Factory centralizes or encapsulates object creation. The client requests an abstraction and does not need to know which concrete class is instantiated. Suppose an order service sends notifications through email or SMS. Direct construction scatters decisions throughout the code: class EmailNotifier:
def send(self, message):
print(f"Email: {message}")
class SmsNotifier:
def send(self, message):
print(f"SMS: {message}")
def create_notifier(channel):
if channel == "email":
return EmailNotifier()
if channel == "sms":
return SmsNotifier()
raise ValueError("Unsupported channel")The client depends on the factory and the common operation: notifier = create_notifier("email")
notifier.send("Order confirmed")In Java, a factory may return an interface: interface Notifier { void send(String message); }
final class NotifierFactory {
static Notifier create(String channel) {
return switch (channel) {
case "email" -> new EmailNotifier();
case "sms" -> new SmsNotifier();
default -> throw new IllegalArgumentException("Unsupported channel");
};
}
}A factory is appropriate when creation logic is repeated, when callers should not depend on concrete classes, or when construction requires validation and configuration. A factory is not automatically appropriate for every constructor. If a class has a transparent, stable constructor, hiding it behind a factory can make the code harder to follow. A Factory Method places creation in a method that subclasses or implementations can vary. A simple factory is usually a function or class with conditional logic. These terms are often used loosely, so precise documentation matters more than the label. 2.2 BuilderThe Builder pattern separates the construction of a complex object from its final representation. It is particularly useful when an object has many optional parameters, when construction involves validation, or when readable configuration is important. class Report:
def __init__(self, title, author, sections, confidential=False):
self.title = title
self.author = author
self.sections = sections
self.confidential = confidential
class ReportBuilder:
def __init__(self, title, author):
self._title = title
self._author = author
self._sections = []
self._confidential = False
def add_section(self, text):
self._sections.append(text)
return self
def confidential(self, value=True):
self._confidential = value
return self
def build(self):
if not self._title.strip():
raise ValueError("Title is required")
if not self._sections:
raise ValueError("At least one section is required")
return Report(self._title, self._author,
tuple(self._sections), self._confidential)Usage is expressive: report = (ReportBuilder("Annual Review", "A. Singh")
.add_section("Performance")
.add_section("Risks")
.confidential()
.build())Builders can support immutable products because all required decisions are completed before construction. Their costs include additional code, mutable builder state, and possible duplication of validation. In Python, keyword-only arguments, dataclasses, or a well-designed constructor may be a simpler alternative. In Java, builders are especially useful for immutable classes with many optional attributes. 2.3 Dependency InjectionDependency Injection (DI) means that an object receives its collaborators from outside rather than constructing them internally. It is a technique and architectural practice rather than a classical pattern with one fixed structure. Without DI: class OrderService:
def __init__(self):
self.repository = SqlOrderRepository()
self.payment = StripePayment()Testing requires real infrastructure or intrusive replacement. With constructor injection: class OrderService:
def __init__(self, repository, payment):
self.repository = repository
self.payment = payment
def place(self, order):
self.payment.charge(order.total)
self.repository.save(order)A test can provide substitutes: service = OrderService(InMemoryOrderRepository(), FakePayment()) Java commonly expresses DI through interfaces and constructors: final class OrderService {
private final OrderRepository repository;
private final PaymentGateway payment;
OrderService(OrderRepository repository, PaymentGateway payment) {
this.repository = repository;
this.payment = payment;
}
}Constructor injection makes required dependencies visible and supports immutable fields. Setter injection can suit optional or replaceable dependencies, but it permits partially initialized objects. Service locators and global registries hide dependencies and often reduce testability, even when they appear convenient. DI does not mean injecting every primitive value or creating an abstraction for every class. Inject boundaries that represent variation, external systems, policy, or expensive resources. A small value object can usually be constructed directly. 3. Structural PatternsStructural patterns describe how objects and classes are composed. They help integrate incompatible interfaces, add responsibilities, or simplify access to subsystems. 3.1 AdapterAn Adapter converts one interface into another expected by a client. It is useful when a reliable existing component cannot be changed but does not match the application interface. class LegacyWeatherApi:
def current_temperature_celsius(self, city_name):
return {"city": city_name, "temp_c": 21.5}
class WeatherProvider:
def temperature(self, city):
raise NotImplementedError
class WeatherAdapter(WeatherProvider):
def __init__(self, legacy_api):
self.legacy_api = legacy_api
def temperature(self, city):
result = self.legacy_api.current_temperature_celsius(city)
return result["temp_c"]The adapter protects the rest of the application from legacy names, data formats, and error conventions. It should perform translation, not become a second business layer. A class adapter using inheritance is possible in languages with suitable multiple-inheritance or interface mechanisms, but object composition is generally more flexible and avoids inheriting unwanted behavior. 3.2 DecoratorA Decorator wraps an object and adds behavior while preserving the same interface. Unlike inheritance, decorators can be combined dynamically and in different orders. class MessageSender:
def send(self, message):
raise NotImplementedError
class BasicSender(MessageSender):
def send(self, message):
print(message)
class LoggingSender(MessageSender):
def __init__(self, wrapped): self.wrapped = wrapped
def send(self, message):
print("LOG: sending message")
return self.wrapped.send(message)
class RetrySender(MessageSender):
def __init__(self, wrapped, attempts=3):
self.wrapped, self.attempts = wrapped, attempts
def send(self, message):
for attempt in range(self.attempts):
try:
return self.wrapped.send(message)
except Exception:
if attempt == self.attempts - 1:
raiseComposition might be: sender = RetrySender(LoggingSender(BasicSender())) Order matters. Logging outside retry logs one logical request; logging inside retry may log every attempt. Decorators are excellent for cross-cutting behavior such as authorization, caching, metrics, compression, and retries. They can become confusing when many wrappers alter error handling, timing, or state. Document ordering and ensure each decorator preserves the contract expected by clients. 3.3 FacadeA Facade supplies a small, coherent interface over a complex subsystem. It does not necessarily eliminate the subsystem's capabilities; it offers a convenient entry point for common workflows. class CheckoutFacade:
def __init__(self, inventory, payment, shipping):
self.inventory = inventory
self.payment = payment
self.shipping = shipping
def checkout(self, cart, customer):
self.inventory.reserve(cart.items)
receipt = self.payment.charge(customer, cart.total)
tracking = self.shipping.create shipment(customer, cart.items)
return {"receipt": receipt, "tracking": tracking}The example contains a typographical spacing issue in the final call if written as create shipment; correct Python is: tracking = self.shipping.create_shipment(customer, cart.items) A production facade must also define failure behavior. If payment succeeds but shipping creation fails, compensation, retry, or a pending state may be required. A facade should coordinate; it should not absorb every business rule until it becomes an untestable “god object.” 4. Behavioral PatternsBehavioral patterns organize communication, algorithms, and responsibilities among objects. 4.1 StrategyStrategy encapsulates interchangeable algorithms behind a common interface. It is preferable to a growing conditional when the algorithms vary independently or need independent testing. class PercentageDiscount:
def __init__(self, rate): self.rate = rate
def calculate(self, total): return total * (1 - self.rate)
class FixedDiscount:
def __init__(self, amount): self.amount = amount
def calculate(self, total): return max(0, total - self.amount)
class PricingService:
def __init__(self, strategy): self.strategy = strategy
def final_price(self, total): return self.strategy.calculate(total)The strategy contract should specify units, valid ranges, rounding policy, and failure behavior. For currency, use decimal types rather than binary floating-point when exact monetary rounding is required. Strategy adds an object or function boundary; in Python, a callable may be sufficient. In Java, an interface, lambda, or functional interface may provide the same design. 4.2 ObserverObserver establishes a one-to-many relationship: when a subject changes state, registered observers receive a notification. It appears in GUI events, domain events, monitoring, and reactive systems. class OrderEvents:
def __init__(self): self._observers = []
def subscribe(self, observer): self._observers.append(observer)
def unsubscribe(self, observer): self._observers.remove(observer)
def publish(self, event):
for observer in tuple(self._observers):
observer(event)Observers may be functions, objects, or message handlers. Important design decisions include synchronous versus asynchronous delivery, event ordering, duplicate registration, exception isolation, and memory management. In a long-lived process, failing to unsubscribe can retain objects and create memory leaks. Strongly typed event objects and explicit ownership improve reliability. Distributed publish/subscribe systems are related but add persistence, delivery guarantees, duplication, and network failure. 4.3 CommandCommand represents a request as an object. It can support queuing, logging, retries, authorization, undo, and delayed execution. class TransferCommand:
def __init__(self, account, amount):
self.account, self.amount = account, amount
def execute(self):
self.account.withdraw(self.amount)
class JobQueue:
def __init__(self): self._jobs = []
def add(self, command): self._jobs.append(command)
def run_next(self):
if self._jobs:
self._jobs.pop(0).execute()A robust command may include an idempotency key, authorization context, timestamp, and result state. Undo is not automatic: it requires a compensating operation and careful treatment of external side effects. Commands are useful when execution must be separated from request creation, but a command class for every trivial method call may add noise. 5. Selecting and Combining PatternsPatterns frequently collaborate. A web application might use a Factory to select a payment gateway, Dependency Injection to supply it, an Adapter to normalize a vendor SDK, a Decorator for metrics and retries, a Facade for checkout, a Strategy for fraud scoring, an Observer for order events, and Commands for asynchronous fulfillment. The combination is justified only when each pattern addresses a distinct pressure. A useful architecture separates: - policy, such as pricing and authorization;
- mechanism, such as database and payment APIs;
- coordination, such as application services and facades;
- communication, such as events and commands.
Prefer composition over inheritance when behavior must vary at runtime, when combinations are expected, or when the relationship is “has-a” rather than “is-a.” Use inheritance when substitutability is genuine and the base contract is stable and meaningful. Pattern Selection Matrix| Design pressure | Likely pattern | Main benefit | Main risk |
|---|
| Concrete type selection varies | Factory | Centralized creation | Conditional factory growth | | Many optional construction choices | Builder | Readable validated construction | Extra code | | External dependency must be replaceable | Dependency Injection | Testability and low coupling | Configuration complexity | | Existing interface does not fit | Adapter | Integration without modifying client | Translation leakage | | Behavior must be layered | Decorator | Runtime composition | Ordering and debugging complexity | | Subsystem is difficult to use | Facade | Simpler application boundary | God-object facade | | Algorithms are interchangeable | Strategy | Extensibility and testing | Too many tiny classes | | Many listeners need notification | Observer | Loose event coupling | Lifecycle and delivery problems | | Requests require queuing or logging | Command | Deferred, auditable execution | Boilerplate and state management |
6. Detailed Applied Case: Modular Checkout PlatformThe following is an illustrative case, not a report of a particular company. BackgroundA growing online retailer initially hard-coded one payment provider, one shipping carrier, and one discount rule into its checkout controller. The controller became difficult to test and changes caused regressions. Marketing needed new discount campaigns, operations wanted alternative carriers, and finance required a record of payment attempts. Problem AnalysisThe system had several independent sources of variation: - payment provider and carrier APIs had incompatible interfaces;
- discount algorithms changed more frequently than checkout workflow;
- external services were slow and unreliable;
- successful orders had to notify inventory, email, and analytics systems;
- tests required isolation from real networks.
A large inheritance hierarchy would not solve these problems because variation was combinatorial. Composition and explicit boundaries were more appropriate. Approach- Dependency Injection supplies payment, inventory, and shipping ports to the checkout service.
- Adapters translate each vendor API into internal
PaymentGateway and ShippingProvider contracts. - Strategy represents percentage, fixed-value, and loyalty discounts.
- Facade exposes one
checkout operation while coordinating subsystem calls. - Decorator adds timing, logging, and retry behavior around external gateways.
- Observer publishes an
OrderPlaced event to independent subscribers. - Command represents fulfillment work placed on a queue.
- Factory selects a provider from configuration, while a Builder creates validated checkout requests.
A simplified flow is: Client → Checkout Facade → Pricing Strategy
↓
Payment Gateway Adapter
↓
Order Repository
↓
OrderPlaced Observer
↓
Fulfillment Command QueueOutcomeThe checkout workflow no longer knows vendor-specific method names. Unit tests inject fake gateways and deterministic strategies. Adding a carrier requires a new adapter and configuration rather than changes throughout the application. The team still records transactional rules explicitly: payment authorization is not treated as proof that shipping succeeded, and retries use idempotency keys to avoid charging a customer twice. Learning PointsThe patterns were selected because the case contained real variation and integration boundaries. They were not added as a complete catalog. The architecture also requires operational practices: structured logs, correlation identifiers, monitoring, secure secret storage, access control, and clear failure states. 7. Testing, Security, and Operational ConcernsPatterns influence quality but do not guarantee it. Test the contract of an abstraction, not merely whether a class exists. For an Adapter, test translation and error mapping. For a Strategy, test boundary values and rounding. For a Decorator, test behavior both with and without the wrapper. For an Observer, test unsubscribe behavior and failure isolation. For a Command, test serialization, authorization, retries, and idempotency when applicable. Dependency Injection improves testability, but fake implementations must remain credible. A fake that never fails can hide production defects. Contract tests can verify that real adapters and test doubles honor the same interface. Security must be designed into patterns. A Command should not permit unauthorized replay. Observers should not receive confidential data unnecessarily. Factories must validate configuration rather than instantiate attacker-controlled classes without restriction. Decorators for authorization and logging must not expose secrets. Facades should enforce transactional and access-control boundaries rather than relying on every caller to coordinate them correctly. 8. Pattern Overuse and Design SmellsPattern overuse occurs when the abstraction cost exceeds the change it protects. Warning signs include: - an interface with one implementation and no expected variation;
- a factory that only calls a constructor;
- a builder with two required values and no meaningful validation;
- a facade that merely forwards every method;
- an observer used for a simple direct function call;
- decorators whose order cannot be explained;
- dependency injection of stable, trivial value objects;
- class names that describe patterns rather than domain concepts;
- excessive indirection requiring several files to understand one action.
Refactoring toward simplicity is not a failure. Start with direct code, identify repeated change or coupling, introduce the smallest abstraction that addresses it, and remove abstractions that no longer provide value. A pattern is successful when developers can understand and safely change the system—not when the system contains many pattern names. Contemporary RelevanceModern frameworks often provide built-in dependency injection containers, middleware chains that resemble decorators, event buses based on Observer, command handlers, and factory mechanisms. Framework support does not remove the need to understand the underlying design. Containers can conceal object graphs, event buses can make control flow difficult to trace, and generated code can amplify a poor abstraction. AI-assisted coding can generate pattern-shaped code quickly, but generated abstractions may be unnecessary, leak resources, or mishandle failures. Developers remain responsible for selecting the pattern, checking contracts, reviewing security implications, and testing behavior. Architectural decisions should be documented with the problem and trade-offs, not merely with the pattern name. Chapter SummaryDesign patterns are reusable design knowledge for recurring object-oriented problems. Creational patterns control construction: Factory centralizes variable creation, Builder supports complex validated construction, and Dependency Injection supplies collaborators from outside. Structural patterns organize composition: Adapter translates interfaces, Decorator layers behavior, and Facade simplifies a subsystem. Behavioral patterns organize communication and algorithms: Strategy encapsulates alternatives, Observer distributes notifications, and Command represents requests for queuing, logging, or deferred execution. Patterns improve maintainability when they isolate genuine variation, clarify responsibilities, and support testing. They also introduce classes, indirection, lifecycle concerns, and configuration costs. Good design therefore depends on disciplined selection, composition, contract testing, security awareness, and willingness to use simpler code when no pattern is needed. Key TermsPattern: A named, reusable solution to a recurring design problem. Creational pattern: A pattern concerned with object construction. Structural pattern: A pattern concerned with object and class composition. Behavioral pattern: A pattern concerned with communication and responsibility. Factory: An abstraction that encapsulates selection or creation of objects. Builder: An object that constructs a complex product step by step. Dependency Injection: Supplying an object's collaborators from outside the object. Adapter: A wrapper that converts one interface into another. Decorator: A wrapper that adds behavior while preserving an interface. Facade: A simplified interface to a complex subsystem. Strategy: An interchangeable implementation of an algorithm or policy. Observer: A subscriber notified when a subject publishes a change or event. Command: An object representing a request or operation. Composition: Building behavior by combining objects. Coupling: The degree to which components depend on one another. Cohesion: The degree to which a component's responsibilities belong together. Idempotency: The property that repeating an operation has the same intended effect as performing it once. Substitutability: The ability to replace an implementation without violating client expectations. Pattern overuse: Applying an abstraction whose costs exceed its design benefit. Review QuestionsShort Answer Questions- What problem does the Adapter pattern solve?
- Why does constructor injection usually improve testability?
- Distinguish a Strategy from a Factory.
- State two risks associated with Observer implementations.
- Why can a Facade become a god object?
Long Answer Questions- Compare Factory and Builder patterns, including their use cases and disadvantages.
- Explain how Decorator differs from inheritance and provide a suitable application example.
- Discuss the role of Dependency Injection in applying the Dependency Inversion Principle.
- Describe how Command can support queuing, auditing, retries, and undo, and explain why undo is not automatic.
- Explain how pattern overuse affects maintainability and propose a disciplined selection process.
Analytical / Application Questions- An application contains a large conditional selecting payment providers, vendor-specific method names throughout the code, and tests that require network access. Select and justify an appropriate combination of patterns.
- A notification system sometimes sends duplicate messages and retains obsolete listeners in memory. Analyze likely Observer design problems and propose corrective measures.
- Design a checkout architecture using at least four patterns from this chapter. For each pattern, identify the variation it isolates, one test strategy, and one possible disadvantage.
Multiple Choice Questions with AnswersQuestion 1Which pattern encapsulates the selection and creation of a concrete object? A. Factory B. Observer C. Adapter D. Command Correct Answer: A. Factory Question 2Which technique supplies collaborators to an object rather than constructing them internally? A. Facade B. Dependency Injection C. Decorator D. Builder Correct Answer: B. Dependency Injection Question 3A legacy API has incompatible method names but must be used through an application's standard interface. Which pattern is most appropriate? A. Strategy B. Factory C. Adapter D. Observer Correct Answer: C. Adapter Question 4What is a principal advantage of the Decorator pattern? A. It guarantees compile-time immutability B. It adds behavior through composable wrapping C. It eliminates all object creation D. It replaces every interface with inheritance Correct Answer: B. It adds behavior through composable wrapping Question 5Which pattern provides a simplified entry point to several subsystem operations? A. Facade B. Command C. Builder D. Observer Correct Answer: A. Facade Question 6A pricing service must support percentage, fixed-value, and loyalty algorithms selected at runtime. Which pattern best represents these alternatives? A. Adapter B. Strategy C. Facade D. Factory Correct Answer: B. Strategy Question 7Which Observer concern is especially important in long-lived applications? A. Preventing all polymorphism B. Ensuring every observer inherits from the subject C. Managing subscription and unsubscription to avoid retained objects D. Replacing events with constructors Correct Answer: C. Managing subscription and unsubscription to avoid retained objects Question 8What is a major reason to represent an operation as a Command object? A. To make the operation impossible to test B. To support queuing, logging, or deferred execution C. To remove the need for authorization D. To guarantee that undo is automatic Correct Answer: B. To support queuing, logging, or deferred execution Question 9Which situation most strongly suggests using a Builder? A. A class has many optional construction choices and validation rules B. A client needs to translate a vendor interface C. Several listeners need an event notification D. A subsystem needs a shorter public entry point Correct Answer: A. A class has many optional construction choices and validation rules Question 10Which statement best describes pattern overuse? A. Using composition instead of inheritance B. Adding abstractions whose complexity exceeds the variation they protect C. Testing an interface contract D. Injecting a replaceable external dependency Correct Answer: B. Adding abstractions whose complexity exceeds the variation they protect
|