Sitemap
Hendra Mulyanto

Hendra Mulyanto⁠

Design Pattern In Javascript (Part 2)

12 min readMar 31, 2024

--

Structural Design Pattern

Structural design patterns are a category of design patterns in software engineering that focus on the composition of classes and objects. These patterns deal with the relationships between classes and how they can be structured to form larger, more complex structures while keeping the system flexible and efficient.

Structural class-creation patterns use inheritance to compose interfaces. Structural object-patterns define ways to compose objects to obtain new functionality.

Adapter

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces by converting the interface of one class into another interface that a client expects. This pattern is particularly useful when integrating new or legacy code into existing systems without modifying their original interfaces.

The main components of the Adapter Pattern are:

  1. Target: This is the interface that the client expects to interact with. The client code is written to this interface.
  2. Adaptee: This is the existing interface that needs to be adapted. It’s the interface that the client cannot directly use.
  3. Adapter: This is the class that adapts the Adaptee interface to the Target interface. It implements the Target interface and contains an instance of the Adaptee.

ES6 Example

// Adaptee (existing interface)
class EuropeanSocket {
powerSupply(voltage) {
console.log(`Supplying power at ${voltage} volts`);
return voltage;
}
}

// Target (expected interface)
class USASocketInterface {
voltage() {}
}

// Adapter
class EuropeanToUSAdapter extends USASocketInterface {
constructor(europeanSocket) {
super();
this.europeanSocket = europeanSocket;
}

voltage() {
// European voltage is 220V, but US expects 110V
return this.europeanSocket.powerSupply(220) / 2;
}
}

// Client code
function main() {
const europeanSocket = new EuropeanSocket();
const adapter = new EuropeanToUSAdapter(europeanSocket);
console.log(`US voltage: ${adapter.voltage()} volts`);
}

main();

ES5 Example

// Adaptee (existing interface)
function EuropeanSocket() {}

EuropeanSocket.prototype.powerSupply = function(voltage) {
console.log("Supplying power at " + voltage + " volts");
return voltage;
};

// Target (expected interface)
function USASocketInterface() {}

USASocketInterface.prototype.voltage = function() {};

// Adapter
function EuropeanToUSAdapter(europeanSocket) {
this.europeanSocket = europeanSocket;
}

EuropeanToUSAdapter.prototype = Object.create(USASocketInterface.prototype);
EuropeanToUSAdapter.prototype.constructor = EuropeanToUSAdapter;

EuropeanToUSAdapter.prototype.voltage = function() {
// European voltage is 220V, but US expects 110V
return this.europeanSocket.powerSupply(220) / 2;
};

// Client code
function main() {
var europeanSocket = new EuropeanSocket();
var adapter = new EuropeanToUSAdapter(europeanSocket);
console.log("US voltage: " + adapter.voltage() + " volts");
}

main();

Bridge

The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation so that the two can vary independently. It’s useful when you want to avoid a permanent binding between an abstraction and its implementation, allowing them to evolve separately.

Here’s how the Bridge Pattern works:

  • Abstraction: This represents the high-level interface that clients interact with. It contains a reference to the Implementor object.
  • Implementor: This represents the low-level interface that defines the operations that can be performed. It’s referenced by the Abstraction.
  • Concrete Abstraction: This extends the Abstraction and provides specific implementations using the Implementor.
  • Concrete Implementor: This provides the actual implementation of the operations defined by the Implementor interface.

ES6 Example

// Implementor
class DrawingAPI {
drawCircle(x, y, radius) {
console.log(`Drawing circle at (${x}, ${y}) with radius ${radius}`);
}
}

// Abstraction
class Shape {
constructor(drawingAPI) {
this.drawingAPI = drawingAPI;
}

draw() {
throw new Error("This method must be overridden!");
}
}

// Concrete Implementor
class DrawingAPI1 extends DrawingAPI {
drawCircle(x, y, radius) {
console.log("Drawing circle with DrawingAPI1");
super.drawCircle(x, y, radius);
}
}

// Concrete Abstraction
class Circle extends Shape {
constructor(x, y, radius, drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}

draw() {
this.drawingAPI.drawCircle(this.x, this.y, this.radius);
}
}

// Usage
const circle1 = new Circle(1, 2, 3, new DrawingAPI1());
circle1.draw();

ES5 Example

// Implementor
function DrawingAPI() {}

DrawingAPI.prototype.drawCircle = function(x, y, radius) {
console.log("Drawing circle at (" + x + ", " + y + ") with radius " + radius);
};

// Abstraction
function Shape(drawingAPI) {
this.drawingAPI = drawingAPI;
}

Shape.prototype.draw = function() {
throw new Error("This method must be overridden!");
};

// Concrete Implementor
function DrawingAPI1() {}
DrawingAPI1.prototype = new DrawingAPI();
DrawingAPI1.prototype.drawCircle = function(x, y, radius) {
console.log("Drawing circle with DrawingAPI1");
this.constructor.prototype.drawCircle.call(this, x, y, radius);
};

// Concrete Abstraction
function Circle(x, y, radius, drawingAPI) {
Shape.call(this, drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}

Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

Circle.prototype.draw = function() {
this.drawingAPI.drawCircle(this.x, this.y, this.radius);
};

// Usage
var circle1 = new Circle(1, 2, 3, new DrawingAPI1());
circle1.draw();

Composite

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 clients treat individual objects and compositions of objects uniformly.

In essence, the Composite Pattern is used when you have objects that can be combined into composite objects that represent a part-whole hierarchy. This pattern enables you to work with both individual objects and compositions of objects in a uniform manner.

The main components of the Composite Pattern are:

  1. Component: This is the base interface or abstract class that declares the common operations for both leaf and composite objects.
  2. Leaf: This represents the “leaf” objects in the hierarchy that have no children. Leaf objects implement the Component interface.
  3. Composite: This represents the “composite” objects in the hierarchy that can have children (either leaf or composite). Composite objects contain a collection of child components. They implement the Component interface but also provide specific operations for adding, removing, and accessing children.

ES6 Example

// Component
class Graphic {
constructor(name) {
this.name = name;
}

draw() {
throw new Error("This method must be overridden!");
}
}

// Leaf
class Line extends Graphic {
draw() {
console.log(`Drawing Line ${this.name}`);
}
}

// Composite
class Picture extends Graphic {
constructor(name) {
super(name);
this.children = [];
}

add(graphic) {
this.children.push(graphic);
}

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

draw() {
console.log(`Drawing Picture ${this.name}`);
this.children.forEach(child => {
child.draw();
});
}
}

// Usage
const line1 = new Line("1");
const line2 = new Line("2");
const line3 = new Line("3");

const picture = new Picture("Composite Picture");
picture.add(line1);
picture.add(line2);
picture.add(line3);

picture.draw();

ES5 Example

// Component
function Graphic(name) {
this.name = name;
}

Graphic.prototype.draw = function() {
throw new Error("This method must be overridden!");
};

// Leaf
function Line(name) {
Graphic.call(this, name);
}

Line.prototype = Object.create(Graphic.prototype);
Line.prototype.constructor = Line;

Line.prototype.draw = function() {
console.log("Drawing Line " + this.name);
};

// Composite
function Picture(name) {
Graphic.call(this, name);
this.children = [];
}

Picture.prototype = Object.create(Graphic.prototype);
Picture.prototype.constructor = Picture;

Picture.prototype.add = function(graphic) {
this.children.push(graphic);
};

Picture.prototype.remove = function(graphic) {
var index = this.children.indexOf(graphic);
if (index !== -1) {
this.children.splice(index, 1);
}
};

Picture.prototype.draw = function() {
console.log("Drawing Picture " + this.name);
for (var i = 0; i < this.children.length; i++) {
this.children[i].draw();
}
};

// Usage
var line1 = new Line("1");
var line2 = new Line("2");
var line3 = new Line("3");

var picture = new Picture("Composite Picture");
picture.add(line1);
picture.add(line2);
picture.add(line3);

picture.draw();

Decorator

The Decorator Pattern is a structural design pattern that allows behavior to be added to individual objects dynamically, without affecting the behavior of other objects from the same class. It is used to extend or enhance the functionality of objects in a flexible and reusable way, by wrapping them with one or more decorator objects.

Get Hendra Mulyanto’s stories in your inbox

Join Medium for free to get updates from this writer.

In the Decorator Pattern:

  • There is a common interface or base class shared by both the original object and the decorators. This allows the client to treat both the original objects and the decorated objects uniformly.
  • Decorators are classes that implement this common interface or inherit from the same base class. They wrap around the original object and provide additional functionality by delegating some or all of the operations to the original object while adding their own behavior.
  • Decorators can be stacked on top of each other, allowing for multiple layers of functionality to be added to an object.

Key components of the Decorator Pattern include:

  1. Component: This is the common interface or abstract class that defines the methods that both the original objects and the decorators implement.
  2. Concrete Component: This is the original object that implements the Component interface. It represents the base functionality to which decorators can be added.
  3. Decorator: This is an abstract class that also implements the Component interface. It holds a reference to a Component object and provides an interface that conforms to the Component interface. Decorators can add additional behavior before or after delegating operations to the wrapped Component object.
  4. Concrete Decorator: These are the actual decorator classes that extend the Decorator class. They add specific behavior to the wrapped Component object.

ES6 Example

// Component interface
class Coffee {
cost() {
return 5;
}
}

// Concrete Component
class SimpleCoffee extends Coffee {}

// Decorator
class CoffeeDecorator extends Coffee {
constructor(coffee) {
super();
this.coffee = coffee;
}

cost() {
return this.coffee.cost();
}
}

// Concrete Decorator
class MilkDecorator extends CoffeeDecorator {
constructor(coffee) {
super(coffee);
}

cost() {
return this.coffee.cost() + 2;
}
}

// Usage
const coffee = new SimpleCoffee();
console.log(coffee.cost()); // Output: 5

const coffeeWithMilk = new MilkDecorator(coffee);
console.log(coffeeWithMilk.cost()); // Output: 7

ES5 Example

// Component interface
function Coffee() {
this.cost = function() {
return 5;
};
}

// Concrete Component
function SimpleCoffee() {
Coffee.call(this);

this.cost = function() {
return 5;
};
}

// Decorator
function CoffeeDecorator(coffee) {
Coffee.call(this);

this.coffee = coffee;

this.cost = function() {
return this.coffee.cost();
};
}

// Concrete Decorator
function MilkDecorator(coffee) {
CoffeeDecorator.call(this, coffee);

this.cost = function() {
return this.coffee.cost() + 2;
};
}

// Usage
var coffee = new SimpleCoffee();
console.log(coffee.cost()); // Output: 5

var coffeeWithMilk = new MilkDecorator(coffee);
console.log(coffeeWithMilk.cost()); // Output: 7

Facade

The Facade Pattern is a structural design pattern that provides a simplified interface to a complex system, hiding its complexities from clients. It aims to encapsulate a complex subsystem behind a simpler interface, making it easier to use and understand.

The main purpose of the Facade Pattern is to provide a single, unified interface to a set of interfaces in a subsystem. It promotes loose coupling between clients and the subsystem by providing a higher-level interface that shields clients from the details of the subsystem’s components and their interactions.

Key components of the Facade Pattern include:

  1. Facade: This is a class or an interface that provides a simplified interface to the complex subsystem. It encapsulates the interactions between the client and the subsystem’s components. Clients interact with the facade to access the subsystem’s functionality without needing to know the details of its internal implementation.
  2. Subsystem Classes: These are the individual components or classes that make up the complex subsystem. They contain the actual implementation of the functionality provided by the subsystem.

ES6 Example

// Subsystem classes
class Inventory {
checkItemAvailability(itemId) {
console.log(`Checking availability of item with ID ${itemId}`);
// Simulate checking item availability in inventory
return Math.random() < 0.5; // Randomly return true or false
}
}

class PaymentProcessor {
processPayment(amount) {
console.log(`Processing payment of ${amount} dollars`);
// Simulate processing payment
return true; // Payment always succeeds in this example
}
}

class Shipping {
shipItem(itemId, address) {
console.log(`Shipping item with ID ${itemId} to ${address}`);
// Simulate shipping item
return true; // Shipping always succeeds in this example
}
}

// Facade
class OnlineShoppingFacade {
constructor() {
this.inventory = new Inventory();
this.paymentProcessor = new PaymentProcessor();
this.shipping = new Shipping();
}

purchaseItem(itemId, address, amount) {
const isAvailable = this.inventory.checkItemAvailability(itemId);
if (isAvailable) {
const paymentResult = this.paymentProcessor.processPayment(amount);
if (paymentResult) {
const shippingResult = this.shipping.shipItem(itemId, address);
if (shippingResult) {
console.log("Purchase completed successfully!");
} else {
console.log("Failed to ship item.");
}
} else {
console.log("Payment processing failed.");
}
} else {
console.log("Item is not available.");
}
}
}

// Usage
const shoppingFacade = new OnlineShoppingFacade();
shoppingFacade.purchaseItem("12345", "123 Main St", 50);

ES5 Example

// Subsystem classes
function Inventory() {}

Inventory.prototype.checkItemAvailability = function(itemId) {
console.log("Checking availability of item with ID " + itemId);
// Simulate checking item availability in inventory
return Math.random() < 0.5; // Randomly return true or false
};

function PaymentProcessor() {}

PaymentProcessor.prototype.processPayment = function(amount) {
console.log("Processing payment of " + amount + " dollars");
// Simulate processing payment
return true; // Payment always succeeds in this example
};

function Shipping() {}

Shipping.prototype.shipItem = function(itemId, address) {
console.log("Shipping item with ID " + itemId + " to " + address);
// Simulate shipping item
return true; // Shipping always succeeds in this example
};

// Facade
function OnlineShoppingFacade() {
this.inventory = new Inventory();
this.paymentProcessor = new PaymentProcessor();
this.shipping = new Shipping();
}

OnlineShoppingFacade.prototype.purchaseItem = function(itemId, address, amount) {
var isAvailable = this.inventory.checkItemAvailability(itemId);
if (isAvailable) {
var paymentResult = this.paymentProcessor.processPayment(amount);
if (paymentResult) {
var shippingResult = this.shipping.shipItem(itemId, address);
if (shippingResult) {
console.log("Purchase completed successfully!");
} else {
console.log("Failed to ship item.");
}
} else {
console.log("Payment processing failed.");
}
} else {
console.log("Item is not available.");
}
};

// Usage
var shoppingFacade = new OnlineShoppingFacade();
shoppingFacade.purchaseItem("12345", "123 Main St", 50);

Flyweight

The Flyweight Pattern is a structural design pattern that aims to minimize memory usage and improve performance by sharing as much data as possible between similar objects. It is particularly useful when dealing with a large number of lightweight objects that have overlapping state, and where the cost of creating and maintaining each individual object is significant.

In the Flyweight Pattern:

  1. Flyweight Interface: This is an interface or abstract class that defines methods to manipulate the extrinsic state of flyweight objects. The extrinsic state is the state that can vary between different contexts.
  2. Concrete Flyweight: This is a concrete implementation of the flyweight interface, representing a shared object that can be used in multiple contexts. It typically contains intrinsic state (state shared among all instances) and, optionally, some portion of extrinsic state.
  3. Flyweight Factory: This is a factory class or method responsible for creating and managing flyweight objects. It ensures that flyweight objects are shared and reused when possible, rather than creating new instances unnecessarily.
  4. Client: This is the code that uses flyweight objects. It may create or request flyweight objects from the flyweight factory and use them in different contexts.

ES6 Example

// Flyweight interface
class Flyweight {
constructor(sharedState) {
this.sharedState = sharedState;
}

operation(extrinsicState) {
throw new Error("This method must be overridden!");
}
}

// Concrete flyweight
class ConcreteFlyweight extends Flyweight {
operation(extrinsicState) {
console.log(`ConcreteFlyweight: ${this.sharedState} - ${extrinsicState}`);
}
}

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

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

// Usage
const factory = new FlyweightFactory();

const flyweight1 = factory.getFlyweight('sharedState1');
flyweight1.operation('extrinsicState1');

const flyweight2 = factory.getFlyweight('sharedState2');
flyweight2.operation('extrinsicState2');

ES5 Example

// Flyweight interface
function Flyweight(sharedState) {
this.sharedState = sharedState;
}

Flyweight.prototype.operation = function(extrinsicState) {
throw new Error("This method must be overridden!");
};

// Concrete flyweight
function ConcreteFlyweight(sharedState) {
Flyweight.call(this, sharedState);
}

ConcreteFlyweight.prototype = Object.create(Flyweight.prototype);
ConcreteFlyweight.prototype.constructor = ConcreteFlyweight;

ConcreteFlyweight.prototype.operation = function(extrinsicState) {
console.log("ConcreteFlyweight: " + this.sharedState + " - " + extrinsicState);
};

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

FlyweightFactory.prototype.getFlyweight = function(sharedState) {
if (!this.flyweights[sharedState]) {
this.flyweights[sharedState] = new ConcreteFlyweight(sharedState);
}
return this.flyweights[sharedState];
};

// Usage
var factory = new FlyweightFactory();

var flyweight1 = factory.getFlyweight('sharedState1');
flyweight1.operation('extrinsicState1');

var flyweight2 = factory.getFlyweight('sharedState2');
flyweight2.operation('extrinsicState2');

Proxy

The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. It acts as an intermediary or wrapper object, allowing you to add additional behavior before or after the core functionality of the original object is executed.

The Proxy Pattern is useful in various scenarios, such as:

  1. Access Control: Controlling access to sensitive or resource-intensive objects by adding authentication, authorization, or logging mechanisms.
  2. Lazy Initialization: Delaying the creation of expensive objects until they are actually needed.
  3. Caching: Caching the results of expensive operations and returning the cached results instead of executing the operation again.
  4. Remote Proxy: Representing objects located in remote servers and providing a local representation that can communicate with the remote object transparently.

Key components of the Proxy Pattern include:

  1. Subject Interface: This is an interface or abstract class shared by both the real subject and the proxy. It defines the common operations that both subjects must implement.
  2. Real Subject: This is the actual object that the proxy represents. It provides the core functionality that the proxy controls access to.
  3. Proxy: This is an object that acts as a substitute or placeholder for the real subject. It implements the same interface as the real subject and forwards requests to it, adding additional behavior as needed.

ES6 Example

// Subject Interface
class Subject {
request() {
throw new Error("This method must be overridden!");
}
}

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

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

request() {
if (this.checkAccess()) {
this.realSubject.request();
this.logRequest();
} else {
console.log("Proxy: Access denied.");
}
}

checkAccess() {
// Simulate access control logic
return Math.random() < 0.5;
}

logRequest() {
console.log("Proxy: Logging request.");
}
}

// Usage
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);

proxy.request(); // Output depends on the result of checkAccess

ES5 Example

// Subject Interface
function Subject() {}

Subject.prototype.request = function() {
throw new Error("This method must be overridden!");
};

// Real Subject
function RealSubject() {}

RealSubject.prototype = Object.create(Subject.prototype);
RealSubject.prototype.constructor = RealSubject;

RealSubject.prototype.request = function() {
console.log("RealSubject: Handling request.");
};

// Proxy
function Proxy(realSubject) {
this.realSubject = realSubject;
}

Proxy.prototype = Object.create(Subject.prototype);
Proxy.prototype.constructor = Proxy;

Proxy.prototype.request = function() {
if (this.checkAccess()) {
this.realSubject.request();
this.logRequest();
} else {
console.log("Proxy: Access denied.");
}
};

Proxy.prototype.checkAccess = function() {
// Simulate access control logic
return Math.random() < 0.5;
};

Proxy.prototype.logRequest = function() {
console.log("Proxy: Logging request.");
};

// Usage
var realSubject = new RealSubject();
var proxy = new Proxy(realSubject);

proxy.request(); // Output depends on the result of checkAccess

--

--