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
}
}
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();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With