简体   繁体   中英

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).

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.

Since you use ABCMeta , you must defined the abstract methods. One could remove your method from the __abstractmethods__ set, but it is a frozenset . Anyway, it involves listing all abstract methods.

So, instead of playing with __getattr__ , you can use a simple descriptor.

For instance:

class Delegated(object):
    def __init__(self, attrname=None):
        self.attrname = attrname

    def __get__(self, instance, owner):
        if instance is None:
            return self
        delegatee = instance.delegatee
        return getattr(delegatee, self.attrname)


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

    myMethod = Delegated('myMethod')

An advantage here: the developer has the explicit information that "myMethod" is delegated.

If you try:

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

It works! But if you forget to implement myMethod in Delegator class, you have the classic error:

Traceback (most recent call last):
  File "script.py", line 40, in <module>
    a = Delegator()
TypeError: Can't instantiate abstract class Delegator with abstract methods myMethod

Edit

This implementation can be generalized as follow:

class DelegatorParent():
    __metaclass__ = ABCMeta

    @abstractmethod
    def myMethod1(self):
        pass

    @abstractmethod
    def myMethod2(self):
        pass

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


class ConcreteDelegatee1():
    def myMethod1(self):
        return 'myMethod1'


class ConcreteDelegatee2():
    def myMethod2(self):
        return 'myMethod2'


class DelegatedTo(object):
    def __init__(self, attrname):
        self.delegatee_name, self.attrname = attrname.split('.')

    def __get__(self, instance, owner):
        if instance is None:
            return self
        delegatee = getattr(instance, self.delegatee_name)
        return getattr(delegatee, self.attrname)


class Delegator(DelegatorParent):
    def __init__(self):
        self.delegatee1 = ConcreteDelegatee1()
        self.delegatee2 = ConcreteDelegatee2()
        DelegatorParent.__init__(self)

    myMethod1 = DelegatedTo('delegatee1.myMethod1')
    myMethod2 = DelegatedTo('delegatee2.myMethod2')


a = Delegator()
result = a.myMethod2()

Here, we can specify the delegatee name and delegatee method.

Here is my current solution. It solves the main problem (prevent ConcreteDelegatee from forgetting to define myMethod), but I'm still not convinced because I still need to define myMethod inside Delegator, which seems redundant

from abc import ABCMeta, abstractmethod

class DelegatorParent(object):
    __metaclass__ = ABCMeta

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

    @abstractmethod
    def myMethod(self):
        pass


class Delegatee(object):
    def checkExistence(self, attrname):
        if not callable(getattr(self, attrname, None)):
            error_msg = "Can't instantiate " + str(self.__class__.__name__) + " without abstract method " + attrname
            raise NotImplementedError(error_msg)


class ConcreteDelegatee(Delegatee):    
    def myMethod(self):
        return 'myMethod'

    def myMethod2(self):
        return 'myMethod2'


class Delegator(DelegatorParent):
    def __init__(self):
        self.delegatee = ConcreteDelegatee()
        DelegatorParent.__init__(self)
        for method in DelegatorParent.__abstractmethods__:
            self.delegatee.checkExistence(method)

    def myMethod(self, *args, **kw):
        return self.delegatee.myMethod(*args, **kw)

    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)



# if I forget to implement myMethod inside ConcreteDelegatee, 
# the following line will correctly raise an exception saying 
# that 'myMethod' is missing inside 'ConcreteDelegatee'.
a = Delegator() 

print a.myMethod() # correctly prints 'myMethod'

print a.myMethod2() #correctly prints 'myMethod2'

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM