After a long discussion with Sven about using lambdas with components [1]
and after re-reading Martijn's emails in the thread about removing lambda
component factories [2], I have a new lambda component proposal based on my
discussion with Sven.
1. I don't think we should consider every onXXX as a candidate for lambda
usage. Specifically, I don't think Component-level methods like
onInitialize(), onConfigure(), onDetach(), etc. should be candidates
because overriding those methods is fairly rare. For example, if I
override those methods, I am almost always in a standalone sub-class of
Panel; it is unusual for me to override them for things like Link or Button
components. Indeed, to deal with concerns like visibility or being enabled
(common reasons to override #onConfigure()), I add a Behavior to control
this rather than sub-class the Component.
2a. I think what makes a method a good candidate for a lambda is where a
little more _specific_ functionality needs to be added to a component to
make it useful/complete. In my mind, the Link, AjaxLink and
AjaxFallbackLink components are quintessential examples of this: the
developer needs to add one (usually small) piece of functionality (the
onClick(...) method) to make the component useful.
2b. In my opinion, the methods that work best with lambda expressions are
onClick(...), onSubmit(...), onError(...) and onUpdate(...). The
components we are talking about are the variations on Link (Link, AjaxLink
and AjaxFallbackLink), Button (Button, AjaxButton and AjaxFallbackButton),
SubmitLink (SubmitLink and AjaxSubmitLink) and AjaxCheckbox.
3. A major concern with introducing lambda expressions is to not penalize
components space-wise that don't use lambdas -- e.g., existing
functionality that sub-classes AjaxLink to override the onClick(...) method
should not have to store a null pointer to an unused lambda reference
4. In the Component class, it is possible to store and access typed
"meta-data" with a component instance (#setMetaData/#getMetaData). If the
lambda handler is stored as meta-data, storage for the lambda will only be
allocated if a lambda handler is actually assigned. If a lambda handler is
not used, no storage for the handler would be allocated.
Here's what this would look like for AjaxLink:
1. Create a @FunctionalInterface AjaxClickHandler with a single method:
void handleClick(AjaxRequestTarget target);
2. Create a MetaDataKey<AjaxClickHandler> constant called
AJAX_CLICK_HANDLER_KEY
3. Add a "fluent setter" to AjaxLink that stores the AjaxClickHandler in
the meta-data:
public AjaxLink<T> setClickHandler(AjaxClickHandler clickHandler) {
setMetaData(AJAX_CLICK_HANDLER_KEY, clickHandler);
return this;
}
4. Make AjaxLink non-abstract and implement onClick(AjaxRequestHandler) by
calling getMetaData(AJAX_CLICK_HANDLER_KEY) and calling the #handleClick
method on the lambda with the AjaxRequestTarget if the lambda is not null.
5. Because AjaxClickHandler is a @FunctionalInterface, the setClickHandler
method can be invoked with a lambda (and without explicitly referencing the
ClickHandler interface at all):
new AjaxLink("id").setClickHandler(target -> target.add(myLink));
The advantage of this is:
1) Usages of AjaxLink that do not use lambdas do not take up any more space
than they did before; and
2) It is still possible to sub-class AjaxLink (e.g., to implement
#onConfigure).
The disadvantage of this is that, as Martijn pointed out, it doesn't make
sense to call setClickHandler() on a subclass where onClick has been
overridden. However, because we're adding lambda handlers to a relatively
small number of components, and adding it to implement a very specific
piece of functionality (i.e., onClick is much more specific than
onConfigure), I don't think this is that confusing.
To make this work for the other components listed above, we would need to
introduce @FunctionalInterface interfaces for each of the different types
of methods (onSubmit, onError and onUpdate -- and also the variations in
the type of argument passed to each method). End-users wouldn't directly
use the @FunctionalInterface interfaces; they would simply pass lambda
handlers into Component classes.
I've implemented this in my local environment with the classes listed
above and it does work. Does this approach seem reasonable?
Thanks
Andrew
[1] https://github.com/wicketstuff/core/pull/578
[2]
http://apache-wicket.1842946.n4.nabble.com/Proposal-Remove-all-component-lambda-based-factory-methods-before-8-0-0-final-td4677009.html