DEV Community

Cover image for OOP poetry Part II: structural patterns
michael matos
michael matos

Posted on

OOP poetry Part II: structural patterns

what are structural patterns in OODesign

Structural patterns are a category of design patterns that focus on how classes and objects are composed to form larger structures. These patterns help in organizing relationships between different components to make the system more manageable, flexible, and efficient. Structural patterns primarily deal with class composition and object composition.

List of patterns

Adapter pattern

In the realm of code where patterns thrive,
There's one that keeps our designs alive.
It's the adapter, a clever guise,
In software's world, it truly flies.

Imagine systems of varied kind,
Their interfaces not aligned.
Yet, in the dance of tech's grand play,
They must communicate, find a way.

Enter the adapter, with its artful grace,
Linking the old with the new in space.
It wraps the incompatible with care,
Making connections where none would dare.

Like a bridge across a vast divide,
It helps two worlds harmonize.
With its form, the old can still converse,
In the language of the universe.

From legacy to the avant-garde,
The adapter pattern plays its card.
It transforms, it molds, it adapts with glee,
Bringing cohesion where there once was spree.

So let's raise our keyboards, give a cheer,
For the adapter pattern, always near.
In the symphony of code's grand scheme,
It's the harmony that makes us dream.

// Adaptee: Temperature sensor data
class TemperatureSensor {
    constructor(private temperature: number) {}

    getTemperature(): number {
        return this.temperature;
    }
}

// Adaptee: Humidity sensor data
class HumiditySensor {
    constructor(private humidity: number) {}

    getHumidity(): number {
        return this.humidity;
    }
}

// Target: Interface expected by the client (JSON format)
interface JSONData {
    toJSON(): object;
}

// Adapter: Adapts TemperatureSensor data to JSONData
class TemperatureSensorToJSONAdapter implements JSONData {
    constructor(private temperatureSensor: TemperatureSensor) {}

    toJSON(): object {
        return {
            type: "Temperature",
            value: this.temperatureSensor.getTemperature(),
            unit: "Celsius"
        };
    }
}

// Adapter: Adapts HumiditySensor data to JSONData
class HumiditySensorToJSONAdapter implements JSONData {
    constructor(private humiditySensor: HumiditySensor) {}

    toJSON(): object {
        return {
            type: "Humidity",
            value: this.humiditySensor.getHumidity(),
            unit: "Percentage"
        };
    }
}

// Client code
function convertToJSON(data: JSONData) {
    return data.toJSON();
}

// Usage
const temperatureSensorData = new TemperatureSensor(25); // 25°C
const humiditySensorData = new HumiditySensor(50); // 50% humidity

const temperatureJSONAdapter = new TemperatureSensorToJSONAdapter(temperatureSensorData);
const humidityJSONAdapter = new HumiditySensorToJSONAdapter(humiditySensorData);

const temperatureJSON = convertToJSON(temperatureJSONAdapter);
const humidityJSON = convertToJSON(humidityJSONAdapter);

console.log("Temperature Sensor Data:", temperatureJSON);
console.log("Humidity Sensor Data:", humidityJSON);

Enter fullscreen mode Exit fullscreen mode

Bridge pattern

In the realm of patterns, there lies a gem,
A Bridge that spans a vast code realm.
With elegance and grace, it stands tall,
Linking concepts, preventing a fall.

Two worlds it binds, with care and precision,
Abstract and concrete in a harmonious vision.
On one side, abstraction reigns supreme,
A realm of ideas, like a waking dream.

On the other, implementation holds sway,
Details and intricacies in a complex array.
Yet, through the Bridge, they find a way to unite,
A symphony of code, shining bright.

With an interface as its guiding light,
The Bridge ensures both realms stay right.
Changes ripple through without a fuss,
Preserving order, without a muss.

So let us celebrate this pattern divine,
A testament to software's design.
For in the Bridge, we find a bridge of trust,
A link between worlds, built of code's lust.

// Implementor: Interface for device control
interface Device {
    powerOn(): void;
    powerOff(): void;
    setVolume(volume: number): void;
}

// Concrete Implementor: TV device
class TV implements Device {
    powerOn(): void {
        console.log("TV is powered on");
    }

    powerOff(): void {
        console.log("TV is powered off");
    }

    setVolume(volume: number): void {
        console.log(`TV volume set to ${volume}`);
    }
}

// Concrete Implementor: DVD player device
class DVDPlayer implements Device {
    powerOn(): void {
        console.log("DVD player is powered on");
    }

    powerOff(): void {
        console.log("DVD player is powered off");
    }

    setVolume(volume: number): void {
        console.log("DVD player does not support volume control");
    }
}

// Abstraction: Remote control
abstract class RemoteControl {
    protected device: Device;

    constructor(device: Device) {
        this.device = device;
    }

    abstract powerOn(): void;
    abstract powerOff(): void;
    abstract setVolume(volume: number): void;
}

// Refined Abstraction: Basic remote control
class BasicRemoteControl extends RemoteControl {
    powerOn(): void {
        this.device.powerOn();
    }

    powerOff(): void {
        this.device.powerOff();
    }

    setVolume(volume: number): void {
        this.device.setVolume(volume);
    }
}

// Refined Abstraction: Advanced remote control with additional features
class AdvancedRemoteControl extends RemoteControl {
    mute(): void {
        this.device.setVolume(0);
        console.log("Volume muted");
    }
}

// Usage
const tv = new TV();
const dvdPlayer = new DVDPlayer();

const basicRemoteForTV = new BasicRemoteControl(tv);
basicRemoteForTV.powerOn();
basicRemoteForTV.setVolume(20);
basicRemoteForTV.powerOff();

const basicRemoteForDVD = new BasicRemoteControl(dvdPlayer);
basicRemoteForDVD.powerOn();
basicRemoteForDVD.setVolume(30);
basicRemoteForDVD.powerOff();

const advancedRemoteForTV = new AdvancedRemoteControl(tv);
advancedRemoteForTV.powerOn();
advancedRemoteForTV.mute();
advancedRemoteForTV.powerOff();

Enter fullscreen mode Exit fullscreen mode

Decorator pattern

In the world of code, a pattern unfolds,
A decorator's tale, as it's often told.
With elegance and flair, it adorns with delight,
A design marvel, gleaming bright.

Imagine a canvas, blank and bare,
Awaiting embellishment, with care.
Enter the decorator, with brushes in hand,
To paint a masterpiece, across the land.

With each stroke, it adds a touch,
Enhancing objects, oh so much.
uA layer of functionality, subtle and fine,
Wrapped in patterns, like a vine.

Like a decorator in a grand old hall,
It dresses up objects, one and all.
But beneath the surface, it retains,
The core essence, where beauty reigns.

So whether it's adding color or spice,
The decorator pattern's advice:
Embellish with grace, but keep in sight,
The essence of objects, shining bright.

In the tapestry of software design,
The decorator pattern, a gem to find.
A touch of class, a stroke of art,
Elevating code, with every part.

// Interface defining the basic functionality
interface Message {
    send(): string;
}

// Concrete component implementing the basic functionality
class SimpleMessage implements Message {
    send(): string {
        return "Simple Message";
    }
}

// Decorator class that adds extra functionality
abstract class MessageDecorator implements Message {
    protected message: Message;

    constructor(message: Message) {
        this.message = message;
    }

    send(): string {
        return this.message.send(); // Delegates message sending to the wrapped object
    }
}

// Concrete decorator adding encryption to the message
class EncryptionDecorator extends MessageDecorator {
    send(): string {
        const originalMessage = this.message.send();
        const encryptedMessage = this.encrypt(originalMessage);
        return `Encrypted Message: ${encryptedMessage}`;
    }

    private encrypt(message: string): string {
        // Just a simple encryption example for demonstration purposes
        return message.split('').reverse().join('');
    }
}

// Concrete decorator adding signature to the message
class SignatureDecorator extends MessageDecorator {
    send(): string {
        const originalMessage = this.message.send();
        const signedMessage = this.sign(originalMessage);
        return `Signed Message: ${signedMessage}`;
    }

    private sign(message: string): string {
        // Just a simple signing example for demonstration purposes
        return message + " [Signed]";
    }
}

// Usage example
let myMessage: Message = new SimpleMessage();
console.log(myMessage.send()); // Output: Simple Message

// Adding encryption to the message
myMessage = new EncryptionDecorator(myMessage);
console.log(myMessage.send()); // Output: Encrypted Message: egasseM elpmiS

// Adding signature to the message
myMessage = new SignatureDecorator(myMessage);
console.log(myMessage.send()); // Output: Signed Message: Encrypted Message: egasseM elpmiS

Enter fullscreen mode Exit fullscreen mode

Facade pattern

In the realm of code, where complexities dwell,
A pattern emerges, weaving its spell.
Its name is Facade, a guardian strong,
Shielding the world from what's complex, and long.

Imagine a maze of tangled threads,
Where confusion reigns, and clarity sheds.
But in the midst, a structure appears,
A facade of simplicity, banishing fears.

Behind its mask, a world is hid,
Of intricate systems, each amid.
Yet to the outside, it presents a face,
A single entry point, a saving grace.

Like a grand gate to a hidden realm,
The facade pattern takes the helm.
It shields the world from chaos deep,
And guides them through with promises to keep.

With elegance and grace, it stands tall,
Simplifying access, for one and all.
Complexity fades, as simplicity reigns,
In the facade's shadow, where order maintains.

So let us hail this pattern divine,
A beacon of clarity in code's design.
For in the facade, we find a guide,
Through the labyrinth of complexity, it will abide.

// Subsystem 1: Lights
class Lights {
    turnOn(): void {
        console.log("Lights are turned on");
    }

    turnOff(): void {
        console.log("Lights are turned off");
    }
}

// Subsystem 2: TV
class TV {
    turnOn(): void {
        console.log("TV is turned on");
    }

    turnOff(): void {
        console.log("TV is turned off");
    }
}

// Subsystem 3: SoundSystem
class SoundSystem {
    turnOn(): void {
        console.log("Sound system is turned on");
    }

    turnOff(): void {
        console.log("Sound system is turned off");
    }
}

// Subsystem 4: SecuritySystem
class SecuritySystem {
    arm(): void {
        console.log("Security system is armed");
    }

    disarm(): void {
        console.log("Security system is disarmed");
    }
}

// Subsystem 5: ClimateControl
class ClimateControl {
    setTemperature(temperature: number): void {
        console.log(`Temperature is set to ${temperature}°C`);
    }

    turnOffAirConditioner(): void {
        console.log("Air conditioner is turned off");
    }

    turnOnHeater(): void {
        console.log("Heater is turned on");
    }
}

// Facade: HomeAutomationFacade
class HomeAutomationFacade {
    private lights: Lights;
    private tv: TV;
    private soundSystem: SoundSystem;
    private securitySystem: SecuritySystem;
    private climateControl: ClimateControl;

    constructor() {
        this.lights = new Lights();
        this.tv = new TV();
        this.soundSystem = new SoundSystem();
        this.securitySystem = new SecuritySystem();
        this.climateControl = new ClimateControl();
    }

    // Method to start the home automation system
    startHome(): void {
        console.log("Starting home automation system");
        this.securitySystem.arm();
        this.lights.turnOn();
        this.tv.turnOn();
        this.soundSystem.turnOn();
        this.climateControl.setTemperature(22);
    }

    // Method to stop the home automation system
    stopHome(): void {
        console.log("Stopping home automation system");
        this.soundSystem.turnOff();
        this.tv.turnOff();
        this.lights.turnOff();
        this.securitySystem.disarm();
        this.climateControl.turnOffAirConditioner();
        this.climateControl.turnOnHeater();
    }
}

// Usage example
const homeAutomationFacade = new HomeAutomationFacade();
homeAutomationFacade.startHome(); // Output: Starting home automation system, Security system is armed, Lights are turned on, TV is turned on, Sound system is turned on, Temperature is set to 22°C
homeAutomationFacade.stopHome(); // Output: Stopping home automation system, Sound system is turned off, TV is turned off, Lights are turned off, Security system is disarmed, Air conditioner is turned off, Heater is turned on

Enter fullscreen mode Exit fullscreen mode

Proxy pattern

In the vast expanse of code's domain,
Where patterns dance and solutions reign,
There dwells a Proxy, noble and true,
A guardian, standing between me and you.

With purpose clear, it takes its stand,
Shielding access with a guiding hand.
A surrogate, it plays its part,
Protecting, controlling, with a gentle heart.

Imagine a door, to secrets unknown,
Where entry's granted, but risks have grown.
Here steps the Proxy, vigilant and wise,
Restricting access, under watchful eyes.

In networking realms, it finds its place,
Caching requests, with effortless grace.
Reducing latency, enhancing speed,
The Proxy serves, fulfilling this need.

In security's embrace, it shines bright,
Filtering requests, with keen foresight.
Guarding sensitive data, from prying eyes,
The Proxy safeguards, as code complies.

But not just in code, its uses expand,
In virtual realms, it lends a hand.
From lazy loading to resource control,
The Proxy pattern plays its role.

So hail the Proxy, in code's grand scheme,
A protector, a guardian, in a digital dream.
With purpose clear and use cases vast,
The Proxy pattern endures, steadfast.

// Interface defining the basic functionality
interface BankAccount {
    deposit(amount: number): void;
    withdraw(amount: number): void;
    getBalance(): number;
}

// Concrete component implementing the basic functionality
class ConcreteBankAccount implements BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

    deposit(amount: number): void {
        this.balance += amount;
        console.log(`Deposited ${amount} into the account. Current balance: ${this.balance}`);
    }

    withdraw(amount: number): void {
        if (amount <= this.balance) {
            this.balance -= amount;
            console.log(`Withdrawn ${amount} from the account. Current balance: ${this.balance}`);
        } else {
            console.log("Insufficient funds!");
        }
    }

    getBalance(): number {
        return this.balance;
    }
}

// Proxy class that controls access to the ConcreteBankAccount
class BankAccountProxy implements BankAccount {
    private concreteBankAccount: ConcreteBankAccount;
    private isAdmin: boolean;

    constructor(initialBalance: number, isAdmin: boolean = false) {
        this.concreteBankAccount = new ConcreteBankAccount(initialBalance);
        this.isAdmin = isAdmin;
    }

    deposit(amount: number): void {
        this.checkAccess();
        this.concreteBankAccount.deposit(amount);
    }

    withdraw(amount: number): void {
        this.checkAccess();
        this.concreteBankAccount.withdraw(amount);
    }

    getBalance(): number {
        return this.concreteBankAccount.getBalance();
    }

    private checkAccess(): void {
        if (!this.isAdmin) {
            console.log("Access denied. You are not authorized to perform this action.");
            throw new Error("Access denied");
        }
    }
}

// Usage example
const bankAccountAdmin: BankAccount = new BankAccountProxy(1000, true); // Admin account
const bankAccountUser: BankAccount = new BankAccountProxy(500); // Regular user account

// Admin can perform all actions
bankAccountAdmin.deposit(500); // Output: Deposited 500 into the account. Current balance: 1500
bankAccountAdmin.withdraw(200); // Output: Withdrawn 200 from the account. Current balance: 1300
console.log("Balance:", bankAccountAdmin.getBalance()); // Output: Balance: 1300

// Regular user can only view balance
console.log("Balance:", bankAccountUser.getBalance()); // Output: Balance: 500
bankAccountUser.deposit(200); // Output: Access denied. You are not authorized to perform this action.
bankAccountUser.withdraw(100); // Output: Access denied. You are not authorized to perform this action.

Enter fullscreen mode Exit fullscreen mode

Composite pattern

In the forest of code, where structures abound,
There blooms a pattern, both simple and profound.
Its name is Composite, a tapestry grand,
Uniting objects, across the land.

With purpose clear, it takes its stand,
To compose hierarchies, with a guiding hand.
A tree of nodes, with branches wide,
A structure flexible, where entities reside.

Imagine a tree, with roots so deep,
Where each node holds, a secret to keep.
But in this realm, there's more to see,
As nodes unite, in harmony.

The Composite, it joins objects diverse,
In a single structure, they converse.
From leaves to branches, to the root they bind,
A cohesive whole, of a different kind.

In GUI realms, it finds its place,
As widgets gather, with seamless grace.
Buttons and panels, in a hierarchy tall,
The Composite pattern unites them all.

In file systems, it reigns supreme,
As directories nest, in a recursive dream.
Files and folders, in a tree they dwell,
The Composite pattern, weaves its spell.

So hail the Composite, in code's grand scheme,
A structure versatile, like a timeless stream.
With purpose clear and use cases vast,
The Composite pattern endures, steadfast.

// Component interface
interface FileSystemComponent {
    getName(): string;
    getSize(): number;
}

// Leaf class representing a file
class File implements FileSystemComponent {
    private name: string;
    private size: number;

    constructor(name: string, size: number) {
        this.name = name;
        this.size = size;
    }

    getName(): string {
        return this.name;
    }

    getSize(): number {
        return this.size;
    }
}

// Composite class representing a directory
class Directory implements FileSystemComponent {
    private name: string;
    private components: FileSystemComponent[];

    constructor(name: string) {
        this.name = name;
        this.components = [];
    }

    add(component: FileSystemComponent): void {
        this.components.push(component);
    }

    remove(component: FileSystemComponent): void {
        const index = this.components.indexOf(component);
        if (index !== -1) {
            this.components.splice(index, 1);
        }
    }

    getName(): string {
        return this.name;
    }

    getSize(): number {
        let totalSize = 0;
        this.components.forEach(component => {
            totalSize += component.getSize();
        });
        return totalSize;
    }
}

// Usage example
const file1 = new File("document1.txt", 100); // File with size 100 bytes
const file2 = new File("document2.txt", 200); // File with size 200 bytes
const file3 = new File("image.jpg", 500);     // File with size 500 bytes

const directory1 = new Directory("Documents");
directory1.add(file1);
directory1.add(file2);

const directory2 = new Directory("Pictures");
directory2.add(file3);

const rootDirectory = new Directory("Root");
rootDirectory.add(directory1);
rootDirectory.add(directory2);

// Calculate size of the entire file system
console.log("Total size of the file system:", rootDirectory.getSize()); // Output: Total size of the file system: 800

Enter fullscreen mode Exit fullscreen mode

Flywieght pattern or Pool pattern

In the realm of software, where memory does dwell,
A pattern arises, with a story to tell.
Its name is Flyweight, a design so light,
A guardian of memory, shining bright.

With purpose clear, it takes its place,
To manage resources with elegance and grace.
A pool of objects, it seeks to share,
Reducing overhead, with a mindful care.

Imagine a garden, with flowers so fair,
Each petal unique, in the fragrant air.
But in this garden, there lies a scheme,
Where petals unite, like a seamless dream.

The Flyweight, it stores common traits,
Intrinsic properties, it deftly waits.
Extrinsic details, it shares among,
To conserve memory, like a gentle song.

In graphical realms, it finds its role,
With sprites and textures, it takes control.
Reusing objects, with colors and shapes,
The Flyweight pattern, its purpose drapes.

In text processing, it lends its hand,
With characters and fonts, across the land.
Caching glyphs, with glyphs so slight,
The Flyweight pattern, brings delight.

So hail the Flyweight, in code's grand quest,
A keeper of memory, at its best.
With purpose clear and use cases vast,
The Flyweight pattern endures, steadfast.

// Flyweight interface
interface Enemy {
    attack(): void;
}

// Concrete flyweight class representing an enemy
class ConcreteEnemy implements Enemy {
    private type: string;
    private strength: number;
    private specialAbility: string;

    constructor(type: string, strength: number, specialAbility: string) {
        this.type = type;
        this.strength = strength;
        this.specialAbility = specialAbility;
    }

    attack(): void {
        console.log(`${this.type} enemy attacks with strength ${this.strength} and uses special ability: ${this.specialAbility}`);
    }
}

// Flyweight factory class
class EnemyFactory {
    private enemies: { [key: string]: ConcreteEnemy } = {};

    getEnemy(type: string, strength: number, specialAbility: string): ConcreteEnemy {
        const key = `${type}-${strength}`;
        if (!this.enemies[key]) {
            this.enemies[key] = new ConcreteEnemy(type, strength, specialAbility);
        }
        return this.enemies[key];
    }
}

// Client code (game simulation)
const factory = new EnemyFactory();

// Simulate spawning enemies in the game
const enemyTypes = ["Goblin", "Orc", "Skeleton"];
const specialAbilities = ["Fireball", "Stealth", "Summon Minions"];

for (let i = 0; i < 15; i++) {
    const enemyType = enemyTypes[Math.floor(Math.random() * enemyTypes.length)]; // Randomly choose enemy type
    const enemyStrength = Math.floor(Math.random() * 5) + 1; // Randomly choose enemy strength (1 to 5)
    const enemyAbility = specialAbilities[Math.floor(Math.random() * specialAbilities.length)]; // Randomly choose enemy special ability
    const enemy = factory.getEnemy(enemyType, enemyStrength, enemyAbility);
    enemy.attack();
}

Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
johannyrondon profile image
Johanny Maria Rondon Ramirez

👏🏽👏🏽👏🏽🙌🏽🙌🏽🙌🏽

Collapse
 
pedroadlcruz profile image
Pedro De la Cruz M.

😎😎😎