Object-Oriented Design PatternsIntroduction and ContextObject-oriented design patterns are reusable approaches to recurring design problems in software systems. A pattern is not a complete program or a code library. It is a named design idea that describes a problem, the forces affecting that problem, and a structure of collaborating classes and objects that commonly provides a good solution. Patterns became especially influential through the work of Gamma, Helm, Johnson, and Vlissides, whose catalog identified 23 widely used patterns. These patterns are commonly grouped into three families: - Creational patterns, concerned with how objects are created.
- Structural patterns, concerned with how classes and objects are composed.
- Behavioral patterns, concerned with communication, responsibility, and algorithms.
Patterns are valuable because they provide a shared vocabulary. A developer can say that a component uses a Strategy or an Adapter, and experienced colleagues immediately have a general understanding of its structure. However, patterns should not be applied merely to make code appear sophisticated. A pattern is justified when it reduces coupling, isolates change, clarifies responsibility, or improves extensibility. In AI and machine-learning systems, patterns are particularly relevant. A data-processing pipeline may need interchangeable algorithms, a model service may need a stable interface around incompatible libraries, and a monitoring system may need to notify several independent consumers when model performance changes. Design patterns help organize these systems without tying the entire application to one implementation. Learning ObjectivesBy the end of this chapter, you should be able to: 1. Explain the purpose and trade-offs of creational, structural, and behavioral patterns. 2. Select appropriate patterns for common object-oriented design problems. 3. Implement Factory, Builder, Adapter, Decorator, Facade, Strategy, Observer, Command, and Template Method patterns. 4. Distinguish genuine design improvement from unnecessary abstraction. 5. Evaluate pattern-based designs in terms of coupling, cohesion, maintainability, testability, and changeability. 6. Apply patterns responsibly in software involving data, machine learning, and AI services. Understanding Patterns as Design DecisionsA design pattern normally contains four elements: 1. Name: a concise label for the design idea. 2. Problem: the situation in which the pattern is useful. 3. Solution: the participating classes, objects, and collaborations. 4. Consequences: benefits, costs, and situations in which the pattern should not be used. Patterns are not algorithms. An algorithm specifies a computational procedure, whereas a pattern specifies a reusable organization of responsibilities. For example, binary search is an algorithm; Strategy is a pattern that allows one of several algorithms to be selected and used through a common interface. A sound design generally seeks: - Low coupling: classes should depend on as few concrete details as practical.
- High cohesion: each class should have a focused responsibility.
- Encapsulation: implementation details should be hidden behind stable interfaces.
- Substitutability: implementations of an abstraction should be usable wherever the abstraction is expected.
- Change isolation: a likely change should affect as few classes as possible.
These aims are related to the SOLID principles. For example, Strategy supports the Open–Closed Principle by allowing new algorithms to be added without modifying the context. Factory supports dependency inversion by allowing client code to depend on a product abstraction rather than a concrete class. These relationships are useful, but no pattern automatically guarantees good design. Creational PatternsFactory PatternThe Factory pattern delegates object creation to a separate method or object. The client requests an abstraction without directly constructing a particular concrete class. A simple Python example is: ```python from abc import ABC, abstractmethod class Notifier(ABC): @abstractmethod def send(self, message: str) -> None: pass class EmailNotifier(Notifier): def send(self, message: str) -> None: print(f"Email: {message}") class SMSNotifier(Notifier): def send(self, message: str) -> None: print(f"SMS: {message}") def notifier_factory(channel: str) -> Notifier: if channel == "email": return EmailNotifier() if channel == "sms": return SMSNotifier() raise ValueError(f"Unsupported channel: {channel}") ``` The client uses `notifier_factory("email")` and then invokes `send`, without knowing how the email object is constructed. A factory is useful when object creation involves configuration, validation, resource selection, or choosing among several implementations. A factory can be a function, a class method, or a separate factory class. A large factory containing an ever-growing conditional statement may become difficult to maintain. In that situation, a registry or dependency-injection mechanism may be preferable. The pattern should not be used when direct construction is simple and unlikely to change. Builder PatternThe Builder pattern separates the construction of a complex object from its representation. It is especially useful when an object has many optional parameters, ordered construction steps, or validation rules. ```python class Experiment: def __init__(self, dataset, model, learning_rate, epochs, seed=None): self.dataset = dataset self.model = model self.learning_rate = learning_rate self.epochs = epochs self.seed = seed class ExperimentBuilder: def __init__(self): self._dataset = None self._model = None self._learning_rate = 0.001 self._epochs = 10 self._seed = None def dataset(self, value): self._dataset = value return self def model(self, value): self._model = value return self def learning_rate(self, value): self._learning_rate = value return self def epochs(self, value): self._epochs = value return self def build(self): if self._dataset is None or self._model is None: raise ValueError("Dataset and model are required") if self._learning_rate <= 0 or self._epochs <= 0: raise ValueError("Training parameters must be positive") return Experiment(self._dataset, self._model, self._learning_rate, self._epochs, self._seed) ``` The fluent interface improves readability and permits defaults. The builder also provides a natural place for cross-field validation. Its costs include additional classes and mutable construction state. For a small object with two or three obvious parameters, a constructor or named factory may be clearer. Structural PatternsAdapter PatternThe Adapter pattern allows an existing class with an incompatible interface to be used where a different interface is expected. It is useful when integrating legacy code, third-party libraries, or external services. ```python class Predictor: def predict(self, features): raise NotImplementedError class LegacyModel: def run_inference(self, feature_vector): return {"class": 1, "confidence": 0.91} class LegacyModelAdapter(Predictor): def __init__(self, legacy_model): self.legacy_model = legacy_model def predict(self, features): result = self.legacy_model.run_inference(features) return result["class"] ``` The adapter translates calls and, where necessary, data formats. It protects the rest of the application from the external interface. An adapter is preferable to scattering conversion code throughout client classes. It does not improve the underlying third-party component; it isolates its incompatibility. Decorator PatternThe Decorator pattern attaches additional responsibilities to an object dynamically. A decorator implements the same interface as the object it wraps and forwards calls while adding behavior. ```python class Predictor: def predict(self, features): raise NotImplementedError class BasePredictor(Predictor): def predict(self, features): return 1 class LoggingPredictor(Predictor): def __init__(self, wrapped): self.wrapped = wrapped def predict(self, features): result = self.wrapped.predict(features) print(f"prediction={result}") return result class TimingPredictor(Predictor): def __init__(self, wrapped, clock): self.wrapped = wrapped self.clock = clock def predict(self, features): start = self.clock() result = self.wrapped.predict(features) print(f"elapsed={self.clock() - start}") return result ``` Decorators can be combined, such as `TimingPredictor(LoggingPredictor(BasePredictor()), clock)`. They support composition without creating a separate subclass for every combination of features. Their disadvantages include complex call stacks, ordering effects, and difficulty tracing behavior when many decorators are nested. A decorator should preserve the essential contract of the wrapped component. Facade PatternA Facade provides a simple entry point to a subsystem containing several complex components. It does not necessarily prevent clients from accessing the subsystem; it offers a convenient, stable interface for common operations. ```python class DataLoader: def load(self, path): return "raw data" class Cleaner: def clean(self, data): return "clean data" class Model: def train(self, data): return "trained model" class TrainingFacade: def __init__(self): self.loader = DataLoader() self.cleaner = Cleaner() self.model = Model() def train_from_file(self, path): raw = self.loader.load(path) clean = self.cleaner.clean(raw) return self.model.train(clean) ``` The facade reduces the knowledge required by a client. It is useful at service boundaries, in application-layer code, and when simplifying an intricate workflow. A facade can become a “god object” if it absorbs every operation in the subsystem. Its responsibilities should remain orchestration and simplification, not all business logic. Behavioral PatternsStrategy PatternStrategy encapsulates a family of interchangeable algorithms behind a common interface. The context delegates a task to the selected strategy. ```python class SortStrategy: def sort(self, items): raise NotImplementedError class AscendingSort(SortStrategy): def sort(self, items): return sorted(items) class DescendingSort(SortStrategy): def sort(self, items): return sorted(items, reverse=True) class Report: def __init__(self, sorter): self.sorter = sorter def ordered_scores(self, scores): return self.sorter.sort(scores) ``` The strategy can be selected at runtime, configured for a user, or replaced in tests. In AI systems, examples include feature-selection strategies, data-sampling methods, routing policies, and model-ranking methods. Strategy increases the number of types but decreases conditional logic in the context. If there are only two fixed cases and no expected variation, a conditional may be more readable. Observer PatternThe Observer pattern defines a one-to-many relationship. A subject maintains a collection of observers and notifies them when relevant state changes. ```python class Subject: def __init__(self): self._observers = [] def subscribe(self, observer): self._observers.append(observer) def unsubscribe(self, observer): self._observers.remove(observer) def notify(self, event): for observer in list(self._observers): observer.update(event) class MetricsDashboard: def update(self, event): print("Dashboard received", event) class AlertService: def update(self, event): if event.get("accuracy", 1) < 0.8: print("Alert: accuracy threshold exceeded") ``` Observer reduces direct dependencies between the subject and its consumers. It is useful for event-driven systems, user-interface updates, audit logging, and model-monitoring notifications. Important design issues include notification order, exception handling, duplicate subscriptions, memory leaks, and synchronous versus asynchronous delivery. In a distributed system, a simple in-memory Observer is not equivalent to a reliable message broker. Delivery guarantees, retries, ordering, and privacy must be designed explicitly. Command PatternThe Command pattern encapsulates a request as an object. A command typically contains a receiver and an `execute` operation. ```python class TrainModel: def __init__(self, trainer, dataset): self.trainer = trainer self.dataset = dataset def execute(self): return self.trainer.train(self.dataset) class JobQueue: def __init__(self): self.commands = [] def add(self, command): self.commands.append(command) def run_all(self): results = [] for command in self.commands: results.append(command.execute()) return results ``` Commands support queues, scheduling, logging, retries, access control, and undo operations. A command should define what information is needed to execute later. If commands are persisted, their data format and version compatibility become important. Retrying a command is safe only when its operation is idempotent or has suitable safeguards. For example, repeating a read is usually harmless, while repeating a payment or data mutation may not be. Template Method PatternThe Template Method pattern defines the skeleton of an algorithm in a base class while allowing subclasses to override selected steps. ```python from abc import ABC, abstractmethod class DataPipeline(ABC): def run(self, source): raw = self.load(source) clean = self.clean(raw) result = self.transform(clean) self.save(result) return result @abstractmethod def load(self, source): pass def clean(self, data): return data.strip() @abstractmethod def transform(self, data): pass def save(self, data): print("Saved", data) ``` The base class controls the invariant order of operations, while subclasses provide variable steps. This is useful when several workflows share a reliable structure. Its main risk is the fragile-base-class problem: changes in the superclass may unexpectedly affect subclasses. It also relies on inheritance, which can create tight coupling. When variation is extensive or behavior must be selected dynamically, composition with Strategy is often more flexible. Comparing and Selecting PatternsThe patterns can be distinguished by the design question they answer: | Design question | Suitable pattern | |---|---| | How should one of several product types be created? | Factory | | How can a complex object be assembled safely? | Builder | | How can an incompatible interface be used? | Adapter | | How can optional behavior be layered? | Decorator | | How can a complicated subsystem be simplified? | Facade | | How can interchangeable algorithms be selected? | Strategy | | How can many components receive state-change information? | Observer | | How can a request be queued or logged as an object? | Command | | How can common algorithm steps be fixed while selected steps vary? | Template Method | Pattern selection should begin with the changing requirement, not with a desire to use a named pattern. Ask: 1. What is likely to change? 2. Which class currently knows too much? 3. Is the variation about object creation, structure, or behavior? 4. Would the pattern reduce duplication or merely add indirection? 5. Can the design be tested easily? 6. Can a future developer understand the collaboration without excessive documentation? A useful decision rule is to introduce a pattern when a concrete design problem has appeared or is strongly evidenced by requirements. Speculative extensibility often produces overengineering. Integrated Illustrative Case: A Model-Serving PlatformThe following fictional case illustrates how several patterns can cooperate in a realistic AI application. BackgroundA university research platform serves image-classification models for different departments. Models come from different frameworks, require different preprocessing, and expose inconsistent prediction interfaces. The platform must support logging, monitoring, scheduled inference, and replacement of models without rewriting the web API. ProblemThe original implementation contains a large conditional statement in the request handler. It directly constructs framework-specific models, performs preprocessing, writes logs, and sends alerts. Adding a new model requires editing the handler. Tests are slow because they load real models and external storage. AnalysisThe likely sources of change are model creation, model-library interfaces, optional operational behavior, algorithm selection, and notification consumers. These concerns should be separated rather than placed in one controller. Approach and SolutionA Factory creates a predictor based on a model configuration. The application depends on the `Predictor` abstraction rather than concrete framework classes. An Adapter converts each framework’s interface into the common `predict(features)` operation. Thus, the request handler does not know whether a model comes from a local Python library, a remote service, or a legacy runtime. A Facade exposes a high-level `classify(image)` operation. Internally it coordinates image loading, preprocessing, prediction, and response formatting. A Decorator adds logging and latency measurement around the predictor. These concerns can be added in tests or production through composition. A Strategy selects preprocessing or confidence-threshold policies according to the model or deployment context. For example, a mobile deployment may use a lightweight preprocessing strategy, while an offline research job may use a more expensive transformation. An Observer notifies a dashboard and an alert service when metrics such as latency, error rate, or accuracy estimates change. The subject should publish only the information authorized for each observer; sensitive input images should not be sent to general monitoring components. A Command represents batch-inference jobs placed in a queue. Commands contain the model identifier, input location, and configuration version. The queue can schedule and retry jobs, but retry policy must distinguish safe inference from non-idempotent side effects. A Template Method can define a common offline evaluation pipeline: load data, validate it, preprocess it, run predictions, calculate metrics, and produce a report. Model-specific subclasses override loading or prediction steps while preserving the evaluation sequence. OutcomeThe web API now depends on stable abstractions. New model frameworks require a new adapter and factory registration rather than changes throughout the platform. Operational features can be tested independently. A fake predictor can be injected into tests, avoiding expensive model loading. The design also makes responsibilities visible: factories create, adapters translate, facades coordinate, decorators enrich, strategies vary algorithms, observers react, commands schedule, and template methods preserve workflow structure. Learning PointsThe case does not imply that every service needs all nine patterns. The patterns were selected because each addressed a demonstrated variation or integration problem. If the platform had only one model and no expected change, a factory, adapter hierarchy, and command queue might be unnecessary. Pattern use should be proportional to system complexity. Implementation and Testing PracticesPattern-based code should be tested at the level of responsibilities. Factory tests verify that valid configurations create the correct abstraction and invalid configurations fail clearly. Adapter tests verify translation of inputs and outputs. Decorator tests verify forwarding and additional behavior. Strategy tests verify each algorithm independently and verify that the context delegates correctly. Observer tests verify subscription, removal, and notification behavior. Command tests verify that execution invokes the intended receiver with the stored data. Dependency injection is often helpful. Instead of constructing a concrete clock, database, model, or message broker inside a class, supply it through the constructor or a factory. This improves determinism and makes tests independent of external infrastructure. Interfaces should be small. A broad interface forces every implementation to provide irrelevant operations and weakens substitutability. Type hints, abstract base classes, protocol definitions, and clear contracts can make pattern collaborations easier to understand. Documentation should explain why a pattern is present, what variation it isolates, and what constraints apply. Naming a class `Strategy` or `Factory` is not sufficient documentation. The code should make the collaboration apparent. Challenges, Limitations, and GovernancePatterns introduce indirection. A simple direct call may become a chain of factory, decorator, facade, and adapter objects. This can complicate debugging and increase cognitive load. More classes also mean more interfaces to maintain. Inheritance-based patterns can produce tight coupling and fragile hierarchies. Composition is often preferable when behavior must vary at runtime. Conversely, composition can create many small objects and require careful lifecycle management. In AI applications, design patterns do not solve problems of data quality, model bias, privacy, explainability, or security. An Observer that publishes prediction events may accidentally expose personal data. A Command queue may retain sensitive input locations longer than permitted. A Factory may load an untrusted model if configuration is not authenticated. These risks require governance controls, including access control, input validation, encryption, auditability, retention limits, and model provenance. A pattern-based architecture should therefore preserve traceability: record model versions, configuration versions, data sources, and relevant transformations. This is important for reproducibility, incident investigation, and regulatory compliance. The design must also define failure behavior. For example, an observer failure should not necessarily prevent the primary prediction from completing, while a failed authorization check must block execution. Contemporary DevelopmentsModern languages and frameworks provide alternatives to some classic patterns. First-class functions can replace small Strategy classes. Dependency-injection containers can provide factory-like object construction. Event buses and message brokers implement distributed forms of Observer. Dataclasses, records, named parameters, and immutable configuration objects can reduce the need for Builder. Middleware pipelines can provide decorator-like composition. These features do not make the underlying design ideas obsolete. They change how the ideas are expressed. A function passed as an argument is still a strategy in conceptual terms, and a message consumer still participates in an observer-like relationship. Developers should recognize the design problem independently of the historical class diagram. Best Practices- Start with a concrete change or integration problem.
- Prefer the simplest design that keeps responsibilities clear.
- Program to stable abstractions, but avoid abstractions with no meaningful variation.
- Use composition when runtime flexibility is important.
- Keep factories focused on creation and facades focused on coordination.
- Make observer delivery, error handling, and lifecycle rules explicit.
- Design command retries with idempotency and security in mind.
- Protect template-method invariants and document subclass obligations.
- Test each pattern participant independently and test important collaborations.
- Remove a pattern if it no longer provides a measurable design benefit.
Chapter SummaryObject-oriented design patterns are named, reusable solutions to recurring design problems. Creational patterns control object construction, structural patterns organize relationships among components, and behavioral patterns organize communication and variation. Factory separates clients from concrete creation decisions, while Builder supports readable construction and validation of complex objects. Adapter isolates incompatible interfaces, Decorator composes optional responsibilities, and Facade simplifies access to a subsystem. Strategy encapsulates interchangeable algorithms, Observer supports one-to-many notification, Command turns requests into objects suitable for queuing and logging, and Template Method fixes an algorithmic skeleton while allowing selected steps to vary. Patterns improve maintainability when they reduce coupling, isolate change, and clarify responsibility. They also introduce indirection, classes, and potential complexity. Effective design therefore depends on judgment: identify the changing requirement, select the smallest suitable abstraction, consider testing and operational consequences, and avoid speculative overengineering. In AI and ML systems, patterns can support model integration, pipelines, monitoring, and job execution, but they must be combined with privacy, security, reproducibility, and governance practices. Key TermsPattern: A reusable design solution to a recurring software design problem. Creational pattern: A pattern concerned with object creation. Structural pattern: A pattern concerned with composing classes and objects. Behavioral pattern: A pattern concerned with communication and responsibility. Factory: An abstraction that delegates or encapsulates object creation. Builder: A pattern that separates complex object construction from representation. Adapter: An object that converts one interface into another expected interface. Decorator: A wrapper that adds responsibilities while preserving an interface. Facade: A simplified interface to a complex subsystem. Strategy: An encapsulated, interchangeable algorithm or policy. Observer: A subscriber notified when a subject’s state or events change. Subject: The source of events in an Observer relationship. Command: An object representing a request or operation. Receiver: The component that performs the work requested by a Command. Template Method: A fixed algorithm skeleton with customizable steps. Coupling: The degree of dependency between software components. 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. Overengineering: Adding unnecessary complexity or abstraction beyond the actual problem. Review QuestionsShort Answer Questions1. What is the difference between a design pattern and an algorithm? 2. What problem does the Adapter pattern solve? 3. Why is Builder useful for complex object construction? 4. What is the role of the subject in the Observer pattern? 5. State one advantage and one limitation of the Template Method pattern. Long Answer Questions1. Explain the Factory and Builder patterns, comparing their purposes, structures, and trade-offs. 2. Describe how Adapter, Decorator, and Facade differ as structural patterns. Include an application example for each. 3. Explain Strategy, Observer, and Command as behavioral patterns and discuss when each is appropriate. 4. Analyze the relationship between design patterns, coupling, cohesion, and the Open–Closed Principle. 5. Discuss the benefits and risks of using design patterns in an AI model-serving platform. Analytical / Application Questions1. A prediction service must support three incompatible model libraries, runtime logging, and two selectable preprocessing methods. Select appropriate patterns and explain how they should collaborate. 2. A team has introduced a factory, builder, five decorators, and an event bus into a small application with one stable algorithm. Evaluate whether this is overengineering and propose a simpler alternative if appropriate. 3. Design a command-based batch-inference system that retries failed jobs. Identify which operations must be idempotent, what data the command should store, and how privacy and audit requirements should be handled. Multiple Choice Questions with AnswersQuestion 1Which pattern encapsulates interchangeable algorithms behind a common interface? A. Strategy B. Facade C. Builder D. Adapter Correct Answer: A. Strategy Question 2A legacy model exposes run_inference(), while the application expects predict(). Which pattern is most appropriate? A. Observer B. Adapter C. Command D. Template Method Correct Answer: B. Adapter Question 3What is the primary purpose of a Facade? A. To create immutable objects B. To notify many subscribers C. To provide a simpler interface to a complex subsystem D. To select among sorting algorithms Correct Answer: C. To provide a simpler interface to a complex subsystem Question 4Which pattern represents a request as an object so that it can be queued or logged? A. Decorator B. Command C. Factory D. Builder Correct Answer: B. Command Question 5What is a principal benefit of the Builder pattern? A. It converts incompatible interfaces B. It simplifies construction of complex objects with optional parameters C. It broadcasts state changes D. It fixes the order of subclass operations Correct Answer: B. It simplifies construction of complex objects with optional parameters Question 6In the Observer pattern, what normally happens when the subject changes state? A. It creates a new factory B. It translates a third-party interface C. It notifies subscribed observers D. It permanently changes the algorithm Correct Answer: C. It notifies subscribed observers Question 7Which pattern adds behavior by wrapping an object that implements the same interface? A. Decorator B. Facade C. Template Method D. Factory Correct Answer: A. Decorator Question 8What is a major risk of retrying a non-idempotent Command? A. The command becomes an Adapter B. The operation may produce unintended repeated side effects C. Observers cannot subscribe D. The factory loses its interface Correct Answer: B. The operation may produce unintended repeated side effects Question 9What does Template Method primarily provide? A. A common algorithm skeleton with customizable steps B. A registry of external services C. A translation between data formats D. A queue of independent requests Correct Answer: A. A common algorithm skeleton with customizable steps Question 10Which consideration is especially important when an Observer publishes AI prediction events? A. Whether the Builder has enough setters B. Whether sensitive data is exposed to subscribers C. Whether every strategy uses inheritance D. Whether the facade creates all objects directly Correct Answer: B. Whether sensitive data is exposed to subscribers
|