Day 19: Design Patterns

Introduction to design patterns

Design patterns are common solutions to recurring problems in software design that have been proven to work effectively over time. They provide a structured approach to solving problems in software design, and promote best practices and maintainability.

Design patterns can be classified into three categories:

  1. Creational Patterns: Creational patterns deal with the creation of objects and instances of classes. They provide various ways to create objects, depending on the situation and requirements. Examples of creational patterns include Singleton, Factory, Abstract Factory, Builder, and Prototype.
  2. Structural Patterns: Structural patterns deal with the composition of objects and classes. They provide ways to organize and structure classes and objects to form larger, more complex structures. Examples of structural patterns include Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy.
  3. Behavioral Patterns: Behavioral patterns deal with the interaction between objects and classes. They provide ways to define how classes and objects communicate with each other and how they behave. Examples of behavioral patterns include Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor.

Design patterns are not strict rules or guidelines that must be followed at all times, but rather flexible solutions that can be adapted to different situations. They can help developers to write cleaner, more modular code that is easier to maintain and extend.

Some benefits of using design patterns include:

  • They provide a common language and understanding among developers.
  • They promote best practices and standardization in software design.
  • They can help to identify and fix design problems early in the development process.
  • They can lead to more modular, maintainable, and extensible code.

However, it is important to use design patterns appropriately and not to overuse them. Overuse of design patterns can lead to overly complex code and reduce the maintainability of the system. It is also important to remember that design patterns are not a substitute for good design principles and best practices, but rather a tool to help implement them.

Singleton and Factory design patterns

Singleton and Factory are two common design patterns used in software development. Here’s an overview of each pattern:

  1. Singleton Pattern: The Singleton pattern is a creational pattern that ensures a class has only one instance, and provides a global point of access to that instance. This can be useful in situations where a single instance of a class needs to coordinate actions across a system or control access to a shared resource.

Here’s an example implementation of the Singleton pattern in Python:

class Singleton:
    __instance = None
    
    @staticmethod
    def get_instance():
        if Singleton.__instance is None:
            Singleton()
        return Singleton.__instance
    
    def __init__(self):
        if Singleton.__instance is not None:
            raise Exception("Singleton cannot be instantiated more than once")
        else:
            Singleton.__instance = self

In this implementation, we use a private class variable __instance to store the single instance of the class. We also define a static method get_instance() that returns the instance, creating it if it doesn’t already exist.

To prevent multiple instances of the class from being created, we define a private constructor that raises an exception if an instance already exists.

  1. Factory Pattern: The Factory pattern is a creational pattern that provides a way to create objects without specifying their exact class. It defines an interface for creating objects, but delegates the responsibility of object creation to its subclasses. This can be useful in situations where a system needs to be flexible and easily customizable.

Here’s an example implementation of the Factory pattern in Python:

class Shape:
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        print("Drawing a circle")

class Square(Shape):
    def draw(self):
        print("Drawing a square")

class ShapeFactory:
    @staticmethod
    def get_shape(shape_type):
        if shape_type == "circle":
            return Circle()
        elif shape_type == "square":
            return Square()
        else:
            return None

In this implementation, we define a Shape class with a draw() method, and two subclasses Circle and Square that implement the draw() method in their own way.

We also define a ShapeFactory class with a static method get_shape() that takes a shape_type parameter and returns an instance of the appropriate subclass based on the type.

This allows the client code to create objects without having to know the exact class or implementation details. For example:

circle = ShapeFactory.get_shape("circle")
square = ShapeFactory.get_shape("square")

circle.draw()  # prints "Drawing a circle"
square.draw()  # prints "Drawing a square"

In this example, we use the ShapeFactory class to create instances of Circle and Square without having to know their exact class or implementation details. This makes the code more flexible and easily customizable.

Decorator design pattern

The Decorator pattern is a structural pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class. It provides a way to extend the functionality of an object without having to subclass or modify the original object.

The Decorator pattern is based on the principle of composition, where objects can be composed of other objects to provide new functionality. It involves creating a decorator class that “wraps” the original object and adds new behavior to it.

Here’s an example implementation of the Decorator pattern in Python:

class Component:
    def operation(self):
        pass

class ConcreteComponent(Component):
    def operation(self):
        return "ConcreteComponent"

class Decorator(Component):
    def __init__(self, component):
        self._component = component
    
    def operation(self):
        return self._component.operation()

class ConcreteDecoratorA(Decorator):
    def operation(self):
        return f"ConcreteDecoratorA({self._component.operation()})"

class ConcreteDecoratorB(Decorator):
    def operation(self):
        return f"ConcreteDecoratorB({self._component.operation()})"

In this implementation, we define a Component class with an operation() method, and a ConcreteComponent subclass that implements the operation() method in a concrete way.

We then define a Decorator class that inherits from Component and wraps another Component object, and two concrete decorator subclasses ConcreteDecoratorA and ConcreteDecoratorB that add new behavior to the original object.

The ConcreteDecoratorA and ConcreteDecoratorB classes override the operation() method to add new functionality to the original object, and call the operation() method of the wrapped component to get the original behavior.

Here’s an example of how to use the Decorator pattern to add new functionality to an object:

component = ConcreteComponent()
decorator1 = ConcreteDecoratorA(component)
decorator2 = ConcreteDecoratorB(decorator1)

print(component.operation())     # prints "ConcreteComponent"
print(decorator1.operation())    # prints "ConcreteDecoratorA(ConcreteComponent)"
print(decorator2.operation())    # prints "ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))"

In this example, we create a ConcreteComponent object and wrap it with two decorator objects to add new behavior. The output shows how the original object is decorated with new functionality by each decorator.

The Decorator pattern is a powerful tool for extending the functionality of objects in a flexible and modular way. It allows you to add new behavior to objects without modifying the original object, and can be used to build complex object hierarchies from simple components.

Exercise

Implement a Singleton class for a logging service in a multi-threaded environment