Understanding The Gang of Four (GOF) design patterns using Python — Part 2
This is the 11th post in a series of learning the Python programming language.
Prototype Design Pattern
The prototype pattern is a creational design pattern that lets you create objects by cloning existing objects, rather than creating new objects from scratch. This is especially useful in cases where creating a new object from scratch can be time-consuming, such as when the object requires complex initialization or setup. In this pattern, a partially or fully initialized object serves as a model, and new objects are created by copying this model.
Here’s an example implementation of the prototype pattern in Python:
import copy
class Prototype:
def __init__(self):
self._objects = {}
def register_object(self, name, obj):
"""Registers an object"""
self._objects[name] = obj
def unregister_object(self, name):
"""Unregisters an object"""
del self._objects[name]
def clone(self, name, **attr):
"""Clones a registered object and updates inner attributes dictionary"""
obj = copy.deepcopy(self._objects.get(name))
obj.__dict__.update(attr)
return obj
class Car:
def __init__(self):
self.name = "Skylark"
self.color = "Red"
self.options = "Ex"
def __str__(self):
return '{} | {} | {}'.format(self.name, self.color, self.options)
c = Car()
prototype = Prototype()
prototype.register_object("skylark", c)
c1 = prototype.clone("skylark")
print(c1)
In this example, we define a Prototype
class that acts as a registry of objects that can be cloned. The register_object
method allows us to register an object with a name, and the clone
method allows us to clone a registered object and update its attributes.
We also define a Car
class to serve as the prototype object. In the example, we create an instance of the Car
class and register it with the Prototype
registry. Then, we clone the registered Car
object and print it. The output of the code will be Skylark | Red | Ex
, which shows that the clone is an exact copy of the original object.
Advantages of Prototype Design Pattern:
- Improves efficiency: The Prototype Design Pattern provides a convenient way to create objects without the need for detailed knowledge of their class definitions. This makes the process of object creation more efficient, especially when creating complex objects.
- Easy to modify and extend: Prototype Design Pattern is open to modification and extension, which means that the objects can be easily altered and added to without affecting the existing code. This makes it an ideal solution for systems that are constantly evolving and need to be updated regularly.
- Reduces the number of subclasses: The Prototype Design Pattern helps to reduce the number of subclasses required in an application. This makes the code easier to maintain, and reduces the risk of bugs and errors.
- Reusability: The Prototype Design Pattern allows for the reuse of objects, as well as their associated properties and methods, which results in improved code efficiency and reduced duplication.
Disadvantages of Prototype Design Pattern:
- Increased complexity: Prototype Design Pattern can make the code more complex and harder to understand, especially for developers who are not familiar with its implementation.
- Performance issues: The process of cloning objects can be resource-intensive and can result in performance issues, especially when working with large objects.
- Increased memory usage: The Prototype Design Pattern requires a significant amount of memory to store the objects that are being cloned. This can lead to increased memory usage and decreased performance.
- Lack of control over object creation: Prototype Design Pattern can result in the creation of objects that are not suitable for a particular use case, as it does not provide any control over the object creation process.
The prototype pattern allows us to create new objects by cloning existing objects, saving time and resources compared to creating new objects from scratch. This can be especially useful in situations where object initialization is complex or time-consuming.
Singleton Design Pattern
Singleton is a design pattern that restricts a class to have only one instance and provides a single point of access to it for any other code. It is used when exactly one object is needed to control the action throughout the execution.
Here’s an example of how to implement the Singleton pattern in Python:
class Singleton:
__instance = None
@staticmethod
def getInstance():
""" Static access method. """
if Singleton.__instance == None:
Singleton()
return Singleton.__instance
def __init__(self):
""" Virtually private constructor. """
if Singleton.__instance != None:
raise Exception("This class is a singleton!")
else:
Singleton.__instance = self
The getInstance
method is used to get the only object available and it creates the object if it is not created. The constructor is defined as private so that the class can’t be instantiated directly. The class only creates an instance if it doesn’t already exist.
Here’s an example of how to use the Singleton class:
s = Singleton.getInstance()
print(s)
s1 = Singleton.getInstance()
print(s1)
The output of the above code will be:
<__main__.Singleton object at 0x7ffff75c03d0>
<__main__.Singleton object at 0x7ffff75c03d0>
As you can see, the two instances of the Singleton class are the same instance.
Advantages of Singleton Design Pattern:
- Control Over Object Creation: The Singleton pattern ensures that only one instance of an object is created, providing complete control over the number of objects in the system.
- Improved Resource Utilization: By having only one instance of an object, the Singleton pattern ensures that resources are utilized efficiently, reducing memory overhead.
- Global Point of Access: Singleton objects provide a single, global point of access to an object, making it easier to access and manipulate the object from any part of the code.
- Easy to Implement: The Singleton pattern is easy to implement and can be easily integrated into existing code.
Disadvantages of Singleton Design Pattern:
- Hard to Test: Singleton objects are difficult to test because they cannot be instantiated multiple times. This makes it difficult to test different scenarios and edge cases.
- Global State: Singleton objects create a global state, which can lead to tight coupling between different parts of the code. This can make the code difficult to maintain and less flexible.
- Limited Flexibility: The Singleton pattern limits the flexibility of the code by restricting the number of instances of an object. This can make it difficult to adapt the code to changing requirements.
- Overuse: Overuse of the Singleton pattern can lead to complex and tightly coupled code, making it harder to understand and maintain.
In Python, it’s important to keep in mind that the global interpreter lock (GIL) makes sure that only one thread can execute Python bytecode at once. This means that you don’t need to worry about thread safety when implementing the Singleton pattern in Python, as the GIL will make sure that the singleton instance is thread-safe.
Adapter Design Pattern
The adapter design pattern is a structural pattern that allows objects with incompatible interfaces to collaborate. It provides a simple way to create a class that converts the interface of one class into another.
In Python, we can implement an adapter by using inheritance or by using a wrapper class. Here’s an example of the adapter pattern using inheritance:
class Square:
def __init__(self, side):
self.side = side
def get_area(self):
return self.side * self.side
class Circle:
def __init__(self, radius):
self.radius = radius
def get_area(self):
return 3.14 * self.radius * self.radius
class SquareToCircleAdapter(Square, Circle):
def __init__(self, square):
Square.__init__(self, square.side)
self.radius = square.side * (2 ** 0.5) / 2
square = Square(10)
adapter = SquareToCircleAdapter(square)
print("Circle area: ", adapter.get_area())
In this example, Square
and Circle
are two classes with incompatible interfaces, but the SquareToCircleAdapter
class acts as a bridge between them. The SquareToCircleAdapter
class takes a Square
object as an argument and converts its interface to that of a Circle
class by initializing the Square
class and computing the radius
attribute.
This is just one way to implement the adapter pattern in Python. Another way is to use a wrapper class that wraps the object that needs to be adapted and implements the target interface.
Here’s an example of the adapter pattern using a wrapper class in Python:
class Square:
def __init__(self, side):
self.side = side
def get_area(self):
return self.side * self.side
class Circle:
def __init__(self, radius):
self.radius = radius
def get_area(self):
return 3.14 * self.radius * self.radius
class SquareToCircleAdapter:
def __init__(self, square):
self.square = square
def get_area(self):
return 3.14 * (self.square.side * (2 ** 0.5) / 2) ** 2
square = Square(10)
adapter = SquareToCircleAdapter(square)
print("Circle area: ", adapter.get_area())
In this example, SquareToCircleAdapter
is a wrapper class that implements the target interface, Circle
. The SquareToCircleAdapter
takes a Square
object as an argument, wraps it and converts its interface to that of a Circle
class by implementing the get_area
method.
Advantages of the Adapter Design Pattern:
- Loose coupling: The adapter pattern helps to decouple the client code from the adaptee code, which results in loose coupling between components.
- Reusability: The adapter pattern enables reusability of existing code by providing a way to connect it to new code.
- Flexibility: The adapter pattern provides a flexible way to extend the functionality of the adaptee by connecting it to the client code.
- Improved performance: In some cases, the adapter pattern can help improve the performance of the system by reducing the number of objects created and by reducing the amount of code that needs to be executed.
Disadvantages of the Adapter Design Pattern:
- Increased complexity: The adapter pattern can increase the complexity of the system, especially if there are multiple adapters used in the system.
- Overhead: The adapter pattern can introduce some overhead in the system, especially if it involves creating multiple objects or converting data between different formats.
- Difficult to debug: Debugging a system that uses the adapter pattern can be more difficult, especially if there are multiple adapters used in the system.
- Violation of the single responsibility principle: In some cases, the adapter pattern can violate the single responsibility principle by adding additional functionality to an object that was not designed for it.
Adapter pattern is useful in situations where we have existing classes with incompatible interfaces, but we need to use them together. The adapter pattern helps to reduce complexity by keeping the existing code intact, and only changing the interface to match the requirements.
Bridge Design Pattern
The Bridge design pattern is a structural pattern that decouples an abstraction from its implementation so that the two can evolve independently. It provides a way to create a bridge between the abstraction and implementation classes and to hide the implementation details from the client.
In Python, we can implement the Bridge design pattern by creating two separate class hierarchies: one for the abstraction and another for the implementation. Here’s an example of the Bridge pattern in Python:
class DrawingAPI:
def draw_circle(self, x, y, radius):
pass
class DrawingAPI1(DrawingAPI):
def draw_circle(self, x, y, radius):
print("API1.circle at {}:{} radius {}".format(x, y, radius))
class DrawingAPI2(DrawingAPI):
def draw_circle(self, x, y, radius):
print("API2.circle at {}:{} radius {}".format(x, y, radius))
class CircleShape:
def __init__(self, x, y, radius, drawing_api):
self._x = x
self._y = y
self._radius = radius
self._drawing_api = drawing_api
def draw(self):
self._drawing_api.draw_circle(self._x, self._y, self._radius)
def scale(self, pct):
self._radius *= pct
circle1 = CircleShape(1, 2, 3, DrawingAPI1())
circle1.draw()
circle2 = CircleShape(2, 3, 4, DrawingAPI2())
circle2.draw()
In this example, DrawingAPI
is an interface that defines the methods that the concrete implementation classes, DrawingAPI1
and DrawingAPI2
, need to implement. The CircleShape
class is the abstraction and it holds a reference to an instance of the DrawingAPI
interface. The CircleShape
class can use this reference to draw a circle using the concrete implementation of DrawingAPI
.
The advantage of using the Bridge pattern is that it decouples the abstraction and implementation so that they can be changed independently. For example, if we need to add a new drawing API, we can do so without changing the CircleShape
class. Similarly, if we need to change the CircleShape
class, we can do so without changing the DrawingAPI
classes.
Advantages of Bridge Design Pattern:
- Abstraction and Implementation Separation: The Bridge pattern allows separation of the abstractions from the implementations. This decouples the classes and makes them more flexible and maintainable.
- Reusability: The abstractions and implementations can be reused independently, making it easier to change the implementations without affecting the rest of the code.
- Improved Extensibility: By separating the abstractions and implementations, the Bridge pattern makes it easier to extend the classes.
- Reduced Complexity: The Bridge pattern reduces the complexity of the code by breaking down complex systems into smaller, more manageable components.
- Increased Flexibility: The Bridge pattern provides greater flexibility compared to traditional inheritance-based approaches.
Disadvantages of Bridge Design Pattern:
- Increased Complexity: The Bridge pattern introduces an additional level of abstraction, making the code more complex and harder to understand.
- Overhead: The Bridge pattern requires extra effort in designing and implementing the abstractions and implementations, which can result in increased overhead.
- Reduced Performance: The additional layer of abstraction in the Bridge pattern can result in reduced performance compared to traditional inheritance-based approaches.
- Increased Testing Effort: The Bridge pattern requires additional testing effort, as both the abstractions and implementations must be tested separately.
The Bridge pattern is useful when we need to develop a flexible and maintainable system, where the abstraction and implementation can be changed independently. It also provides a way to encapsulate the implementation details, making it easier to understand and maintain the system.
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”