Python design patterns #3: the decorator

· Design Patterns, Python
Authors

Continuing with one of my favorites – the decorator.
It’s a smart one, so a little more talking before coding..

 

Why?
What do you do when you need a flexible combination of properties in an object?

If you’re thinking why not sub-classing? you’re ok..
Sub classing is just right for allowing slightly different flavors of your base object, when the number of flavors is very specific, and the goal is a different meaning child object.

When you need any combination of object properties that doesn’t change the real meaning of an object, just “decorating” it with a different properties combination, sub-classing will shortly lead to “class explosion”.
How quickly? since we’re peaking k properties out of n, order is meaningless and repetition is not allowed – it’s a simple “n choose k” scenario. So even for n=3 we’ll get 3 to 7 sub-classes (choosing 1 option, 2 options and all 3 options).

Another strong argument, often forgotten, is the agility of layering those “decorations”. Due to its tree-like nature, inheritance has everything to do with order and careful planning. Changing the order will break all kinds of hell loose. Layering, on the other hand, has a more “linear” sense to it. The order of the combination has only logical meaning, if any.

I usually think of it as using OOP to achieve an “on the fly property enrichment” we can do pure dynamically with a scripting language like python or javascript.

 

Use case?
red_sports_car_decorator
The classic scenario is an assembly-line like, where final products are actually layers of product features decorated over each other.
A red sports car is a sport decoration over a red decoration over a car. The car by itself might be a layered decoration, and the options are wild here.

You can even steal the inheritance idea here, and logically use different “base” decorations.
For a coffee shop you can start with a “base” hot beverage decorator with 2 layers: a cardboard sleeve over hot water over a base paper cup (our 1st “base” object).
Later, define your base coffee decorator with 1 layer: coffee beans over a hot beverage.
Finally, assemble a “soy milk mocha to go” order with 3 layers: to go over soy cream over chocolate over a coffee decorator. (A total of 6 layers over our initial paper cup).

In the assembly line scenario, each layer will have an accumulated meaning, like price or number of parts, later to be gathered / iterated to achieve a grand total price or a complete part listing.

 

How does it work? (or: TLDR)

  • Both the target object (the “decoratee”) and the decorator have the same interface
  • The decorator wraps (contains) the target

This allows for the iterated, recursion like, call from the outer layer to the final inner object.

 

Some code?
In this example:

  • Every decorator can accept a tuple of decorations (each is a name-price dict), instead of just a single one. Good for when your “base” decoration has no meaning for less than several parts, like a base car decoration of a metal frame and 4 wheels
  • 2 iterable methods: one listing the sub parts and one accumulating the total price
class Target(object):

    def __init__(self, members, item_name=None):
        self.members = members
        self.item_name = item_name

    def list_items(self):
        for member in self.members:
            print(member.keys()[0] + ", "),

    def price(self):
        p = 0.0
        for member in self.members:
            p = p + member[member.keys()[0]]
        return p

The Decorator keeps the same interface (in this case by inheritance), wraps another decorator and handles the recursion like calls:

class Decorator(Target):

    def __init__(self, target, decorations, item_name=None):
        self.target = target
        super(Decorator, self).__init__(decorations, item_name)

    def list_items(self):
        if self.item_name != None:
            print (self.item_name + ': '),
        self.target.list_items()
        super(Decorator, self).list_items()

    def price(self):
        p = self.target.price()
        p = p + super(Decorator, self).price()
        return p

And give it a run with 2 orders:

  • Herbal tea to-go
  • Mocha cream and sugar to-go
if __name__ == '__main__':
    hot_beverage = Target(({'hot water': 0},
                           {'paper glass': 0.2},),)
    coffee_decor = Decorator(hot_beverage, ({'coffee beans': 1.2},))
    tea_decor = Decorator(hot_beverage, ({'herbal leaves': 0.9},))
    mocha_decor = Decorator(coffee_decor, ({'chocolate': 1.1},))

    herbal_tea_ta = Decorator(tea_decor, ({'TA': 0.5},), 'tea to go')
    mocha_cream_sugar_ta = Decorator(mocha_decor, ({'cream': 0.8},
                                                   {'sugar': 0},
                                                   {'TA': 0.5}),
                                     'mocha cream & sugar to go')

    herbal_tea_ta.list_items()
    print('')
    print(herbal_tea_ta.price())

    mocha_cream_sugar_ta.list_items()
    print('')
    print(mocha_cream_sugar_ta.price())

Output:

>>> tea to go: hot water, paper glass, herbal leaves, TA,
>>> 1.6

>>> mocha cream & sugar to go: hot water, paper glass, coffee beans, chocolate, cream, sugar, TA,
>>> 3.8

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: