Python Design Patterns Cookbook: Recipes for Clean and Reusable Code (Observer)

Python Design Patterns Cookbook: Recipes for Clean and Reusable Code (Observer)

Streamlining Complex Systems: A Guide to the Facade Design Pattern in Software Development

Design Patterns: An Introduction and Explanation

Design patterns are like recipes that software developers use to solve common problems that come up when building complex software. Like how different chefs might use slightly different ingredients or techniques to make the same dish, developers can use different approaches to implement the same design pattern. Design patterns are not strict rules that apply to every situation, but rather a blueprint that can be customized to fit the specific problem at hand. They can be used with any programming language and it's important to choose the correct design pattern for each situation.

Observer Design Pattern

Introduction

The Observer design pattern is a behavioral pattern that enables objects to establish a one-to-many dependency, where multiple observers are notified automatically when the state of a subject object changes. This pattern promotes loose coupling between the subject (also known as the observable) and the observers, allowing them to interact without having explicit knowledge of each other.

Issue You May Face

As a developer, you have been assigned to implement a key functionality in a Trello-like application that involves tracking the status changes of tasks. The main objective is to design a system that allows users to efficiently monitor the progress of tasks, record the history of status changes, and update the respective boards automatically.

Your responsibility is to create robust software that ensures users have a clear and transparent view of task statuses, enabling them to track the progress of their projects effectively. It is crucial to develop a solution that abstracts the complexities of status management, providing users with a seamless experience while maintaining a comprehensive history of status changes and board updates.

To achieve this, you can design a solution with the following components:

  1. Task Status Management:

    • Implement a database table to store task information, including the current status.

    • Provide an intuitive interface for users to change the status of tasks, such as moving them between "To-Do," "In Progress," "Testing," "Deployed," and other relevant statuses.

  2. History Tracking:

    • Create a separate database table to store the history of status changes.

    • Whenever a user modifies the status of a task, record the timestamp, the user who made the change, the previous status, and the new status in the history table.

  3. Board Management:

    • Develop a mechanism to update the board automatically whenever a task's status changes.

    • Implement logic to determine the appropriate board for a task based on its current status.

    • Whenever a status change occurs, update the corresponding board by adding or removing the task accordingly.

By implementing these components, you can provide users with an efficient and transparent workflow management system. Users can easily change task statuses, while the application automatically records the history of status changes and updates the respective boards.

It's important to consider security measures, such as authentication and authorization, to ensure that only authorized users can modify task statuses and access the history and board management functionalities.

Additionally, you can enhance the functionality by implementing features like notifications or alerts to keep users informed about status changes or board updates.

Remember to thoroughly test the implementation to ensure the smooth functioning of the application and provide a seamless user experience.

A Logical Solution

To efficiently track status changes in a Trello-like application, we can apply the Observer pattern. This pattern involves establishing a relationship between a subject, representing a task, and its observers, such as board updaters and status history trackers. The subject class manages tasks, allowing for task creation and status updates. Meanwhile, the observer classes receive notifications from the subject whenever a status change occurs, enabling them to perform specific actions like updating the board or tracking status history. By employing the Observer pattern, we ensure a clear and transparent view of task statuses, empowering users to monitor project progress effectively while maintaining a comprehensive history of status changes.

By implementing the Observer pattern, we enhance the functionality of a Trello-like application. The subject and observer classes work in tandem to facilitate seamless tracking of task status changes. Users can easily create tasks and modify their statuses, while the application automatically notifies the relevant observers. The board updater observer ensures that the task's status reflects accurately on the board, while the status history tracker maintains a comprehensive record of status changes. This approach simplifies the complexities of status management, providing users with a streamlined and transparent workflow management system.

Using Observer Design Pattern

Define the abstract base class TaskSubject using the ABC module. This class will serve as the interface for the subject. Inside this class, declare three abstract methods: attach, detach, and notify_observers. These methods will be implemented by the concrete subject class.

After that, create the concrete subject class called Task, which inherits from TaskSubject. In the initializer method, define an empty list observers to store the observers. Implement the attach, detach, and notify_observers methods, which will manage the attachment, detachment, and notification of observers, respectively. Additionally, implement the create and change_status methods to create a new task and update the status of an existing task, respectively.

Next, define the abstract base class Observer using the ABC module. Inside this class, declare one abstract method: update. This method will be implemented by the concrete observer classes.

Now, create the concrete observer classes: BoardUpdater and StatusHistoryTracker. Both classes inherit from the Observer class. Implement the update method in each class to define the behavior when an update is received from the subject. The BoardUpdater class updates the board based on the task's status, while the StatusHistoryTracker class tracks the history of status changes for each task.

Finally, in the main section, create an instance of the Task class and instances of the concrete observer classes. Attach the observer instances to the task using the attach method. Simulate a status change in the task by calling the create method with a sample task and then calling the change_status method. Print the state of the tasks, status history, and boards to observe the changes.

Code Example

from abc import ABC, abstractmethod
from datetime import datetime, timedelta
import time
import json

# Subject interface
class TaskSubject(ABC):
    @abstractmethod
    def attach(self, observer):
        pass

    @abstractmethod
    def detach(self, observer):
        pass

    @abstractmethod
    def notify_observers(self, task, user):
        pass

tasks = []
status_history = []
boards = {
    'To-Do': [],
    'In-Progress': []
}
# Concrete subject
class Task(TaskSubject):
    def __init__(self):
        self.observers = []

    def attach(self, observer):
        self.observers.append(observer)

    def detach(self, observer):
        self.observers.remove(observer)

    def notify_observers(self, task, user_id):
        for observer in self.observers:
            observer.update(task, user_id)

    def create(self, task, user_id):
        task['id'] = len(tasks) + 1
        task['created_by'] = user_id
        task['created_at'] = datetime.now()
        tasks.append(task)
        self.notify_observers(task, user_id)

    def change_status(self, task_id, new_status, user_id):
        for task in tasks:
            if task['id'] == task_id:
                task['status'] = new_status
                task['updated_by'] = user_id
                task['updated_at'] = datetime.now()
                self.notify_observers(task, user_id)
                break

# Observer interface
class Observer(ABC):
    @abstractmethod
    def update(self, task, user):
        pass

# Concrete observer for board update
class BoardUpdater(Observer):
    def update(self, task, user):
        self.update_board(task, user)

    def update_board(self, task, user):
        prev_status = None
        for obj in status_history:
            if obj['task_id'] == task['id']:
                prev_status = obj['history'][-1]['status']
                break
        if prev_status and task['id'] in boards[prev_status]:
            boards[prev_status].remove(task['id'])
        boards[task['status']].append(task['id'])
        # Code to update the board with the task's new status

# Concrete observer for status history tracking
class StatusHistoryTracker(Observer):
    def update(self, task, user_id):
        self.track_history(task, user_id)

    def track_history(self, task, user_id):
        history = None
        data = {
            'status': task['status'],
            'updated_by': user_id,
            'updated_at': datetime.now()
        }
        for obj in status_history:
            if obj['task_id'] == task['id']:
                obj['history'].append(data)
                history = obj
                break 
        if not history:
            history = {
                'task_id': task['id'],
                'history': [data]
            }
            status_history.append(history)
        # Code to store the history entry in the database or file

# Usage example
if __name__ == "__main__":
    # Create the task and observers
    task = Task()
    board_updater = BoardUpdater()
    history_tracker = StatusHistoryTracker()

    # Attach the observers to the task
    task.attach(board_updater)
    task.attach(history_tracker)

    # Simulate a status change in the task
    task.create({
        'task_name': 'Sample Task',
        'status': 'To-Do',
        'description': 'This is a sample task.'
    }, 'user123')
    print('After Create')
    print(json.dumps(tasks, indent=4, default=str))
    print(json.dumps(status_history, indent=4, default=str))
    print(json.dumps(boards, indent=4, default=str), end='\n\n\n')    
    time.sleep(1)
    task.change_status(1, "In-Progress", 'user123')
    print('After Update')
    print(json.dumps(tasks, indent=4, default=str))
    print(json.dumps(status_history, indent=4, default=str))
    print(json.dumps(boards, indent=4, default=str), end='\n\n\n')

Why Use Observer Design Pattern Instead of Event-Driven Architecture

The choice between using the Observer design pattern and an Event-Driven Architecture (EDA) depends on the specific requirements and characteristics of the system you are building. Here are some reasons why you might choose the Observer design pattern over an EDA:

  1. Simplicity: The Observer design pattern provides a simpler and more straightforward implementation compared to an EDA. It involves defining a clear relationship between the subject and its observers, making it easier to understand and maintain the code.

  2. Tight coupling: In the Observer design pattern, the subject and observers have a direct relationship and are tightly coupled. This can be beneficial in situations where there is a strong dependency between the subject and observers, and you want to ensure that updates are immediately propagated to the observers.

  3. Centralized control: With the Observer design pattern, the subject has control over the notification process. It decides when and how to notify the observers, providing a centralized point of control. This can be useful when you need to enforce specific rules or logic regarding the update process.

  4. Synchronous communication: The Observer design pattern typically involves synchronous communication between the subject and observers. When an update occurs, the subject directly calls the update method on each observer. This synchronous communication can be advantageous in scenarios where immediate response and processing are required.

Conclusion

In conclusion, both the Observer design pattern and Event-Driven Architecture have their merits and are valuable tools in different scenarios.

The Observer design pattern provides a simple and straightforward approach for establishing a direct relationship between a subject and its observers. It is beneficial when you require tight coupling, centralized control, and synchronous communication between the subject and observers. This pattern is particularly useful for smaller systems or situations where immediate response and updates are crucial.

On the other hand, an Event-Driven Architecture offers a more flexible and scalable solution with loose coupling, decentralized control, and asynchronous communication. It is well-suited for complex systems where components need to operate independently, handle a large number of events, and exhibit responsiveness and scalability.

When choosing between the Observer design pattern and an Event-Driven Architecture, it's important to consider the specific requirements, coupling needs, control over updates, and nature of communication in your system. Both approaches have their strengths and can be utilized effectively based on the characteristics of your project. Understanding these patterns will enable you to make informed decisions and design robust software architectures.