panhandlefamily.com

Applying the Decorator Pattern to Enhance Object Functionality

Written on

Overview

This article illustrates how the Decorator Pattern can be utilized in practical scenarios rather than just theoretical examples.

The Decorator Pattern serves two primary design objectives: - Extensibility - Reusability

This pattern allows us to add new functionalities to objects without modifying their existing class structures. A formal definition will be provided after the demonstration.

Demo Application

Consider a menu management system for a pizzeria that handles various aspects, including pizza pricing.

Pizza components can be categorized into two main types: - The base, representing a straightforward pizza offering with specific ingredients (e.g., Farmhouse Pizza, Peppy Paneer Pizza, Margherita Pizza). - Toppings, which can be added to the base pizza. Examples include olives, onions, and jalapenos.

The pizza's price is determined by the selected base and the toppings added.

Class Structure

An abstract class is created to represent a pizza.

from abc import ABCMeta, abstractmethod

class Pizza(object, metaclass=ABCMeta):

@abstractmethod

def get_price(self):

pass

Each specific pizza variant should inherit from the Pizza class and implement the get_price method.

class Margherita(Pizza):

def get_price(self):

return 1.0

class FarmHouse(Pizza):

def get_price(self):

return 1.5

class PeppyPaneer(Pizza):

def get_price(self):

return 1.75

Assume the pizzeria allows one or more of the following toppings, with their respective prices: - Jalapenos: 0.2 - Onion: 0.25 - Olive: 0.3

The pizza price changes based on the selected topping(s). For instance, a Margherita Pizza with onions would cost 1.25, while a Farmhouse pizza with both jalapenos and olives would be priced at 2.0 (1.5 + 0.2 + 0.3).

Creating all combinations of classes, each with its own get_price method, would result in:

class MargheritaWithOnion(Pizza):

def get_price(self):

return 1.25

class FarmHouseWithJalapenosAndOlive(Pizza):

def get_price(self):

return 2.0

Understanding the Problem

This design has a significant flaw, leading to a combinatorial class explosion. Soon, classes like MargheritaWithOlive, MargheritaWithJalapenos, and others would need to be created, complicating maintenance.

Consider the potential issues when the price of olives increases; modifying get_price for multiple classes like MargheritaWithOlive and FarmhouseWithOlive would be cumbersome.

The Decorator Pattern provides a solution!

Decorator Pattern

We can start with a pizza and "decorate" it with toppings.

The Decorator Pattern separates the problem into two key components: - Decorated objects - Decorator objects (also referred to as "wrappers")

In the MargheritaWithOlive scenario, the Margherita object serves as the decorated object, while the Olive object functions as the decorator or wrapper.

The decorator must match the type of the object it decorates, meaning both must share the same interface and methods. This is a crucial aspect of the decorator pattern.

Toppings like Onion, Olive, and Jalapenos act as decorators, each needing a get_price method.

Thus, a Margherita pizza wrapped in an Onion remains a Margherita, allowing the get_price method to be invoked seamlessly. This compatibility means that clients can interact with either the original or decorated object without issues.

Decorator Class

We will create a superclass named Topping to represent the toppings, with all specific toppings inheriting from it.

As decorators need to implement the same methods as the objects they wrap, it makes sense for Topping to extend Pizza.

A decorator encompasses an object, necessitating an attribute to reference the decorated object—hence, a decorator has a decorated pizza.

We can establish a module called toppings.py, containing classes for Jalapenos, Onion, and Olive.

from menu import Pizza

class Topping(Pizza):

def __init__(self, pizza):

self.pizza = pizza

Since Pizza is an abstract class and Topping does not implement the get_price method, it too is an abstract class.

Now, let's create concrete classes for each topping.

class Jalapenos(Topping):

def get_price(self):

return 0.2 + self.pizza.get_price()

class Onion(Topping):

def get_price(self):

return 0.25 + self.pizza.get_price()

class Olive(Topping):

def get_price(self):

return 0.3 + self.pizza.get_price()

Notice how the get_price() method in each topping delegates to the pizza's get_price() method.

This means the topping (decorator) effectively delegates to the same method of the decorated object.

Driver Code

Now, let's integrate everything. Assume we want to apply onion as a topping to a Margherita pizza.

from menu import Margherita

margherita = Margherita()

margherita.get_price() # Returns 1.0

Now, let's decorate the Margherita with an Onion topping.

from toppings import Onion

onion_margherita = Onion(margherita)

onion_margherita.get_price() # Returns 1.25

If we pass the margherita around in client code and later replace it with onion_margherita, the get_price() method will still function correctly, showcasing the benefit of decorators implementing the same interface.

Multiple toppings can also be applied.

from menu import FarmHouse

from toppings import Jalapenos, Olive

farmhouse = FarmHouse()

jalapenos_farmhouse = Jalapenos(farmhouse)

olive_jalapenos_farmhouse = Olive(jalapenos_farmhouse)

olive_jalapenos_farmhouse.get_price() # Returns 2.0, adding the price of jalapenos and olives to the farmhouse pizza.

We can nest decorations as needed.

Type Annotation

Type annotations improve code readability and provide enhanced editor support.

We can annotate the pizza parameter in Topping.__init__ to clarify the type of this attribute.

class Topping(Pizza):

def __init__(self, pizza: Pizza):

self.pizza = pizza

This makes it clear that the decorator is working with a pizza that it is decorating.

Design Principle

The Decorator Pattern exemplifies the Open-Closed Principle, which states:

Classes should be open for extension but closed for modification.

The Pizza class is open for extension, as Topping extends it, adding new responsibilities to the get_price() method without entirely replacing it. It still calls the superclass's get_price, thus adding functionality without modifying existing behavior.

Formal Definition

To conclude, here's a formal definition:

The Decorator Pattern dynamically attaches additional responsibilities to an object without altering the underlying class.

We successfully added the capability to include topping prices on pizza without changing the Pizza class.

Voilà!

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

Navigating Uncertainty: Harnessing Hard Trends for Business Growth

Discover how businesses can leverage hard trends to thrive amidst uncertainty and drive anticipatory action for success.

Kickstart Your Data Science Journey with These Essential Posts

Explore essential posts to guide your Data Science learning, covering crucial skills, resources, and career tips.

MidJourney V5: A Leap Forward in AI Image Generation

Discover the groundbreaking enhancements in MidJourney V5, including improved image quality, stylistic range, and dynamic color capabilities.

Rediscovering Fulfillment: A Journey Through Disconnection

A deep exploration of the struggle for fulfillment amidst societal expectations and personal disconnection.

Redefining Productivity: Embracing a New Approach to Time

Explore a fresh perspective on productivity that prioritizes well-being over mere busyness.

Maximize Your Day Job: Four Essential Steps for Success

Discover four impactful strategies to enhance your work experience and foster a more positive workplace environment.

# Navigating the Writer's Balance: Self-disclosure vs. Privacy

Exploring the balance between self-disclosure and privacy in writing, highlighting personal experiences and the quest for authenticity.

Unlocking the Power of Curiosity: A Path to Connection

Explore how curiosity enhances understanding in life and business, fostering empathy and innovation.