DEV Community

Cover image for OOP poetry Part I: Creational patterns
michael matos
michael matos

Posted on • Edited on

OOP poetry Part I: Creational patterns

A note: while learning new concepts, what has proven the most successful strategy to me is not the meticulous reading of a good article but immerse my eye balls in tons of articles, exposing my mind to different point of views, ways of teaching, different examples and use cases until they finally click. This piece is meant to add to that diversity.

what are creational patterns in OODesign

Creational patterns are a category of design patterns that deal with the process of object creation, providing mechanisms for creating objects in a manner suitable for the situation at hand. These patterns help in abstracting the instantiation process, decoupling the system from the specifics of how objects are created, and promoting flexibility and reusability.

List of patterns

Factory method

In software's realm, where logic reigns supreme,
The Factory Method, like a guiding beam,
A pattern woven with meticulous care,
To handle object creation, it's rare.

When classes spawn with varied traits in mind,
The Factory stands as a bridge well-designed.
It crafts the objects, tailored and refined,
As needs arise, its versatility defined.

No longer bound by static instantiation,
It offers freedom, a fluid creation.
Through inheritance's flexible embrace,
New instances emerge in code's embrace.

With Factory Method, complexity tamed,
Creation's process, elegantly framed.
In software's symphony, it takes its part,
A design pattern, a work of art.

abstracting different logistics options:

// Define the Product interface
interface Transport {
    deliver(): string;
}

// Concrete Products
class Truck implements Transport {
    deliver(): string {
        return 'Delivering by Truck';
    }
}

class Ship implements Transport {
    deliver(): string {
        return 'Delivering by Ship';
    }
}

// Creator interface
abstract class LogisticsCompany {
    abstract createTransport(): Transport;

    // Other methods can be here
    shipGoods(): string {
        const transport = this.createTransport();
        return `Goods are ${transport.deliver()}`;
    }
}

// Concrete Creators
class RoadLogistics extends LogisticsCompany {
    createTransport(): Transport {
        return new Truck();
    }
}

class SeaLogistics extends LogisticsCompany {
    createTransport(): Transport {
        return new Ship();
    }
}

// Usage
function clientCode(logistics: LogisticsCompany) {
    console.log(logistics.shipGoods());
}

// Creating Concrete Creators
const roadLogistics = new RoadLogistics();
clientCode(roadLogistics);

const seaLogistics = new SeaLogistics();
clientCode(seaLogistics);
Enter fullscreen mode Exit fullscreen mode

Rationale:
consider using the Factory Method pattern when you need to defer object creation to subclasses or runtime

  • Creating objects without specifying their concrete classes
  • Encapsulating object creation logic
  • Supporting extensibility and flexibility
  • Decoupling client code from concrete classes

Abstract factory

In software's realm, where structures take flight,
The Abstract Factory, a beacon of light.
A pattern born from creative might,
In code's embrace, it weaves with delight.

With elegance, it crafts a domain wide,
A family of objects, in unity abide.
Abstracting creation, it does confide,
In harmonious design, it takes its stride.

Variants emerge, with subtle grace,
From factories abstract, in their designated space.
Each product unique, yet in common trace,
A symphony of classes, in code's embrace.

In software's tapestry, it finds its place,
The Abstract Factory, a design of grace.

Abstract factory for families of furniture

// Abstract Product A
interface Chair {
    sit(): string;
}

// Concrete Product A1
class ModernChair implements Chair {
    sit(): string {
        return "You are sitting on a modern chair.";
    }
}

// Concrete Product A2
class VictorianChair implements Chair {
    sit(): string {
        return "You are sitting on a victorian chair.";
    }
}

// Abstract Product B
interface Sofa {
    relax(): string;
}

// Concrete Product B1
class ModernSofa implements Sofa {
    relax(): string {
        return "You are relaxing on a modern sofa.";
    }
}

// Concrete Product B2
class VictorianSofa implements Sofa {
    relax(): string {
        return "You are relaxing on a victorian sofa.";
    }
}

// Abstract Factory
interface FurnitureFactory {
    createChair(): Chair;
    createSofa(): Sofa;
}

// Concrete Factory 1
class ModernFurnitureFactory implements FurnitureFactory {
    createChair(): Chair {
        return new ModernChair();
    }

    createSofa(): Sofa {
        return new ModernSofa();
    }
}

// Concrete Factory 2
class VictorianFurnitureFactory implements FurnitureFactory {
    createChair(): Chair {
        return new VictorianChair();
    }

    createSofa(): Sofa {
        return new VictorianSofa();
    }
}

// Client
class Client {
    private chair: Chair;
    private sofa: Sofa;

    constructor(factory: FurnitureFactory) {
        this.chair = factory.createChair();
        this.sofa = factory.createSofa();
    }

    useFurniture(): void {
        console.log(this.chair.sit());
        console.log(this.sofa.relax());
    }
}

// Usage
const modernFactory = new ModernFurnitureFactory();
const victorianFactory = new VictorianFurnitureFactory();

const modernClient = new Client(modernFactory);
modernClient.useFurniture();

const victorianClient = new Client(victorianFactory);
victorianClient.useFurniture();
Enter fullscreen mode Exit fullscreen mode

Rationale:
consider using the Abstract Factory pattern when you need to create families of related objects

  • Creating families of related objects
  • Encapsulating object creation logic
  • Providing a platform-independent interface

Builder pattern

In software's realm, where structures rise,
The Builder Pattern, a grand disguise.
With methodical craft, it shapes the wise,
In code's embrace, its essence lies.

A builder's hand, with precision keen,
Constructs objects from a dream serene.
Step by step, it brings to scene,
A creation tailored, like a serene.

Each step a choice, each choice a thread,
In the fabric of creation, finely bred.
Properties set, with care they're led,
Towards a masterpiece, where they're wed.

Flexibility reigns, in this design's sway,
Variety thrives, in its array.
From simple to complex, day by day,
The Builder Pattern guides the way.

So in software's tapestry, let it adorn,
The Builder Pattern, from dusk to morn.
With its craft, new structures are born,
In the symphony of code, forever sworn.

Builder pattern for building a customizable burger:

// Product
class Burger {
    private size: string;
    private cheese: boolean;
    private lettuce: boolean;
    private tomato: boolean;
    private bacon: boolean;
    private sauce: string;

    constructor(builder: BurgerBuilder) {
        this.size = builder.size;
        this.cheese = builder.cheese;
        this.lettuce = builder.lettuce;
        this.tomato = builder.tomato;
        this.bacon = builder.bacon;
        this.sauce = builder.sauce;
    }

    describe(): string {
        let description = `Size: ${this.size}, Cheese: ${this.cheese ? 'Yes' : 'No'}, Lettuce: ${this.lettuce ? 'Yes' : 'No'}, Tomato: ${this.tomato ? 'Yes' : 'No'}, Bacon: ${this.bacon ? 'Yes' : 'No'}, Sauce: ${this.sauce}`;
        return description;
    }
}

// Builder
class BurgerBuilder {
    size: string;
    cheese: boolean = false;
    lettuce: boolean = false;
    tomato: boolean = false;
    bacon: boolean = false;
    sauce: string = "Ketchup";

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

    addCheese(): BurgerBuilder {
        this.cheese = true;
        return this;
    }

    addLettuce(): BurgerBuilder {
        this.lettuce = true;
        return this;
    }

    addTomato(): BurgerBuilder {
        this.tomato = true;
        return this;
    }

    addBacon(): BurgerBuilder {
        this.bacon = true;
        return this;
    }

    setSauce(sauce: string): BurgerBuilder {
        this.sauce = sauce;
        return this;
    }

    build(): Burger {
        return new Burger(this);
    }
}

// Usage
const burger1 = new BurgerBuilder("Large")
    .addCheese()
    .addLettuce()
    .addTomato()
    .addBacon()
    .setSauce("BBQ")
    .build();

const burger2 = new BurgerBuilder("Medium")
    .addCheese()
    .addTomato()
    .setSauce("Mustard")
    .build();

console.log("Burger 1:", burger1.describe());
console.log("Burger 2:", burger2.describe());
Enter fullscreen mode Exit fullscreen mode

Rationale:
consider using the Builder pattern when you need to simplify complex object creation

  • Complex object creation
  • Flexibility and readability
  • Variability in object construction
  • Immutability and thread safety
  • Testability

Prototype pattern

In software's realm, where innovations gleam,
The Prototype Pattern, a visionary dream.
A pattern born from creativity's stream,
In code's embrace, it reigns supreme.

Like a master artist with canvas in hand,
It crafts objects from a fertile land.
Cloning instances, a wondrous command,
A blueprint of creation, finely planned.

With Prototype, new instances bloom,
A replication of a given room.
Efficient and swift, it banishes gloom,
In software's garden, it finds its bloom.

Variations emerge, each with its flair,
From the original, they boldly dare.
Customizations woven with care,
Prototype Pattern, beyond compare.

In the code's symphony, it sings its song,
A design pattern, forever strong.
With its power, new realms belong,
In the world of software, it marches along.

we need an assistant for our example and who's better than dolly the sheep

// Prototype interface
interface Cloneable {
    clone(): Cloneable;
}

// Concrete prototype
class Sheep implements Cloneable {
    private name: string;
    private age: number;
    private color: string;

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

    // Clone method to create a deep copy
    clone(): Sheep {
        const clonedSheep = new Sheep(this.name, this.age, this.color);
        return clonedSheep;
    }

    // Method to display sheep details
    display(): string {
        return `Name: ${this.name}, Age: ${this.age}, Color: ${this.color}`;
    }
}

// Client code
const originalSheep = new Sheep("Dolly", 2, "White");
console.log("Original sheep:", originalSheep.display());

// Cloning the original sheep
const clonedSheep1 = originalSheep.clone();
clonedSheep1.display();

// Modifying the cloned sheep
clonedSheep1.color = "Black";
console.log("Cloned sheep 1:", clonedSheep1.display());

// Cloning the original sheep again
const clonedSheep2 = originalSheep.clone();
console.log("Cloned sheep 2:", clonedSheep2.display());
Enter fullscreen mode Exit fullscreen mode

Rationale:
consider using the Prototype pattern when you need to create new objects based on existing ones

  • Object creation is costly
  • Variations of objects are needed
  • Avoiding subclassing
  • Dynamic runtime changes
  • Reducing coupling

Singleton pattern

In software's realm, where codes converge,
A Singleton pattern, with its gentle urge.
A solitary instance, it does emerge,
In the symphony of design, it does surge.

Alone it stands, a guardian strong,
Ensuring only one instance, where it belongs.
Threads may dance, and time may prolong,
But Singleton's essence remains lifelong.

In shared resources, its value shines,
A single point of access, in design's confines.
Concurrency tamed, like rhythmic rhymes,
Singleton's presence, in code defines.

With care it's crafted, in classes it dwells,
A unique instance, its story tells.
In software's tapestry, where wisdom swells,
Singleton pattern, its legacy compels.

So in the landscape of software's domain,
Singleton's silhouette shall forever reign.
A solitary guardian, with wisdom to sustain,
In the patterns of design, it shall retain.

class Logger {
    private static instance: Logger | null = null;
    private logs: string[] = [];

    // Private constructor to prevent instantiation from outside
    private constructor() {}

    // Static method to get the singleton instance
    public static getInstance(): Logger {
        if (!Logger.instance) {
            Logger.instance = new Logger();
        }
        return Logger.instance;
    }

    // Method to add a log message
    public log(message: string): void {
        this.logs.push(message);
    }

    // Method to display all log messages
    public displayLogs(): void {
        console.log("===== Log Messages =====");
        this.logs.forEach((log, index) => {
            console.log(`[${index + 1}] ${log}`);
        });
        console.log("========================");
    }
}

// Usage
const logger1 = Logger.getInstance();
logger1.log("Error: File not found.");

const logger2 = Logger.getInstance();
logger2.log("Warning: Database connection lost.");

 // Check if both instances are the same 
 console.log(logger1 === logger2); // Output: true

// Display all log messages
logger1.displayLogs();

Enter fullscreen mode Exit fullscreen mode

Rationale:

consider using the Singleton pattern when you need to ensure there is only one instance of a class in your application

  • Global access point
  • Resource management
  • State management
  • Performance optimization
  • Immutable objects

For more examples please visit refactoring.guru

Top comments (2)

Collapse
 
pedroadlcruz profile image
Pedro De la Cruz M.

πŸ’ͺ🏾πŸ’ͺ🏾

Collapse
 
johannyrondon profile image
Johanny Maria Rondon Ramirez

πŸ‘πŸ½πŸ‘πŸ½πŸ‘πŸ½