GoF Structural patterns
Definition
Structural Design Patterns are concerned with object composition and typically identify simple ways to realize relationships between different objects. They help ensure that when one part of a system changes, the entire structure of the system doesn't need to do the same. They also assist in recasting parts of the system which don't fit a particular purpose into those that do.
Adapter
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
Adapter
Adapter allows classes with incompatible interfaces to work together by wrapping its own interface around that of an already existing class.
Example:
class Soldier {
constructor(level) {
this.level = level;
}
attack() {
return this.level * 1;
}
}
class Jedi {
constructor(level) {
this.level = level;
}
attackWithSaber() {
return this.level * 100;
}
}
class JediAdapter {
constructor(jedi) {
this.jedi = jedi;
}
attack() {
return this.jedi.attackWithSaber();
}
}
Bridge
Bridge decouples an abstraction from its implementation so that the two can vary independently.
Example:
class Printer {
constructor(ink) {
this.ink = ink;
}
}
class EpsonPrinter extends Printer {
constructor(ink) {
super(ink);
}
print() {
return "Printer: Epson, Ink: " + this.ink.get();
}
}
class HPprinter extends Printer {
constructor(ink) {
super(ink);
}
print() {
return "Printer: HP, Ink: " + this.ink.get();
}
}
class Ink {
constructor(type) {
this.type = type;
}
get() {
return this.type;
}
}
class AcrylicInk extends Ink {
constructor() {
super("acrylic-based");
}
}
class AlcoholInk extends Ink {
constructor() {
super("alcohol-based");
}
}
Composite
Composite composes zero-or-more similar objects so that they can be manipulated as one object.
Example:
//Equipment
class Equipment {
getPrice() {
return this.price || 0;
}
getName() {
return this.name;
}
setName(name) {
this.name = name;
}
}
// --- composite ---
class Composite extends Equipment {
constructor() {
super();
this.equipments = [];
}
add(equipment) {
this.equipments.push(equipment);
}
getPrice() {
return this.equipments.map(equipment => {
return equipment.getPrice();
}).reduce((a, b) => {
return a + b;
});
}
}
class Cabinet extends Composite {
constructor() {
super();
this.setName('cabinet');
}
}
// --- leafs ---
class FloppyDisk extends Equipment {
constructor() {
super();
this.setName('Floppy Disk');
this.price = 70;
}
}
class HardDrive extends Equipment {
constructor() {
super();
this.setName('Hard Drive');
this.price = 250;
}
}
class Memory extends Equipment {
constructor() {
super();
this.setName('Memory');
this.price = 280;
}
}
Decorator
Decorator dynamically adds/overrides behavior in an existing method of an object.
Example:
class Pasta {
constructor() {
this.price = 0;
}
getPrice() {
return this.price;
}
}
class Penne extends Pasta {
constructor() {
super();
this.price = 8;
}
}
class PastaDecorator extends Pasta {
constructor(pasta) {
super();
this.pasta = pasta;
}
getPrice() {
return this.pasta.getPrice();
}
}
class SauceDecorator extends PastaDecorator {
constructor(pasta) {
super(pasta);
}
getPrice() {
return super.getPrice() + 5;
}
}
class CheeseDecorator extends PastaDecorator {
constructor(pasta) {
super(pasta);
}
getPrice() {
return super.getPrice() + 3;
}
}
Facade
Facade provides a simplified interface to a large body of code.
The Facade pattern is used when we want to show the higher level of abstraction and hide the complexity behind the large codebase. Think of it as simplifying the API being presented to other developers, something which almost always improves usability
Pros and Cons
Pros:
Reduces the learning curve necessary to successfully leverage the subsystem
Promotes decoupling the subsystem from its potentially many clients
Cons:
Facade is the only access point for the subsystem, it will limit the features and flexibility that "power users" may need
performance costs of having one more high-level abstraction
Facade vs Adapter
Facade defines a new interface, whereas Adapter uses an old interface. Remember that Adapter makes two existing interfaces work together as opposed to defining an entirely new one. The intent of Facade is to produce a simpler interface, and the intent of Adapter is to design an existing interface. While Facade routinely wraps multiple objects and Adapter wraps a single object; Facade could front-end a single complex object and Adapter could wrap several legacy objects.
Example:
class Discount {
calc(value) {
return value * 0.9;
}
}
class Shipping {
calc() {
return 5;
}
}
class Fees {
calc(value) {
return value * 1.05;
}
}
class ShopFacade {
constructor() {
this.discount = new Discount();
this.shipping = new Shipping();
this.fees = new Fees();
}
calc(price) {
price = this.discount.calc(price);
price = this.fees.calc(price);
price += this.shipping.calc();
return price;
}
}
Example in FE world: A great example of this pattern is used in the common DOM manipulation libraries like jQuery, which simplifies the selection and events adding mechanism of the elements.
Flyweight
Flyweight reduces the cost of creating and manipulating a large number of similar objects.
Example:
class Color {
constructor(name) {
this.name = name
}
}
class colorFactory {
constructor(name) {
this.colors = {};
}
create(name) {
let color = this.colors[name];
if (color) return color;
this.colors[name] = new Color(name);
return this.colors[name];
}
};
Proxy
Proxy provides a placeholder for another object to control access, reduce cost, and reduce complexity.
Example:
class Car {
drive() {
return "driving";
};
}
class CarProxy {
constructor(driver) {
this.driver = driver;
}
drive() {
return (this.driver.age < 18) ? "too young to drive" : new Car().drive();
};
}
class Driver {
constructor(age) {
this.age = age;
}
}
Last updated
Was this helpful?