dimanche 24 février 2019

Python - Observer design pattern, sending data between Observers?

thanks for taking a look,

I'd like to start out by saying that I've taken a good look at similar questions on Stackoverflow, but they were not satisfactory.

I'm implementing the Observer design pattern for a project in python. The idea is that you have classes (Observers) that listen for (observe) a certain event, and when that event occurs it triggers all the callback functions associated with that event.

My issue is that I've hit a roadblock with this pattern - and the pattern is not at fault. Most of the events that can occur in my program are simply fired and thereby trigger the appropriate callbacks, and no other data or information is required between observers (GOOD). However, there is one scenario in which I would like to fire a specific event AND send some data to the associated callback. I realize this isn't really something you'd try to do with the observer pattern (it's meant to decouple things), so I'm open to alternative/additional patterns that are more suited.

Below you'll find a minimal example of what I'm trying to achieve. It works, but I don't really like it. Here are my Observer and Event classes:

class Observer:
    observers = []

    def __init__(self):
        self.observers.append(self)
        self.events = {}

    def observe(self, event_type, callback):
        self.events[event_type] = callback


class Event:
    from enum import Enum

    class Type(Enum):
        ACTION_PROGRAM_START = 0
        ACTION_SEND_DATA = 1

    def __init__(self, event_type, auto_fire=True, data=None):
        self.event_type = event_type
        self.data = data
        if auto_fire:
            self.fire()

    def fire(self):
        for observer in Observer.observers:
            if self.event_type in observer.events:
                callback = observer.events[self.event_type]
                try:
                    callback()
                except TypeError:
                    callback(data=self.data)

And here are the classes that inherit from Observer:

class DataGenerator(Observer):

    def __init__(self):

        super(DataGenerator, self).__init__()

        self.observe(Event.Type.ACTION_PROGRAM_START, self.on_program_start)

    def on_program_start(self):
        print("DataGenerator on_program_start triggered")

    def send_data(self):
        data = [1, 2, 3]
        Event(Event.Type.ACTION_SEND_DATA, data=data)


class DataDisplay(Observer):

    def __init__(self):

        super(DataDisplay, self).__init__()

        self.observe(Event.Type.ACTION_PROGRAM_START, self.on_program_start)
        self.observe(Event.Type.ACTION_SEND_DATA, self.on_send_data)

    def on_program_start(self):
        print("DataDisplay on_program_start triggered")

    def on_send_data(self, *, data):
        print(f"DataDisplay got data {data}")


def main():

    dataGenerator = DataGenerator()
    dataDisplay = DataDisplay()

    Event(Event.Type.ACTION_PROGRAM_START)
    dataGenerator.send_data()

    return 0


if __name__ == "__main__":
    from sys import exit
    exit(main())

Program output:

DataGenerator on_program_start triggered
DataDisplay on_program_start triggered
DataDisplay got data [1, 2, 3]

As you can see, there are events (ACTION_PROGRAM_START) whose associated callbacks (on_program_start) do not accept any additional data (which is the norm in this pattern). However, the DataGenerator must send data to the DataDisplay, and I would like to do it by way of an Event (or something that feels like an Event, if that makes sense). I would like for the DataGenerator not to know about the DataDisplay, which is why the impulse is to somehow "pack" the data along with the Event.

As I'm sure you've noticed, the hacky way I've managed to implement this is by simply trying to invoke a given event's associated callback with no arguments, and if that fails, provide the optional data parameter. DataDisplay.on_send_data is the only callback that takes advantage of this - in my actual project, there are many more event types, and all of them except one actually use the optional data parameter in this way. It is for this reason that I would also like to avoid changing the signature of all my callbacks to accept optional data parameters, because there is really only one callback that takes advantage of it.

So I guess the real question is this: Is this solution really as hacky as I think? My solution really seems to go against the grain of the observer pattern. I was thinking about using decorators in some way, but I can't seem to realize the idea. Any help is appreciated - sorry if it's broad.

Aucun commentaire:

Enregistrer un commentaire