Removing inheritance (decorator pattern ?)

Maric Michaud maric at aristote.info
Mon Jun 16 23:10:57 EDT 2008


Le Monday 16 June 2008 20:35:22 George Sakkis, vous avez écrit :
> On Jun 16, 1:49 pm, Gerard flanagan <grflana... at gmail.com> wrote:
> > George Sakkis wrote:
> > > I have a situation where one class can be customized with several
> > > orthogonal options. Currently this is implemented with (multiple)
> > > inheritance but this leads to combinatorial explosion of subclasses as
> > > more orthogonal features are added. Naturally, the decorator pattern
> > > [1] comes to mind (not to be confused with the the Python meaning of
> > > the term "decorator").
> > >
> > > However, there is a twist. In the standard decorator pattern, the
> > > decorator accepts the object to be decorated and adds extra
> > > functionality or modifies the object's behavior by overriding one or
> > > more methods. It does not affect how the object is created, it takes
> > > it as is. My multiple inheritance classes though play a double role:
> > > not only they override one or more regular methods, but they may
> > > override __init__ as well. Here's a toy example:
> >
> > I don't know if it will map to your actual problem, but here's a
> > variation of your toy code. I was thinking the Strategy pattern,
> > different classes have different initialisation strategies? But then you
> > could end up with as many Strategy classes as subclasses, I don't know.
> > (Also in vaguely similar territory
> > -http://bazaar.launchpad.net/~grflanagan/python-rattlebag/trunk/annota...
> > )
> >
> > class MetaBase(type):
> >
> >      def __init__(cls, name, bases, data):
> >          cls.strategies = []
> >          cls.prefixes = []
> >          for base in bases:
> >              print base
> >              if hasattr(base, 'strategy'):
> >                  cls.strategies.append(base.strategy)
> >              if hasattr(base, 'prefix'):
> >                  cls.prefixes.append(base.prefix)
> >          super(MetaBase, cls).__init__(name, bases, data)
> >
> > class Joinable(object):
> >      __metaclass__ = MetaBase
> >      strategy = list
> >      prefix = ''
> >
> >      def __init__(self, words):
> >          self._words = words
> >          for strategy in self.strategies:
> >              self._words = strategy(self._words)
> >
> >      def join(self, delim=','):
> >          return '%s %s' % (' '.join(self.prefixes),
> > delim.join(self._words))
> >
> > class Sorted(Joinable):
> >      strategy = sorted
> >      prefix = '[sorted]'
> >
> > class Reversed(Joinable):
> >      strategy = reversed
> >      prefix = '[reversed]'
> >
> > class SortedReversed(Sorted, Reversed):
> >      pass
> >
> > class ReversedSorted(Reversed, Sorted):
> >      pass
> >
> > if __name__ == '__main__':
> >      words = 'this is a test'.split()
> >      print SortedReversed(words).join()
> >      print ReversedSorted(words).join()
>
> This doesn't solve the original problem, the combinatorial explosion
> of empty subclasses. At the end of the day, I'd like a solution that
> uses a (mostly) flat, single-inheritance, hierarchy, allowing the
> client say:
>

Yes, and it fails to implement the strategy pattern as well... which would 
have solved the problem as it is intended exactly for this purpose.

> j = Joinable(words)
> if sort:
>   j = Sorted(j)
> if reverse:
>   j = Reversed(j)
> ...
> print j.join()
>

The example given by Gerard is hard to translate into a strategy pattern 
because it's more often a use case for a decorator-like design (which is 
easily rendered with method decorators in python).

A abstract strategy pattern is basically implemented as follow.

class Strategy(object) :

    def do_init(self, *args) : raise NotImplementedError
    def do_job(self, *args) : raise NotImplementedError
    def do_finalize(self, *args) : raise NotImplementedError


# modules can define their own strategies now

class algo_strategy(Strategy) :
    ...

class algo1_strategy(Strategy) :
    ...

# whether this is possible or not depend on the implementation
# and should be documented
class algo2_strategy(algo1_strategy) :
    ...

class MyClassUsingStrategies(object) :

    def __init__(self, meth1_strategies=[], meth2_strategies=[]) :
        self._meth1_strategies = meth1_strategies
        if [ s for s in meth2_strategies
                   if not isinstance(s, algo_strategy) ] :
             raise RuntimeError("Not a valid strategy !")
        self._meth2_strategies = meth2_strategies

    def meth1(self, arg) :
        for i in self._meth1_strategies :
            i.do_init(...)

        ....

        for i in self._meth1_strategies :
            i.do_job(...)

        ....

        for i in self._meth1_strategies :
            i.do_finalize(...)

    ...

The class complextiy problem is actually solved by :

inst_with_alg1 = MyClassUsingStrategies((algo1_strategy,), (algo1_strategy,))
inst_with_alg1_alg2 = MyClassUsingStrategies(
                                                      (algo1_strategy,),
                                                      (algo2_strategy,)
                                  )
inst_with_alg12 = MyClassUsingStrategies(
                                             (algo1_strategy, algo2_strategy),
                                             (algo1_strategy, algo2_strategy)
                            )


etc...


-- 
_____________

Maric Michaud



More information about the Python-list mailing list