Structural Pattern
Description
Structural patterns in software engineering deal with the composition of classes or objects to form larger structures while keeping the system flexible and efficient. These patterns focus on how classes and objects are connected or structured to provide new functionality or improve system architecture. Structural patterns often involve creating interfaces or abstract classes that define the structure of the system and providing implementations that realize this structure.
Types of Structural Pattern
Adapter Pattern
Description
The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, converting the interface of a class into another interface that a client expects. The Adapter Pattern allows classes to work together that couldn't otherwise because of incompatible interfaces, promoting code reuse and interoperability.
Imagine if we have two existing classes with incompatible interfaces (methods, properties) that need to collaborate. The Adapter Pattern bridges this gap by:
Defining an Adapter Class: This class implements the target interface (the interface the client code expects) and also holds a reference to the incompatible object (the adaptee).
Adapting the Interface: The adapter class implements the target interface methods by delegating the work to the adaptee's methods or by converting data as needed.
Benefits of Adapter Pattern
Promotes reusability: Allows you to reuse existing incompatible classes without modifying their code.
Improves maintainability: Keeps the core functionality of the incompatible class separate from the adapter, making changes easier.
Increases flexibility: Enables working with different implementations as long as they can be adapted to the target interface.
Drawbacks of Adapter Pattern
Increased complexity: Introduces an extra layer of abstraction (the adapter class) which can add complexity.
Potential performance overhead: Adapting method calls might introduce some overhead compared to direct calls.
Tight coupling to the adaptee: Changes to the adaptee's interface might require modifications in the adapter.
When to Use Adapter Pattern
The Adapter Pattern is suitable when:
You need to integrate with existing, incompatible classes or libraries.
You want to isolate the core functionality of a class from the way it's used by clients.
You anticipate needing to support different implementations that can be adapted to a common interface.
Example 1: Legacy printer interface and modern computer (client)
Consider a example where we have a legacy printer that only supports printing in plain text format, and we want to connect it to a modern computer that expects to print in PDF format. We can use the Adapter Pattern to create an adapter class that converts the modern computer's PDF printing interface into the legacy printer's plain text printing interface
We have target interface Printer
that defines the interface expected by the client code for printing, an adaptee class LegacyPrinter
that represents the legacy printer with a method to print in plain text format, an adapter class PrinterAdapter
that implements the Printer
interface and wraps the LegacyPrinter
object.
Example 2: Legacy payment processor and e-commerce application (client)
Consider a scenario where you want to use a legacy payment processor library (adaptee) with your new e-commerce application (client code). The legacy library might have methods like chargeCreditCard
while your application expects a processPayment
method.
The LegacyPaymentProcessor
interface represents the incompatible library. The PaymentProcessor
interface defines the expected interface for your application. The LegacyPaymentProcessorAdapter
implements the PaymentProcessor
and adapts the chargeCreditCard
method to the processPayment
method.
Bridge Pattern
Description
The Bridge Pattern is a structural design pattern that separates the abstraction from its implementation so that they can vary independently. It allows to create two separate hierarchies one for abstraction (interface or abstract class) and one for implementation (concrete class) and then connect them together using composition. This pattern promotes loose coupling between abstraction and implementation, enabling changes in one part of the system without affecting the other.
Imagine if we have a system with a complex hierarchy of classes representing different functionalities (e.g., shapes with different colors). The Bridge Pattern promotes flexibility and maintainability by separating these concerns:
Defining an Abstraction Interface: This interface defines the operations that can be performed on the object (e.g., draw a shape).
Creating Concrete Implementations (Implementors): These classes implement the functionalities behind the abstraction (e.g., specific shapes like circle, square).
Creating a Bridge Class: This class holds a reference to an implementor object and implements the abstraction interface. It delegates the actual work to the implementor object based on the chosen functionality.
Benefits of Bridge Pattern
Decoupling abstraction and implementation: Allows independent changes to both aspects without affecting the other.
Improved maintainability: Easier to modify or extend shapes and colors independently.
Increased flexibility: Enables creating new combinations of shapes and colors easily.
Drawbacks of Bridge Pattern
Increased complexity: Introduces additional classes (interfaces and bridge class) which can add complexity.
Potential performance overhead: Delegation through the bridge class might introduce some overhead compared to direct calls.
Overkill for simple scenarios: If the relationship between abstraction and implementation is straightforward, the pattern might be unnecessary.
When to Use Bridge Pattern
The Bridge Pattern is suitable when:
We need to decouple an abstraction from its implementation for independent variation.
We have a large hierarchy of classes with multiple variations (e.g., shapes with different behaviors and appearances).
We anticipate the need to extend the system with new functionalities (shapes, colors) in the future.
Example
Let's consider example of a remote control for electronic devices, such as TVs and DVD players. Each device (TV or DVD player) can have different functionalities (turn on/off, adjust volume, change channels, etc.). We can use the Bridge Pattern to separate the abstraction (remote control) from its implementation (devices) and allow them to vary independently.
Composite Pattern
Description
The Composite Pattern is a structural design pattern that allows to compose objects into tree-like structures to represent part-whole hierarchies. It enables clients to treat individual objects and compositions of objects uniformly. In other words, clients can treat a single object and a group of objects in a uniform manner without distinguishing between them. This pattern is useful when you want to represent hierarchical structures of objects and apply operations uniformly across the entire hierarchy.
Imagine we have a complex system with objects that can be treated individually or as part of a larger group. The Composite Pattern allows you to handle them uniformly by:
Defining a Component Interface: This interface declares the operations (methods) that both individual objects (leaves) and composite objects (containers) can perform. These operations might include adding or removing child components and performing actions on the component itself.
Creating Concrete Classes: These classes implement the
Component
interface and represent individual objects (leaves) or composite objects (containers). Leaves typically implement the operations directly, while containers can delegate them to their child components.
Benefits of Composite Pattern
Uniform treatment of objects: Allows treating individual objects and composite objects in the same way.
Hierarchical representation: Models part-whole hierarchies effectively.
Flexible structure: Enables building complex structures by composing objects.
Drawbacks of Composite Pattern
Increased complexity: Introduces an extra layer of abstraction (the interface) which can add complexity.
Overkill for flat structures: If you only have flat collections of objects, the pattern might be unnecessary.
When to Use Composite Pattern
The Composite Pattern is suitable when:
We need to represent hierarchical structures where objects can be treated individually or as a whole.
We want to perform operations on entire branches of the hierarchy.
We anticipate needing to extend the structure with new types of objects
Example 1
Consider a file system where we have files and folders. Both files and folders can be treated as components in the hierarchy. The Composite Pattern allows to manage them uniformly.
The FileSystemComponent
interface defines methods for managing and displaying files and folders. The File
class implements the interface for individual files. The Folder
class implements the interface for folders and can contain other components. Both files and folders can be treated uniformly using the displayInfo()
method, which recursively traverses the tree structure for folders.
Example 2
Let's consider another example of an organization structure, where employees are organized into departments, and departments can contain both individual employees and sub-departments. We can use the Composite Pattern to represent the organization structure as a tree-like hierarchy, with departments and individual employees as nodes.
In this example, we have an interface Employee
representing individual employees and departments in the organization structure, a leaf class IndividualEmployee
representing individual employees, a composite class Department
representing departments which can contain both individual employees and sub-departments and the Department
class contains a list of employees (individual employees and sub-departments) and implements the displayDetails()
method to display details of the department and its employees.
Decorator Pattern
Description
The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects dynamically without affecting the behavior of other objects from the same class. It is useful when you want to add new functionalities to objects without altering their structure. The Decorator Pattern involves creating a set of decorator classes that are used to wrap concrete components. Each decorator class adds its own functionality to the component, which can be stacked on top of each other to create a combination of behaviors.
Imagine we have objects with functionalities that we want to modify or extend at runtime without changing their core implementation. The Decorator Pattern achieves this by:
Defining a Component Interface: This interface declares the core functionality of the objects you want to decorate.
Creating Concrete Component Classes: These classes implement the
Component
interface and represent the base objects with their core functionalities.Creating Decorator Classes: These classes implement the
Component
interface and "wrap" a concrete component object. They add new functionalities or modify the behavior of the wrapped object dynamically. Decorators typically hold a reference to the wrapped component and delegate calls to it while potentially adding their own behavior before or after.
Benefits of Decorator Pattern
Dynamic extension of functionality: Allows adding new functionalities to objects at runtime without modifying their original code.
Flexible composition: You can combine different decorators to achieve complex behavior.
Loose coupling: Decorators and components are loosely coupled, promoting maintainability.
Drawbacks of Decorator Pattern
Increased complexity: Introduces additional classes (decorators) which can add complexity.
Potential performance overhead: Decorator method calls can add some overhead compared to direct calls.
Can lead to long chains of decorators: Managing a large number of decorators might become cumbersome.
When to Use Decorator Pattern
The Decorator Pattern is suitable when:
We need to add functionalities to objects dynamically without subclassing.
We want to support multiple layers of optional functionality.
We anticipate the need to extend functionality in the future without modifying existing objects.
Example 1
Consider a example of a coffee ordering system, where customers can order various types of coffee with optional toppings such as milk, sugar, and whipped cream. We can use the Decorator Pattern to create decorators for each optional topping and then dynamically add them to the base coffee order.
In this example, we have an interface Coffee
representing the base component for coffee orders, with methods to get the description and cost of the coffee. We have a concrete component BasicCoffee
representing the basic coffee order without any toppings. We have an abstract decorator class CoffeeDecorator
that implements the Coffee
interface and wraps concrete components. We have concrete decorator classes Milk
and Sugar
that add milk and sugar toppings to the coffee order. We create a basic coffee order and then dynamically add milk and sugar toppings to it using decorators.
Example 2
Consider another example of a text editor where we can format text with functionalities like bold, italic, and underline. The Decorator Pattern allows to add these features dynamically:
In this example, the Text
interface defines a method to get the text content. The PlainText
class implements the interface for plain text. The TextDecorator
is an abstract class that wraps a Text
object and provides a base for concrete decorators. Concrete decorators like BoldDecorator
and ItalicDecorator
modify the text by adding HTML formatting tags
Facade Pattern
Description
The Facade Pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem. It hides the complexities of the subsystem and provides a single interface that the client can use to interact with the subsystem. The Facade Pattern promotes loose coupling between the client and the subsystem by providing a high-level interface that shields the client from the details of the subsystem's implementation.
Imagine we have a complex system with many interacting objects and functionalities. The Facade Pattern provides a simplified interface (facade) to this complexity, hiding the underlying implementation details. Here's the approach:
Defining a Facade Class: This class acts as a single point of entry for interacting with the subsystem. It provides a simplified set of methods that expose essential functionalities of the complex system.
Facade Implementation: The facade class encapsulates the logic for interacting with the various objects within the subsystem. It might delegate calls to specific objects or orchestrate a sequence of operations to fulfill the requested functionality.
Benefits of Facade Pattern
Simplified interface: Provides a simpler and more user-friendly way to interact with a complex system.
Decoupling client code: Client code only interacts with the facade, hiding the internal implementation details.
Improved maintainability: Changes within the subsystem can be contained within the facade without affecting client code.
Drawbacks of Facade Pattern
Reduced flexibility: The facade might limit access to certain functionalities of the underlying objects.
Tight coupling between facade and subsystem: Changes in the subsystem might require modifications to the facade.
Potential complexity for large systems: For very large systems, managing a single facade class might become complex.
When to Use Facade Pattern
The Facade Pattern is suitable when:
We have a complex system with many interacting objects.
We want to provide a simplified interface for client code to interact with the system.
We need to decouple client code from the implementation details of the subsystem.
Example
Consider a example of a home theater system, which consists of various subsystems such as the DVD player, amplifier, speakers, and screen. Each subsystem may have its own complex interface. We can use the Facade Pattern to create a home theater facade that provides a simple interface for common operations such as watching a movie.
In this example, we have a facade class HomeTheaterFacade
that provides a simple interface for common operations such as watching a movie and ending the movie. We have subsystem classes DVDPlayer
, Amplifier
, Speakers
, and Screen
that represent the individual components of the home theater system. The HomeTheaterFacade
class encapsulates the interactions with the subsystems and hides the complexities of the subsystems' interfaces.
Flyweight Pattern
Description
The Flyweight Pattern is a structural design pattern that aims to minimize memory usage and improve performance by sharing as much data as possible with similar objects. It is particularly useful when dealing with a large number of objects that have similar or identical intrinsic state, and when the extrinsic state can be managed externally.
Imagine we have an application that deals with a large number of similar objects. Each object might have some unique state, but also share a lot of common data or functionality. The Flyweight Pattern promotes memory efficiency by:
Defining a Flyweight Interface: This interface declares the methods that all flyweight objects can perform.
Creating Concrete Flyweight Classes: These classes implement the
Flyweight
interface and represent the intrinsic state (unchanging data) of the objects. They typically avoid storing any extrinsic state (data specific to each instance) within themselves.Creating a Flyweight Factory: This class (optional) is responsible for managing the pool of flyweight objects and ensuring efficient reuse. It might return existing flyweight objects with the appropriate intrinsic state or create new ones if necessary.
Benefits of Flyweight Pattern
Reduced memory usage: By sharing common data among objects, the Flyweight Pattern can significantly reduce memory consumption, especially for large numbers of similar objects.
Improved performance: Object creation can become faster as flyweight objects are reused instead of being created every time.
Drawbacks of Flyweight Pattern
Increased complexity: Introduces additional classes (flyweight factory) which can add complexity.
Limited applicability: Not suitable for objects with a lot of unique data or complex state management.
Example
Consider a game with many forest trees. Each tree might have a unique location (extrinsic state) but share the same image data (intrinsic state). The Flyweight Pattern can optimize memory usage.
In this example, the Tree
interface defines a method to draw the tree. Concrete flyweight classes like OakTree
and PineTree
hold the image data (intrinsic state) and implement the draw
method. The TreeFactory
(optional) manages a pool of flyweight objects and provides a way to retrieve them based on the type. The Forest
class uses the TreeFactory
to plant trees, potentially reusing existing flyweight objects for the same tree type.
Proxy Pattern
Description
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It allows you to create a representative object (proxy) that controls the access to the original object (subject). The proxy object acts as an intermediary between the client and the real object, providing additional functionality such as lazy initialization, access control, logging, or caching.
Benefits of Proxy Pattern
Improved control: Provides an extra layer of control over access to the real subject.
Increased flexibility: Enables adding functionalities like caching, security checks, or lazy loading.
Decoupling client code: Client code only interacts with the proxy, hiding the details of the real subject.
Drawbacks of Proxy Pattern
Increased complexity: Introduces an extra layer of abstraction (the proxy class) which can add complexity.
Potential performance overhead: Method calls might have some overhead due to the extra layer of indirection through the proxy.
Example
Let's consider a example of internet access control in an organization, where employees need to access certain websites through a proxy server. The proxy server acts as an intermediary between the employees' computers and the external websites, controlling and monitoring the internet access.
In this example, we have a Internet
interface that defines the common method connectTo()
for connecting to websites. We have a real subject class RealInternet
that implements the Internet
interface and represents the real internet connection. We have a proxy class InternetProxy
that implements the Internet
interface and acts as a proxy for controlling access to the real internet connection. The InternetProxy
class intercepts the requests to connect to websites and checks if the requested website is in the list of blocked websites.
Last updated