Essential Design Patterns -An Easy Explanation through Real-World Examples - Part 2: Observer and Decorator

Welcome! to the second part of the series on Object-Oriented Design Patterns through Python. Read the first part here. In this post, we will discuss the next two important design patterns: the Observer pattern and the Decorator pattern*.*

Observer Design Pattern

The Observer design pattern is classified as a behavioral design pattern that focuses on defining the communication pattern between objects. It allows one or more objects to be notified when the state of another object changes. It is often used in situations where multiple objects are interested in being notified of changes to a particular object.

Before delving into the implementation details of the Observer design pattern, let's first consider a real-world use case that demonstrates its practical application.

Use Case

Consider an online news application(subject) that allows users(observers) to subscribe to news categories such as sports, politics, business, and entertainment. Whenever a news article is published in any of these categories, subscribers who have opted to receive updates for that category should be notified.

To implement this functionality, we can use the Observer design pattern, where the online news application serves as the subject or publisher object, and the user-subscribed news categories serve as observer objects. Whenever a new article is published, the online news application notifies all the observer objects subscribed to that category, and they receive the update.

Benefits

  • Provides a way to react to events happening in other objects without coupling(loose coupling) to their classes. Meaning one class should be aware of changes in another class. In our example this pattern allows us to decouple the online news application from its subscribers, making it easier to add or remove news categories without affecting the application's core functionality. Additionally, it allows us to easily add new types of observer objects, such as email subscribers without modifying the subject(publisher) object's code.

  • Separation of Concerns by separating the responsibilities of the subject and observer objects.

  • Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Implementation

Let's Implement a news application and subscribers

class NewsSubject:
    def __init__(self):
        self.observers = []
        self.news = {}

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

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

    def notify(self, category):
        for observer in self.observers:
            observer.notify(category, self.news[category])

    def add_news(self, category, news):
        self.news[category] = news
        self.notify(category)


class NewsObserver:
    def __init__(self, name):
        self.name = name

    def notify(self, category, news):
        print(f'{self.name} received news in {category}: {news}')


# Create Subject
news_subject = NewsSubject()

# Create some observers
sports_observer = NewsObserver("Sports fan")
politics_observer = NewsObserver("Politics fan")
business_observer = NewsObserver("Business fan")

# Register the observers with the subject
news_subject.register(sports_observer)
news_subject.register(politics_observer)
news_subject.register(business_observer)

# Add some news articles
news_subject.add_news('Sports', 'Some sports news')
news_subject.add_news("Politics", "Some Policts news")
news_subject.add_news("Business", "Some business news")

# Unregister an observer
news_subject.unregister(business_observer)

we define a NewsSubject class that acts as the subject object, and a NewsObserver class that acts as the observer object. The NewsSubject maintains a list of observers and notifies them when a new news article is added.

UML Diagram

Now that we have covered the Observer design pattern, let's shift our focus to the Decorator design pattern.

Decorator Design Pattern

This design pattern can be classified under structural pattern which means organizing different classes and objects to form larger structures dynamically and provide new functionality.

It allows objects to be extended dynamically at runtime by wrapping them in an object of a decorator class. It attaches new behaviors to objects without changing their implementation and by placing these objects inside the wrapper objects that contain the behaviors. This makes it a powerful tool for adding functionality to existing objects without modifying their source code.

Why not inheritance?

You might be thinking inheritance can also achieve the same thing by extending a class then what is the actual use case of the decorator design pattern?

Let's understand a scenario from the famous Design Pattern Book Gangs of Four(GOF)

Suppose you have a TextView class. Then in someplace, you want a scrolled text view, so you subclass TextView and create ScrolledTextView class. And in some other places, you want a border around the text view. So you subclass again and create BorderedTextView. Well, now in someplace you want border and scroll both. None of the previous two subclasses have both capabilities. So you need to create a 3rd one. When creating a ScrolledBorderedTextView you are actually duplicating the effort. You don't need this class if you have any way to compose the capability of the previous two. And Things can go worse and these may lead to an unnecessary class explosion.

By using the decorator pattern you can add any number of additional responsibilities to an object at RUNTIME which you can not achieve by subclassing without potentially damaging your code structure or adding many duplicate combinations for subclasses.

Benefits

  • Extensibility: New functionality can be added to an object at runtime, without modifying its source code.

  • Open/Closed Principle: This principle states that closed for modification but open enough for extension or adding new functionality. This pattern allows for new functionality to be added to an object without modifying existing code, thus following the Open/Closed Principle.

  • Composition over Inheritance: The pattern emphasizes composition over inheritance, which can result in a more flexible and modular design.

Implementation

Python example of the decorator pattern:

# Define the Component interface
class Pizza:
    def __init__(self):
        self.description = 'Unknown Pizza'

    def getDescription(self):
        return self.description

    def cost(self):
        pass


# Define the ConcreteComponent class
class MargheritaPizza(Pizza):
    def __init__(self):
        self.description = 'Margherita Pizza'

    def cost(self):
        return 100.0


# Define the Decorator class
class Toppings(Pizza):
    def __init__(self, pizza):
        self.pizza = pizza

    def getDescription(self):
        return self.pizza.getDescription()

    def cost(self):
        return self.pizza.cost()


# Define the ConcreteDecorator classes
class ExtraCheese(Toppings):
    def __init__(self, pizza):
        super().__init__(pizza)
        self.description = 'Extra Cheese'

    def cost(self):
        return self.pizza.cost() + 20.0


class Jalapeno(Toppings):
    def __init__(self, pizza):
        super().__init__(pizza)
        self.description = 'Jalapeno'

    def cost(self):
        return self.pizza.cost() + 15.0


# Create a Margherita pizza with Extra Cheese and Jalapeno toppings
pizza = MargheritaPizza()
# Decorate the pizza with ExtraCHeese and Jalapeno 
pizza = Jalapeno(ExtraCheese(pizza))
print(pizza.getDescription() + ' $' + str(pizza.cost()))

In this example, we have a Pizza interface, a MargheritaPizza concrete class, and a Toppings decorator class. We have also defined two concrete decorator classes ExtraCheese and Jalapeno that add extra cheese and jalapeno toppings to the Margherita pizza.

We create a Margherita pizza and then wrap it with ExtraCheese and Jalapeno decorators. We then print the description and cost of the pizza.

Diagram representation for our Decorator example.

That concludes part two of our discussion on object-oriented design patterns in Python, with a focus on the Observer and Decorator patterns. In the next part, we'll explore Command and Chain of Responsibility design patterns and their use cases, so stay tuned for more insights and examples.