CodeCreational Design PatternsDesign PatternsPython

Understanding The Gang of Four (GOF) design patterns using Python

This is the 10th post in a series of learning the Python programming language.

The Gang of Four (GOF) design patterns are a set of solutions to common software design problems.

Here are brief explanations for each of the 23 design patterns described in the book “Design Patterns: Elements of Reusable Object-Oriented Software”.

  1. Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
  2. Builder: Separates object construction from its representation, allowing the same construction process to create different representations.
  3. Factory Method: Defines an interface for creating an object, but lets subclasses decide which class to instantiate.
  4. Prototype: A fully initialized instance to be copied or cloned.
  5. Singleton: A class of which only a single instance can exist.
  6. Adapter: Convert the interface of a class into another interface clients expect.
  7. Bridge: Decouples an abstraction from its implementation so that the two can vary independently.
  8. Composite: Compose objects into tree structures to represent part-whole hierarchies.
  9. Decorator: Attach additional responsibilities to an object dynamically.
  10. Facade: A single class that represents an entire subsystem.
  11. Flyweight: Use sharing to support a large number of similar objects efficiently.
  12. Proxy: Provide a surrogate or placeholder for another object to control access to it.
  13. Chain of Responsibility: A way of passing a request between a chain of objects until one of them handles it.
  14. Command: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
  15. Interpreter: A way to include language elements in a program to match the grammar of the intended language.
  16. Iterator: Sequentially access the elements of a collection without exposing its underlying representation.
  17. Mediator: Define an object that encapsulates how a set of objects interact.
  18. Memento: Without violating encapsulation, capture and externalize an object’s internal state allowing the object to be restored to this state later.
  19. Observer: A way of notifying change to a number of classes.
  20. State: Allow an object to alter its behavior when its internal state changes.
  21. Strategy: Define a family of algorithms, encapsulate each one, and make them interchangeable.
  22. Template Method: Define the skeleton of an algorithm in an operation, deferring some steps to subclasses.
  23. Visitor: Represent an operation to be performed on the elements of an object structure.

Let’s understand Abstract Factory, Builder design & Factory Method patterns in detail:

Abstract Factory Design Pattern

The Abstract Factory pattern is a design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is often used to create objects in a system that are related to a common theme or purpose, such as a set of GUI objects or a set of objects used to perform a specific task.

In Python, an abstract factory can be implemented by creating an abstract class that defines the interface for creating objects. This abstract class should include methods for creating each type of object that belongs to the family. Concrete implementations of the abstract factory then provide the actual implementation for creating each type of object.

Here’s an example of how the Abstract Factory pattern can be implemented in Python:

# Abstract class for the factory
class AbstractFactory:
def create_product_a(self):
pass

def create_product_b(self):
pass

# Concrete implementation of the factory
class ConcreteFactory1(AbstractFactory):
def create_product_a(self):
return ConcreteProductA1()

def create_product_b(self):
return ConcreteProductB1()

class ConcreteFactory2(AbstractFactory):
def create_product_a(self):
return ConcreteProductA2()

def create_product_b(self):
return ConcreteProductB2()

# Abstract class for the products
class AbstractProductA:
pass

class AbstractProductB:
pass

# Concrete implementations of the products
class ConcreteProductA1(AbstractProductA):
pass

class ConcreteProductA2(AbstractProductA):
pass

class ConcreteProductB1(AbstractProductB):
pass

class ConcreteProductB2(AbstractProductB):
pass

# Client code
def client_code(factory):
product_a = factory.create_product_a()
product_b = factory.create_product_b()

# Use the products
# ...

if __name__ == "__main__":
factory = ConcreteFactory1()
client_code(factory)

factory = ConcreteFactory2()
client_code(factory)

In this example, the abstract factory class AbstractFactory defines the interface for creating objects. The concrete implementations ConcreteFactory1 and ConcreteFactory2 provide the actual implementation for creating each type of object. The abstract product classes AbstractProductA and AbstractProductB define the interface for the products and the concrete product classes ConcreteProductA1ConcreteProductA2ConcreteProductB1, and ConcreteProductB2 provide the actual implementation for the products. The client code uses the factory to create products and then uses the products as needed.

Advantages of the Abstract Factory design pattern:

  1. Abstraction: The Abstract Factory pattern provides an abstraction layer between the client and the concrete factories. This makes it possible to change the concrete factories without affecting the client code.
  2. Flexibility: The Abstract Factory pattern provides great flexibility, as it allows you to create different products for different platforms or contexts, such as desktop, web, or mobile applications.
  3. Improved Reusability: The Abstract Factory pattern can improve reusability, as you can reuse product families across different applications.
  4. Better Support for Interface-based Programming: The Abstract Factory pattern provides better support for interface-based programming, as it separates the implementation details from the interface definition, making it easier to implement new products and extend existing ones.
  5. Simplified Code: The Abstract Factory pattern can simplify code by reducing the amount of conditional logic required to create products, as the client only needs to know the abstract factory, and not the individual concrete factories.

Disadvantages of the Abstract Factory design pattern:

  1. Increased Complexity: The Abstract Factory pattern can add complexity to an application, as it requires creating multiple classes and objects to manage the creation of products.
  2. Overhead: The Abstract Factory pattern can add overhead to an application, especially if the application requires multiple product families or large numbers of products.
  3. Debugging: Debugging an Abstract Factory can be challenging, as it requires understanding the relationships between the abstract factory, concrete factories, and products.

The Abstract Factory pattern is useful for creating objects in a system that are related to a common theme or purpose. It allows the client code to be decoupled from the concrete implementations of the objects, making it easier to change the implementation of the objects without affecting the client code. Additionally, it can make it easier to create new implementations of the objects, as the interface for creating objects is already defined in the abstract factory.

Builder Design Pattern

The Builder pattern is a design pattern that separates the construction of a complex object from its representation, allowing the same construction process to create different representations. This pattern is often used to create objects that have many optional parts or components, as the Builder pattern provides a way to specify which parts should be included in the final object.

In Python, a Builder class can be created that is responsible for building the object. This Builder class should include methods for specifying each part of the object, as well as a method for returning the final object. The client code uses the Builder to create the object by calling the methods for specifying each part and then calling the method for returning the final object.

Here’s an example of how the Builder pattern can be implemented in Python:

# Builder class
class CarBuilder:
def __init__(self):
self._wheels = 4
self._doors = 4
self._type = "sedan"

def set_wheels(self, wheels):
self._wheels = wheels
return self

def set_doors(self, doors):
self._doors = doors
return self

def set_type(self, car_type):
self._type = car_type
return self

def build(self):
return Car(self._wheels, self._doors, self._type)

# Car class
class Car:
def __init__(self, wheels, doors, car_type):
self.wheels = wheels
self.doors = doors
self.type = car_type

def __str__(self):
return f"{self.type} car with {self.wheels} wheels and {self.doors} doors."

# Client code
if __name__ == "__main__":
car_builder = CarBuilder()
car = car_builder.set_type("sedan").set_wheels(4).set_doors(4).build()
print(car)

In this example, the Builder class CarBuilder is responsible for building the Car object. The CarBuilder class includes methods for specifying each part of the Car object, as well as a method for returning the final object. The Car class is a simple class that includes the number of wheels, the number of doors, and the type of car. The client code uses the CarBuilder to create a Car object with 4 wheels, 4 doors, and a type of “sedan”.

Advantages of the Builder design pattern:

  1. Separation of Concerns: The Builder pattern separates the construction of a complex object from its representation, making it easier to modify and extend the object’s behavior.
  2. Improved Flexibility: The Builder pattern provides improved flexibility, as you can easily change the construction process or add new steps to the construction process.
  3. Improved Reusability: The Builder pattern can improve reusability, as you can reuse the same construction process for different types of objects.
  4. Improved Readability: The Builder pattern can improve the readability of code, as it encapsulates the construction process in a separate class, making it easier to understand the behavior of an object.
  5. Simplified Code: The Builder pattern can simplify code by reducing the amount of conditional logic required to construct complex objects, as the construction process is handled by the Builder.

Disadvantages of the Builder design pattern:

  1. Increased Complexity: The Builder pattern can add complexity to an application, as it requires creating multiple classes and objects to manage the construction of complex objects.
  2. Overhead: The Builder pattern can add overhead to an application, especially if the construction process is complex or involves multiple steps.
  3. Debugging: Debugging a Builder can be challenging, as it requires understanding the relationships between the builder, the product, and the director.

The Builder pattern is useful for creating objects that have many optional parts or components. It provides a way to specify which parts should be included in the final object, making it easier to create objects with only the parts that are needed. Additionally, it makes it easier to change the representation of the object without affecting the client code, as the client code only needs to call the methods for specifying each part and then call the method for returning the final object.

Factory Method Design Pattern

The Factory Method is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. The Factory Method pattern is often used when a class can’t anticipate the type of objects it needs to create, or when a class wants its subclasses to specify the objects it creates.

In Python, the Factory Method can be implemented using a method that returns an object. This method can be overridden in subclasses to create objects of a different type. The method in the superclass should be called in the client code, which will return an object of the appropriate type.

Here’s an example of how the Factory Method pattern can be implemented in Python:

# Creator class
class Creator:
def factory_method(self):
pass

def some_operation(self):
product = self.factory_method()
result = f"Creator: The same creator's code has just worked with {product.operation()}"
return result

# ConcreteCreator class
class ConcreteCreator(Creator):
def factory_method(self):
return ConcreteProduct()

# Product class
class Product:
def operation(self):
pass

# ConcreteProduct class
class ConcreteProduct(Product):
def operation(self):
return "{Result of the ConcreteProduct1}"

# Client code
if __name__ == "__main__":
concrete_creator = ConcreteCreator()
result = concrete_creator.some_operation()
print(result)

In this example, the Creator class is the superclass that provides an interface for creating objects. The ConcreteCreator class is a subclass that overrides the factory_method to create ConcreteProduct objects. The Product class is an abstract class that defines the interface for objects that the Factory Method creates. The ConcreteProduct class is a concrete implementation of the Product class that returns a string with the result of its operation.

The client code uses the ConcreteCreator to create a ConcreteProduct object, which is returned by the factory_method. The some_operation method in the Creator class is called, which uses the ConcreteProduct object to perform some operation. The result of the operation is returned to the client code, which is printed to the console.

Advantages of the Factory Method design pattern:

  1. Abstraction: The Factory Method design pattern provides an abstract interface for creating objects, which makes it easier to modify and extend the behavior of an application.
  2. Improved Encapsulation: The Factory Method design pattern improves encapsulation, as it hides the implementation details of object creation from the client, making it easier to change the implementation without affecting the client.
  3. Improved Flexibility: The Factory Method design pattern provides improved flexibility, as you can easily change the implementation of an object’s behavior by creating a new factory method or subclass.
  4. Improved Reusability: The Factory Method design pattern can improve reusability, as you can reuse the same factory method to create objects of different types.
  5. Improved Readability: The Factory Method design pattern can improve the readability of code, as it encapsulates the implementation details of object creation in a separate class, making it easier to understand the behavior of an object.

Disadvantages of the Factory Method design pattern:

  1. Increased Complexity: The Factory Method design pattern can add complexity to an application, as it requires creating multiple classes and objects to manage the creation of objects.
  2. Overhead: The Factory Method design pattern can add overhead to an application, especially if the creation process is complex or involves multiple steps.
  3. Debugging: Debugging a Factory Method can be challenging, as it requires understanding the relationships between the factory method, the product, and the client.

The Factory Method pattern is useful when a class can’t anticipate the type of objects it needs to create, or when a class wants its subclasses to specify the objects it creates. It provides a way to create objects without specifying the exact type of object that will be created, making it easier to change the type of object that is created without affecting the client code. Additionally, it allows for the creation of objects that have a common interface, making it easier to use the objects in a consistent way.

If you like the post, don’t forget to clap. If you’d like to connect, you can find me on LinkedIn.

References:

Book “Design Patterns: Elements of Reusable Object-Oriented Software”

Leave a Reply

Your email address will not be published.