Object-Oriented Design Patterns Explained
35 carteThis note provides a comprehensive overview of design patterns in object-oriented software design. It covers the definition, classification, and detailed descriptions of various patterns, including creational, structural, and behavioral types. The note also explores their advantages, disadvantages, and provides examples of their implementation in Java.
35 carte
Design Patterns: Solutions for Reusable Object-Oriented Software Design
Design Patterns are standardized solutions to recurring design problems in software development. They provide a common vocabulary and structured approach to building flexible, modular, extensible, and reusable software while operating under limited resources.
The field of design patterns was significantly popularized by the book "Design Patterns: Elements of Reusable Object-Oriented Software" by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, commonly known as the Gang of Four (GoF). This seminal work, published in 1995, described 23 design patterns, offering generic solutions to common programming and design challenges.
Introduction to Design Patterns
Software design is often a complex and iterative process, benefiting greatly from accumulated experience. Recurring design problems suggest the need for structured, proven solutions.
Concept Origin
The idea of "patterns" originated with architect Christopher Alexander, who developed a theory of architectural design based on recurring motifs. Alexander's work, particularly "A Pattern Language" (1977), defines a pattern as:
"Each motif describes a problem that one encounters incessantly in our environment; then it describes the core of the solution to this problem in such a way that the solution can be used millions of times, without ever implementing it in the same way."
This implies that a pattern:
Describes a recurring problem.
Outlines the core of a solution.
Can lead to multiple distinct implementations.
Challenges in Software Design
Developing robust software involves balancing conflicting objectives:
Encapsulation vs. Access: Protecting data while allowing necessary interaction.
Object Granularity: Choosing the appropriate size and scope for objects.
Reduced Coupling: Minimizing dependencies between objects.
Simple API: Providing an easy-to-use public interface.
Performance Optimization: Ensuring efficient execution.
These challenges often require compromises to achieve reusability, extensibility, adaptability, and performance.
Classification of Design Patterns
Design patterns are classified based on their roles (what they do) and their domains (how they apply to classes or objects).
Roles: Creational, Structural, or Behavioral
Creational Patterns: Focus on object creation mechanisms. They abstract the instantiation process, making the system independent of how its objects are created, composed, and represented. These patterns isolate the instantiation instructions, making the rest of the code independent of the type of objects created.
Structural Patterns: Deal with the composition of classes and objects to form larger structures. They describe how to assemble objects to realize new functionality. They establish associations and compositions between objects and classes.
Behavioral Patterns: Focus on communication and collaboration between objects. They identify common communication patterns and encapsulate them. They interconnect objects and classes to make them communicate and collaborate.
Domains: Classes or Objects
The domain specifies whether a pattern primarily applies to classes or objects:
Class Patterns:
Define relationships between classes using inheritance.
Relationships are defined at compile time (static).
For creational patterns, they delegate parts of object construction to subclasses.
For structural patterns, they use inheritance to compose classes.
For behavioral patterns, they use inheritance to define algorithms and control structures.
Object Patterns:
Describe relationships between objects, mainly using composition.
Relationships are established at runtime (dynamic).
For creational patterns, they delegate parts of object construction to other objects.
For structural patterns, they describe ways to assemble objects.
For behavioral patterns, they describe how a group of objects collaborates to achieve a task that a single object cannot accomplish alone.
GoF Pattern Classification Table
The GoF (Gang of Four) categorized their 23 patterns within this matrix:
Créateurs (Creational) | Structurels (Structural) | Comportementaux (Behavioral) | |
|---|---|---|---|
Classes | Fabrication (Factory Method) | Adaptateur (Adapter) | Interprète (Interpreter) |
Objets | Fabrique Abstraite (Abstract Factory) | Adaptateur (Adapter) | Chaîne de Responsabilité (Chain of Responsibility) |
Description Format for Design Patterns
Graphical notations like UML are often insufficient to fully describe design patterns because they primarily show class and object relationships but fail to capture the underlying motivations, alternatives, and compromises. The GoF proposed a unique, comprehensive format for describing design patterns, which typically includes the following elements:
Name and Classification: A concise name that describes the pattern's essence, along with its classification (creational, structural, behavioral; class or object). A good name is crucial for communication.
Intentions (Intent): What the pattern does, its purpose, the design problem it addresses, and its reason for being.
Alias: Other names by which the pattern is known.
Motivation: An illustrative scenario demonstrating the pattern's use and how it solves a particular problem.
Indications d'utilisation (Applicability):
When should the pattern be used?
What situations justify its application?
How to recognize these situations?
Structure: A graphical representation, typically using UML diagrams, showing the classes and objects involved in the pattern and their relationships.
Constituents (Participants): A list of the classes and objects participating in the pattern.
Collaborations: How the participating classes and objects interact to carry out their responsibilities.
Conséquences (Consequences): The results and trade-offs of using the pattern, including both positive and negative impacts.
Implémentation (Implementation): Practical tips, tricks, pitfalls to avoid, and common solutions for specific programming languages (e.g., C++, Smalltalk, Java).
Exemples de code (Code Examples): Code fragments illustrating possible implementations of the pattern.
Utilisations remarquables (Known Uses): Examples of where the pattern has been applied in existing systems.
Modèles apparentés (Related Patterns): Other design patterns that are related to the one described, including how they differ or can be used together.
Catalogue of Design Patterns
This section details several commonly used design patterns from the GoF catalogue.
Singleton Pattern
Category: Creational, Object
Intention: Guarantees that a class has only one instance and provides a global point of access to that instance.
Motivation: Some classes, like a system's logger, configuration manager, or a single database connection pool, should inherently have only one instance. Managing multiple instances of such classes could lead to inconsistencies or resource exhaustion. The Singleton pattern ensures this constraint while offering a global access point.
Structure: A Singleton class typically has a private constructor to prevent direct instantiation, a private static instance of itself, and a public static method to provide access to that single instance. Clients access the unique instance through its class method.
Example (Java):
public class Singleton {
private static Singleton monInstance;
private Singleton() {
// Private constructor to prevent direct instantiation
// ... initialization code ...
}
// Synchronized to ensure thread-safety for lazy initialization
public static synchronized Singleton getInstance() {
if (monInstance == null) {
monInstance = new Singleton();
}
return monInstance;
}}
Consequences:
Advantages:
Provides controlled access to its unique instance.
Offers an alternative to global variables, encapsulating the instance.
Allows for a variable number of instances; the pattern can be modified to permit multiple instances if needed, offering flexibility in strategy.
Disadvantages/Considerations:
Can make testing more difficult due to global state.
Violation of the Single Responsibility Principle if it manages its own creation and controls functionality.
Potential for misuse as a global access point for frequently used objects, leading to tightly coupled code.
Thread-safety must be carefully considered during implementation, especially in multi-threaded environments (e.g., using or Double-Checked Locking).
Known Uses: Loggers, configuration objects, print spoolers, window managers.
Adapter Pattern
Category: Structural, Object or Class
Alias: Wrapper
Intention: Converts the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Motivation: Imagine you have an existing component (the "adaptee") that provides valuable functionality, but its interface doesn't match the interface that your client code expects to interact with (the "target interface"). Rather than modifying the existing client or adaptee (which might be impossible, e.g., if it's a third-party library), the Adapter pattern allows you to create an intermediate object that translates calls from the target interface to the adaptee's interface.
Example Scenario: Consider two email sending services: and .
has a method like:
has a method like:
If your application is designed to use the interface but you need to integrate , an Adapter can bridge this gap.
Applicability: The Adapter pattern should be used when:
You want to use an existing class, but its interface does not coincide with the one you need.
You want to create a reusable class that collaborates with unrelated, and possibly unknown, classes (i.e., whose interfaces might not be compatible).
(For object adapters) You need to use several existing subclasses, but adapting each of them by subclassing is not feasible. An object adapter can adapt the interface of its parent class.
Observer Pattern
Category: Behavioral, Object
Alias: Dependents, Publish-Subscribe
Intention: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Motivation: Often, there's a need for objects to be aware of changes in other objects without being tightly coupled. For example, in a GUI application, multiple widgets (like charts, tables) might need to reflect changes in a data model. The Observer pattern allows the data model (subject) to notify subscribing widgets (observers) of changes without knowing their concrete types, promoting loose coupling.
Existing Implementations: The Observer pattern has been widely implemented in various frameworks:
Java's (now deprecated since Java 9, with recognized bugs since 2000).
Java Beans and AWT/Swing event models.
Bindings in modern frameworks.
Structure: The core components are a Subject and an Observer.
Subject (Sujet): The object whose state is being observed. It maintains a list of its dependents (observers) and provides methods to attach (), detach (), and notify () them of state changes.
Observer (Observateur): Defines an updating interface for objects that should be notified of changes in a subject. It typically has a method.
Concrete Subject: Implements the Subject interface and stores the state of interest to ConcreteObserver objects. It sends a notification to its observers when its state changes.
Concrete Observer: Implements the Observer updating interface to keep its state consistent with the subject's.
Example (Java Code Excerpts):
Subject class:
public abstract class Sujet {
private List<Observateur> observateurs = new ArrayList<>();
public void attache(Observateur obs){
observateurs.add(obs);
obs.setSujet(this); // Assuming observer needs reference to its subject
}
public void detache(Observateur obs){
observateurs.remove(obs);
obs.setSujet(null); // Clear subject reference
}
public void notifie(){
for(Observateur obs : observateurs){
obs.miseAJour();
}
}}
Observer interface (or abstract class):
public interface Observateur {
void miseAJour();
void setSujet(Sujet sujet); // To allow observers to know their subject
}Concrete Subject Example (`Compteur`):
public class Compteur extends Sujet implements Runnable {
private int valeur; // Should be 'value'
public int getValue(){
return valeur; // Should be 'value'
}
private void incremente(){
valeur++; // Should be 'value'
notifie();
}
public void run(){
// ... (thread logic to increment value and notify) ...
Random random = new Random();
while(true){
try {
Thread.sleep(random.nextInt(2)*1000);
} catch(InterruptedException e) { System.exit(0); }
incremente();
}
}}
Main Application (`Main`) demonstrating usage:
public class Main {
public static void main(String[] args) {
Compteur compteur = new Compteur();
Thread thread = new Thread(compteur);
ObservateurCompteur compteur1 = new ObservateurCompteur("compteur 1");
ObservateurCompteur compteur2 = new ObservateurCompteur("compteur 2");
ObservateurCompteur compteur3 = new ObservateurCompteur("compteur 3");
ObservateurCompteur compteur4 = new ObservateurCompteur("compteur 4");
thread.start(); // Start the counter thread
compteur.attache(compteur1);
try { Thread.sleep(3000); } catch (InterruptedException e) { System.exit(0); }
compteur.attache(compteur2);
compteur.attache(compteur3);
try { Thread.sleep(2000); } catch (InterruptedException e) { System.exit(0); }
compteur.attache(compteur4);
}}
Applicability: The Observer pattern is used when:
A concept has two representations, one depending on the other. Encapsulating these two representations in different objects allows them to evolve independently.
A change in one object requires changes in others, and you don't know how many other objects need changing.
An object needs to be able to notify other objects without making assumptions about their concrete types.
Consequences:
Advantages:
Loose Coupling: The subject and observers are loosely coupled. A subject only knows that it has a list of observers, not their concrete classes. This allows for independent evolution.
Support for Broadcast Communication: No need to specify recipients; notifications are broadcast to all subscribed objects.
Disadvantages/Considerations:
Unexpected Updates: Since observers are not directly aware of each other, updates can become heavy or complex if not carefully managed.
Order of Notification: The order in which observers are notified is not guaranteed and can sometimes be critical.
Memory Leaks: If detached observers are not properly garbage collected, they can lead to memory leaks, especially when the subject holds strong references to them.
Factory Method Pattern
Category: Creational, Class
Alias: Virtual Constructor
Intention: Defines an interface for creating an object, but lets subclasses decide which class to instantiate. The Factory Method pattern allows a class to delegate instantiation to subclasses.
Motivation: Consider creating an application that handles different types of documents (e.g., text documents, image documents, spreadsheet documents). If the application needs to create new document objects, and the specific type of document to create depends on the subclass of the application (e.g., a text editor creates text documents), a direct instantiation using would couple the application to concrete document classes. The Factory Method decouples the creator from the concrete products.
Structure: The pattern involves:
Product: Defines the interface of objects the factory method creates.
ConcreteProduct: Implements the Product interface.
Creator: Declares the factory method, which returns an object of type Product. It may also define a default implementation of the factory method that returns a default ConcreteProduct object. It can call the factory method to create a Product object.
ConcreteCreator: Overrides the factory method to return an instance of a ConcreteProduct.
The Creator delegates definition of the factory to its subclasses so that it returns an instance of the appropriate Concrete Product.
Example Scenario: An application framework that allows users to create and open documents of various types. A generic application class might have an operation to create a new document. Subclasses could then override this operation to create specific document types (e.g., creating , creating ).
Applicability: The Factory Method pattern should be used when:
A class cannot anticipate the class of objects it must create.
A class wants its subclasses to specify the objects it creates.
Consequences:
Advantages:
Decoupling: The Factory Method decouples the client code (Creator) from concrete product classes. The code only concerns itself with the interface and can therefore work with any . This promotes flexibility and makes it easier to introduce new product types.
Extensibility: New product types can be added by creating new and subclasses without modifying existing code.
Disadvantages/Considerations:
Can lead to a proliferation of subclasses, as a new Creator subclass might be needed for every new Concrete Product.
Abstract Factory Pattern
Category: Creational, Object
Alias: Kit
Intention: Provides an interface for creating families of related or interdependent objects without specifying their concrete classes.
Motivation: Consider a graphical user interface (GUI) toolkit that supports different look-and-feel styles (e.g., Unix, Windows, macOS). An application built with this toolkit should be able to switch its entire look-and-feel by changing a single parameter, without modifying the code that creates individual GUI components (buttons, menus, windows). The Abstract Factory pattern allows the creation of consistent families of products (e.g., all Unix-style widgets, or all Windows-style widgets).
Structure: The pattern involves:
AbstractFactory: Declares an interface for operations that create abstract products.
ConcreteFactory: Implements the operations to create concrete product objects. There will be one concrete factory for each product family (e.g., , ).
AbstractProduct: Declares an interface for a type of product object.
ConcreteProduct: Implements the AbstractProduct interface.
Client: Uses interfaces declared by AbstractFactory and AbstractProduct classes.
A single instance of a is created at runtime. It is responsible for creating concrete products. To create different concrete products, different instances are required. An delegates the creation of instances to its subclass.
Example Scenario: A cross-platform application that needs to create GUI widgets (buttons, text fields) conforming to the operating system's native look and feel. The application shouldn't be hard-coded to create Windows buttons or Unix buttons directly. Instead, it uses an Abstract Factory interface () that has methods like . Concrete factories like and would implement this interface, each returning the appropriate concrete button type.
Applicability: The Abstract Factory pattern is recommended when:
A system must be independent of how its products are created, combined, and represented.
A system is configured with one of multiple families of products.
You want to enforce a consistency constraint on product usage, ensuring that objects from only one product family are used together.
Consequences:
Advantages:
Isolates Concrete Classes: Clients manipulate classes through their abstract interfaces. The names of concrete product classes are isolated within the concrete factory's implementation and do not appear in the client code.
Facilitates Product Family Exchange: A concrete factory class appears in client code only where it is instantiated. It is therefore easy to change the concrete factory used by an application, allowing for easy switching of product families.
Promotes Consistency: If product objects from the same family are designed to work together, it is important that an application uses objects from only one family at a time. The Abstract Factory ensures this condition.
Disadvantages/Considerations:
Difficulty in Handling New Product Types: It is difficult to make Abstract Factories create new types of products. This requires modifying the factory's interface, which implies modifying the Abstract Factory class and all its subclasses.
Strategy Pattern
Category: Behavioral, Object
Alias: Policy
Intention: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. The Strategy pattern allows algorithms to vary independently from clients that use them.
Motivation: Imagine an object that performs an operation with various possible algorithms (e.g., sorting algorithms, tax calculation methods, payment processing strategies). Instead of embedding all these algorithms within the object (leading to large, inflexible conditional statements), the Strategy pattern extracts them into separate, interchangeable objects. This allows the client object to choose an algorithm at runtime without changing its own structure.
Structure: The pattern involves:
Strategy: Declares an interface common to all supported algorithms. Context uses this interface to call the algorithm defined by a ConcreteStrategy.
ConcreteStrategy: Implements the Strategy interface, providing a specific algorithm.
Context: Maintains a reference to a ConcreteStrategy object. It can be configured with a ConcreteStrategy object and optionally defines an interface that lets the Strategy access its data.
Example Scenario:An "Agent" class needs to greet people, but the greeting method (e.g., "Hello," "Bonjour," "Hola") can change. The Strategy pattern would define a interface with a method. Concrete strategies like and would implement this. The class holds a reference to a object and delegates the greeting action to it.
Example (Java Code Excerpts):
// Strategy Interface
public interface Strategie {
void bonjour();
}
// Concrete Strategy 1
public class StrategieAnglais implements Strategie {
public void bonjour() {
System.out.println("Hello!");
}
}
// Concrete Strategy 2
public class StrategieFrancais implements Strategie {
public void bonjour() {
System.out.println("Bonjour!");
}
}
// Context Class
public class Agent {
private Strategie strategie;
public Agent (Strategie strategie) {
this.strategie = strategie; // Corrected assignment
}
// Accessor and Mutator (if needed to change strategy at runtime)
public void setStrategie(Strategie strategie) {
this.strategie = strategie;
}
public void saluer() {
strategie.bonjour();
}}
// Client usage
public class Client {
public static void main(String[] args) {
Agent agentEnglish = new Agent(new StrategieAnglais());
agentEnglish.saluer(); // Outputs "Hello!"
Agent agentFrench = new Agent(new StrategieFrancais());
agentFrench.saluer(); // Outputs "Bonjour!"
// Change strategy at runtime
agentFrench.setStrategie(new StrategieAnglais());
agentFrench.saluer(); // Outputs "Hello!"
}}
Consequences:
Advantages:
Algorithm Flexibility: Allows exchanging algorithms at runtime.
Eliminates Conditional Statements: Avoids complex conditional logic ( or statements) for selecting algorithms.
Open/Closed Principle: New algorithms can be added without modifying the context class.
Disadvantages/Considerations:
Increased Number of Objects: Introduces new classes for each strategy, potentially increasing complexity.
Client Must Know Strategies: The client must know the different strategies to choose the appropriate one (unless delegated to another object).
Facade Pattern
Category: Structural, Object
Intention: Provides a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
Motivation: Large software systems often consist of many complex subsystems. Directly interacting with the numerous classes and objects within a subsystem can be overwhelming and error-prone. The Facade pattern offers a simplified, higher-level entry point to the subsystem, hiding its complexity from clients. This reduces coupling between clients and the subsystem, making the subsystem easier to use and evolve independently.
Structure: The pattern involves:
Facade: Knows which subsystem classes are responsible for a request. It delegates client requests to appropriate subsystem objects.
Subsystem Classes: Implement the subsystem functionality. They handle the work assigned by the Facade object and have no knowledge of the facade.
Clients communicate with the subsystem by sending requests to the facade, which then relays these requests to the appropriate objects within the subsystem. Clients using the facade do not need to directly access the objects of its subsystem.
Example Scenario: Configuring a complex graphical editor. Instead of clients having to interact directly with (trace management), (canvas management), (text management), and (UI management) classes, a can provide simplified methods like or that orchestrate calls to the underlying subsystem components.
Example (Java Code Excerpts):
public class FacadeEditeur {
GestionTracé tracéMgr;
GestionCanvas canvasMgr;
GestionTexte texteMgr;
GestionIHM ihmMgr;
public FacadeEditeur() {
// Initialize subsystem components via singletons or factories
GestionGraphique graphicMgr = GestionGraphique.getGestionGraphique();
tracéMgr = graphicMgr.getTracéManager();
canvasMgr = graphicMgr.getCanvasManager();
texteMgr = graphicMgr.getTexteManager();
ihmMgr = GestionIHM.getIHMManager();
}
public void changerPolice(String nom, int taille, String style) {
Police police = new Police(nom, taille, style);
texteMgr.setPolice(police); // Delegates to subsystem
}
public void setCouleur(Color couleur) {
tracéMgr.setCouleur(couleur); // Delegates to multiple subsystem components
canvasMgr.setCouleur(couleur);
texteMgr.setCouleur(couleur);
}}
Applicability: The Facade pattern is used when:
You want to provide a simple interface to a complex subsystem.
You want to decouple a subsystem from its clients and other subsystems. This promotes independence and portability of the subsystem.
You want to layer your subsystem. A facade can define an entry point to each level of the subsystem.
Consequences:
Advantages:
Simplifies Usage: Hides the subsystem’s components from the client, making it easier to use.
Reduced Coupling: Promotes loose coupling between the subsystem and its clients. This allows the subsystem to evolve without affecting its clients.
Flexibility: Does not prevent clients from using the subsystem's classes directly if necessary, offering a choice between comprehensiveness and ease of use.
Disadvantages/Considerations:
Can become a "God Object" if too much responsibility is pushed into the facade.
The facade might introduce an additional layer that sometimes needs to be bypassed for advanced usage.
Composite Pattern
Category: Structural, Object
Intention: Composes objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Motivation: Many systems deal with hierarchical data where individual items and groups of items are often treated similarly. For example, a file system contains individual files and directories (which contain other files and directories). A graphical drawing application might have individual shapes (lines, circles) and groups of shapes. The Composite pattern allows you to represent these hierarchies and treat both individual objects and compositions of objects in the same manner, simplifying client code.
Applicability: The Composite pattern should be used when:
You want to represent part-whole hierarchies.
You want clients to ignore the difference between individual objects and compositions of objects. The client can then treat all objects in the composite structure uniformly.
Consequences:
Advantages:
Uniformity: Simplifies clients by treating composites and individual components uniformly. Clients don't have to distinguish between them, simplifying coding logic.
Extensibility: Makes it easy to add new types of components (leaves or composites) without changing existing client code.
Flexibility: Provides flexibility in representing complex object structures.
Disadvantages/Considerations:
Over-generalization: Can make your design overly general, making it harder to restrict the types of components a composite can contain.
Runtime Checks: Operations that only make sense for specific component types might require runtime checks.
Proxy Pattern
Category: Structural, Object
Alias: Surrogate
Intention: Provides a surrogate or placeholder for another object to control access to it.
Motivation: Sometimes, direct access to an object is either not feasible, undesirable, or requires additional logic. For instance, an object might be very expensive to create, located remotely, or demand access control. A proxy acts as an intermediary, presenting the same interface as the real object but adding a layer of control (or "indirection") before delegating the request to the real object when appropriate.
Types of Proxy:
Remote Proxy (Procuration à distance): Represents an object located in a different address space. It handles the encoding of requests and results, facilitating communication over a network.
Virtual Proxy (Procuration virtuelle): Creates expensive objects on demand (e.g., an image proxy loads the full image only when it needs to be displayed, not upon creation). This defers instantiation until it's actually needed.
Protection Proxy (Procuration de protection): Controls access to the original object, perhaps by checking user permissions before allowing an operation.
Smart Reference (Références intelligentes): Replaces a raw pointer or reference. It can perform additional actions such as reference counting, lazy loading, or locking when the object is accessed.
Consequences:
Advantages:
Controlled Access: Provides a level of indirection that can control access to the real subject.
Reduced Overhead: Can defer expensive operations (e.g., in Virtual Proxy).
Remote Object Handling: Simplifies interaction with remote objects.
Disadvantages/Considerations:
Increased Complexity: Adds a layer of indirection, which can complicate the system.
Performance Overhead: Each request going through the proxy might incur a small performance overhead.
Iterator Pattern
Category: Behavioral, Object
Alias: Cursor
Intention: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
Motivation: Collections of objects (lists, trees, hash maps) store their elements in different ways. Clients often need to traverse these collections to process their elements. Hard-coding traversal logic for each collection type in the client code is fragile and violates the Single Responsibility Principle. The Iterator pattern abstracts the traversal process, allowing clients to iterate over different types of collections using a common, uniform interface.
Existing Implementations: Modern programming languages extensively use the Iterator pattern:
Java's interface (and its predecessor ).
C++ Standard Template Library (STL) iterators.
Structure: The pattern involves:
Iterator: Defines an interface for accessing and traversing elements.
ConcreteIterator: Implements the Iterator interface and keeps track of the current position in the traversal of the aggregate.
Aggregate: Defines an interface for creating an Iterator object.
ConcreteAggregate: Implements the Iterator creation interface to return an instance of the appropriate ConcreteIterator.
A keeps track of the current object within the aggregate and can determine the next object in the traversal.
Example (Java Code Excerpts for a custom array iterator):
// Generic Iterator Interface (conceptual, modern Java has java.util.Iterator)
public interface Iterateur<E> {
E premier(); // Get first element
void suivant(); // Move to next
boolean estTermine(); // Check if traversal is complete
E elementCourant(); // Get current element
}
// Concrete Iterator for a hypothetical Tableau (Array) class
public class IterateurTableau<E> implements Iterateur<E> {
private Tableau<E> monTableau;
private int indice = 0;
public IterateurTableau(Tableau<E> tab) {
this.monTableau = tab;
}
public E premier() {
indice = 0;
return monTableau.getElement(indice);
}
public void suivant() {
indice = indice + 1;
}
public boolean estTermine() {
return indice == monTableau.size();
}
public E elementCourant() {
if (estTermine()) {
throw new NoSuchElementException(); // Handle out of bounds
}
return monTableau.getElement(indice);
}}
Consequences:
Advantages:
Multiple Traversal Methods: Different iterators can enable various traversal methods for the same aggregate object (e.g., infix, postfix, prefix traversal for a tree).
Concurrent Traversal: Multiple iterators can traverse the same aggregate simultaneously without interfering with each other.
Uniform Interface: Iterators provide a uniform interface for traversing diverse aggregate structures, regardless of their internal representation.
Decoupling: Decouples the algorithm for traversal from the data structure, improving reusability of both.
Disadvantages/Considerations:
Overhead: Can add a slight overhead due to the additional object.
External Iterators: External iterators (where the client controls the traversal) can be less encapsulated than internal iterators (where the aggregate controls it).
Command Pattern
Category: Behavioral, Object
Alias: Action, Transaction
Intention: Encapsulate a request as an object, thereby allowing parameterization of clients with different requests, queuing and logging of requests, and support for undoable operations.
Motivation: In many applications, you need to issue requests to objects without knowing anything about the operation being requested or the receiver of the request. For example, in a menu system, various menu items might trigger different actions (copy, paste, open). Instead of directly binding menu items to concrete actions, the Command pattern abstracts each action into a command object. This decouples the invoker (menu item) from the receiver (document, application) and the action itself, enabling features like undo/redo, macros, and queuing.
Structure: The pattern involves:
Command: Declares an interface for executing an operation.
ConcreteCommand: Implements the Command interface by binding a receiver to an action. It calls the operation(s) on its receiver to perform the action.
Client: Creates a ConcreteCommand object and sets its receiver.
Invoker: Asks the command to carry out the request. It does not know the ConcreteCommand class or the Receiver.
Receiver: Knows how to perform the operations associated with carrying out a request. Any class can serve as a Receiver.
Example Components:
Concrete Command: (Paste Command), (Copy Command)
Client: Application
Invoker: (Menu Item)
Receiver: document, application
Consequences:
Advantages:
Decoupling: Decouples the object that invokes the operation from the object that knows how to perform it.
Extensibility: Makes it easy to add new commands without changing existing classes.
Support for Undo/Redo: Commands can store their state to reverse operations.
Queueing and Logging: Commands can be stored in a queue for asynchronous execution or logged for persistent storage.
Macro Support: Composite commands can be built from simpler commands.
Disadvantages/Considerations:
Increased Number of Classes: Each command generally requires its own class, potentially leading to a larger codebase.
Visitor Pattern
Category: Behavioral, Object
Intention: Represents an operation to be performed on the elements of an object structure. The Visitor pattern allows you to define a new operation on instances of a class without changing the class on which the operation is applied.
Motivation: Imagine an object structure (e.g., a tree of different node types: and ). You frequently need to perform new operations on these elements. If you add these operations directly to the element classes, the classes become cluttered, and adding new operations requires modifying all element classes, violating the Open/Closed Principle. The Visitor pattern allows you to define new operations (visitors) in a separate object, keeping the element classes stable.
Structure: The pattern typically involves:
Visitor: Declares a operation for each class of in the object structure. The operation's name identifies the class of the element it visits.
ConcreteVisitor: Implements each operation declared by . Each operation implements a fragment of the algorithm for the corresponding class of object.
Element: Defines an operation that takes a visitor as an argument.
ConcreteElement: Implements the operation, calling the appropriate operation on the visitor.
Example (Java Code Excerpts):
// Abstract Element
public abstract class Element {
abstract void accept(Visitor v);
}
// Concrete Element A
public class ElementA extends Element {
void accept(Visitor v) {
v.visit(this); // Double dispatch: calls visit(ElementA) on the visitor
}
}
// Concrete Element B
public class ElementB extends Element {
void accept(Visitor v) {
v.visit(this); // Double dispatch: calls visit(ElementB) on the visitor
}
}
// Abstract Visitor
public abstract class Visitor {
abstract void visit(ElementA a);
abstract void visit(ElementB b);
}
// Concrete Visitor for "Goodbye" operation
public class VisitorGoodbye extends Visitor {
void visit(ElementB b) {
System.out.println("Goodbye b");
}
void visit(ElementA a) {
System.out.println("Goodbye a");
}
}
// Concrete Visitor for "Hello" operation
public class VisitorHello extends Visitor {
void visit(ElementB b) {
System.out.println("Hello b");
}
void visit(ElementA a) {
System.out.println("Hello a");
}
}
// Client usage
public class Client {
public static void main(String[] args) {
Random r = new Random();
ArrayList<Element> liste = new ArrayList<>();
// Populate list randomly with ElementA and ElementB
for (int i = 0; i < 10; i++) {
if (r.nextBoolean()) {
liste.add(new ElementA());
} else {
liste.add(new ElementB());
}
}
// Apply a random operation (VisitorGoodbye or VisitorHello) to elements
for (Element e : liste) {
if (r.nextBoolean()) {
e.accept(new VisitorGoodbye());
} else {
e.accept(new VisitorHello());
}
}
}}
Consequences:
Advantages:
Adding New Operations Simplified: Adding new operations is simplified by the Visitor pattern. You just need to add a new concrete visitor class, implementing the new operation's logic for each element type.
Operation Centralization: Operations on an object (or class) are centralized within its visitor, making it easy to see all operations for a specific element type in one place.
Decoupling: Decouples algorithms from the object structure.
Disadvantages/Considerations:
Adding New Concrete Element Types is Cumbersome: It is tedious to add new concrete element classes. This requires adding a new method, taking the new concrete object into account, in each concrete visitor class.
Breaks Encapsulation: Visitors often need to access the internal state of elements, potentially breaking their encapsulation.
Learning and Application
To effectively use design patterns, engineers need to:
Identify Quality Software Design Challenges: Recognize the issues that design patterns aim to solve (e.g., lack of reusability, tight coupling).
Know the Tools: Understand how to use patterns to achieve modular and extensible designs.
Recognize Common Patterns: Be familiar with the most frequently used design patterns.
Select Appropriate Patterns: Identify which patterns are best suited to address a particular problem.
Adapt Patterns: Integrate suitable design patterns into existing designs.
Pedagogical Methods and Prerequisites
Effective learning typically involves:
Interactive Courses: Engaging discussions and explanations of pattern concepts.
Virtual OS TPs (Practical Work): Hands-on experience implementing patterns in a controlled environment.
Case Studies: Analyzing real-world scenarios where patterns are applied to solve design problems.
Prerequisites for successful engagement with design patterns include the ability to:
Develop information systems using a language like Java, adhering to best practices.
Propose object-oriented designs for information systems.
Utilize UML diagrams to model systems and formalize their behavior.
Work autonomously.
Conclusion
Design patterns are not ready-made code; they are documented solutions, templates that can be adapted to specific problems. They provide a common language for designers, making communication about complex design structures more efficient and less ambiguous. Understanding and applying design patterns is a crucial skill for any software engineer aiming to build robust, maintainable, and scalable object-oriented systems.
References
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1995). Design Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley. (The original "Gang of Four" book)
Alexander, C., Ishikawa, S., & Silverstein, M. (1977). A Pattern Language: Towns, Buildings, Construction. Oxford University Press.
Jacobson, M., & Jacobson, K. (2002). Patterns of Home: The Ten Essentials of Enduring Design. Taunton Press.
Inizia un quiz
Testa le tue conoscenze con domande interattive