Getting Started with Design Patterns -Part 2: Structural Patterns
Design patterns are essential tools in software development that help solve recurring design problems and improve code maintainability, scalability, and readability. Java, being a widely-used programming language, offers a plethora of design patterns to address various challenges.
In the Part 1 of this series, I discussed around the Creational Pattern.
In this article, we’ll put our focus on Structural Pattern.
Structural Patterns
Structural design patterns are a subset of design patterns in software engineering. They focus on the structure of classes and objects and how they can be combined to form larger, more flexible, and efficient structures. These patterns define relationships between different parts of a software system, emphasizing how they can work together to create well-organized and maintainable code.
The primary objectives that structural design patterns help in achieving include:
- Separation of Concerns: Structural patterns promote the separation of concerns in a software system. They help in organizing code by separating different aspects of a system into distinct classes and objects. This separation makes the codebase more modular and easier to understand, maintain, and modify.
- Reusability: By providing standardized ways to create object structures, structural patterns facilitate code reuse. Developers can use these patterns to compose classes and objects in a consistent manner across different parts of a system or in multiple projects, reducing redundancy and promoting a more efficient development process.
- Flexibility and Extensibility: Structural patterns allow for flexibility in the design of a system. They enable changes to be made more easily by adding, removing, or modifying components without affecting the entire structure. This makes the system more adaptable to evolving requirements and reduces the risk of introducing bugs when making changes.
- Abstraction: Structural patterns often involve abstract classes and interfaces, which promote a high level of abstraction. This abstraction allows developers to work with concepts and relationships at a higher level, making the codebase more intuitive and easier to work with.
- Encapsulation: These patterns promote encapsulation by defining clear interfaces and encapsulating the implementation details within classes. This helps in hiding complexity and provides a clear boundary between the public and private aspects of a class, improving code maintainability and reducing the risk of errors.
- Performance Optimization: Some structural patterns, like the Flyweight pattern, focus on optimizing performance by reducing memory or computational overhead. By sharing common components and minimizing resource usage, these patterns help in achieving better system performance.
- Improved Maintainability: Structural patterns contribute to code maintainability by providing well-defined and organized structures. This makes it easier for developers to understand and modify the code, which is crucial for long-term maintenance and bug fixing.
- Promotion of Good Coding Practices: Structural patterns encourage adherence to best practices in software design. They promote concepts like loose coupling, which reduces dependencies between classes, and high cohesion, which ensures that related functionality is grouped together. These principles lead to more maintainable and robust codebases.
Below are some of the most commonly used structural patterns.
Bridge Pattern
The Bridge Pattern is a structural design pattern that separates the abstraction from its implementation so that the two can vary independently. It is used to decouple an abstraction (such as an interface or an abstract class) from its concrete implementation, allowing both to evolve independently without affecting each other. This pattern is particularly useful when you have a set of abstractions and a set of concrete implementations, and you want to avoid a permanent binding between them.
Let’s break down the Bridge Pattern in more detail.
Components of the Bridge Pattern
- Abstraction: This is an interface or an abstract class that defines the high-level interface that clients use to interact with the system. It contains a reference to the Implementor.
- Refined Abstraction: These are subclasses of the Abstraction that provide additional functionality or customization.
- Implementor: This is an interface or an abstract class that defines the low-level interface for the concrete implementations. It is referenced by the Abstraction.
- Concrete Implementor: These are the concrete classes that implement the Implementor interface. They provide the actual implementation of the low-level operations.
Pros of the Bridge Pattern
- Separation of Concerns: The Bridge Pattern promotes a clean separation between the abstraction and its implementation. This separation allows changes in one part of the system to have minimal impact on the other, making the code more maintainable.
- Flexibility: It allows you to create new abstractions or implementations independently, providing flexibility in extending or modifying the system.
- Reusability: Both abstractions and implementations can be reused independently in different contexts, which promotes code reuse.
- Open-Closed Principle: The Bridge Pattern adheres to the Open-Closed Principle, which means that you can introduce new abstractions or implementations without modifying existing code.
Cons of the Bridge Pattern
- Complexity: The Bridge Pattern can introduce some complexity, especially when you have a large number of abstractions and implementations. It might require additional classes and interfaces.
- Increased Development Time: Implementing the Bridge Pattern can take more time compared to a simpler, tightly coupled design. This can be a drawback for small projects or when strict deadlines are in place.
Use Case
Let’s consider a practical example of a drawing application where we want to draw different shapes (e.g., circles, squares) using different rendering techniques (e.g., raster or vector).
// Implementor interface
interface DrawingAPI {
void drawCircle(double x, double y, double radius);
}
// Concrete Implementor - Raster Drawing API
class DrawingAPIRaster implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("Drawing a circle at (%.2f, %.2f) with radius %.2f in raster mode%n", x, y, radius);
}
}
// Concrete Implementor - Vector Drawing API
class DrawingAPIVector implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("Drawing a circle at (%.2f, %.2f) with radius %.2f in vector mode%n", x, y, radius);
}
}
// Abstraction
abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
}
// Refined Abstraction - Circle
class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
drawingAPI.drawCircle(x, y, radius);
}
}
public class BridgePatternDemo {
public static void main(String[] args) {
DrawingAPI rasterAPI = new DrawingAPIRaster();
DrawingAPI vectorAPI = new DrawingAPIVector();
Shape circleRaster = new Circle(1, 2, 3, rasterAPI);
Shape circleVector = new Circle(2, 3, 4, vectorAPI);
circleRaster.draw();
circleVector.draw();
}
}
In this example, the Bridge Pattern separates the abstraction (Shape) from its implementation (DrawingAPI). We have two concrete implementations (Raster and Vector) for drawing, and we can create various shapes using different drawing techniques without modifying the shape classes. This promotes flexibility, maintainability, and reusability in the drawing application.
Composite Pattern
The Composite Pattern is a structural design pattern that allows you to compose objects into tree-like structures to represent part-whole hierarchies. It lets you treat individual objects and compositions of objects uniformly, making it easier to work with complex structures of objects. The key idea behind the Composite Pattern is to create a common interface for both individual objects and composite objects (collections of objects) so that clients can interact with them in a consistent manner.
Let’s delve into the Composite Pattern in more detail
Components of the Composite Pattern
- Component: This is the common interface or abstract class that defines the operations that can be performed on both leaf (individual) objects and composite (collection) objects.
- Leaf: These are the individual objects that do not have any children. They implement the Component interface.
- Composite: These are objects that can contain other objects, including both leaf objects and other composite objects. The Composite class also implements the Component interface.
Pros of the Composite Pattern
- Simplifies Client Code: Clients can treat individual objects and composite objects uniformly through the common Component interface, making client code simpler and more intuitive.
- Flexibility: It provides a flexible way to create complex structures by nesting objects. You can create hierarchies of objects of varying complexities easily.
- Recursive Structure: The pattern supports a recursive structure, which is suitable for representing recursive data structures like file systems, organization hierarchies, or graphic scenes.
- Scalability: You can add or remove objects dynamically from a composite without affecting the client code. This scalability is useful for handling varying numbers of objects in a structure.
Cons of the Composite Pattern
- Complexity: Managing a composite structure can be complex, especially when dealing with deeply nested hierarchies. This complexity can lead to performance and maintenance challenges.
- Limited Type Checking: Since the common interface treats leaf and composite objects uniformly, it can be challenging to enforce type-specific operations at compile-time. You may need to use runtime checks or casting, which can lead to potential errors.
Use Case
Let’s consider a use case where we want to model a hierarchical organization structure of employees in a company using the Composite Pattern
// Component interface
interface Employee {
String getName();
double getSalary();
}
// Leaf class - Individual Employee
class IndividualEmployee implements Employee {
private String name;
private double salary;
public IndividualEmployee(String name, double salary) {
this.name = name;
this.salary = salary;
}
@Override
public String getName() {
return name;
}
@Override
public double getSalary() {
return salary;
}
}
// Composite class - Department
class Department implements Employee {
private String name;
private List<Employee> employees = new ArrayList<>();
public Department(String name) {
this.name = name;
}
public void addEmployee(Employee employee) {
employees.add(employee);
}
@Override
public String getName() {
return name;
}
@Override
public double getSalary() {
double totalSalary = 0;
for (Employee employee : employees) {
totalSalary += employee.getSalary();
}
return totalSalary;
}
}
public class CompositePatternDemo {
public static void main(String[] args) {
Employee john = new IndividualEmployee("John", 50000);
Employee jane = new IndividualEmployee("Jane", 60000);
Department engineering = new Department("Engineering");
engineering.addEmployee(john);
engineering.addEmployee(jane);
Employee sarah = new IndividualEmployee("Sarah", 75000);
Department management = new Department("Management");
management.addEmployee(sarah);
Department company = new Department("Company");
company.addEmployee(engineering);
company.addEmployee(management);
// Calculate total company salary
double totalSalary = company.getSalary();
System.out.println("Total company salary: $" + totalSalary);
}
}
In this example, we use the Composite Pattern to model a company’s organizational structure. Employees can be either individual employees (leaf objects) or departments (composite objects) that contain other employees or sub-departments. The common interface Employee
allows us to calculate the total company salary uniformly, whether it's an individual employee or a department.
Mixin Pattern
The Mixin Pattern is a design pattern in object-oriented programming that allows you to add or “mix in” new behaviors or properties to classes dynamically, without the need for inheritance. It’s a way to achieve code reuse and composability by combining multiple classes or components into a single class.
Components of the Mixin Pattern
- Mixin: A mixin is a class or module that encapsulates a specific behavior or functionality. It is designed to be added or mixed into other classes to extend their capabilities.
- Target Class: The target class is the class that receives the mixin’s functionality. The target class typically does not inherit from the mixin but includes it or uses it in some way.
Pros of the Mixin Pattern
- Code Reuse: Mixins allow you to reuse code across multiple classes without creating deep inheritance hierarchies. This promotes a more modular and maintainable codebase.
- Composability: Mixins can be combined to create complex behaviors by mixing in multiple mixins. This promotes composability and flexibility in class design.
- Avoids Inheritance Issues: Unlike traditional inheritance, mixins do not introduce the issues associated with deep inheritance hierarchies, such as the diamond problem in multiple inheritance.
- Dynamic Composition: You can add or remove mixins from a class dynamically at runtime, allowing for dynamic behavior modification.
Cons of the Mixin Pattern
- Name Conflicts: If two mixins define methods or properties with the same name, it can lead to naming conflicts. Careful naming and namespace management are essential to avoid such issues.
- Complexity: As more mixins are used, the complexity of the code can increase. It may become challenging to understand how the composed behaviors interact.
- Limited Language Support: Some programming languages provide limited support for mixins, requiring developers to implement mixins manually, which can be error-prone.
Use Case
Let’s consider a simple use case of the Mixin Pattern in a programming language that supports mixins, such as Python. Suppose we have a set of mixins that represent different behaviors for vehicles, and we want to create various vehicle classes by mixing in these behaviors.
// Mixins (Interfaces representing behaviors)
interface EngineMixin {
void startEngine();
}
interface ElectricMixin {
void chargeBattery();
}
interface GasMixin {
void refuel();
}
// Base class
class Vehicle {
private String make;
private String model;
public Vehicle(String make, String model) {
this.make = make;
this.model = model;
}
public void displayInfo() {
System.out.println("Make: " + make);
System.out.println("Model: " + model);
}
}
// Vehicle classes with mixed-in behaviors
class ElectricCar extends Vehicle implements ElectricMixin, EngineMixin {
public ElectricCar(String make, String model) {
super(make, model);
}
@Override
public void chargeBattery() {
System.out.println("Battery charging");
}
@Override
public void startEngine() {
System.out.println("Engine started");
}
}
class GasCar extends Vehicle implements GasMixin, EngineMixin {
public GasCar(String make, String model) {
super(make, model);
}
@Override
public void refuel() {
System.out.println("Gas tank refueled");
}
@Override
public void startEngine() {
System.out.println("Engine started");
}
}
public class MixinPatternDemo {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar("Tesla", "Model S");
electricCar.displayInfo();
electricCar.chargeBattery();
electricCar.startEngine();
GasCar gasCar = new GasCar("Ford", "Mustang");
gasCar.displayInfo();
gasCar.refuel();
gasCar.startEngine();
}
}
In this Java example, we have interfaces (EngineMixin
, ElectricMixin
, and GasMixin
) representing different behaviors. We then create vehicle classes (ElectricCar
and GasCar
) by implementing these mixins and extending the Vehicle
base class. The classes can exhibit mixed-in behaviors along with their own methods. The MixinPatternDemo
demonstrates how these classes can be used with mixed-in behaviors.
Facade Pattern
The Facade Pattern is a structural design pattern that provides a simplified interface to a more complex system, making it easier to interact with that system. It acts as a facade, or a front-facing interface, that hides the underlying complexity of a subsystem, providing clients with a single entry point for accessing its functionality. The primary goal of the Facade Pattern is to simplify the usage of a complex system and improve its maintainability by reducing dependencies.
Components of the Facade Pattern
- Facade: This is the main interface provided to clients. It represents a unified, simplified interface to the complex subsystem. Clients interact with the facade to access the subsystem’s functionality.
- Subsystems: Subsystems are components or classes that make up the complex system. They are responsible for performing specific tasks or operations. The facade delegates client requests to these subsystems.
Pros of the Facade Pattern
- Simplified Interface: It provides a simplified and user-friendly interface for clients, reducing the complexity of using the underlying system.
- Decoupling: The facade isolates clients from the details of the subsystem, reducing dependencies and promoting loose coupling between the client and the subsystem.
- Encapsulation: It encapsulates the subsystem’s functionality, allowing changes to be made within the subsystem without affecting clients.
- Improved Maintainability: Facades improve the maintainability of the codebase by centralizing the interaction with the subsystem. This makes it easier to make changes and updates.
- Promotes Best Practices: The pattern encourages good design practices, such as separation of concerns and abstraction.
Cons of the Facade Pattern
- Limited Customization: While the facade simplifies interaction, it may limit the flexibility of clients to customize or access certain subsystem components directly.
- Additional Abstraction: Introducing a facade adds another layer of abstraction, which can increase complexity for simple subsystems.
Use Case
Let’s consider a use case of a Facade Pattern in a hypothetical multimedia player application. The multimedia player can play audio and video files, but the underlying subsystem for handling media formats is complex. We’ll create a facade to simplify the interaction for clients.
// Subsystem components
class AudioPlayer {
public void playAudio(String audioFile) {
System.out.println("Playing audio: " + audioFile);
}
}
class VideoPlayer {
public void playVideo(String videoFile) {
System.out.println("Playing video: " + videoFile);
}
}
// Facade for multimedia player
class MultimediaPlayer {
private AudioPlayer audioPlayer;
private VideoPlayer videoPlayer;
public MultimediaPlayer() {
audioPlayer = new AudioPlayer();
videoPlayer = new VideoPlayer();
}
public void playMedia(String mediaFile) {
String fileType = mediaFile.substring(mediaFile.lastIndexOf('.') + 1);
if ("mp3".equalsIgnoreCase(fileType) || "wav".equalsIgnoreCase(fileType)) {
audioPlayer.playAudio(mediaFile);
} else if ("mp4".equalsIgnoreCase(fileType) || "avi".equalsIgnoreCase(fileType)) {
videoPlayer.playVideo(mediaFile);
} else {
System.out.println("Unsupported media format: " + fileType);
}
}
}
public class FacadePatternDemo {
public static void main(String[] args) {
MultimediaPlayer player = new MultimediaPlayer();
player.playMedia("song.mp3");
player.playMedia("movie.mp4");
player.playMedia("document.pdf"); // Unsupported format
}
}
In this example, the MultimediaPlayer
facade simplifies the interaction with the audio and video players. Clients can use the playMedia
method to play media files, and the facade delegates the task to the appropriate subsystem component based on the file type. This hides the complexity of handling different media formats from the client code, making it more user-friendly and maintainable.
Adapter Pattern
The Adapter Pattern is a structural design pattern used to make two incompatible interfaces work together. It allows objects with incompatible interfaces to collaborate by providing a wrapper, or adapter, around one of the objects, effectively making it compatible with the other. The Adapter Pattern is especially useful when integrating existing classes, libraries, or components that have different interfaces.
Components of the Adapter Pattern
- Target: This is the interface that the client code expects or the class that the client code is designed to work with.
- Adaptee: This is the class or component with the incompatible interface that you want to integrate into your system.
- Adapter: This is the class that acts as a bridge between the client code and the Adaptee. It implements the Target interface and internally delegates the calls to the Adaptee.
Pros of the Adapter Pattern
- Compatibility: It allows integration of new or existing classes with different interfaces, promoting code reuse and interoperability.
- Flexibility: You can add adapters for different Adaptees without modifying the existing client code.
- Encapsulation: It encapsulates the complexities of interfacing with the Adaptee, keeping the client code clean and simple.
- Solves the “Open-Closed” Principle: The Adapter Pattern follows the Open-Closed Principle, as you can introduce new adapters without changing the existing code.
Cons of the Adapter Pattern
- Complexity: If you have a large number of Adaptees and adapters, it can introduce complexity to the codebase.
- Performance Overhead: Depending on the implementation, using adapters may introduce some performance overhead, as calls are forwarded through an additional layer.
Use Case
Let’s consider a use case where we have an existing legacy system that provides weather information in Fahrenheit, but our new application requires weather information in Celsius. We can use the Adapter Pattern to adapt the legacy system’s interface to match our application’s expectations.
// Target interface (Celsius temperature)
interface TemperatureProvider {
double getTemperatureInCelsius();
}
// Adaptee class (Fahrenheit temperature)
class FahrenheitTemperatureProvider {
public double getTemperatureInFahrenheit() {
// Simulate fetching temperature data in Fahrenheit from a legacy system
return 68.0; // Example temperature in Fahrenheit
}
}
// Adapter class that adapts Fahrenheit to Celsius
class FahrenheitToCelsiusAdapter implements TemperatureProvider {
private FahrenheitTemperatureProvider fahrenheitProvider;
public FahrenheitToCelsiusAdapter(FahrenheitTemperatureProvider fahrenheitProvider) {
this.fahrenheitProvider = fahrenheitProvider;
}
@Override
public double getTemperatureInCelsius() {
// Convert Fahrenheit to Celsius
double fahrenheitTemperature = fahrenheitProvider.getTemperatureInFahrenheit();
return (fahrenheitTemperature - 32.0) * 5.0 / 9.0;
}
}
public class AdapterPatternDemo {
public static void main(String[] args) {
// Legacy Fahrenheit temperature provider
FahrenheitTemperatureProvider fahrenheitProvider = new FahrenheitTemperatureProvider();
// Adapter to convert Fahrenheit to Celsius
TemperatureProvider celsiusAdapter = new FahrenheitToCelsiusAdapter(fahrenheitProvider);
// Application code expects temperature in Celsius
double temperatureInCelsius = celsiusAdapter.getTemperatureInCelsius();
System.out.println("Temperature in Celsius: " + temperatureInCelsius);
}
}
In this example, we have a legacy FahrenheitTemperatureProvider
class with an incompatible interface. We create an adapter FahrenheitToCelsiusAdapter
that implements the TemperatureProvider
interface and converts Fahrenheit to Celsius. The adapter allows the client code to fetch temperature information in Celsius, even though the legacy system provides it in Fahrenheit.
Proxy Pattern
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 add an additional layer of control over object access, such as lazy loading, access control, or logging, without altering the core functionality of the original object. In essence, a proxy is a wrapper class for an object that adds a level of indirection.
Key Concepts of the Proxy Pattern
- Subject: This is the interface that both the RealSubject (the actual object being proxied) and the Proxy implement. The Proxy forwards requests to the RealSubject.
- RealSubject: This is the real object that the Proxy represents. It provides the core functionality that the Proxy may enhance or control.
- Proxy: This is the class that acts as a surrogate for the RealSubject. It implements the same interface as the RealSubject, and it can perform additional tasks before or after forwarding requests to the RealSubject.
Types of Proxies
- Virtual Proxy: A virtual proxy is used to defer the creation and initialization of a resource-intensive object until it is actually needed. It can be useful for lazy loading of objects.
- Remote Proxy: A remote proxy represents an object that is located remotely, such as on a different server or in a different address space. It manages communication and marshalling between the local client and the remote object.
- Protection Proxy: A protection proxy controls access to an object by enforcing access control rules. It is often used for authorization and security checks.
Pros of the Proxy Pattern
- Lazy Initialization: It allows for lazy loading of objects, which can improve performance by deferring the creation of expensive objects until they are needed.
- Access Control: Proxies can enforce access control policies, allowing or denying access to certain resources or operations.
- Logging and Monitoring: Proxies can add logging, monitoring, and profiling capabilities without modifying the core functionality of the real object.
- Caching: A proxy can implement caching mechanisms to store and reuse the results of expensive operations, reducing redundant work.
- Remote Communication: Remote proxies facilitate communication between objects in different address spaces or on different servers.
Cons of the Proxy Pattern
- Complexity: Introducing proxies can add complexity to the codebase, especially when dealing with multiple types of proxies or complex access control logic.
- Performance Overhead: Depending on the proxy implementation, there may be a performance overhead due to the additional layer of indirection.
Use Case
Let’s consider a use case where we use the Proxy Pattern to implement lazy loading of images in a document viewer application. The application loads and displays documents containing images, but we want to avoid loading images until they are actually viewed.
// Subject (interface)
interface Image {
void display();
}
// RealSubject (concrete class)
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("Loading image: " + filename);
}
@Override
public void display() {
System.out.println("Displaying image: " + filename);
}
}
// Proxy (concrete class)
class ProxyImage implements Image {
private RealImage realImage;
private String filename;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
realImage = new RealImage(filename);
}
realImage.display();
}
}
public class ProxyPatternDemo {
public static void main(String[] args) {
// Create a proxy image (image not loaded yet)
Image image = new ProxyImage("sample.jpg");
// Load and display the image (loading occurs only when display() is called)
image.display();
// The image is already loaded, so no loading occurs this time
image.display();
}
}
In this example, the RealImage
class represents the actual image object that may be resource-intensive to load. The ProxyImage
class acts as a proxy for RealImage
and defers the loading of the image until the display()
method is called. This allows for lazy loading of images, improving performance when not all images are needed immediately.
Flyweight Pattern
The Flyweight Pattern is a structural design pattern that focuses on optimizing memory usage by sharing as much as possible among similar objects. It’s particularly useful when you have a large number of objects that share common characteristics and can be divided into intrinsic (shared) and extrinsic (unique) state. The Flyweight Pattern ensures that the intrinsic state is shared, while the extrinsic state is stored separately for each object.
Components of the Flyweight Pattern
- Flyweight: This is the interface or abstract class that defines the shared and non-shared (intrinsic and extrinsic) properties of objects. It declares methods through which the objects can receive and act on the extrinsic state.
- Concrete Flyweight: This is the concrete implementation of the Flyweight interface. It stores the intrinsic state that can be shared among multiple objects.
- Flyweight Factory: This is a factory class responsible for creating and managing flyweight objects. It ensures that flyweights are shared and reused whenever possible.
Intrinsic vs. Extrinsic State
- Intrinsic State: This is the part of the object’s state that can be shared among multiple objects. It is independent of the object’s context. Intrinsic state is typically stored within the flyweight objects.
- Extrinsic State: This is the part of the object’s state that varies between objects and cannot be shared. It depends on the object’s context and is typically passed as an argument when operations are performed on flyweight objects.
Pros of the Flyweight Pattern
- Memory Efficiency: The pattern reduces memory consumption by sharing common intrinsic state among multiple objects, especially when dealing with a large number of similar objects.
- Performance Improvement: Sharing of intrinsic state can lead to performance improvements, as it reduces the amount of data that needs to be stored and processed.
- Simplicity: It simplifies the design of classes by separating intrinsic and extrinsic state, making it easier to manage objects with shared characteristics.
- Maintainability: It improves code maintainability by centralizing the management of shared flyweight objects within a factory.
Cons of the Flyweight Pattern
- Complexity: The pattern can introduce complexity in the code, especially when managing the separation of intrinsic and extrinsic state.
- Reduced Encapsulation: Storing shared intrinsic state externally may reduce encapsulation, as the internal state is exposed to the client.
Use Case
Let’s consider a use case of text formatting in a text editor, where we use the Flyweight Pattern to optimize the storage of character formatting (e.g., font, size, color) for individual characters in a document.
import java.util.HashMap;
import java.util.Map;
// Flyweight interface
interface CharacterFormatting {
void applyFormatting(String text);
}
// Concrete Flyweight (shared formatting)
class SharedCharacterFormatting implements CharacterFormatting {
private String font;
private int size;
private String color;
public SharedCharacterFormatting(String font, int size, String color) {
this.font = font;
this.size = size;
this.color = color;
}
@Override
public void applyFormatting(String text) {
System.out.printf("Text: '%s' Font: %s, Size: %d, Color: %s%n", text, font, size, color);
}
}
// Flyweight Factory
class CharacterFormattingFactory {
private Map<String, CharacterFormatting> formattingCache = new HashMap<>();
public CharacterFormatting getFormatting(String font, int size, String color) {
String key = font + size + color;
formattingCache.putIfAbsent(key, new SharedCharacterFormatting(font, size, color));
return formattingCache.get(key);
}
}
public class FlyweightPatternDemo {
public static void main(String[] args) {
CharacterFormattingFactory formattingFactory = new CharacterFormattingFactory();
// Simulate text with character formatting
String text = "Hello, world!";
String font = "Arial";
int size = 12;
String color = "Black";
for (char c : text.toCharArray()) {
CharacterFormatting formatting = formattingFactory.getFormatting(font, size, color);
formatting.applyFormatting(String.valueOf(c));
}
}
}
In this example, the CharacterFormatting
interface represents the shared and non-shared properties of character formatting. The SharedCharacterFormatting
class is the concrete flyweight that stores the shared formatting properties. The CharacterFormattingFactory
manages the creation and caching of flyweight objects.
As characters in the text are processed, the same formatting properties (font, size, color) are shared among multiple characters, optimizing memory usage. This demonstrates the Flyweight Pattern’s ability to efficiently handle a large number of similar objects while reducing memory consumption.
Decorator Pattern
The Decorator Pattern is a structural design pattern that allows you to add behavior to individual objects, either statically or dynamically, without affecting the behavior of other objects from the same class. It is used to extend the functionality of classes in a flexible and reusable way. This pattern is part of the Gang of Four (GoF) design patterns.
Components of the Decorator Pattern
- Component: This is the abstract class or interface that defines the common interface for both concrete components (objects you want to decorate) and decorators. It declares the methods that represent the core functionality.
- Concrete Component: This is the class that implements the Component interface and represents the base object that you want to decorate.
- Decorator: This is the abstract class that also implements the Component interface and contains a reference to a Component object. It acts as a base class for concrete decorators.
- Concrete Decorator: These are the classes that extend the Decorator class to add specific behavior to the components. They override methods to modify or extend the behavior of the wrapped component.
Pros of the Decorator Pattern
- Open-Closed Principle: The pattern allows you to add new behaviors or functionality to classes without modifying their source code, adhering to the Open-Closed Principle.
- Flexibility: Decorators can be combined in various ways to create different combinations of behaviour, providing more flexibility than inheritance.
- Single Responsibility Principle: It helps in adhering to the Single Responsibility Principle by separating concerns related to the core functionality and additional responsibilities.
- Reusable Decorators: Decorators can be reused with different components, allowing for a wide range of combinations.
Cons of the Decorator Pattern
- Complexity: When multiple decorators are used together, the code can become complex and harder to understand.
- Ordering of Decorators: The order in which decorators are applied can impact the final behavior of the component, and it may require careful consideration.
Use Case
Let’s consider a use case of a text editor application where you want to apply various formatting options (e.g., bold, italic, underline) to text. We can use the Decorator Pattern to achieve this.
// Component interface
interface Text {
String getContent();
}
// Concrete Component
class PlainText implements Text {
private String content;
public PlainText(String content) {
this.content = content;
}
@Override
public String getContent() {
return content;
}
}
// Decorator abstract class
abstract class TextDecorator implements Text {
private Text text;
public TextDecorator(Text text) {
this.text = text;
}
@Override
public String getContent() {
return text.getContent();
}
}
// Concrete Decorators
class BoldDecorator extends TextDecorator {
public BoldDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<b>" + super.getContent() + "</b>";
}
}
class ItalicDecorator extends TextDecorator {
public ItalicDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<i>" + super.getContent() + "</i>";
}
}
class UnderlineDecorator extends TextDecorator {
public UnderlineDecorator(Text text) {
super(text);
}
@Override
public String getContent() {
return "<u>" + super.getContent() + "</u>";
}
}
public class DecoratorPatternDemo {
public static void main(String[] args) {
Text plainText = new PlainText("Hello, world!");
Text boldText = new BoldDecorator(plainText);
Text italicBoldText = new ItalicDecorator(boldText);
Text decoratedText = new UnderlineDecorator(italicBoldText);
System.out.println(decoratedText.getContent());
}
}
In this example, we have a Text
component interface representing the core functionality. The PlainText
class is a concrete component. Decorators such as BoldDecorator
, ItalicDecorator
, and UnderlineDecorator
extend the TextDecorator
abstract class and modify the behavior of the wrapped component.
By combining these decorators in various ways, you can achieve different combinations of formatting for the text content while keeping the core Text
component unchanged.
Comparison
Below is a comparitive study of the design patterns discussed in the article.
Conclusion
In conclusion, structural design patterns provide essential tools for designing software systems that are robust, flexible, and maintainable. These patterns focus on the composition of classes and objects, enabling us to create complex systems from simpler building blocks. By understanding and applying structural patterns effectively, developers can achieve a multitude of benefits, including improved code organization, reduced code duplication, enhanced reusability, and increased adaptability to changing requirements.
While each pattern serves distinct purposes, they all share the common goal of enhancing the overall structure of software systems. Choosing the right structural pattern for a particular context requires a deep understanding of the problem domain and a thoughtful consideration of the trade-offs involved.
In the ever-evolving world of software development, structural design patterns remain invaluable tools for architects and developers. By applying these patterns judiciously, we can build software systems that are not only robust and scalable but also adaptable to the dynamic challenges of the modern software landscape. So, as we continue to innovate and shape the digital future, let’s remember the enduring significance of structural design patterns in crafting software that stands the test of time.