Structural Design Patterns

0

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. 


Structural Design Patterns


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:

  1. Target is the target interface that the client code expects to work with.
  2. Adaptee is a class with a different interface that the client code can't use directly.
  3. Adapter is a class that extends Target and uses composition to adapt Adaptee to the Target interface.
  4. The client code uses Adaptee directly and also uses Adapter to make Adaptee compatible with the Target 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:

  1. Shape is the abstraction class that defines the interface for shapes. It contains a reference to a Color object but doesn't know the specific implementation.
  2. Color is the implementor class that defines the interface for colors.
  3. RedColor and BlueColor are concrete implementor classes that provide specific implementations of colors.
  4. Circle and Square are refined abstraction classes that extend Shape and implement the applyColor method.
  5. 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:

  1. Employee is the component interface that defines the common interface for all concrete classes.
  2. Developer is a leaf class representing an individual employee with no subordinates.
  3. Manager is a composite class representing a manager who can have subordinates. It contains a list of subordinates and can add them.
  4. 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:

  1. Coffee is the component interface that defines the common interface for all concrete coffee classes.
  2. SimpleCoffee is a concrete component that represents a basic coffee.
  3. CoffeeDecorator is the decorator class that extends Coffee and contains a reference to another Coffee object. It provides a common interface for all concrete decorators.
  4. MilkDecorator and SugarDecorator are concrete decorator classes that add milk and sugar, respectively, to the coffee's cost.
  5. 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:

  1. The CPU, Memory, and HardDrive classes represent subsystem components with their specific functionalities.
  2. The ComputerFacade class acts as the facade, providing a simplified interface for starting the computer. It coordinates the interactions between the subsystem components.
  3. 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:

  1. The CoffeeFlyweightFactory is responsible for managing and sharing CoffeeFlavor flyweights. It ensures that each unique flavor is only created once and shared among multiple orders.
  2. CoffeeFlavor represents a flyweight that holds an intrinsic state, which is shared, such as the flavor of coffee.
  3. 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:

  1. RealImage is the real subject class representing an actual image. It loads and displays images from the disk.
  2. 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.
  3. In the client code, we create instances of ImageProxy and call the display 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.

Post a Comment

0Comments
Post a Comment (0)