Reimplement Events using the Observable pattern
See original GitHub issueEvents are useful for interacting with Minecraft in a portable way. Unfortunately, events in their current form are unsuitable for the Observer pattern, since events can consist of multiple arguments and don’t provide a direct way to modify the event as it is being propagated to listeners. I suggest reimplementing all Events to conform to the Observer pattern, by placing the input arguments into a record-like class. Note that this suggestion applies to the internal implementation only, the external API can remain the same (Although obviously, we should allow mods direct access to the record-like if they so desire, and perhaps the multi-argument lambda functions should eventually be removed by a deprecation cycle, although that is a separate issue).
A couple of advantages for Observables over Events:
- Additional methods can be added for convenience or additional functionality (a
Event#cancel();
or e.g.ChatMessageEvent#changeMessage(message -> toUpperCase(message));
) - Adding additional inputs or methods later on will not break any existing code.
- Generating events or event listeners is much simpler, and doesn’t require Reflection or ASM,
@Annotations
would suffice. - All Observable operators can be applied as usual, e.g.
WorldTickEvent.filter(event -> isOverworld(event.getWorld()))
. In the current system, operators likemap
cannot be implemented. In general, Observables are much nicer to deal with because of this. - We can provide native (“hardware-accelerated”) support for common operations, e.g. we could implement the above filter as
WorldTickEvent.filter(OVERWORLD_FILTER)
, such that if 20 mods apply this filter, the code for it will only have to be invoked once per event, instead of 20 times using 20 differentif
statements. - Different parts of a listener can be executed asynchronously on different threads if desired, much like
.stream()
in Java.
A couple of concerns debunked:
Q: Doesn’t wrapping inputs in record-likes create unnecessary allocations? A: No. The record-likes can be cached between listeners, and between events for non-recursive events (Which is most events). This is also why it is important for Fabric to provide this functionally itself, because other mods can’t reliably cache events themselves (without mixing into Fabric, of course), meaning that even if an external mod were to implement Observables for themselves by manually wrapping the inputs into a record-like, the implementation would still be much less efficient.
Q: Can’t listeners unexpectedly mutate the events they are passed? A: No. record-like members can be made final, or record-likes can instead be implemented as interfaces with getter methods.
Q: Aren’t Observables much slower than the current system?
A: No. Internally, Observables are implemented the same way as the current system, with the only difference being that only listeners that implement Consumer<T>
are allowed.
Listeners can cache the inputs into their own local variables, meaning the overhead per event only has to be a single GETFIELD
copy per listener, and you have to remember that this copy overhead already exists in the current system because the arguments have to be copied to the stack for every method invocation. Observer listeners only have to copy the fields that they actually use, whereas the current system always needs to copy all inputs for every listener invocation.
Q: How can we cache record-likes if their contents are final? A: We can use ASM or Reflection to change the contents once per event.
Q: Registering a lambda with multiple arguments is much more convenient! A: We can still keep supporting the old system with minimal overhead.
Q: Wouldn’t implementing an entire Reactive Library for fabric require lots of maintenance? A: Although it would be nice for performance tuning, Fabric doesn’t strictly have to provide any Observable methods itself, libraries like RxJava provide adapter methods to turn Events into propper Observables. The only change that is required to come from fabric is to limit listener inputs to one object.
Issue Analytics
- State:
- Created 3 years ago
- Reactions:8
- Comments:9 (8 by maintainers)
Top GitHub Comments
You’re currently proposing a change that would fundamentally break the entire event architecture of Fabric. While I’m not fundamentally opposed to fundamental reworks, you have yet to provide any example code of how this would work, much less an analysis of its performance impact. This is a forge-y way of doing events that is incompatible with the Fabric ethos. If you really want this, bring it back as a PR so we can audit and test it. I’m using my authority as a member of the triage team to close this, as further bickering won’t do anything unless we have real code to inspect.
Please, I want to see actual code, I don’t think throwing around concepts and arguments like this is very useful. 😉