Search code examples
pythonoopdesign-patternsbridgedependency-inversion

How do I decouple lamp from button using the dependency inversion principle?


As per figure 5 in DIP I wrote the following code. How do I write it to conform to figure 6 in the same document?

class Lamp:
    def __init__(self):
        self.state = "off"
    def turn_on(self):
        self.state = "on"
    def turn_off(self):
        self.state = "off"

class Button:
    def __init__(self, lamp):
        self.lamp = lamp
        self.state = "off"
        self.lamp.state = "off" 
    def on(self):
        self.state = "on"
        self.lamp.turn_on()
    def off(self):
        self.state = "off"
        self.lamp.turn_off()

lamp = Lamp()
button = Button(lamp)
assert lamp.state == "off"
button.on()
assert lamp.state == "on"

I am thinking of using the bridge pattern, but I am not sure how to write it. I am trying the following code but it still feels wrong to me despite the asserts running ok:

from abc import ABC, abstractmethod

class AbstractButton(ABC):
    def __init__(self, button_client):
        self.button_client = button_client
    @abstractmethod
    def on(self):
        pass
    @abstractmethod
    def off(self):
        pass

class Button(AbstractButton):
    def on(self):
        self.button_client.on()
    def off(self):
        self.button_client.off()

class ButtonClient(ABC):
    @abstractmethod
    def on(self):
        pass
    @abstractmethod
    def off(self):
        pass

class Lamp(ButtonClient):
    def __init__(self):
        self.state = "off"
    def on(self):
        self.state = "on"
    def off(self):
        self.state = "off"


lamp = Lamp()
button = Button(lamp)
assert lamp.state == "off"
button.on()
assert lamp.state == "on"
button.off()
assert lamp.state == "off"

Now inspired by Python protocols, and here I can write the following simpler code, that gets me a Button containing a Lamp Switchable device. There is only one place where the state is stored (the Lamp device) and I get controls on both for free. But is this code decoupled?

class Switchable:
    def __init__(self, device):
        self.device = device
        self.device.state = 'off'
    def turn_on(self):
        self.device.state = 'on'
    def turn_off(self):
        self.device.state = 'off'


class Lamp(Switchable):
    def __init__(self):
       super().__init__(self) 
    def __str__(self):
        return f"Lamp {self.state}"

lamp = Lamp()
button = Switchable(lamp)
assert str(lamp) == "Lamp off"
button.turn_on()
assert str(lamp) == "Lamp on"
button.turn_off()
assert str(lamp) == "Lamp off"
lamp.turn_on()
assert str(lamp) == "Lamp on"
lamp.turn_off()
assert str(lamp) == "Lamp off"
assert button.device.state == "off"
 

Solution

  • You don't need the complicated inheritance design that is required when programming in C++. You can just simplify your first code snippet:

    class Lamp:
        def __init__(self):
            self.state = "off"
        def turn_on(self):
            self.state = "on"
        def turn_off(self):
            self.state = "off"
    
    
    class Button:
        def __init__(self, lamp):
            self.lamp = lamp
        def on(self):
            self.lamp.turn_on()
        def off(self):
            self.lamp.turn_off()
    
    
    lamp = Lamp()
    button = Button(lamp)
    assert lamp.state == "off"
    button.on()
    assert lamp.state == "on"
    

    The three parts of the above can be in completely different files, with only the last part needing imports to get hold of the names Lamp and Button.