The Partially Constructed Object Dillema
Wicket naturally lends itself to object-oriented representations of web pages, and one aspect of this approach is using inheritance to pull behavior common to a set of pages into a superclass shared by those pages. For example, several pages might have a shared base class in Template.java
(with corresponding markup in Template.html
) which sets up a header and side navigation menu that is common to those pages.
A problem is encountered, however, when it comes to allowing subclasses to modify the base class behavior through method overrides. For example, let's assume the Template
has a header with the page title, and that this title changes per page. A plausible solution is to provide an abstract getPageTitle
method which subclasses will override. In the Template
constructor, it will add a label with the result of this method:
public abstract class Template extends WebPage { public Template(PageParameters params) { super(params); add(new Label("headerTitle",getPageTitle())); } protected abstract String getPageTitle(); }
Let's also assume that a certain page subclass of Template
wants the title in question to depend upon data passed in through PageParameters
at construction:
public class class SomePage extends Template { Object myObject; public SomePage(PageParameters params) { super(params); long objectId = params.getLong("objectId"); myObject = SomeService.lookupById(objectId); } protected String getPageTitle() { return "Editing: " + myObject.getSomeProperty(); //Null Pointer Exception } }
The problem we encounter is that the code to look up myObject
above has not yet run when getPageTitle()
is invoked in the constructor of Template
. In general, invoking overridable methods from a java constructor is considered bad practice, because the object in question has not necessarily been completely constructed (meaning that subclass constructor(s) have not yet run) when a base class constructor is being invoked.
Possible Solutions
There is no consensus in the wicket community on a standard pattern for solving this problem.
This is not entirely true. The core-devs have a consensus - which is to use Component#onBeforeRender() to construct subclass-driven dynamic component hierarchies. This approach is demonstrated towards the end of the page. - ivaynberg
. There are however a number of typical solutions.
Two-phase construction with an internal init() method called by subclasses
In this solution, an init()
method is declared in the base class, and subclasses are expected to:
- Override
init()
to do any setup work, rather than doing that work in the constructor. The constructor is instead used to do setup of the model as well as the initialization of any local data that is dependent on PageParameter data. - Call
super.init()
as the first line ininit()
, ensuring that init() mimics a constructor, so that the base class is set up before the subclass - Call
init()
at the end of their constructor(s), so that initialization is sure to happen - Use Component#onBeforeRender to construct dynamic subclass driven hierarchy
There is no way to ensure that the above set up constraints are obeyed, but it nevertheless provides a simple solution to the partially-constructed-object problem without reliance on any class outside the pages in question. The above example, adopting this strategy, would look like:
public abstract class Template extends WebPage { public Template(PageParameters params) { super(params); } protected void init() { add(new Label("headerTitle",getPageTitle())); } protected abstract String getPageTitle(); } public class class SomePage extends Template { Object myObject; public SomePage(PageParameters params) { super(params); long objectId = params.getLong("objectId"); myObject = SomeService.lookupById(objectId); init(); } protected String getPageTitle() { return "Editing: " + myObject.getSomeProperty(); } }
One remaining problem with this strategy is that if SomePage
is itself subclassed, there is no clean way to ensure that init() is called after the most-derived class is constructed, and ensure that it is called only once, without testing and setting an isInitialized
field.
Two-phase construction with an init() method called by the base class
In this variant of the last solution, subclasses do no work in their constructor, but instead set up all data in an init() method that is called by the base class before it calls any other overridable methods:
This is very error prone. You must ensure that no overridable methods are called, which is not really doable as code is refactored, etc. In fact this is equivalent to calling overridable methods from the constructor
public abstract class Template extends WebPage { public Template(PageParameters params) { super(params); init(); add(new Label("headerTitle",getPageTitle())); } protected void init() { } protected abstract String getPageTitle(); } public class class SomePage extends Template { Object myObject; public SomePage(PageParameters params) { super(params); } /** * override init() to setup myObject */ protected void init() { super.init(); long objectId = params.getLong("objectId"); myObject = SomeService.lookupById(objectId); } protected String getPageTitle() { return "Editing: " + myObject.getSomeProperty(); } }
This variant does not rely on subclasses to call init(), and allows for subclasses existing subclasses of Template, provided all overrides of init() call super.init().
Using Component#onBeforeRender
The problem of subclass-driven component hieararchies is easily solved using Component#onBeforeRender() callback and simple factory methods
public abstract class Template extends WebPage { protected void onBeforeRender() { if (get("left")==null) { // subclass-driven components not yet initilized add(newLeftPanel("left")); add(newRightPanel("right")); } /** cascades the call to its children */ super.onBeforeRender(); } /** factory method to create left panel */ protected abstract Component newLeftPanel(String id); /** factory method to create right panel */ protected abstract Component newRightPanel(String id); } // concrete template implementation which fills in factory methods public class MyPage extends Template { protected Component newLeftPanel(String id) { return new Label(id, "i am left"); } protected Component newRightPanel(String id) { return new Label(id, "i am right"); } }