< Magazine />


Apache Wicket - Best practices (german)

Tuesday, November 15, 2011 by Carsten Hufe   Tags:  apache   wicket   best   practice 

Apache Wicket erfreut sich immer steigender Popularität und findet mehr und mehr Einsatz in Projekten. Dank der Mächtigkeit von Wicket lassen sich viele Features einfach und schnell realisieren. Für die Umsetzung dieser Features gibt es viele Wege. Dieser Artikel bietet einige Kochrezepte zum richtigen, effizienten und nachhaltigen Umgang mit Apache Wicket.

 

Dieser Artikel richtet sich an Entwickler, die bereits erste Erfahrungen mit Apache Wicket gesammelt haben. Entwickler, die in Wicket-Welt einsteigen tun sich oftmals schwer, weil sie Entwicklungsmethoden aus der JSF- oder Struts-Welt adaptieren. Diese Frameworks setzen vorrangig auf prozedurale Programmierung. Wicket hingegen setzt massiv auf Objektorientierung. Also vergessen Sie die Struts und JSF-Patterns, sonst werden Sie nicht lange Freude an Wicket haben.

Kapseln Sie die Komponenten richtig

Eine Komponente sollte in sich geschlossen sein. Ein Nutzer der Komponente sollte nichts über deren internen Aufbau wissen müssen, um sie zu verwenden. Interessant sind für ihn nur die externen Schnittstellen. Erbt eine Komponente von der Klasse Panel, so muss sie ihr eigenes HTML-Template mitbringen. Erbt hingegen eine Komponente von den Klassen WebMarkupContainer oder Form, so bringen diese kein eigenes Markup mit. Dies hat zur Folge, dass diese keine vererbten Komponenten besitzen sollten.

// Schlechte Komponente
public class RegistrationForm extends Form<Registration> {
    public RegistrationForm(String id, IModel<Registration> regModel) {
        super(id, new CompoundPropertyModel<Registration>(regModel))
        // Falsch: RegistrationForm bringt eigene hinzugefügte Komponenten mit
        add(new TextField("username"));
        add(new TextField("firstname"));
        add(new TextField("lastname"));
    }
}

Listing 1

 

Listing 1 ist ein Beispiel für eine schlechte Komponente. Hier muss der Nutzer dieses RegistrationForm’s genau wissen wie sich das Markup aufbaut.

public class RegistrationPage extends Page {
    public RegistrationPage(IModel<Registration> regModel) {
        Form<?> form = new RegistrationForm("form");
        form.add(new SubmitButton("register") {
            public void onSubmit() {
                 // do something               
            }
        });
        add(form);
    }
}

<html>
<body>
    <form wicket:id="form">
        <!-- Dies sind interne Informationen aus RegistrationForm -->
        Benutzername <input type="text" wicket:id="username"/>
        Vorname <input type="text" wicket:id="firstname"/>
        Nachname <input type="text" wicket:id="lastname"/>
        <!-- Ab hier kommen neue Komponenten die aus der Seite stammen und die der Nutzer kennt  -->
        <input type="submit" wicket:id="register" value="Registrieren"/>
    </form>
</body>
</html>

Listing 2

 

Listing 2 zeigt die Verwendung der schlechten Komponente im Java-Code und das entsprechende Markup, welches für die RegistrationPage benötigt wird. Hier erkennt man, dass die Input-Felder firstname, lastname und username verwendet werden obwohl diese nicht explizit in der RegistrationPage hinzugefügt wurden. Dies ist schlecht, da aus der RegistrationPage Klasse nicht ersichtlich ist, dass diese Komponenten vorhanden sind.

// Gute Komponente
public class RegistrationInputPanel extends Panel{
    public RegistrationInputPanel(String id, IModel<Registration> regModel) {
        super(id, regModel);
        IModel<Registration> compound = new CompoundPropertyModel<Registration(regmodel)
        Form<Registration> form = new Form<Registration>("form", compound);
        // Richtig: Hier erfolgt das Komponenten hinzufügen über die Instanz-Variable
        form.add(new TextField("username"));
        form.add(new TextField("firstname"));
        form.add(new TextField("lastname"));
        add(form);
    }
}

<html>
<body>
    <wicket:panel>
    <form wicket:id="form">
        Benutzername <input type="text" wicket:id="username"/>
        Vorname <input type="text" wicket:id="firstname"/>
        Nachname <input type="text" wicket:id="lastname"/>
    </form>
    </wicket:panel>
</body>
</html>

Listing 3

 

Listing 3 zeigt nun eine sauber geschnittene Eingabe-Komponente, die Ihr eigenes Markup mitliefert. Außerdem sehen wir hier die richtige Verwendung des Forms. Es werden nur Komponenten von außen über form.add(Component) hinzugefügt. Behaviors und Validatoren hingegen können auch über Vererbung hinzukommen.

public class RegistrationPage extends Page {
    public RegistrationPage(IModel<Registration> regModel) {
        Form<?> form = new Form("form");
        form.add(new RegistrationInputPanel("registration", regModel);
        form.add(new SubmitButton("register") {
            public void onSubmit() {
              // do something               
            }
        });
        add(form);
    }
}

<html>
<body>
    <form wicket:id="form">
        <div wicket:id="registration">
           Hier erscheint das RegistrationInputPanel
        </div>
        <input type=”submit” wicket:id="register" value="Registrieren"/>
    </form>
</body>
</html>

Listing 4

 

Listing 4 zeigt die Verwendung des RegistrationInputPanel’s. Es ist kein Markup mehr aus einer anderen Komponente vorhanden, sondern nur noch Markup von Komponenten die direkt hinzugefügt werden. Die RegistrationPage bringt ihr eigenes Formular mit, welches beim Submit an alle unteren Wicket-Formulare den Submit weiter delegiert.

Speichern Sie Models und Seiteninformationen in Member-Variablen

Im Gegensatz zu Struts sind in Wicket die Instanzen von Pages und Komponenten keine Singletons, sondern sessionbezogene Instanzen. Dadurch bietet sich die Möglichkeit userspezische und flowgebundene Informationen innerhalb dieser Komponenten und Pages abzulegen. Die Informationen sollten in Member-Variablen abgelegt werden. So kann innerhalb der gleichen Klasse auf die Informationen zugegriffen werden und man vermeidet lange Methoden-Signaturen an die man die Informationen weiterreichen muss. Wicket-Pages sind somit stateful. Trotzdem können sie über mehrere Requests bestehen. Beispielsweise eine Seite mit einem Formular das beim Abschicken einen Validierungsfehler produziert. Hier wird die selbe Page-Instanz verwendet. Eine weitere Möglichkeit, wo die selbe Page-Instanz verwendet wird, ist wenn ein User den Back-Button drückt und auf die vorherige Seite gelangt und das Formular noch einmal abschickt. Informationen die von außen über den Konstruktur hinein gegeben werden, sollten einer Member-Variable zugewiesen werden (in der Regel sind dies Models). Bei der Ablage von Informationen in Member-Variablen ist jedoch darauf zu achten, dass diese serialisierbar sind, denn in Wicket werden die erstellten Pages in der Pagemap abgelegt. Die Pagemap speichert standardmäßig die Pages auf der Festplatte. Sind die Informationen nicht serialisierbar führt dies zwangsläufig zu NullPointerExceptions und NonSeriablizableExceptions. Weiterhin sollten große Informationsblöcke - wie Binärdaten - auch nicht direkt in einer Member-Variable abgelegt werden, da dies zu Performance-Einbußen beim Serialisieren führen kann. Hierfür sollte zum Beispiel ein LoadableDetachableModel verwendet werden, welches in einer Member-Variable abgelegt werden darf, da sich dieses um das Laden und Speicherfreigeben kümmert.

Wicket IDs richtig benennen

Benennung spielt für viele Entwickler eine nebensächliche Rolle ist aber eine der grundlegensten Themen in der Softwareentwicklung. Anhand der richtigen Benennung von Variablen und Methoden identifiziert man schnell die fachlichen Aspekte eines Softwarebestandteils. Gute Benennung vermeidet zudem überflüssige Kommentare.

Schlechte Benennungen für Wicket-IDs sind zum Beispiel birthdateTextField, firstnameField oder addressPanel. Warum? In der Benennung sind zwei Aspekte vorhanden: Der technische Aspekt "TextField" und der fachliche Aspekt "birthdate". Interessant ist aber nur der fachliche Aspekt, da im HTML Template durch <input type="text"/> und im Java-Code durch new TextField die technischen Aspekte schon beschrieben sind. Zudem erhöht diese falsche Benennung den Aufwand bei technischen Umbauten. Sollte zum Beispiel das TextField durch einen DatePicker ausgetauscht werden, so müssten zusätzlich alle IDs in birthdateDatePicker mit umbenannt werden. Ein weiterer Grund den technischen Aspekt nicht in die Wicket-ID Benennung mit aufzunehmen ist das CompoundPropertyModel. Dabei wird das Model in einem Formular auf die Kind-Formelemente delegiert (siehe Listing 3). So wird beim TextField username automatisch setUsername() oder getUsername() auf dem Registration-Objekt aufgerufen. Hier wäre eine Benennung setUsernameTextfield() äußerst unpraktisch.

Vermeiden Sie Veränderungen am Komponentenbaum

Den Wicket-Komponentenbaum sollte man sich als ein starres Gerüst vorstellen, welches erst zum Leben erweckt wird, wenn es mit einem Model gefüllt ist, ähnlich einem Roboter ohne Gehirn. Ohne Gehirn kann er nichts und ist nur eine starre Hülle. Füllt man ihn jedoch mit Informationen, so wird er zum Leben erweckt und führt Handlungen aus. Einzelne Komponenten können selbst über Ihren Zustand entscheiden, z.B. über die Sichtbarkeit.
In Wicket sollte der Komponentenbaum so wenig wie möglich manipuliert werden, das heißt die Nutzung von Methoden wie Component.replace(Component) und  Component.remove(Component) sollte vermieden werden. Die Verwendung dieser Methoden deutet auf die Nicht- oder Falsch-Verwendung  von Models hin. Weiterhin sollten Komponentenbäume nicht konditional aufgebaut werden (siehe Listing 5).

// typisch struts
if(MySession.get().isNotLoggedIn()) {
    add(new LoginBoxPanel("login"))
}
else {
    add(new EmptyPanel("login"))   
}

Listing 5

 

Statt das LoginBoxPanel konditional aufzubauen, empfiehlt sich das Panel immer hinzuzufügen und die Sichtbarkeiten über setVisibityAllowed(boolean) zu steuern. So kann innerhalb des LoginBoxPanel entschieden werden, ob es angezeigt wird oder nicht. Wir verschieben die Verantwortlichkeit für die Sichtbarkeit des Logins direkt in die Komponente, die den Login ausführen soll. Genial. Fachlichkeit wird sauber gekapselt. Es erfolgt keine Entscheidung mehr von Außen. In "Sichtbarkeiten von Komponenten" folgt hierzu ein Beispiel.

Sichtbarkeiten von Komponenten richtig implementieren

Dieser Absatz wurde am 15.11.2011 aktualisiert.

Sichtbarkeiten von Seitenbestandteilen sind ein wichtiges Thema. In Wicket wird die Sichtbarkeit über die Methoden isVisible() und setVisible() gesteuert. Diese Methoden befinden sich innerhalb der Wicket-Basis-Klasse Component und betreffen somit ausnahmslos jede Komponente und Page. Kommen wir zu einem konkreten Beispiel, dem LoginBoxPanel. Das Panel wird nur dann angezeigt, wenn ein Benutzer nicht eingeloggt ist.

// Schlechte Implementierung
LoginBoxPanel loginBox = new LoginBoxPanel("login");
loginBox.setVisible(MySession.get().isNotLoggedIn());
add(loginBox);

Listing 6

 

Listing 6 zeigt wiederum eine schlechte Implementierung, denn es wird bereits beim Instanziieren der Seite eine Entscheidung über die Sichtbarkeit der Komponente getroffen. In Wicket werden Instanzen von Pages und Komponenten über mehrere Requests hin verwendet. Um die gleiche Instanz der Seite nach einem Login weiter verwenden zu können, müsste man loginBox.setVisible(false) aufrufen. Äußerst unpraktisch, wir müssen uns jedesmal explizit um das Setzen der Sichtbarkeit kümmern. Leider lässt es sich nicht vermeiden die Informationen zu duplizieren, d.h. visible entspricht "nicht eingeloggt". So haben wir zwei gespeicherte States, einmal für den fachlichen Aspekt "nicht eingeloggt" und einmal für den technischen Aspekt visible. Früher habe ich empfohlen die Methode isVisible() zu überschreiben, um die Dupizierung zu vermeiden. Heute tue ich dies nicht mehr, da nicht immer garantiert werden kann wann und wie häufig diese Methode aufgerufen wird. Dies kann auch Seiteneffekte mit sich bringen. Um diese States mit möglichst wenig Aufwand synchron zu halten empfiehlt es sich die Methode onConfigure() zu überschreiben und dort setVisibilityAllowed(boolean) aufzurufen. Es sollte die Methode setVisibilityAllowed(boolean) aufgerufen werden, da isVisibilityAllowed() final ist und so im Gegensatz zu isVisible() nicht überschrieben werden kann. Im Diagramm sehen Sie den Ablauf der Aktionen und die dazugehörigen Aufgaben. Durch dieses Prinzip fallen allein drei Aufgaben weg und es muss nur noch das LoginBoxPanel instanziiert werden, welches seine Sichtbarkeit selbst steuert.

 

Login Panel

public class LoginBoxPanel {
    // Konstruktor etc
    @Override
    protected void onConfigure() {
        setVisibilityAllowed(MySession.get().isNotLoggedIn());
    }
};

Listing 7

 

In Listing 7 wird die Steuerung über die Sichtbarkeit umgekehrt, jetzt entscheidet das LoginBoxPanel über seine Sichtbarkeit selbstständig. Jedes mal, wenn die Seite gerendert wird, also wenn onConfigure() aufgerufen wird, wird eine neue Auswertung über den Login-Zustand erfragt, so werden keine alten Informationen über die Sichtbarkeit gehalten. Die Logik ist genau auf einer Zeile Code zentralisiert und nicht mehr breit im Code gestreut. Weiterhin lässt sich der technische Aspekt visibility für die fachliche Anforderung “nicht eingeloggt” herauslesen. Äquivalent gilt die Beschreibung für die Methode isEnabled(), nur dass hier die Methode setEnabled() anstelle von setEnabledAllowed() aufgerufen wird. Bei isEnabled() werden die Komponenten ausgegraut dargestellt. Formulare die sich innerhalb einer deaktivierten oder unsichtbaren Komponente befinden werden nicht ausgeführt. Es gibt Fälle in denen man um setVisiblilityAllowed() außerhalb der onConfigure()-Methode nicht herumkommt. Beispiel: Der User klickt einen Button, um das Registrierungsformular aufzuklappen. Generell gilt, wenn Komponenten datengetrieben sind, so wird setVisibiltyAllowed() innerhalb von onConfigure() gesetzt. Usergesteuerte Aktionen rufen direkt setVisibilityAllowed(boolean) auf, jedoch innerhalb eines Events wie zum Beispiel onClick(). Die Methoden können auch mit einer Inline-Implementierung überschrieben werden (siehe Listing 8).

new Label("headline", headlineModel) {
    @Override    
    protected void onConfigure() {
     // Verstecke Überschrift wenn Berlosconi drin vorkommt
        String headline = getModelObject();
        return headline.startWith("Berlosconi")
    }
}

Listing 8

Verwenden Sie ausschließlich Models

Verwenden Sie ausschließlich Models! Geben Sie keine rohen Objekte direkt an Komponenten weiter. Pages und Komponenten können über mehrere Request-Zyklen bestehen. Wenn rohe Objekte verwendet werden, so können diese nicht nachträglich austauscht werden. Ein Beispiel hierfür könnte zum Beispiel eine Entity sein, die in einem LoadableDetachableModel mit jedem Request neugeladen wird. Über den EntityManager würde jedesmal ein neues Objekt erzeugt werden und die Seite würde noch die alte Instanz halten. Übergeben Sie im Konstruktor immer IModel<MeinObjekt> (siehe Listing 9).

public class RegistrationInputPanel extends Panel{
    // Richtig: Das Objekt Registration wird innerhalb eines IModels übergeben
    public RegistrationInputPanel(String id, IModel<Registration> regModel) {
        // add components
    }
}

Listing 9

 

Mit der Lösung in Listing 9 kann sich hinter dem Model jede Implementierung verbergen. Angefangen von der Klasse Model über das PropertyModel bis hin zur eigenen Implementierung von LoadableDetachableModel, welche die Werte automatisch lädt und persistiert. Die Model-Implementierungen werden so leicht austauschbar. Sie - als Nutzer - interessiert nur Folgendes: wenn IModel.getObject() aufgerufen wird, erhält man ein Objekt vom Typ Registration. Woher dieses Objekt kommt ist Aufgabe der Model-Implementierung und aufrufenden Komponente. Das Model können Sie beispielsweise beim Instanziieren der Komponenten übergeben und weiterreichen. Sollten sie keine Models verwenden, werden Sie früher oder später in die Verlegenheit kommen, den Komponentenbaum zu manipulieren. Womit Sie wiederum Informationen über States duplizieren und somit produzieren Sie schlecht wartbaren Code. Ein weiterer Punkt warum Sie Models verwenden sollten ist die Serialisierung. Objekte die direkt ohne Models in den Komponenten und Pages in Member-Variablen gespeichert werden, werden mit jedem Request serialisiert und deserialisiert. Dies kann unter Umständen unperformant sein.

Entpacken Sie keine Models innerhalb der Konstruktor-Hierarchie

Vermeiden Sie es, Wicket-Models innerhalb der Konstruktor-Hierarchie zu entpacken, d.h. rufen Sie innerhalb der Konstruktor-Hierarchie IModel.getObject() nicht auf. Wie bereits erwähnt kann eine Page-Instanz mehrere Request-Zyklen überleben, so halten Sie dann veraltete und redundante Informationen. Wicket-Models dürfen bei Events entpackt werden (User-Aktionen), also Methoden wie onUpdate(), onClick() oder onSubmit() (siehe Listing 10).

new Form("register") {
    public void onSubmit() {
        Registration reg = registrationModel.getObject()
        userService.register(reg);
    }
}

Listing 10

 

Eine weitere Möglichkeit das Model zu entpacken ist durch das Implementieren der Methoden isVisible(), isEnabled() oder onBeforeRender().

Reichen Sie Models immer an die Komponenten weiter

Reichen Sie Models immer an die Komponente weiter von der Sie erben. So wird sicher gestellt, dass am Ende jedes Requests die Methode IModel.detach() aufgerufen wird. Diese sorgt dafür, dass Informationen aufgeräumt werden. Beispielsweise haben Sie ein eigenes Model implementiert, welches in der detach()-Methode die Daten persistiert. Ist dieses Model an keiner Komponente weitergegeben worden, so wird die Methode detach() niemals aufgerufen und die Daten somit nicht persistiert. Ein musterhafte Übergabe an den super-Konstruktor sehen Sie in Listing 11.

public class RegistrationInputPanel extends Panel{
    public RegistrationInputPanel(String id, IModel<Registration> regModel) {
        super(id, regModel)
        // add components
    }
}

Listing 11

Validatoren dürfen keine Models und Daten verändern

Validatoren sollen nur validieren. Beispielsweise ein Formular, welches die Kontodaten eines Kunden erfasst. An dem Formular hängt ein BankFormValidator, welcher die Bankdaten über einen Webservice prüft und den Banknamen korrigiert. Niemand rechnet damit, dass ein Validator Informationen verändert. Solche Logik gehört nach Form.onSubmit() oder in die Event-Logik eines Buttons.

Komponenten sollten nicht an Konstruktoren übergeben werden

Übergeben Sie Komponenten oder Pages nicht über den Konstruktor an andere Komponenten weiter.

// Schlechte Lösung
public class SettingsPage extends Page {
    public SettingsPage (IModel<Settings> settingsModel, final Webpage backToPage) {
        Form<?> form = new Form("form");
        // add components
        form.add(new SubmitButton("changeSettings") {
            public void onSubmit() {
               // do something   
               setResponsePage(backToPage)           
            }
        });
        add(form);
    }
}

Listing 12

Schauen Sie sich Listing 12 an: Die SettingsPage erwartet im Konstruktor die Seite zu der sie nach einem erfolgreichen Submit zurückspringen soll. Diese Variante funktioniert, ist aber äußerst unflexibel und unschön. Man muss bereits zum Zeitpunkt des Instanziierens der SettingsPage wissen, wohin man den Benutzer leitet. Dies setzt eine Instanzierungsreihenfolge voraus. Viel besser wäre es die Anwendungen nach der fachlichen Reihenfolge zu strukturieren. Die Lösung ist wiederum das Hollywood-Prinzip. Hierfür erzeugen wir eine abstrakte Methode oder einen Hook (Listing 13).

// Gute Lösung
public class SettingsPage extends Page {
    public SettingsPage (IModel<Settings> settingsModel) {
        Form<?> form = new Form("form");
        // add components
        form.add(new SubmitButton("changeSettings") {
            public void onSubmit() {
               // do something
               // e.g. persist data   
               onSettingsChanged()   
            }
         });
         add(form);
    }

    // Hook
    protected void onSettingsChanged() {
    }
}

// Und die Verwendung der neuen Komponente
Link<Void> settings = new Link<Void>("settings") {
    public void onClick() {       
        setResponsePage(new SettingsPage(settingsModel) {
            @Override
            protected void onSettingsChanged() {
               // Referenz der aktuellen Seite
               setResponsePage(MyPage.this);
            }
        });
    }
}
add(settings);

Listing 13

 

Die Variante aus Listing 13 hat ersteinmal mehr Code ist aber deutlich flexibler und aussagekräftiger. Wir wissen, dass es ein Event onSettingsChanged() gibt und dieses wird nach dem Ändern aufgerufen. Zudem ist es möglich weitaus mehr Code auszuführen als nur die Back-Seite zu setzen, beispielsweise kann man zusätzliche Informationen ausgeben oder andere Informationen persistieren.

Die Wicket-Session nur für globale Informationen nutzen

Die Wicket-Session ist ein typisiertes Objekt. Es werden keine Informationen mehr über eine Map-Struktur gespeichert. Benutzen Sie die Wicket-Session nur für globale Informationen. Authentifizierung ist das Parade-Beispiel für globale Informationen. Die Login- und User-Informationen werden im Normalfall auf fast jeder Seite benötigt. Im Beispiel von einem Blog ist es sinnvoll zu wissen, ob es sich um einen Author handelt der Beiträge verfassen darf. So wird ein Link zum Bearbeiten ein- oder ausgeblendet. Generell gehört sämtliche Logik zur Authentifizierung in die Wicket-Session, da sie normalerweise applikationsweit benötigt wird. Und wenn nicht, dann ist es trotzdem gut, dass die Logik in der Session liegt, da man sie dort erwarten würde.
Daten von Formularen, die sich beispielsweise über mehrere Seiten erstrecken, haben in der Session nichts verloren. Diese Daten können beim Instanziieren der Seiten über Models von einer Seite auf die Nächste weitergereicht werden (Listing 14). So haben die Models und Daten einen festen Lebenszyklus über den Seitenverlauf.

public class MyPage extends WebPage {
    IModel<MyData> myDataModel

    public MyPage(IModel<MyData> myDataModel) {
        this.myDataModel = myDataModel;
        Link<Void> next = new Link<Void>("next") {
             public void onClick() {       
                  // do something
                  setResponsePage(new NextPage(myDataModel));
             }
        }
        add(next);
    }
}

Listing 14

 

So wie im Listing 14 werden konkrete Informationen über die Pages direkt weitergereicht. Sämtliche Models können bedenkenlos in Member-Variablen gespeichert werden. Wicket-Pages sind im Gegensatz zu Struts-Actions keine Singletons, sondern userspezifische Instanzen. Der große Vorteil dieses Vorgehens ist, dass die Daten automatisch aufgeräumt werden, wenn der Benutzer den Page-Flow beendet hat oder aus ihm vorzeitig aussteigt. Nie wieder manuelles aufräumen! Dies ist quasi ein Garbage-Collector für Ihre Session.

Verwenden Sie keine Factories für Komponenten

Das Factory-Pattern ist ein nützliches Pattern, jedoch für Wicket-Komponenten ungeeignet.

public class CmsResource {
   public Label getCmsLabel(String markupId, final String url) {
       IModel<String> fragment = new AbstractReadOnlyModel<String>() {
          @Override
          public String getObject() {
             return loadSomeContent(url);
          }
       };
       Label result = new Label(markupId, fragment);
       result.setRenderBodyOnly(true);
       result.setEscapeModelStrings(false);
       return result;
   }

   public String loadContent(String url) {
      // load some content
   }
}

// Erstellen der Komponente innerhalb einer Page:
public class MyPage extends WebPage {
   @SpringBean
   CmsResource cmsResource;
   
   public MyPage() {
      add(cmsFactory.getCmsLabel("id", "http://url.to.load.from"));
   }
}

Listing 15

 

Die Lösung in Listing 15 für das Hinzufügen des Labels aus der CmsFactory sieht zunächst nicht schlecht aus, bringt aber Nachteile mit sich. Es ist nun nicht mehr möglich mit Vererbung zu arbeiten, es ist nicht mehr möglich Methoden (wie bsp. onClick()) zu überschreiben. Es ist auch nicht mehr möglich eine Inline-Klasse zu erstellen, diese werden in Wicket sehr oft verwendet. Die Factory könnte auch ein Spring-Service sein, der eine Komponente instanziiert. Die richtige Variante wäre hier ein CmsLabel zu erstellen (Listing 16).

public class CmsLabel extends Label {
   @SpringBean
   CmsResource cmsResource;
   public CmsLabel(String id, IModel<String> urlModel) {
      super(id, urlModel);
      IModel<String> fragment = new AbstractReadOnlyModel<String>(){
         @Override
         public String getObject() {
            return cmsResource.loadSomeContent(urlModel.getObject());
         }
      };
      setRenderBodyOnly(true);
      setEscapeModelStrings(false);
   }
}

// Erstellen der Komponente innerhalb einer Page:
public class MyPage extends WebPage {
   public MyPage() {
      add(new CmsLabel("id", Model.of("http://url.to.load.from")));
   }
}

Listing 16

 

In Listing 16 ist das Label sauber in der Komponente gekapselt und das ohne Factory. Es ist problemlos möglich eine Inline-Implementierung zu erstellen und so Methoden zu überschreiben. Jetzt folgt das Argument: "Ich benötige eine Factory für das Initialisieren von diversen Werten in der Komponente.". Hierfür gibt es in Wicket die IComponentInstantiationListener, diese werden direkt im super-Konstruktor von der Component-Klasse aufgerufen. Das bekannteste Beispiel ist der SpringComponentInjector, welcher dafür sorgt, dass Spring-Beans bei der Annotation @SpringBean injected werden. Hier können problemlos weitere IComponentInstantiationListener geschrieben und hinzugefügt werden. Somit gibt es kein Argument mehr, welches für eine Factory spricht. Weitere Informationen zum IComponentInstantiationListener finden Sie in der JavaDoc.

Jede Seite und Komponente erwartet einen Test

Jede Seite und Komponente sollte einen entsprechenden Test besitzen. Der Basis-Test rendert einfach die Komponente und prüft, ob diese technisch korrekt ist. Beispielsweise sei genannt, ob für jede Kind-Komponente eine entsprechende Wicket-ID im Markup vergeben ist. Ist eine Wicket-ID nicht korrekt gebunden so schlägt der Test fehl. Ein weiterführender Test könnte zum Beispiel ein Formular sein bei dem ein entsprechender Backend-Call stattfindet und man über einen Mock diesen Call validiert. Auf jeden Fall können so technische und fachliche Fehler bereits im Build-Prozess erkannt und behoben werden. Auch für Test-Driven Development ist Wicket bestens geeignet. Stellen Sie sich vor, Sie erstellen eine Seite und bevor der Server hochfahren wird, lassen Sie den Unit-Test laufen, der Ihnen sagt, dass Sie eine Wicket-ID nicht gebunden haben. Nachdem der Fehler behoben und der Unit-Test erfolgreich durchgelaufen ist, fahren Sie den Server hoch. Einen Server hochzufahren dauert länger als einen Unit-Test auszuführen. Dies verkürzt den Entwicklungs-Turnaround deutlich. Einziger Nachteil beim WicketTester ist, dass sich AJAX-Komponenten schwer testen lassen. Jedoch sind die Test-Möglichkeiten, die Wicket bereits bietet deutlich größer als bei jedem anderen Web-Framework.

Interaktion mit anderen Servlet-Filtern vermeiden

Bleiben Sie so lang sie können innerhalb der Wicket-Welt. Vermeiden Sie die Verwendung von Servlet-Filtern, hierfür kann man den WebRequestCycle verwenden, wo die Methoden onBeginRequest() und onEndRequest() existieren. Die HttpSession ist genauso tabu. Das Äquivalent hierfür ist die WebSession von Wicket, einfach von WebSession erben und in der Application-Klasse, durch das Überschreiben der newSession()-Methode, registrieren. Es gibt wenige Ausnahme-Fälle, um auf die Servlet-Schnittstellen zuzugreifen. Ein Beispiel ist, wenn ein externes Cookie auswertet werden muss, um den User zu authentifizieren. Diese Schnittpunkte sollten möglichst gekapselt und minimiert werden. Für dieses Beispiel könnte man die Auswertung des Cookies in der Wicket-Session erledigen, da dies eine Authentifizierung ist.

Schneiden Sie kleine Klassen und Methoden

Vermeiden Sie monolithische Klassen. Oft kommt es vor, dass Entwickler alles in den Konstruktor packen. Diese Klassen werden sehr schnell unübersichtlich, da in Wicket oft Inline-Implementierungen über mehrere Ebenen gemacht werden. Gruppieren sie logische Einheiten und extrahieren Sie hierfür eigene Methoden mit einer fachlich korrekten Bezeichnung. Dies steigert die Übersicht und das Verständnis für den fachlichen Hintergrund der Komponenten. Navigiert ein Entwickler in die Komponente so interessiert ihm Anfangs nicht die technische Ausprägung, sondern die Fachliche. Um detaillierte technische Informationen zu erhalten navigiert man schließlich in die Methoden. Im Zweifel sollten Sie auch in Betracht ziehen eigene Komponenten herauszuschneiden. Kleinere Komponenten erhöhen die Wahrscheinlichkeit der Wiederverwendung und lassen sich deutlich einfacher testen. Listing 17 zeigt ein Beispiel für eine mögliche Strukturierung Ihrer Komponenten.

public class BlogEditPage extends WebPage {
    private IModel<Blog> blogModel;

    public BlogEditPage(IModel<Blog> blogModel) {
        super(new PageParameters());
        this.blogModel = blogModel;
        add(createBlogEditForm());
    }

    private Form<Blog> createBlogEditForm() {
        Form<Blog> form = newBlogEditForm();
        form.add(createHeadlineField());
        form.add(createContentField());
        form.add(createTagField());
        form.add(createViewRightPanel());
        form.add(createCommentRightPanel());
        form.setOutputMarkupId(true);
        return form;
    }
    
    // more methods here
}

Listing 17

Das Argument "Schlechte Dokumentation"

Des öfteren höre ich, dass Wicket über eine schlechte Dokumentation verfügt. Dieses Argument stimmt nur zum Teil. Es gibt aber eine Menge an Wicket-Code Beispielen, welche man als Vorlagen verwenden kann. Zudem gibt es eine große Community die innerhalb kürzester Zeit auch komplexeste Fragen klärt. In Wicket ist es recht schwierig alles zu dokumentieren, da fast alles austauschbar und erweiterbar ist. Reicht einem eine Methode oder Implementierung nicht aus, so wird diese erweitert oder überschrieben. Die Arbeit mit Wicket gleicht einem ständigem Navigieren durch den Code. Als Beispiel  seien die Validatoren genannt. Wie finde ich alle Validatoren heraus die es gibt? Man öffne das Interface IValidator (Eclipse Strg + Shift + T) und anschließend die Type-Hierachy (Strg + T) und schon haben wir alle Validatoren im Überblick.

 

Type Hierachy

 

Fazit

Die Ratschläge sollten Ihnen helfen, besseren und wartbaren Code in Wicket zu schreiben.
Alle beschriebenden Methodiken wurden bereits erfolgreich in mehreren Wicket-Projekten erprobt. Wenn Sie diese Ratschläge befolgen sind Ihre Wicket-Projekte gut für die Zukunft gerüstet und werden mit Sicherheit ein Erfolg.


Links:
1. http://wicket.apache.org/ (Apache Wicket)
2. http://wicketstuff.org/wicket14/ (Wicket Examples)
3. http://en.wikipedia.org/wiki/Hollywood_Principle

 

Vielen Dank an Daniel Bartl für das Korrektur lesen.

 

© 2009-2012 - www.devproof.org