Skip to content

Decorator

The decorator is a structural pattern whose main task is to add extra functionality to an existing object without modifying the underlying behavior.

Design

The decorator adds functionality without using the inheritance mechanism, and enables you to enrich functionality dynamically (while the application is running). It is based on a common interface (or base class) that wraps the decorated object as well as implements it.

By using a common interface, it is possible to use multiple decorators at once, in any order.

decorator

Example

The following example consists of the following parts:

  • common interface FragStatistics
  • base class to be decorated with FirstPersonShooterFragStatistics
  • four decorators: DeathCountInfoDecorator, DisplayCountersDecorator, FragDeathRatioDecorator, FragInfoDecorator
  • usage case
class FragStatistics:
    def increment_frag_count(self):
        pass

    def increment_death_count(self):
        pass

    def reset(self):
        pass
class FirstPersonShooterFragStatistics(FragStatistics):
    def __init__(self):
        self._frags_count = 0
        self._death_count = 0

    def increment_frag_count(self):
        self._frags_count += 1
        return self._frags_count

    def increment_death_count(self):
        self._death_count += 1
        return self._death_count

    def reset(self):
        self._frags_count = 0
        self._death_count = 0
class DeathCountInfoDecorator(FragStatistics):
    def __init__(self, frag_statistics):
        self._frag_statistics = frag_statistics

    def increment_frag_count(self):
        return self._frag_statistics.increment_frag_count()

    def increment_death_count(self):
        print('Fragged by an enemy')
        return self._frag_statistics.increment_death_count()

    def reset(self):
        self._frag_statistics.reset()
class DisplayCountersDecorator(FragStatistics):
    def __init__(self, frag_statistics):
        self._frag_statistics = frag_statistics

    def increment_frag_count(self):
        frag_count = self._frag_statistics.increment_frag_count()
        print(f"Your frag count is: {frag_count}")
        return frag_count

    def increment_death_count(self):
        death_count = self._frag_statistics.increment_death_count()
        print(f"Your death count is: {death_count}")
        return death_count

    def reset(self):
        self._frag_statistics.reset()
        print("Stats reset - KDR is equal to 0")
class FragDeathRatioDecorator(FragStatistics):
    def __init__(self, frag_statistics):
        self._frag_statistics = frag_statistics
        self._current_frag_count = None
        self._current_death_count = None

    def increment_frag_count(self):
        self._current_frag_count = self._frag_statistics.increment_frag_count()
        self._display_frag_deaths_ratio()
        return self._current_frag_count

    def increment_death_count(self):
        self._current_death_count = self._frag_statistics.increment_death_count()
        self._display_frag_deaths_ratio()
        return self._current_death_count

    def _display_frag_deaths_ratio(self):
        if self._current_frag_count and self._current_death_count:
            print(f'KDR is {self._current_frag_count/self._current_death_count}')

    def reset(self):
        self._frag_statistics.reset()
class FragInfoDecorator(FragStatistics):
    def __init__(self, frag_statistics):
        self._frag_statistics = frag_statistics

    def increment_frag_count(self):
        print('Enemy fragged')
        return self._frag_statistics.increment_frag_count()

    def increment_death_count(self):
        return self._frag_statistics.increment_death_count()

    def reset(self):
        self._frag_statistics.reset()
def main():
    statistics = FirstPersonShooterFragStatistics()

    statistics.increment_death_count()  # nothing will appear on the screen
    statistics.increment_frag_count()   # nothing will appear on the screen

    decorated_statistics = FragDeathRatioDecorator(FragInfoDecorator(DisplayCountersDecorator(DeathCountInfoDecorator(statistics))))

    decorated_statistics.increment_frag_count()
    decorated_statistics.increment_frag_count()
    decorated_statistics.increment_frag_count()
    decorated_statistics.increment_death_count()

if __name__ == '__main__':
    main()

After executing the main method, the following output will appear on the screen:

Enemy fragged
Your frag count is: 2
Enemy fragged
Your frag count is: 3
Enemy fragged
Your frag count is: 4
Fragged by an enemy
Your death count is: 2
KDR is 2.0

Note that the order of the decorative classes does not matter much to the functionality of the application.

Built-in python decorator

In Python, the decorator implementation can be based on the functionality of built-in decorators. The Python decorator technically does not correspond to the constructs described above: it is a callable entity that takes a function object and returns another function object.

The following example shows how to implement a cache that stores the results of previous computations with built-in decorators:

import functools

def cache(fn):
    _cache = dict()

    @functools.wraps(fn)
    def cacher(*args):
        if args not in _cache:
            _cache[args] = fn(*args)
        return _cache[args]
    return cacher


@cache
def fibonacci(n):
    if n in (0, 1):
        return n
    else:
        return fibonacci(n-1) + fibonacci(n-2)


def main():
  fibonacci(35)


if __name__ == '__main__':
  main()