Search code examples
pythondesign-patterns

Recursion error when combining abstract factory pattern with delegator pattern


I am learning about design patterns in Python and wanted to combine the abstract factory with the delegation pattern (to gain deeper insights into how the pattern works). However, I am getting a weird recursion error when combining the two patterns, which I do not understand.

The error is:

  [Previous line repeated 987 more times]
  File "c:\Users\jenny\Documents\design_pattern\creational\abstract_factory.py", line 60, in __getattribute__
    def __getattribute__(self, name: str):

RecursionError: maximum recursion depth exceeded 

It is raised when client_with_laptop.display() is called. However a/the recursion error is already stored in client_with_laptop._hardware during the __init__, although factory.get_hardware() returns a laptop instance.

The code is:

from abc import abstractmethod


class ITechnique:
    #abstract product

    @abstractmethod
    def display(self):
        pass

    def turn_on(self):
        print("I am on!")

    def turn_off(self):
        print("I am off!")


class Laptop(ITechnique):
    #concrete product
    def display(self):
        print("I'am a Laptop")

class Smartphone(ITechnique):
    #concrete product
    def display(self):
        print("I'am a Smartphone")

class Tablet(ITechnique):
    #concrete product
    def display(self):
        print("I'm a tablet!")


class IFactory:

    @abstractmethod
    def get_hardware():
        pass

class SmartphoneFactory(IFactory):

    def get_hardware(self):
        return Smartphone()


class LaptopFactory(IFactory):

    def get_hardware(self):
        return Laptop()

class TabletFactory(IFactory):

    def get_hardware(self):
        return Tablet()


class Client():

    def __init__(self, factory: IFactory) -> None:
        self._hardware = factory.get_hardware()

    def __getattribute__(self, name: str):
        return getattr(self._hardware, name)

if __name__ == "__main__":

    client_with_laptop = Client(LaptopFactory())
    client_with_laptop.display()

    client_with_tablet = Client(TabletFactory())
    client_with_tablet.display()

    client_with_smartphone = Client(SmartphoneFactory())
    client_with_smartphone.display()

When I access the attribute _hardware and remove the get_attribute section (so, basically, when I remove the delegation pattern), everything works as expected. See below the modified code section, which works:

class Client():

    def __init__(self, factory: IFactory) -> None:
        self._hardware = factory.get_hardware()

if __name__ == "__main__":

    client_with_laptop = Client(LaptopFactory())
    client_with_laptop._hardware.display()

    client_with_tablet = Client(TabletFactory())
    client_with_tablet._hardware.display()

    client_with_smartphone = Client(SmartphoneFactory())
    client_with_smartphone._hardware.display()

Can anybody help me explain why the recursion error occurs or how to fix it. My objective was (1) to have varying devices depending on the factory used in client and (2) to be able to call the methods from the _hardware without typing client._hardware all the time but calling it directly from the client object, e.g. client.display(). It is not whether this is, in reality, a useful approach or not; I simply want to understand the pattern - and the occurring error - better. :-)


Solution

  • from abc import abstractmethod
    
    
    class ITechnique:
        #abstract product
    
        @abstractmethod
        def display(self):
            pass
    
        def turn_on(self):
            print("I am on!")
    
        def turn_off(self):
            print("I am off!")
    
    
    class Laptop(ITechnique):
        #concrete product
        def display(self):
            print("I'am a Laptop")
    
    class Smartphone(ITechnique):
        #concrete product
        def display(self):
            print("I'am a Smartphone")
    
    class Tablet(ITechnique):
        #concrete product
        def display(self):
            print("I'm a tablet!")
    
    
    class IFactory:
    
        @abstractmethod
        def get_hardware():
            pass
    
    class SmartphoneFactory(IFactory):
    
        def get_hardware(self):
            return Smartphone()
    
    
    class LaptopFactory(IFactory):
    
        def get_hardware(self):
            return Laptop()
    
    class TabletFactory(IFactory):
    
        def get_hardware(self):
            return Tablet()
    
    
    class Client():
    
        def __init__(self, factory: IFactory) -> None:
            self._hardware = factory.get_hardware()
    
        def __getattribute__(self, name: str):
            return getattr(self._hardware, name)
    

    After you created a Client object, that object has the method __getattribute__. Inside this method you then proceed to call this object's __getattribute__ method when you access this objects display method. This causes instant recursion. To solve this you need to allow for figuring out what needs done. Read the documentation for __getattr__ and __getattribute__ to determine how you want to handle it.