Building with Structures: Class and Object Harmony
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.
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:
-
Reusability: The Adapter pattern enables the reuse of existing classes in scenarios where their original interface doesn't match the desired interface.
-
Compatibility: It facilitates the integration of classes with disparate interfaces, promoting compatibility within the application.
-
Preservation: Existing classes remain unchanged, preserving their integrity and avoiding the need for modifications.
-
Flexibility: The adapter allows adding new functionality to existing classes without altering their structure.
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:
- The
Window
class acts as an abstraction, containing a reference to the platform-specificWindowImp
. - The
WindowImp
class defines the interface for platform-specific implementations. - Concrete implementations like
XWindowImp
andPMWindowImp
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 creatingWindow
interfaces and separate platform-specific implementations (XWindowImp
,PMWindowImp
).
const windowForX = new Window(new XWindowImp());
windowForX.draw();
windowForX.resize();
- Benefits:
- Flexibility: The Bridge pattern decouples abstractions and implementations, allowing them to evolve independently.
- Extensibility: It's easier to add new abstractions or implementations without modifying the existing ones.
- Reusability: Abstractions and implementations can be reused independently, promoting code reuse.
- Portability: Client code can create windows without committing to a concrete platform, enhancing portability.
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
orGraphic
) 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:
- The
Component
abstract class declares operations specific to individual objects and shared by composite objects. - Concrete classes like
Leaf
represent primitive objects, while classes likeComposite
aggregate other components. - 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:
- Uniformity: The Composite pattern enables clients to treat individual objects and compositions uniformly.
- Flexibility: The pattern supports the creation of complex structures through recursive composition.
- Simplified Client Code: Clients can ignore the differences between individual objects and compositions, simplifying application logic.
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:
- The
Component
interface declares the common interface for concrete components and decorators. - The
ConcreteComponent
class represents the basic object to which additional behavior can be added. - The
Decorator
class is the base class for all decorators, containing a reference to aComponent
. - Concrete decorator classes like
ConcreteDecoratorA
andConcreteDecoratorB
extend theDecorator
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:
- Flexibility: Decorators can be combined in various ways, allowing for flexible and reusable extensions.
- Avoiding Subclass Proliferation: Instead of creating a large number of subclasses, decorators offer a more granular approach to extending functionality.
- Open/Closed Principle: The pattern follows the Open/Closed Principle, allowing classes to be extended without modifying their source code.
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:
- The
SubsystemA
,SubsystemB
, andSubsystemC
classes represent the components of the subsystem with their specific operations. - The
Facade
class encapsulates the complexities of the subsystem and provides a simplified interface with theoperation
method.
- The
-
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:
- Simplified Interface: Facade provides a single, simplified interface to a complex subsystem, making it easier for clients to use.
- Decoupling: Clients interact with the Facade rather than the individual components of the subsystem, reducing dependencies and promoting decoupling.
- 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 aMultimediaProcessor
facade. Clients can now perform operations likeprocessMultimedia
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();
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:
- The
FlyweightFactory
manages the creation and sharing of Flyweight objects. - The
Flyweight
interface declares the operation that can be performed with extrinsic state. - The
ConcreteFlyweight
class implements theFlyweight
interface and contains intrinsic state. - The
Client
uses the Flyweight objects, providing extrinsic state when needed.
- The
-
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:
- Memory Efficiency: By sharing common state, the Flyweight pattern reduces memory consumption.
- Performance Improvement: Computational costs are minimized by reusing shared objects.
- Simplified Client Code: Clients focus on the extrinsic state, while the intrinsic state is managed by the Flyweight objects.
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:
- The
Subject
interface declares the common methods for both theRealSubject
and theProxy
. - The
RealSubject
class implements theSubject
interface and represents the real object that the proxy controls access to. - The
Proxy
class also implements theSubject
interface and contains a reference to theRealSubject
. It can perform additional tasks before or after forwarding requests to the real object. - The
Client
interacts with either theRealSubject
or theProxy
interchangeably.
- The
-
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:
- Controlled Access: The Proxy pattern allows you to add an additional layer of control over how clients access the real object.
- Lazy Loading: Resources can be loaded on-demand, improving performance and resource utilization.
- 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