Building with Structures: Class and Object Harmony

11 October, 2023
structural patterns

Structural patterns refine class and object organization.

They encompass two categories: structural class patterns and structural object patterns.

  • Structural Class Patterns:

    • Objective: Employ inheritance for interface or implementation composition.
    • Application: Enhances interoperability among independently developed class libraries.
    • Instance: Adapter pattern ensures seamless interface conformity.
  • Structural Object Patterns:

    • Objective: Directs object composition for added functionality.
    • Benefit: Enables dynamic object composition, offering runtime flexibility.
    • Instance: Composite pattern facilitates adaptable composition for intricate structures.
    • Control: Proxy pattern adds control using surrogates.
adpater pattern

Adapter Pattern

In the world of software design, the Adapter pattern steps onto the stage with a mission:

  • Intent: To restrict the instantiation of a class to a single instance and provide a global point of access to that instance.

  • Why: Sometimes, it's necessary to have exactly one instance of a class, for example, to control actions that should be performed once across the entire application.

  • How: By defining a class that has a method for creating its instance if it doesn't exist or returning the existing instance if it does.

This pattern proves its versatility when:

  • You want to use an existing class, yet its interface doesn't quite harmonize with your needs.
  • You aspire to craft a reusable class that can collaborate seamlessly with unrelated or unforeseen classes—those that might not initially share a compatible interface.

Example usage

// Target interface
class Shape 
{
  draw() {}
  resize() {}
}

// Adaptee
class TextView 
{
  showText() {}
  adjustSize() {}
}

// Adapter (Object Adapter)
class TextShapeAdapter extends Shape 
{
  constructor(textView) 
  {
    super();
    this.textView = textView;
  }

  draw() 
  { this.textView.showText(); // Adapted method }

  resize() 
  { this.textView.adjustSize(); // Adapted method }
}

// Client (DrawingEditor)
const textShape = new TextShapeAdapter(new TextView());
textShape.draw();
textShape.resize();
  • Explanation:

In this illustration, the Adapter pattern orchestrates a seamless dance, ensuring that the TextShapeAdapter integrates with both the Shape interface expected by the client (DrawingEditor) and the existing TextView class.

  • Benefits:
  1. Reusability: The Adapter pattern enables the reuse of existing classes in scenarios where their original interface doesn't match the desired interface.

  2. Compatibility: It facilitates the integration of classes with disparate interfaces, promoting compatibility within the application.

  3. Preservation: Existing classes remain unchanged, preserving their integrity and avoiding the need for modifications.

  4. Flexibility: The adapter allows adding new functionality to existing classes without altering their structure.

bridge pattern

Bridge pattern

The Bridge pattern addresses challenges when an abstraction can have multiple implementations, providing flexibility and independence for modifications, extensions, and reuse of abstractions and implementations.

  • Intent: To separate an abstraction from its implementation, allowing them to vary independently.

  • Why: When an abstraction can have multiple implementations, and traditional inheritance leads to inflexibility, making it challenging to extend and modify abstractions and implementations independently.

  • How: By putting the abstraction and its implementation in separate class hierarchies and using composition to connect them. The abstraction has a reference to the implementation, and clients interact with the abstraction.

// Abstraction
class Window 
{
  constructor(platform) 
  { this.platform = platform; }

  draw() 
  { this.platform.drawImplementation(); }

  resize() 
  { this.platform.resizeImplementation(); }
}

// Platform-specific Implementation
class WindowImp 
{
  drawImplementation() 
  { // Implementation logic }

  resizeImplementation() 
  { // Implementation logic }
}

// Concrete Implementations
class XWindowImp extends WindowImp 
{ // Platform-specific implementation }

class PMWindowImp extends WindowImp 
{// Platform-specific implementation }
  • Explanation:
  1. The Window class acts as an abstraction, containing a reference to the platform-specific WindowImp.
  2. The WindowImp class defines the interface for platform-specific implementations.
  3. Concrete implementations like XWindowImp and PMWindowImp provide platform-specific details.
  • Application Scenario: Consider a user interface toolkit needing a portable Window abstraction that works on both the X Window System and IBM's Presentation Manager (PM). The Bridge pattern allows creating Window interfaces and separate platform-specific implementations (XWindowImp, PMWindowImp).
const windowForX = new Window(new XWindowImp());
windowForX.draw();
windowForX.resize();
  • Benefits:
  1. Flexibility: The Bridge pattern decouples abstractions and implementations, allowing them to evolve independently.
  2. Extensibility: It's easier to add new abstractions or implementations without modifying the existing ones.
  3. Reusability: Abstractions and implementations can be reused independently, promoting code reuse.
  4. Portability: Client code can create windows without committing to a concrete platform, enhancing portability.
composite pattern

Composite Pattern

The Composite pattern is a structural design pattern that facilitates the composition of objects into tree structures. It allows clients to treat individual objects and compositions uniformly, simplifying the interaction with complex hierarchies.

  • Intent: To compose objects into tree structures to represent part-whole hierarchies. Clients can treat individual objects and compositions of objects uniformly.

  • Why: When dealing with part-whole hierarchies where objects can be composed into larger structures, and clients should treat all objects uniformly, regardless of their individual or composite nature.

  • How: By defining an abstract class (Component or Graphic) that represents both primitives and containers. The abstract class declares operations specific to graphical objects and operations shared by composite objects. Concrete subclasses implement primitive graphics, while composite objects aggregate other objects.

// Abstract Component class
class Component 
{
  operation() 
  { throw new Error("Method not implemented"); }

  addChild(component) 
  { throw new Error("Method not implemented"); }

  removeChild(component) 
  { throw new Error("Method not implemented"); }

  getChild(index) 
  { throw new Error("Method not implemented"); }
}

// Concrete Component classes
class Leaf extends Component 
{
  operation() 
  { // Leaf operation logic }
}

class Composite extends Component 
{
  constructor() 
  {
    super();
    this.children = [];
  }

  operation() 
  { this.children.forEach((child) => child.operation()); }

  addChild(component) 
  { this.children.push(component); }

  removeChild(component) 
  {
    const index = this.children.indexOf(component);

    if (index !== -1) 
    { this.children.splice(index, 1); }

  }

  getChild(index) 
  { return this.children[index]; }
}
  • Explanation:
  1. The Component abstract class declares operations specific to individual objects and shared by composite objects.
  2. Concrete classes like Leaf represent primitive objects, while classes like Composite aggregate other components.
  3. The Composite class allows clients to treat individual objects (Leaf) and compositions (Composite) uniformly.
  • Application Scenario: The Composite pattern is widely used in graphics applications, GUI frameworks, and any scenario where part-whole hierarchies need to be represented and manipulated uniformly.

  • Benefits:

  1. Uniformity: The Composite pattern enables clients to treat individual objects and compositions uniformly.
  2. Flexibility: The pattern supports the creation of complex structures through recursive composition.
  3. Simplified Client Code: Clients can ignore the differences between individual objects and compositions, simplifying application logic.
decorator pattern

Decorator Pattern

The Decorator pattern is a structural design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.

  • Intent: To attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

  • Why: When you need to extend the behavior of objects in a flexible and reusable way, avoiding the need for a large number of subclasses.

  • How: By defining a set of decorator classes that are used to wrap concrete components. These decorators add or override behaviors of the wrapped components.

// Component interface
class Component 
{
  operation() 
  { throw new Error("Method not implemented"); }
}

// Concrete Component class
class ConcreteComponent extends Component 
{
  operation() 
  { // Basic operation logic }
}

// Decorator class
class Decorator extends Component 
{
  constructor(component) 
  {
    super();
    this.component = component;
  }

  operation() 
  { this.component.operation(); }
}

// Concrete Decorator classes
class ConcreteDecoratorA extends Decorator 
{
  operation() 
  {
    super.operation();
    // Additional operation logic for ConcreteDecoratorA
  }
}

class ConcreteDecoratorB extends Decorator 
{
  operation() 
  {
    super.operation();
    // Additional operation logic for ConcreteDecoratorB
  }
}
  • Explanation:
  1. The Component interface declares the common interface for concrete components and decorators.
  2. The ConcreteComponent class represents the basic object to which additional behavior can be added.
  3. The Decorator class is the base class for all decorators, containing a reference to a Component.
  4. Concrete decorator classes like ConcreteDecoratorA and ConcreteDecoratorB extend the Decorator class, adding or overriding specific behaviors.
  • Application Scenario: The Decorator pattern is useful when you need to extend the behavior of objects in a flexible way, such as adding features to graphical objects in a drawing application.

  • Benefits:

  1. Flexibility: Decorators can be combined in various ways, allowing for flexible and reusable extensions.
  2. Avoiding Subclass Proliferation: Instead of creating a large number of subclasses, decorators offer a more granular approach to extending functionality.
  3. Open/Closed Principle: The pattern follows the Open/Closed Principle, allowing classes to be extended without modifying their source code.
facade pattern

Facade Pattern

The Facade pattern is a structural design pattern that provides a simplified interface to a set of interfaces in a subsystem, making it easier to use and reducing dependencies between components.

  • Intent: To provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.

  • Why: When you want to simplify the usage of a complex subsystem or when there are multiple dependencies between client code and the subsystem.

  • How: By creating a Facade class that represents a higher-level, simplified interface, encapsulating the complexities of the subsystem.

// Subsystem components
class SubsystemA 
{
  operationA() 
  { // Implementation of operation A }
}

class SubsystemB 
{
  operationB() 
  { // Implementation of operation B }
}

class SubsystemC 
{
  operationC() 
  { // Implementation of operation C }
}

// Facade class
class Facade 
{
  constructor() 
  {
    this.subsystemA = new SubsystemA();
    this.subsystemB = new SubsystemB();
    this.subsystemC = new SubsystemC();
  }

  // Higher-level methods that simplify the subsystem usage
  operation() 
  {
    this.subsystemA.operationA();
    this.subsystemB.operationB();
    this.subsystemC.operationC();
  }
}
  • Explanation:

    1. The SubsystemA, SubsystemB, and SubsystemC classes represent the components of the subsystem with their specific operations.
    2. The Facade class encapsulates the complexities of the subsystem and provides a simplified interface with the operation method.
  • Application Scenario: The Facade pattern is commonly used in scenarios where a subsystem is complex, and there is a need to provide a simplified entry point for client code, such as a multimedia processing library.

  • Benefits:

    1. Simplified Interface: Facade provides a single, simplified interface to a complex subsystem, making it easier for clients to use.
    2. Decoupling: Clients interact with the Facade rather than the individual components of the subsystem, reducing dependencies and promoting decoupling.
    3. Easier Maintenance: Changes to the subsystem can be isolated within the Facade, preventing the need for widespread modifications in client code.
  • Example Usage: Suppose you have a multimedia processing library with intricate modules for image processing (SubsystemA), audio processing (SubsystemB), and video processing (SubsystemC). Instead of requiring clients to manage each module separately, the library provides a MultimediaProcessor facade. Clients can now perform operations like processMultimedia without worrying about the internal complexities of individual processing modules.

// Usage of the multimedia processing library Facade
const multimediaProcessor = new MultimediaProcessor();

// Clients can now perform multimedia processing without dealing with individual subsystem details
multimediaProcessor.processMultimedia();
flyWeight pattern

FlyWeight Pattern

The Flyweight pattern is a structural design pattern that minimizes memory usage or computational expenses by sharing as much as possible with related objects. It achieves this by effectively dividing objects into intrinsic (shared) and extrinsic (unique) states.

  • Intent: To use sharing to support a large number of similar objects efficiently.

  • Why: When a large number of similar objects need to be created, and the associated memory or computational costs are significant. The Flyweight pattern reduces redundancy by sharing common state among multiple objects.

  • How: By separating the shared (intrinsic) state from the unique (extrinsic) state. The intrinsic state is stored in a Flyweight object, and the extrinsic state is provided by the client when needed.

// Flyweight factory
class FlyweightFactory 
{
  constructor() 
  { this.flyweights = {}; }

  getFlyweight(key) 
  {
    if (!this.flyweights[key]) 
    { this.flyweights[key] = new ConcreteFlyweight(key); }

    return this.flyweights[key];
  }
  // Additional methods for managing Flyweights can be added here
}

// Flyweight interface
class Flyweight 
{
  operation(extrinsicState) 
  { // Common operation using intrinsic state }
}

// Concrete Flyweight
class ConcreteFlyweight extends Flyweight 
{
  constructor(intrinsicState) 
  {
    super();
    this.intrinsicState = intrinsicState;
  }

  operation(extrinsicState) 
  { console.log(`Operation with intrinsic state ${this.intrinsicState} and extrinsic state ${extrinsicState}`); }
}

// Client
class Client 
{
  constructor(flyweightFactory) 
  { this.flyweightFactory = flyweightFactory; }

  run() 
  {
    const flyweightA = this.flyweightFactory.getFlyweight('A');
    const flyweightB = this.flyweightFactory.getFlyweight('B');

    flyweightA.operation('X');
    flyweightB.operation('Y');
  }
}

// Example usage
const flyweightFactory = new FlyweightFactory();
const client = new Client(flyweightFactory);
client.run();
  • Explanation:

    1. The FlyweightFactory manages the creation and sharing of Flyweight objects.
    2. The Flyweight interface declares the operation that can be performed with extrinsic state.
    3. The ConcreteFlyweight class implements the Flyweight interface and contains intrinsic state.
    4. The Client uses the Flyweight objects, providing extrinsic state when needed.
  • Application Scenario: The Flyweight pattern is suitable when there are a large number of similar objects, and memory or computational efficiency is crucial. For example, in a text editor, the characters in a font can be represented as Flyweights, where the font properties are intrinsic and the position is extrinsic.

  • Benefits:

    1. Memory Efficiency: By sharing common state, the Flyweight pattern reduces memory consumption.
    2. Performance Improvement: Computational costs are minimized by reusing shared objects.
    3. Simplified Client Code: Clients focus on the extrinsic state, while the intrinsic state is managed by the Flyweight objects.
proxy pattern

Proxy Pattern

The Proxy pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. This can be useful in various scenarios, such as implementing lazy loading, access control, logging, or monitoring.

  • Intent: To act as a substitute or placeholder for another object to control access.

  • Why: When you want to add a level of control over the access to an object, which can involve tasks such as lazy loading of resources, access control, logging, or monitoring, without the client needing to be aware of these functionalities.

  • How: By creating a proxy class that has the same interface as the real object. The proxy forwards requests to the real object, but it can also perform additional tasks before or after the request is forwarded.

// Subject interface
class Subject 
{
  request() 
  { // Common interface for RealSubject and Proxy }
}

// RealSubject
class RealSubject extends Subject 
{
  request() 
  { console.log("RealSubject: Handling request."); }
}

// Proxy
class Proxy extends Subject 
{
  constructor(realSubject) 
  {
    super();
    this.realSubject = realSubject;
  }

  request() 
  {
    // Additional logic can be added here before or after forwarding the request
    console.log("Proxy: Handling request.");
    this.realSubject.request();
  }
}

// Client
class Client 
{
  constructor(subject) 
  { this.subject = subject; }

  executeRequest() 
  { this.subject.request(); }
}

// Example usage
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);

const client1 = new Client(realSubject);
client1.executeRequest(); // Direct access to RealSubject

const client2 = new Client(proxy);
client2.executeRequest(); // Access through Proxy
  • Explanation:

    1. The Subject interface declares the common methods for both the RealSubject and the Proxy.
    2. The RealSubject class implements the Subject interface and represents the real object that the proxy controls access to.
    3. The Proxy class also implements the Subject interface and contains a reference to the RealSubject. It can perform additional tasks before or after forwarding requests to the real object.
    4. The Client interacts with either the RealSubject or the Proxy interchangeably.
  • Application Scenario: The Proxy pattern is applicable in scenarios where you want to control access to an object. Examples include lazy loading of resources (loading the real object only when needed), access control (restricting certain operations), logging (recording requests), or monitoring (measuring performance).

  • Benefits:

    1. Controlled Access: The Proxy pattern allows you to add an additional layer of control over how clients access the real object.
    2. Lazy Loading: Resources can be loaded on-demand, improving performance and resource utilization.
    3. Access Control and Security: Proxy can enforce access control policies to restrict certain operations.

Subscribe to the Newsletter

Get my latest posts and project updates by email