Object-Oriented Programming (Java/Python)

Chapter 10
Design Patterns and Reusable Solutions

Bachelors Program in Full Stack Development
Object-Oriented Programming (Java/Python)   •   Bachelor

Design Patterns and Reusable Solutions

Introduction

Software 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 Objectives

By 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 Knowledge

A pattern generally contains four elements:

  1. Name: a shared vocabulary for discussing a design.
  2. Context: the situation in which the problem occurs.
  3. Problem and forces: the competing requirements, such as flexibility versus simplicity.
  4. 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 Patterns

Creational patterns control how objects are created. They are valuable when construction is conditional, complex, expensive, or likely to change.

2.1 Factory

A 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 Builder

The 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 Injection

Dependency 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 Patterns

Structural patterns describe how objects and classes are composed. They help integrate incompatible interfaces, add responsibilities, or simplify access to subsystems.

3.1 Adapter

An 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 Decorator

A 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:
                    raise

Composition 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 Facade

A 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 Patterns

Behavioral patterns organize communication, algorithms, and responsibilities among objects.

4.1 Strategy

Strategy 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 Observer

Observer 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 Command

Command 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 Patterns

Patterns 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 pressureLikely patternMain benefitMain risk
Concrete type selection variesFactoryCentralized creationConditional factory growth
Many optional construction choicesBuilderReadable validated constructionExtra code
External dependency must be replaceableDependency InjectionTestability and low couplingConfiguration complexity
Existing interface does not fitAdapterIntegration without modifying clientTranslation leakage
Behavior must be layeredDecoratorRuntime compositionOrdering and debugging complexity
Subsystem is difficult to useFacadeSimpler application boundaryGod-object facade
Algorithms are interchangeableStrategyExtensibility and testingToo many tiny classes
Many listeners need notificationObserverLoose event couplingLifecycle and delivery problems
Requests require queuing or loggingCommandDeferred, auditable executionBoilerplate and state management

6. Detailed Applied Case: Modular Checkout Platform

The following is an illustrative case, not a report of a particular company.

Background

A 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 Analysis

The 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

  1. Dependency Injection supplies payment, inventory, and shipping ports to the checkout service.
  2. Adapters translate each vendor API into internal PaymentGateway and ShippingProvider contracts.
  3. Strategy represents percentage, fixed-value, and loyalty discounts.
  4. Facade exposes one checkout operation while coordinating subsystem calls.
  5. Decorator adds timing, logging, and retry behavior around external gateways.
  6. Observer publishes an OrderPlaced event to independent subscribers.
  7. Command represents fulfillment work placed on a queue.
  8. 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 Queue

Outcome

The 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 Points

The 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 Concerns

Patterns 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 Smells

Pattern 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 Relevance

Modern 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 Summary

Design 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 Terms

Pattern: 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 Questions

Short Answer Questions

  1. What problem does the Adapter pattern solve?
  2. Why does constructor injection usually improve testability?
  3. Distinguish a Strategy from a Factory.
  4. State two risks associated with Observer implementations.
  5. Why can a Facade become a god object?

Long Answer Questions

  1. Compare Factory and Builder patterns, including their use cases and disadvantages.
  2. Explain how Decorator differs from inheritance and provide a suitable application example.
  3. Discuss the role of Dependency Injection in applying the Dependency Inversion Principle.
  4. Describe how Command can support queuing, auditing, retries, and undo, and explain why undo is not automatic.
  5. Explain how pattern overuse affects maintainability and propose a disciplined selection process.

Analytical / Application Questions

  1. 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.
  2. A notification system sometimes sends duplicate messages and retains obsolete listeners in memory. Analyze likely Observer design problems and propose corrective measures.
  3. 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 Answers

Question 1

Which pattern encapsulates the selection and creation of a concrete object?

A. Factory

B. Observer

C. Adapter

D. Command

Correct Answer: A. Factory

Question 2

Which 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 3

A 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 4

What 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 5

Which pattern provides a simplified entry point to several subsystem operations?

A. Facade

B. Command

C. Builder

D. Observer

Correct Answer: A. Facade

Question 6

A 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 7

Which 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 8

What 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 9

Which 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 10

Which 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

Object-Oriented Programming (Java/Python)
Chapter 10 of 15