Decorator Pattern

Issue

An object behavior (-> base object or component) is supposed to be changed at runtime (not compile time), which means that the object’s code is not supposed to be changed, but merely adapted for one call.

Design Principles & Definition

Open-Closed Principle

Classes should be open for extension, but closed for modification

The goal is to allow classes to be easily extendable to incorporate new behavior without modifying existing code. Focus its appliance to those areas, which are most likely to change as applying this principle everywhere in your code is unnecessary and can lead to complex, hard-to-understand code.

Definition

The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Idea

  • The base object is “wrapped” by additional objects, that add behavior to it without the need to change the base class

  • The amount and type of wrappers used for a function call defines the behavior

  • Each wrapper layer may define custom behavior to the function call or its return value

../../../_images/DecoratorPattern1.jpg

Outmost wrapper function is called first (speak()), which calls the next inner function until base object returns something, which is then again eventually passed along to the original function call

How wrappers work:
  • All wrappers must be of the same type as the base class is (IS-A) in order to act like it (from outside they behave just like the base class) -> implementing a wrapper super-class which itself extends the base class supertype

  • All wrappers must contain an object of the base class type (HAS-A) to use composition (adding behavior dynamically) (required to call the next inner wrapper or base class) -> wrapped object is assigned during wrapper instantiation

  • Each wrapper implements the desired base class functions differently

  • When a method is called on a wrapped object, the method call is first processed by the outermost wrapper

  • That outermost wrapper calls the same function of its wrapped object and make some characteristic changes to the result returned from it

  • That wrapped object can be the base object or another wrapper of the same type or any other type inheriting from the same wrapper super-class

        classDiagram
    Beverage <|-- Decaf
    Beverage <|-- Espresso
    Beverage <|-- AddonDecorator : IS-A
    Beverage <-- AddonDecorator : HAS-A
    AddonDecorator <|-- CaramelDecorator
    AddonDecorator <|-- SoyDecorator
    class Beverage{
        +get_desc()
        +cost()
    }
    class Decaf{
        +cost()
    }
    class Espresso{
        +cost()
    }
    class AddonDecorator{
        +get_desc()
    }
    class CaramelDecorator{
        +get_desc()
        +cost()
    }
    class SoyDecorator{
        +get_desc()
        +cost()
    }
    
  • In this example, all classes of type Beverage function as the base class and all classes of type AddonDecorator function as decorator classes (wrappers)

  • Instead of creating lots of concrete Beverage classes (like DecafWithCaramelAndSoy), we are able to decorate the base class (e.g. Decaf) with a random amount of decorators (e.g. CaramelDecorator & SoyDecorator)

In general

  • each decorator refers to the inner object (may it be the base class or another wrapper). For this, it needs an object of that type (HAS-A), but the same time it can be used as that inside object by another wrapper of the same type (therefore IS-A)

  • a wrapper function implementations are completely dependent on the scenario. Some may not even refer to the inner wrapped object (e.g. purpose is to multiple input with 0, which always will be zero, no matter what the inner “thing” is), but immediately turn direction and return a value to the outside layer

  • it is an flexible alternative to sub-classing by providing more functionality to a base object

Component:

Base class for all concrete component (undecorated classes)

Decorator:

Base class for all concrete decorator

Appliance

Find a template example for Python on https://refactoring.guru/design-patterns/decorator/python/example

Implementation of the upper example:

class Beverage:

    def get_desc(self) -> str:
        pass

    def cost(self) -> int:
        pass


class Decaf(Beverage):

    def get_desc(self) -> str:
        return "Decaf"

    def cost(self) -> int:
        return 2


class Espresso(Beverage):

    def get_desc(self) -> str:
        return "Espresso"

    def cost(self) -> int:
        return 1


class AddonDecorator(Beverage):

    cost = 0

    def __init__(self, beverage: Beverage):
        self._beverage = beverage

    def get_desc(self) -> str:
        return self._beverage.get_desc()

    def cost(self) -> int:
        pass


class CaramelDecorator(AddonDecorator):

    def __init__(self, beverage: Beverage):
        super().__init__(beverage)

    def cost(self) -> int:
        return self._beverage.cost() + 2

    def get_desc(self) -> str:
        return self._beverage.get_desc() + " + Caramel"


class SoyDecorator(AddonDecorator):

    def __init__(self, beverage: Beverage):
        super().__init__(beverage)

    def cost(self) -> int:
        return self._beverage.cost() + 1

    def get_desc(self) -> str:
        return self._beverage.get_desc() + " + Soy"


if __name__ == "__main__":
    espresso = Espresso()
    espresso_with_latte = CaramelDecorator(espresso)
    print(f"{espresso_with_latte.get_desc()}: {espresso_with_latte.cost()}")

Warning

The decorator pattern is not very suitable for this example, as

  • the decorator implementation of cost() don’t significantly differ (only added value is different differs). This could be more easily done within constructor of a concrete beverage, passing in a list of addon prices.

  • using the decorator pattern here violates the rule of separating concerns: here the cost() method implementation does specify the way of how (logic) the addon affects the cost of the base object and also specifies the value by how much (magnitude) the cost is changed. In this particular case, it is unnecessary to specify the logic in each concrete addon decorator, since that is unlikely to change (costs for each addon will probably always be added to base price, not multiplied or in more complex ways).

  • the added change of any decorator does not affect the behavior of the decorate object (Beverage) but merely its state (cost parameter), whereas the decorator pattern should foremost be used for the former.