CodeDesign PatternsPython

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

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

Composite Design Pattern

The Composite design pattern is a structural pattern that allows us to treat individual objects and compositions of objects in a uniform way. It allows us to create a tree-like structure of objects, where each object can be either a leaf node or a composite node.

In Python, we can implement the Composite pattern by defining an abstract Component class that defines the common interface for both leaf nodes and composite nodes. Here’s an example of the Composite pattern in Python:

class Component:
def __init__(self, name):
self._name = name

def operation(self):
pass

def add(self, component):
pass

def remove(self, component):
pass

def get_child(self, index):
pass

class Leaf(Component):
def operation(self):
print("Leaf {}".format(self._name))

class Composite(Component):
def __init__(self, name):
super().__init__(name)
self._children = []

def operation(self):
print("Composite {}".format(self._name))
for child in self._children:
child.operation()

def add(self, component):
self._children.append(component)

def remove(self, component):
self._children.remove(component)

def get_child(self, index):
return self._children[index]

composite = Composite("composite")
composite.add(Leaf("leaf1"))
composite.add(Leaf("leaf2"))
composite.operation()

Output:

Composite composite
Leaf leaf1
Leaf leaf2

In this example, Leaf and Composite classes inherit from the Component class and implement the operation method. The Leaf class represents the leaf node in the tree structure and it contains the leaf-specific behavior. The Composite class represents the composite node in the tree structure and it contains the composite-specific behavior.

The add and remove methods allow us to add or remove a Component object to or from a Composite object. The get_child method returns a child Component object at a given index.

Advantages of the Composite Design Pattern:

  1. Simplifies the code: The Composite Design Pattern makes it easier to build complex tree structures and manipulate them as a single object.
  2. Increases Flexibility: By breaking down complex objects into smaller components, it becomes easier to modify and extend the code without having to make significant changes to the existing code.
  3. Improved Reusability: Components can be reused in different parts of the application, making it easier to maintain and update the code.
  4. Better Abstraction: The Composite Design Pattern provides a good abstraction, making it easier to understand the code and maintain it.

Disadvantages of the Composite Design Pattern:

  1. Increased Complexity: While the Composite Design Pattern simplifies the code in some ways, it can also add complexity to the code if not implemented correctly.
  2. Performance Overhead: The Composite Design Pattern requires a lot of object instantiation, which can slow down the performance of the application.
  3. Increased Memory Usage: The Composite Design Pattern requires more memory to store the objects and their relationships, which can negatively impact the performance of the application.
  4. Limited Functionality: The Composite Design Pattern is limited in terms of the operations it can perform, making it less suitable for complex applications with complex requirements.

The advantage of using the Composite pattern is that it makes it easier to work with a tree-like structure of objects. It allows us to treat individual objects and compositions of objects in a uniform way and it provides a way to encapsulate the implementation details, making it easier to understand and maintain the system.

Decorator Design Pattern

The decorator pattern is a design pattern in Object-Oriented Programming (OOP) that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. The decorator pattern is often useful for adhering to the Single Responsibility Principle, as you can use a decorator to add a specific action to a single method, without changing the class implementation.

In Python, the decorator pattern can be implemented by using the @ symbol, called the “decorator”, to modify the behavior of a function or class method. Decorators in Python are essentially just wrapper functions that modify the behavior of the code they decorate.

Here’s an example to demonstrate the usage of decorators in Python:

def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

def say_hello():
print("Hello!")

say_hello = my_decorator(say_hello)

say_hello()

Output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

We can simplify the code by using the @ symbol, like this:

def my_decorator(func):
def wrapper():
print("Something is happening before the function is called.")
func()
print("Something is happening after the function is called.")
return wrapper

@my_decorator
def say_hello():
print("Hello!")

say_hello()

Output:

Something is happening before the function is called.
Hello!
Something is happening after the function is called.

In this example, the my_decorator function is used to wrap the behavior of the say_hello function. The decorated say_hello function now has the behavior specified by the wrapper function in my_decorator, which adds additional behavior before and after calling the original say_hello function.

Advantages of the Decorator Design Pattern:

  1. Increased Flexibility: The Decorator Design Pattern allows you to add new functionalities to objects dynamically at runtime.
  2. Open-Closed Principle: The pattern helps to adhere to the Open-Closed Principle, which states that objects should be open for extension but closed for modification.
  3. Reusable Code: The Decorator Design Pattern helps you to reuse the existing code instead of rewriting it.
  4. Easy Maintenance: The design pattern makes it easy to maintain the code as you can add or remove functionalities dynamically.
  5. Better Organization: The design pattern makes it easier to organize the code, especially when the number of functionalities increases.

Disadvantages of the Decorator Design Pattern:

  1. Increased Complexity: The Decorator Design Pattern can increase the complexity of the code, especially when there are many functionalities that need to be added.
  2. Performance Overhead: The design pattern can add an overhead to the performance of the code as the additional functionalities add to the processing time.
  3. Confusing Structure: The design pattern can create a confusing structure, especially when there are many nested decorators.
  4. Limited Functionality: The design pattern is limited in terms of the functionalities it can add. In some cases, you might have to use another design pattern to achieve the desired result.

The decorator pattern is a powerful tool for adding behavior to objects in a modular and dynamic manner, and it can be easily implemented in Python through the use of decorator functions and the @ symbol.

Facade Design Pattern

The Facade design pattern is a structural pattern that provides a simplified interface to a complex system of classes, libraries, or APIs. The Facade design pattern allows you to present a unified interface to a set of interfaces in a subsystem, which makes the subsystem easier to use. The Facade pattern makes it easier to work with a complex system because it reduces the number of objects that a client has to interact with, and it also reduces the dependencies between the client and the subsystem.

In Python, you can implement the Facade pattern by creating a class that acts as an interface to the underlying subsystem. This class acts as a “facade” that provides a simplified interface to the underlying system. Here’s an example:

class Engine:
def start(self):
print("Engine started")

class Fuel:
def pump(self):
print("Fuel pumped")

class Ignition:
def turn_on(self):
print("Ignition turned on")

class Car:
def __init__(self):
self._engine = Engine()
self._fuel = Fuel()
self._ignition = Ignition()

def start(self):
self._fuel.pump()
self._ignition.turn_on()
self._engine.start()

car = Car()
car.start()

Output:

Fuel pumped
Ignition turned on
Engine started

In this example, the Car class acts as a facade to the underlying subsystem of classes, EngineFuel, and Ignition. The Car class provides a simplified interface to start the car, which calls the methods of the underlying classes in the correct order. This makes it easier to use the complex system of classes, and it also reduces the dependencies between the client code and the underlying subsystem.

Advantages of the Facade Design Pattern:

  1. Simplifies the interface: The facade design pattern provides a simple and unified interface for a complex system, which makes it easier to use and understand.
  2. Hides complexity: The facade design pattern hides the complexity of the underlying system, making it easier for the client to use.
  3. Loose coupling: The facade design pattern promotes loose coupling between the client and the underlying system, making it easier to make changes to the system without affecting the client.
  4. Easy to maintain: The facade design pattern makes it easier to maintain the underlying system because it reduces the amount of code that needs to be changed when making changes to the system.

Disadvantages of the Facade Design Pattern:

  1. Increased coupling: The facade design pattern may increase the coupling between the facade and the underlying system, making it more difficult to make changes to the system without affecting the facade.
  2. Overuse of facades: Overuse of facades can lead to increased complexity and a loss of visibility into the underlying system, making it harder to understand and maintain.
  3. Limited functionality: The facade design pattern may limit the functionality of the underlying system by only exposing a subset of its functionality through the facade.
  4. Performance: The use of facades can impact performance because it adds an extra layer of abstraction and indirection to the system.

The Facade design pattern provides a simplified interface to a complex system, which makes it easier to use and reduces dependencies between the client code and the underlying system. The Facade pattern can be easily implemented in Python by creating a class that acts as a facade to the underlying subsystem.

Flyweight Design Pattern

The Flyweight design pattern is a structural pattern that enables you to create a large number of similar objects efficiently by reusing a shared portion of their state, while allowing the unique portion of their state to vary. The Flyweight pattern is useful for reducing memory usage and improving performance when dealing with a large number of similar objects.

In Python, you can implement the Flyweight pattern by defining a class to represent the shared portion of the object state and a factory method to manage the shared objects. The factory method maintains a pool of shared objects and returns a reference to an existing object from the pool if it exists, or creates a new object if it doesn’t. Here’s an example:

class Flyweight:
def __init__(self, shared_state):
self._shared_state = shared_state

def operation(self, unique_state):
s = str(self._shared_state)
u = str(unique_state)
print("Flyweight: Displaying shared ({0}) and unique ({1}) state.".format(s, u))

class FlyweightFactory:
_flyweights = {}

def get_flyweight(self, shared_state):
if shared_state not in self._flyweights:
print("FlyweightFactory: Can't find a flyweight, creating new one.")
self._flyweights[shared_state] = Flyweight(shared_state)
else:
print("FlyweightFactory: Reusing existing flyweight.")
return self._flyweights[shared_state]

def add_flyweight(shared_state):
flyweight = factory.get_flyweight(shared_state)
flyweight.operation(unique_state[shared_state])

if __name__ == "__main__":
factory = FlyweightFactory()
unique_state = {}
unique_state["State1"] = 10
unique_state["State2"] = 20
add_flyweight("State1")
add_flyweight("State1")
add_flyweight("State2")
add_flyweight("State2")
print("\n")
print("FlyweightFactory: I have {0} flyweights:".format(len(factory._flyweights)))
for key in factory._flyweights.keys():
print(key)

Output:

FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (State1) and unique (10) state.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (State1) and unique (10) state.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (State2) and unique (20) state.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (State2) and unique (20) state.

FlyweightFactory: I have 2 flyweights:
State1
State2

In this example, the Flyweight class represents the shared portion of the object state, while the FlyweightFactory class manages the shared objects. The add_flyweight function creates or reuses a Flyweight object, depending on whether the shared state already exists in the factory’s pool of flyweights.

Advantages of Flyweight Design Pattern:

  1. Memory Optimization: The Flyweight design pattern is useful for optimizing memory usage. It reduces the amount of memory needed to store objects by reusing objects instead of creating new ones.
  2. Performance Improvement: The Flyweight design pattern can lead to significant performance improvements as it reduces the number of objects created and reduces the time required to initialize them.
  3. Simplification: The Flyweight design pattern simplifies the code by separating the intrinsic and extrinsic state of an object, making it easier to manage and maintain.

Disadvantages of Flyweight Design Pattern:

  1. Increased Complexity: The Flyweight design pattern increases the complexity of the code, making it harder to understand and maintain.
  2. Limitations on Sharing: There may be limitations on how objects can be shared, which can affect the effectiveness of the Flyweight design pattern.
  3. Difficulty in Debugging: The Flyweight design pattern can make it difficult to debug code as it requires tracking multiple objects with similar states.
  4. Risk of Performance Decrease: If the Flyweight design pattern is not implemented correctly, it can lead to a decrease in performance as the overhead of managing shared objects can become significant.

The Flyweight pattern is useful for reducing memory usage and improving performance when dealing with a large number of similar objects. By sharing the common state between objects, the Flyweight pattern helps to reduce memory consumption and provides an efficient way to manage similar objects. It’s important to note that the Flyweight pattern is most effective when the shared state is larger than the unique state, and the number of objects created is large.

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.