There are many things to consider when designing an API. Usability of
the API, readability of the resulting code, maintenance burden for the
core developers, but also performance, memory consumption and
serialization overhead.
Adding lifecycle fields to Component for any extension point will
dramatically increase the footprint of any Wicket application.
Lambda's are not a free lunch and add considerable memory and
serialization overhead to Java code.
Wicket is not only used for small demos of how fancy lambda's can be,
but also for applications where 10's of thousands or even millions of
users concurrently use the application. Adding a byte to the footprint
of Component can increase the memory footprint of the application with
10's of megabytes.
So yes we have considered adding these things but find the problem
they solve not worth the extra footprint. Furthermore the clarity of
the code does not increase when you have 3 or 4 methods that need
implementing, but those add considerable memory footprint (8 bytes per
reference when unused, much more when used) when provided through
lambdas.
Calculation
Say you have 1 million components in memory. Say we add lambda's for
the primary life cycle events and enabled/visible flags. This is:
- onInitialize
- onEvent
- onReAdd
- onRemove
- onRender
- onConfigure
- onBeforeRender
- onBeforeRenderChildren
- onComponentTag
- onComponentTagBody
- onAfterRender
- onAfterRenderChildren
- onDetach
- isVisible
- isEnabled
This is without onFoo methods available in Link, Form etc. 15
references that may or may not be used. 15*8 = 120 bytes per
component. Now we have added 120MB to an application without anyone
even using the functionality. The cost is considerably higher when
actually using the lambda's. See the measurements I did for my talk at
ApacheCon 2016 in Sevilla [1]
Of course we can try to minimize the memory footprint, and store the
event references in the data bucket of Component and store only those
events that are actually used. But this will add an O(n^2) for every
request because we need to loop through the data array at each life
cycle event to find if there's a registered reference, and if so use
that. While for a single request this is not too big an issue, it adds
up when you consider that this must be done for millions of components
for all lifecycle events.
API design
So let's consider the case when we were to add setters for lambda's to
Component et al. We can't remove the old way of subclassing because
that would break the world. Thus we have two ways of how to add an
onConfigure or onDetach event to a component. Which one is canon? We
already have at least 3 ways to determine whether a component is
visible or not:
- call setVisible (static visibility)
- override isVisible
- setVisibilityAllowed
- override onConfigure() and call setVisible
This has harmed the Wicket API in my opinion and is a result of us not
understanding at the time how, when and how often visibility needed to
be evaluated.
Adding setters for lambda's would at least double the API surface.
Furthermore you need to take into account that these setters can't be
hidden by subclasses. So when you use a component that has onConfigure
overridden, what happens when you setOnConfigure()?
public class FooPanel extends Panel {
public FooPanel(String id) { super(id); }
@Override
protected void onConfigure() {
super.onConfigure();
// if something set visiblity
}
}
add(new FooPanel("foo").setOnConfigure(() -> { ... do something
completely different ... });
So which one should prevail? the lambda or the overridden method? Ah I
hear you say "rewrite the FooPanel such that it doesn't override
onConfigure". But then we have 2 onConfigure lambda's set:
public class FooPanel extends Panel {
public FooPanel(String id) {
super(id);
setOnConfigure(() -> {
/* if something set visibility */
});
}
}
add(new FooPanel("foo").setOnConfigure(() -> { ... do something
completely different ... });
So which one should prevail? Well, call both! OK, this means that we
need to store both lambdas. So each lifecycle event lambda store needs
to be a data structure that can hold multiple lambdas, which increases
the memory footprint even further. Then we have to issue of which
lambda to call first. The one added in the constructor of FooPanel, or
the one that was set after construction?
This doesn't even take into account the semantics of for example the
onInitialize lifecycle event, which states that it is called when a
component is added to the hierarchy of a page (i.e. a parent-path
exists to the page). A setOnInitialize can be invoked at any moment
after construction. Therefore its semantics become even muddier than
they already are:
Label label = new Label(...);
add(label);
label.setOnInitialize(() -> { ... });
OK, so we don't add a lambda possibility for onInitialize (queue the
pull requests).
Another thing to consider is that a component builder currently can
decide to close off particular events from being overridden or
modified. For example:
public class FooPanel extends Panel {
public FooPanel(String id) { super(id); }
/** No longer overridable */
@Override
protected final void onConfigure() {
super.onConfigure();
// if something set visiblity
}
}
The component designer found it necessary to remove the ability to
override onConfigure().
However you can't remove the ability to add/set lambda's from
Component. These methods need to be public for them to be usable, and
you can't hide those methods.
Summary
Lambdas are a great tool but should be used wisely and we should be
careful about our API and take good design into consideration instead
of just papering our API with lambdas. There are many aspects to
consider, not just the fact that you can write 2 lines of
non-executable code less.
Martijn
[1] http://www.slideshare.net/dashorst/whats-up-with-wicket-8-and-java-8
On Wed, Feb 8, 2017 at 11:45 AM, Andrew Geery <[email protected]> wrote:
> Rather than using static factory methods, would we ever consider pushing
> the lambdas into the component classes themselves?
>
> For example, if we did this with the Button class, the change would be:
>
> - Add two private fields, SerializableConsumer<Button> submitHandler,
> errorHandler
> - Add "setters" for these fields -- e.g., Button
> submitHandler(SerializableConsumer<Button> submitHandler)
> - Modify the onSubmit() and onError() methods to call the handler methods
> if they are not null
>
> A call would be something like:
>
> add(new Button("button").submitHandler(b -> System.out.println("clicked"));
>
> Obviously, it is still possible to sub-class Button and override
> onSubmit(), so existing code will continue to work. However, for code
> going forward, it should be much less needed and, in my opinion, much
> clearer.
>
> Another advantage to doing things this way is that sub-classes inherit
> these methods and there is no need to add static factory methods for every
> sub-class (sub-classes of AjaxButton would be a better example of this).
>
> Thanks
> Andrew
>
> On Wed, Feb 8, 2017 at 3:42 AM, Martijn Dashorst <[email protected]
>> wrote:
>
>> It is that your trivial use case is not my trivial use case and that
>> we will end up with a 100,000 trivial use cases.
>>
>> And no, confusion is not the big issue (though for onsubmit+onerror it
>> is) but creating a good API is hard. It takes time and understanding.
>> Minimizing lines of code is not the only metric for a good API.
>>
>> Just as using inheritance/annotations/generics for everything is bad,
>> introducing factory methods everywhere will not age well.
>>
>> These methods are trivial for anyone to implement and should we reach
>> the conclusion that we actually need the factory methods in core, it
>> is trivial to refactor them towards the Wicket supplied factories.
>>
>> Are the factory methods in the way? Yes they are, because once we add
>> them, we can't evolve them without adding many more (introduce bloat)
>> or having to wait until a new major release.
>>
>> Martijn
>>
>>
>>
>>
>> On Tue, Feb 7, 2017 at 11:40 PM, Martin Grigorov <[email protected]>
>> wrote:
>> > Hi,
>> >
>> > These methods are factories for the trivial use cases where nothing else
>> > needs to be overridden but the action method (methods, when there is
>> > onError()). They do help to reduce the verbosity!
>> > There are plenty of those cases. You can see many usages in
>> > wicket-examples. I have used such methods from wicketstuff-scala in one
>> of
>> > my projects in the past and I use similar ones in a project with Kotlin
>> now.
>> >
>> > A builder that provides methods like onConfigure(), onComponentTag(),
>> > onDetach(), ... would look really strange!
>> > Who will use it instead of just creating a (inner) class ?!
>> >
>> > But if those factory methods confuse core developers then they will be
>> even
>> > more confusing to normal users :-/
>> >
>> > 0 from me.
>> >
>> > Martin Grigorov
>> > Wicket Training and Consulting
>> > https://twitter.com/mtgrigorov
>> >
>> > On Tue, Feb 7, 2017 at 5:07 PM, Martijn Dashorst <
>> [email protected]
>> >> wrote:
>> >
>> >> All,
>> >>
>> >> I want to remove all component factory methods that are added for
>> lambda's.
>> >>
>> >> My reasoning is that:
>> >> - removing them in 8.x (x > 0) is impossible
>> >> - adding them in 8.x (x > 0) is trivial
>> >> - we currently don't have a way to know what good idioms are
>> >> - having these factories push (new) developers to use the wrong idiom
>> >> - factory methods don't allow for additional overriding, thus a
>> >> combinatorial explosion of API
>> >> - I tend to think that builders are better suited as component factories
>> >>
>> >> Because it is trivial to re-introduce these factories or their
>> >> replacements at a later time, I propose to remove them now and figure
>> >> out in our actual applications what works, and what doesn't. I also
>> >> think that just doing the conversion from W7 to W8 isn't enough to
>> >> figure this out.
>> >>
>> >> Example 1:
>> >>
>> >> Button.java has two factory methods:
>> >>
>> >> Button#onSubmit(String, SerializableConsumer<Button>), and
>> >> Button#onSubmit(String, SerializableConsumer<Button>,
>> >> SerializableConsumer<Button>)
>> >>
>> >> It is not possible to see without resorting to parameter naming,
>> >> hovering etc to see which functionality is bound by which parameter. I
>> >> find the naming confusing: onSubmit and onSubmit.
>> >>
>> >> Example 2:
>> >>
>> >> Behavior.java has two factory methods:
>> >>
>> >> Behavior onTag(SerializableBiConsumer<Component, ComponentTag>)
>> >> Behavior onAttribute(String name,
>> >> SerializableFunction<String, CharSequence> onAttribute)
>> >>
>> >> These achieve basically the same functionality, but other life cycle
>> >> events of Behavior don't have factory methods. NOR should they.
>> >>
>> >> Example 3:
>> >>
>> >> Lambdas.java has many factory methods, most of which are better
>> >> implemented by just using an anonymous inner class. For example,
>> >> Lambdas.link: often times you need to override onconfigure or
>> >> oncomponenttag as well as onclick.
>> >>
>> >> Martijn
>> >>
>>
>>
>>
>> --
>> Become a Wicket expert, learn from the best: http://wicketinaction.com
>>
--
Become a Wicket expert, learn from the best: http://wicketinaction.com