Structural design patterns are a category of design patterns in software development that deal with object composition to form larger, more complex structures. They help in organizing and managing relationships between objects, making the software more flexible and efficient.
Here are some common structural design patterns with a brief description of each:
1. Adapter Pattern:
- Allows the interface of an existing class to be used as another interface.
- Often used to make existing classes work with others without modifying their source code.
The Adapter Pattern is a structural design pattern that allows the interface of an existing class to be used as another interface. It's often used to make existing classes work with others without modifying their source code.
Here's a JavaScript example of the Adapter Pattern:
// Target interface that the client expects class Target { request() { console.log('Target: Default behavior'); } } // Adaptee class with a different interface class Adaptee { specificRequest() { console.log('Adaptee: Specific behavior'); } } // Adapter class that makes Adaptee compatible with Target class Adapter extends Target { constructor(adaptee) { super(); this.adaptee = adaptee; } request() { console.log('Adapter: Translated behavior'); this.adaptee.specificRequest(); } } // Client code const adaptee = new Adaptee(); const adapter = new Adapter(adaptee); console.log('Client: Using Adaptee'); adaptee.specificRequest(); console.log('Client: Using Adapter'); adapter.request();
In this example:
Target
is the target interface that the client code expects to work with.Adaptee
is a class with a different interface that the client code can't use directly.Adapter
is a class that extendsTarget
and uses composition to adaptAdaptee
to theTarget
interface.- The client code uses
Adaptee
directly and also usesAdapter
to makeAdaptee
compatible with theTarget
interface.
When the client uses the Adapter
, it gets the behavior it expects through the translated behavior of the Adapter
, which in turn calls the specific behavior of the Adaptee
.
The Adapter Pattern allows you to make existing classes work together without modifying their source code, making it a useful pattern for integrating third-party libraries or components into your application.
2. Bridge Pattern:
- Separates an object’s abstraction from its implementation.
- Allows you to change the implementation of an object without altering its interface.
The Bridge Pattern is a structural design pattern that separates an object’s abstraction from its implementation. It allows you to change the implementation of an object without altering its interface.
Here's a JavaScript example of the Bridge Pattern:
// Abstraction class Shape { constructor(color) { this.color = color; } applyColor() { throw new Error('Method applyColor() must be implemented'); } } // Implementor class Color { constructor(value) { this.value = value; } } // Concrete Implementors class RedColor extends Color { constructor() { super('red'); } } class BlueColor extends Color { constructor() { super('blue'); } } // Refined Abstractions class Circle extends Shape { applyColor() { return `Circle filled with ${this.color.value}`; } } class Square extends Shape { applyColor() { return `Square filled with ${this.color.value}`; } } // Usage const red = new RedColor(); const blue = new BlueColor(); const redCircle = new Circle(red); const blueSquare = new Square(blue); console.log(redCircle.applyColor()); // Output: Circle filled with red console.log(blueSquare.applyColor()); // Output: Square filled with blue
In this example:
Shape
is the abstraction class that defines the interface for shapes. It contains a reference to aColor
object but doesn't know the specific implementation.Color
is the implementor class that defines the interface for colors.RedColor
andBlueColor
are concrete implementor classes that provide specific implementations of colors.Circle
andSquare
are refined abstraction classes that extendShape
and implement theapplyColor
method.- The client code creates instances of colors and uses them to create colored shapes, allowing the color and shape to vary independently.
The Bridge Pattern allows you to decouple abstractions from their implementations, making it easier to add new shapes and colors without altering the existing code. It promotes flexibility and extensibility in your design.
3. Composite Pattern:
- Composes objects into tree structures to represent part-whole hierarchies.
- Clients can treat individual objects and compositions of objects uniformly.
The Composite Pattern is a structural design pattern that allows you to compose objects into tree structures to represent part-whole hierarchies. It lets clients treat individual objects and compositions of objects uniformly.
Here's a JavaScript example of the Composite Pattern:
// Component interface class Employee { constructor(name, title) { this.name = name; this.title = title; } print() { console.log(`${this.title} ${this.name}`); } } // Leaf class class Developer extends Employee { constructor(name) { super(name, 'Developer'); } } // Composite class class Manager extends Employee { constructor(name) { super(name, 'Manager'); this.subordinates = []; } addSubordinate(subordinate) { this.subordinates.push(subordinate); } print() { super.print(); this.subordinates.forEach((subordinate) => { subordinate.print(); }); } } // Client code const dev1 = new Developer('John Doe'); const dev2 = new Developer('Jane Smith'); const manager = new Manager('Eva Johnson'); manager.addSubordinate(dev1); manager.addSubordinate(dev2); manager.print();
In this example:
Employee
is the component interface that defines the common interface for all concrete classes.Developer
is a leaf class representing an individual employee with no subordinates.Manager
is a composite class representing a manager who can have subordinates. It contains a list of subordinates and can add them.- In the client code, we create developers and a manager. The manager can have developers as subordinates, and when we call
manager.print()
, it prints the manager's name and the names of all subordinates.
The Composite Pattern is useful for creating hierarchical structures of objects while treating individual objects and compositions of objects uniformly. It simplifies the client code and allows you to work with complex structures of objects in a unified way.
4. Decorator Pattern:
- Attaches additional responsibilities to an object dynamically.
- Provides a flexible alternative to subclassing for extending functionality.
The Decorator Pattern is a structural design pattern that allows you to attach additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.
Here's a JavaScript example of the Decorator Pattern:
// Component interface class Coffee { cost() { return 5; } } // Concrete component class SimpleCoffee extends Coffee { cost() { return super.cost(); } } // Decorator class CoffeeDecorator extends Coffee { constructor(coffee) { super(); this._coffee = coffee; } cost() { return this._coffee.cost(); } } // Concrete decorators class MilkDecorator extends CoffeeDecorator { cost() { return super.cost() + 2; } } class SugarDecorator extends CoffeeDecorator { cost() { return super.cost() + 1; } } // Client code const myCoffee = new SimpleCoffee(); console.log(`Cost of my coffee: $${myCoffee.cost()}`); const milkCoffee = new MilkDecorator(myCoffee); console.log(`Cost of milk coffee: $${milkCoffee.cost()}`); const sweetMilkCoffee = new SugarDecorator(milkCoffee); console.log(`Cost of sweet milk coffee: $${sweetMilkCoffee.cost()}`);
In this example:
Coffee
is the component interface that defines the common interface for all concrete coffee classes.SimpleCoffee
is a concrete component that represents a basic coffee.CoffeeDecorator
is the decorator class that extendsCoffee
and contains a reference to anotherCoffee
object. It provides a common interface for all concrete decorators.MilkDecorator
andSugarDecorator
are concrete decorator classes that add milk and sugar, respectively, to the coffee's cost.- In the client code, we create a simple coffee and then decorate it with milk and sugar, with each decorator adding its own cost to the final price.
The Decorator Pattern allows you to add behavior to objects without modifying their source code. It's useful when you want to extend the behavior of objects at runtime and when subclassing is impractical or leads to a complex class hierarchy.
5. Facade Pattern:
- Provides a simplified interface to a set of interfaces in a subsystem.
- Makes a complex system easier to use by providing a unified interface.
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 offers a unified interface to the client.
Here's a JavaScript example of the Facade Pattern:
// Subsystem components class CPU { freeze() { console.log('CPU is frozen'); } jump(position) { console.log(`CPU jumped to position ${position}`); } execute() { console.log('CPU is executing commands'); } } class Memory { load(address, data) { console.log(`Loaded data "${data}" into memory at address ${address}`); } } class HardDrive { read(lba, size) { console.log(`Read data of size ${size} from Hard Drive at LBA ${lba}`); } } // Facade class ComputerFacade { constructor() { this.cpu = new CPU(); this.memory = new Memory(); this.hardDrive = new HardDrive(); } start() { console.log('Computer starting...'); this.cpu.freeze(); this.memory.load(0, 'BOOT'); this.cpu.jump(0); this.cpu.execute(); } } // Client code const computer = new ComputerFacade(); computer.start();
In this example:
- The
CPU
,Memory
, andHardDrive
classes represent subsystem components with their specific functionalities. - The
ComputerFacade
class acts as the facade, providing a simplified interface for starting the computer. It coordinates the interactions between the subsystem components. - In the client code, the
ComputerFacade
is used to start the computer, but the client doesn't need to know the internal details of how the CPU, memory, and hard drive work together. The facade abstracts these complexities.
The Facade Pattern is useful when you need to simplify a complex system by providing a high-level interface for clients. It promotes loose coupling between clients and subsystems, making the system easier to maintain and extend.
6. Flyweight Pattern:
- Minimizes memory usage or computational expenses by sharing as much as possible with related objects.
- Useful when a large number of similar objects need to be created.
The Flyweight Pattern is a structural design pattern that minimizes memory usage or computational expenses by sharing as much as possible with similar objects. It's especially useful when you need to manage a large number of similar objects efficiently.
Here's a JavaScript example of the Flyweight Pattern:
// Flyweight factory class CoffeeFlyweightFactory { constructor() { this.coffeeFlyweights = {}; } getCoffeeFlavor(flavor) { if (!this.coffeeFlyweights[flavor]) { this.coffeeFlyweights[flavor] = new CoffeeFlavor(flavor); } return this.coffeeFlyweights[flavor]; } getTotalCoffeeFlavorsMade() { return Object.keys(this.coffeeFlyweights).length; } } // Flyweight class CoffeeFlavor { constructor(flavor) { this.flavor = flavor; } getFlavor() { return this.flavor; } } // Client code const coffeeFactory = new CoffeeFlyweightFactory(); function takeOrder(flavor, table) { const coffee = coffeeFactory.getCoffeeFlavor(flavor); console.log(`Serving ${coffee.getFlavor()} to table ${table}`); } takeOrder('Cappuccino', 1); takeOrder('Espresso', 2); takeOrder('Cappuccino', 3); takeOrder('Espresso', 4); takeOrder('Espresso', 5); takeOrder('Cappuccino', 6); console.log(`Total coffee flavors made: ${coffeeFactory.getTotalCoffeeFlavorsMade()}`);
In this example:
- The
CoffeeFlyweightFactory
is responsible for managing and sharingCoffeeFlavor
flyweights. It ensures that each unique flavor is only created once and shared among multiple orders. CoffeeFlavor
represents a flyweight that holds an intrinsic state, which is shared, such as the flavor of coffee.- In the client code, when orders are taken, the factory provides shared flyweights if they exist and creates new ones if needed.
The Flyweight Pattern optimizes memory usage and is useful when you have a large number of objects with a shared intrinsic state, where the intrinsic state can be separated from the extrinsic state (state specific to each instance). This pattern can lead to significant memory savings and improved performance.
7. Proxy Pattern:
- Provides a surrogate or placeholder for another object to control access to it.
- Can be used for various purposes, such as lazy initialization, access control, and monitoring.
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It is commonly used to add a level of control and indirection to objects.
Here's a JavaScript example of the Proxy Pattern:
// Real subject class RealImage { constructor(filename) { this.filename = filename; this.loadFromDisk(); } display() { console.log(`Displaying image: ${this.filename}`); } loadFromDisk() { console.log(`Loading image: ${this.filename}`); } } // Proxy class ImageProxy { constructor(filename) { this.filename = filename; this.realImage = null; } display() { if (!this.realImage) { this.realImage = new RealImage(this.filename); } this.realImage.display(); } } // Client code const image1 = new ImageProxy('image1.jpg'); const image2 = new ImageProxy('image2.jpg'); // The real image is loaded only when requested to display image1.display(); // Loading and displaying image: image1.jpg image2.display(); // Loading and displaying image: image2.jpg image1.display(); // Displaying image: image1.jpg (already loaded)
In this example:
RealImage
is the real subject class representing an actual image. It loads and displays images from the disk.ImageProxy
is the proxy class that acts as a surrogate for the real image. It controls access to the real image and loads it only when necessary.- In the client code, we create instances of
ImageProxy
and call thedisplay
method. The real image is loaded only when requested to display, thanks to the proxy.
The Proxy Pattern is useful for scenarios where you want to control access to an object, add lazy initialization, or provide additional functionality without modifying the real object. It's often used for cases like on-demand loading of resources, access control, logging, and more.
These structural design patterns are used to simplify and optimize the structure of the software, making it easier to maintain, extend, and understand. The choice of which pattern to use depends on the specific problem you are trying to solve.