Search code examples
pythoninheritancedesign-patternsdelegationabc

Delegation design pattern with abstract methods in python


I have the following classes implementing a "Delegation Design Pattern" with an additional DelegatorParent class:

class DelegatorParent():

    def __init__(self):
        self.a = 'whatever'    

class ConcreteDelegatee():

    def myMethod(self):
        return 'myMethod'


class Delegator(DelegatorParent):

    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)

    def __getattr__(self, attrname):
        return getattr(self.delegatee, attrname)

a = Delegator()
result = a.myMethod()

Everything looks fine.

Now I would like to put an abstract method in DelegatorParent, to ensure that "myMethod" is always defined.

from abc import ABCMeta, abstractmethod

class DelegatorParent():
    __metaclass__ = ABCMeta

    @abstractmethod
    def myMethod(self):
        pass

    def __init__(self):
        self.a = 'whatever'


class ConcreteDelegatee():

    def myMethod(self):
        return 'myMethod'


class Delegator(DelegatorParent):

    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)

    def __getattr__(self, attrname):
        return getattr(self.delegatee, attrname)

    # This method seems unnecessary, but if I erase it an exception is
    # raised because the abstract method's restriction is violated
    def myMethod(self): 
        return self.delegatee.myMethod()


a = Delegator()
result = a.myMethod()

Can you help me find an "elegant" way to remove "myMethod" from "Delegator"... Intuition tells me that it is somehow redundant (considering that a custom getattr method is defined).

And more importantly, notice that with this implementation, if I forget to define myMethod in ConcreteDelegatee the program compiles, but it may crash in runtime if I call Delegator.myMethod(), which is exactly what I wanted to avoid by using abstract methods in DelegatorParent.

Obviously a simple solution would be to move @abstractmethod to the Delegator class, but I want to avoid doing that because in my program DelegatorParent is a very important class (and Delegator is just an auxiliary class).


Solution

  • You can decide to automatically implement abstract methods delegared to ConcreteDelegatee.

    For each abstract method, check if it's name exist in the ConcreteDelegatee class and implement this method as a delegate to this class method.

    from abc import ABCMeta, abstractmethod
    
    class DelegatorParent(object):
        __metaclass__ = ABCMeta
    
        def __init__(self):
            self.a = 'whatever'
    
        @abstractmethod
        def myMethod(self):
            pass
    
    
    class Delegatee(object):
        pass
    
    
    class ConcreteDelegatee(Delegatee):    
        def myMethod(self):
            return 'myMethod'
    
        def myMethod2(self):
            return 'myMethod2'
    
    
    class Delegator(DelegatorParent):
    
        def __new__(cls, *args, **kwargs):
            implemented = set()
            for name in cls.__abstractmethods__:
                if hasattr(ConcreteDelegatee, name):
                    def delegated(this, *a, **kw):
                        meth = getattr(this.delegatee, name)
                        return meth(*a, **kw)
                    setattr(cls, name, delegated)
                    implemented.add(name)
            cls.__abstractmethods__ = frozenset(cls.__abstractmethods__ - implemented)
            obj = super(Delegator, cls).__new__(cls, *args, **kwargs)
            obj.delegatee = ConcreteDelegatee()
            return obj
    
        def __getattr__(self, attrname):
            # Called only for attributes not defined by this class (or its bases).
            # Retrieve attribute from current behavior delegate class instance.
            return getattr(self.delegatee, attrname)
    
    # All abstract methods are delegared to ConcreteDelegatee
    a = Delegator() 
    
    print(a.myMethod()) # correctly prints 'myMethod'
    
    print(a.myMethod2()) #correctly prints 'myMethod2'
    

    This solves the main problem (prevent ConcreteDelegatee from forgetting to define myMethod). Other abstract methods are still checked if you forgot to implement them.

    The __new__ method is in charge of the delegation, that frees your __init__ to do it.