DEV Community

Hernán Chilabert
Hernán Chilabert

Posted on • Edited on

Creational Patterns: Enhancing Your Coding Arsenal

[UPDATED 12/7/2023]
✨ GitHub repo with real-world use cases: https://github.com/herchila/design-patterns/tree/main/creational_patterns

Singleton

Purpose: A singleton ensures that a class has only one instance and provides a global point of access to it.

Example: In applications that require thread management, a Singleton can be used to manage and optimize the use of threads, ensuring that threads are reused and not excessively created and destroyed.

Basic sample

class Singleton:
    _instance = None

    @staticmethod
    def getInstance():
        if Singleton._instance == None:
            Singleton()
        return Singleton._instance

    def __init__(self):
        if Singleton._instance != None:
            raise Exception("This class is a singleton!")
        else:
            Singleton._instance = self

# Usage
s = Singleton.getInstance()
print(s)

s1 = Singleton.getInstance()
print(s1)  # Will print the same instance as s
Enter fullscreen mode Exit fullscreen mode

Advance sample
The Singleton pattern can be effectively used in scenarios involving message producers in systems like Apache Kafka. Apache Kafka is a distributed streaming platform that allows for high-throughput, fault-tolerant handling of streaming data. In such a system, a Singleton can be used to manage a Kafka producer instance to ensure that only one instance is used to send messages to a Kafka topic, which can be crucial for optimizing resource usage and managing connections.

Here's an example of how you might implement a Kafka producer as a Singleton in Python:

from kafka import KafkaProducer
import json

class KafkaProducerSingleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(KafkaProducerSingleton, cls).__new__(cls, *args, **kwargs)
        return cls._instance

    def __init__(self, kafka_servers):
        # This will only be executed once
        self.producer = KafkaProducer(bootstrap_servers=kafka_servers,
                                      value_serializer=lambda v: json.dumps(v).encode('utf-8'))

    def send_message(self, topic, value):
        self.producer.send(topic, value)
        self.producer.flush()

# Usage example
kafka_servers = ['localhost:9092']  # Replace with your Kafka server addresses
producer1 = KafkaProducerSingleton(kafka_servers)
producer2 = KafkaProducerSingleton(kafka_servers)

print(producer1 == producer2)  # True

# Sending a message
producer1.send_message('my_topic', {'key': 'value'})
Enter fullscreen mode Exit fullscreen mode

In this implementation:

The __new__ method ensures that only one instance of KafkaProducerSingleton is created. If an instance already exists, it returns that existing instance.
The __init__ method initializes the Kafka producer. This initialization only happens once since __new__ ensures only one instance is created.
The send_message method is used to send messages to a specified Kafka topic. It uses the producer to send the message and then flushes the producer to ensure the message is sent immediately.

Abstract Factory Pattern

Purpose: To provide an interface for creating families of related or dependent objects without specifying their concrete classes.

Example: A UI library where we need to create UI elements that are consistent across different operating systems.

class Button:
    def paint(self): pass

class LinuxButton(Button):
    def paint(self):
        return "Render a button in a Linux style"

class WindowsButton(Button):
    def paint(self):
        return "Render a button in a Windows style"

class GUIFactory:
    def create_button(self): pass

class LinuxFactory(GUIFactory):
    def create_button(self):
        return LinuxButton()

class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

# Usage
def client_code(factory: GUIFactory):
    button = factory.create_button()
    print(button.paint())

client_code(LinuxFactory())  # Output: Render a button in a Linux style
client_code(WindowsFactory())  # Output: Render a button in a Windows style
Enter fullscreen mode Exit fullscreen mode

Builder Pattern

Purpose: To separate the construction of a complex object from its representation, allowing the same construction process to create different representations.

Example: Building different types of documents (Text, HTML, Markdown) with the same content.

class Document:
    def __init__(self):
        self.parts = []

    def add(self, part):
        self.parts.append(part)

    def __str__(self):
        return '\n'.join(self.parts)

class Builder:
    def build_title(self, title): pass
    def build_body(self, body): pass
    def get_result(self): pass

class TextBuilder(Builder):
    def __init__(self):
        self.document = Document()

    def build_title(self, title):
        self.document.add(f"Title: {title}")

    def build_body(self, body):
        self.document.add(body)

    def get_result(self):
        return self.document

# Usage
builder = TextBuilder()
builder.build_title("My Title")
builder.build_body("Hello, world!")
document = builder.get_result()
print(document)
Enter fullscreen mode Exit fullscreen mode

Factory Pattern

Purpose: To create objects without specifying the exact class of object that will be created.

Example: Generating different types of charts based on user input.

class Chart:
    def draw(self): pass

class BarChart(Chart):
    def draw(self):
        return "Drawing a bar chart"

class PieChart(Chart):
    def draw(self):
        return "Drawing a pie chart"

def chart_factory(chart_type):
    charts = {
        "bar": BarChart,
        "pie": PieChart
    }
    return charts[chart_type]()

# Usage
chart = chart_factory("bar")
print(chart.draw())  # Output: Drawing a bar chart
Enter fullscreen mode Exit fullscreen mode

Prototype Pattern

Purpose: To create new objects by copying existing objects, thus reducing the cost of creation.

Example: Duplicating graphic objects in a graphic editor.

import copy

class Prototype:
    def clone(self): pass

class ConcretePrototype(Prototype):
    def __init__(self, number):
        self.number = number

    def clone(self):
        return copy.deepcopy(self)

# Usage
prototype = ConcretePrototype(1)
cloned_prototype = prototype.clone()
print(cloned_prototype.number)  # Output: 1
Enter fullscreen mode Exit fullscreen mode

Top comments (0)