Immutable event handling

Viewed 1306

I have implemented an immutable system in Java. Pretty much every class is immutable and it's worked out much better than I expected.

My problem is trying to send events. Normally you'd have an event source and an event listener. The source simply holds a reference to the listener and sends the event when it happens.

But with immutables the event listener reference changes when you modify a field and create a new object. So the event source is sending to some old reference that's been garbage collected.

So all my GUI classes are mutable for that reason, because they naturally use a lot of events. But i'd like to find an elegant way to handle events so I can make those immutable as well.

Edit: Example code as requested:

public final class ImmutableButton {
    public final String text;

    public ImmutableButton(String text) {
        this.text = text;
    }

    protected void onClick() {
        // notify listeners somehow, hoping they haven't changed
    }
}

public final class ImmutableWindow {
    public final ImmutableButton button;

    public ImmutableWindow(ImmutableButton button) {
        this.button = button;
    }

    protected void listenForButtonClick() {
        // somehow register with button and receive events, despite this object
        // being entirely recreated whenever a field changes
    }
}
3 Answers

GUIs are good case where mutability is more handy and more performant to use as object model. You create one backing model for a GUI component with fields ready for read and write operations. User mutates the display, which gets reflected to the backing model - all happening in one object. Imagine having to recreate an object everytime user changes something on one component? That will surely hog your system.

Albeit the disadvantage, if you really want your GUI objects to be immutable, you can create a global event bus to solve the listener attachment problem. In this way, you don't need to worry about the object instance which the event listener will be registered. The event bus will be responsible for dispatching of events, registration of listener, and maintenance of the mapping between them.

Here is a draft design for a simple event bus.

public class EventBus {

    private Map<Event, List<EventListener>> REGISTRY;

    public void registerEventListener(Event event, EventListener listener) {
        List<EventListener> listeners = REGISTRY.getOrDefault(event, new ArrayList<>());
        listeners.add(listener);
    }

    public void fireEvent(Event event, Object... args) {
        List<EventListener> listeners = REGISTRY.get(event);

        if(listeners != null) {
            for(EventListener listener : listeners) {
                listener.handleEvent(args);
            }
        }
    }
}

// The events
enum Event {
    ADD_BUTTON_CLICKED, DELETE_BUTTON_CLICKED;
}

// Listeners must conform to one interface
interface EventListener {
    public void handleEvent(Object... args);
}

EDIT

Listeners are handlers - they are supposed to perform business logic and not to hold state. Also, they are not supposed to be attached to a component. In your code above, the listener code must be decoupled from ImmutableWindow - both should be standing on its own. The interaction between ImmutableWindow and ImmutableButton must be configured (in the event bus) somewhere during the startup of your application.

You should also have a central registry of your UI components where it can be identified by a unique id and use this registry to find (traverse the component tree) the latest instance of the component and interact with.

In practice, something like this...

// The main class. Do the wirings here.
public class App {

    @Inject
    private EventBus eventBus;

    @PostConstruct
    public void init() {
        ImmutableWindow window = new ImmutableWindow ();
        ImmutableButton addButton = new ImmutableButton ();

        eventBus.registerEventListener(Events.ADD_BUTTON_CLICKED, new AddButtonClickListener());
    }
}

public class AddButtonClickListener implements EventListener  {

    @Inject
    private SomeOtherService someOtherSvc;

    @Inject
    private UiRegistry uiRegistry;

    public void handleEvent(Object... args) {
        ImmutableButton addButton = args[0].getSource; // The newset instance of the button must be packed and delivered to the eventlistners when firing an event
        ImmutableWindow targetWindow = uiRegistry.lookUp("identifier_of_the_window", ImmutableWindow.class);

        // Perform interaction between the two components;
    }
}

Now you have a totally decoupled UI and business logic. You can recreate your components all you want, the listeners will not be affected because they are not attached to any component.

Yes, the conflict in your design is you cant register listeners against objects which get replaced when you rebuild the immutable data model. My solution below is to remove the listeners from the model all together. Even thought most of your objects are immutable, you'll need at least 1 mutable variable at the base to hold the created / recreated Window. Here I'v used inner classes as listeners which you register once. They then dispatch doStuff() calls against the objects in the model, but they get looked up against the single base mutable window reference. eg. window.getButton1().doStuff();

I don't claim this is a great solution but it's the simplest and cleanest I could come up with for your requirements. The listeners don't become invalid and everything from Window down can be immutable.

public final MutableBase {
    private ImmutableWindow window;  // single mutable variable

    public MutableBase() {
        Magic.registerClickListener(new WindowClickEvent());
        Magic.registerClickListener(new Button1ClickEvent());
        rebuildWindow();
    }

    public void rebuildWindow() {
        // rebuild here when needed - change single mutable variable
        this.window = new ImmutableWindow(new ImmutableButton("text"));
    }

    class WindowClickEvent implements ClickListener {
        public void onClick() {
            this.window.doStuff();
        }
    }

    class Button1ClickEvent implements ClickListener {
        public void onClick() {
            this.window.getButton1().doStuff();
        }
    }
}

I think it's not possible to make everything immutable because somewhere you need to store a changing system state (usually, in the model). Let me explain with some example. Sorry for the lengthy reply, I'm trying to be as clear as possible as this is a complex issue.

I assume that you want to follow MVC (model view controller) paradigm for your GUI. Your example code is not so clear so I will rewrite it to proper MVC.

In MVC, the V (view) listens to changes in the model M and updates the display accordingly.

In general, a listenable like M contains a Set (eg. the View V and Controller C). M must remain the same object in memory because it's shared by several users (V and C). It has to contain a "addListener" function that changes the Set in the original object. If it would only create a modified local copy of M, addListener would add the listener only for the object that calls addListener, but not the original that is used by the objects that want to call the added listener. Therefore a listenable in general can not be immutable.

Let me make this more specific, if this was too dense.

Your code is not proper MVC so I modify it to this to introduce the Model M.

public class Model {
  final?? boolean clicked = false;
  final?? Set<Listeners> listeners

  void addChangeListener(l) {
    listeners.add(l);
  }

  void doClick() { 
    clicked = true; // can not be final ? 
    listeners.forEach(l -> l.notifyChange(..event..));
  }

  boolean getClickedState() {
    return clicked; // makes no sense if final ? 
  }
}

public final class ImmutableWindow {
  final Model model;
  public ImmutableWindow(Model m) {
    this.model = m;
    this.model.addChangeListener( evt -> 
      updatePanel(this.model.getClickedState() );
  }
}

public final class ImmutableButton {
  final Model model;
  public ImmutableButton(Model m) {
    this.model = m
  }

    protected void onClick() {
        this.model.doClick();
    }
}

The Model extends the listener pattern. Now it even has 2 properties that require it to be mutable 1. The Model must reflect the actual state of the system. 2. The Model is observable, so it contains a mutable set of listeners.

@1. m.doClick and m.getClickedState() would work (change Model/system state) only if Model is mutable. The field Model.clicked can't be final.

@2. listenable can't be immutable,as discussed above.

Now notice that ImmutableButton and ImmutableWindow are not really immutable anymore, because Model will turn out to be mutable. The "final" annotations will still work though because final only refers to the object reference , not to the contents of the referred object.

Alternatively, you might propose to make Model immutable and let it just forward all changes of Model to its listeners. But then ImmutableButton would have to store updates of the model. But if it would, then ImmutableButton.model can not be final and the ImmutableButton object would not be immutable...

In the end, someone has to store the changed objects. In MVC it's in the model, but whatever you do, the object that stores the change will be immutable.

I think the idea above with the event bus tries to hide the model behind some interface. The idea being that the interface can be the same fixed (or even static) object that it can point to some mutable data. But it does not work, as any object pointing to some mutable object also is mutable. Specifically, any object containing a mutable eventBus is mutable.

More radically you might try to remove the whole event and Listeners. But I don't see how to do the event handling without the shared listener system.

The best solution I have is to minimize the mutability of the model. Put only a SINGLE mutable object in the model, like SystemState, and mutate only that.

Related