Suche
Teil 20: Backend meets Frontend: Trainer for kids 10

Änderungen im UI schneller sehen und umsetzen

Sven Ruppert

© Shutterstock / HelenField

Kleine Änderungen am User Interface sollten sich auch fix testen lassen, ohne dass der Entwickler ewiglange Klickstrecken abarbeiten und unnötigen Ladezeiten hinnehmen muss. Ein klar getrenntes Komponentenmodell hilft auch hier weiter.

In unserem letzten Teil haben wir begonnen, dem Benutzer eine grafische Auswertung seiner Ergebnisse zu generieren. Dabei wurde ausschließlich auf die Daten zurückgegriffen, die sich derzeitig in der Session befinden. Die Darstellung erfolgt mithilfe des Vaadin Add-ons Charts. Wenn man beginnt sich mit Charts auseinanderzusetzen, kommt man schnell an den Punkt, an dem man ausprobieren möchte, wie sich die einzelnen Konfigurationsmöglichkeiten der gewählten Elemente auf dem Bildschirm auswirken. Heute werden wir uns ansehen, wie wir die Entwicklung in diesem Bereich beschleunigen können.

Zusätzlich zu den Quelltextbeispielen zu diesem Artikel verwende ich auch die Sourcen des Open-Source -Projekts Functional-Reactive. Die Sourcen befinden sich auf GitHub. Ebenfalls werde ich damit beginnen, funktionale Aspekte in die Entwicklung einfließen zu lassen. Hierzu stütze ich mich auf die Serie hier auf JAXenter unter dem Namen Checkpoint Java.

Die Herausforderung ist, dass die Darstellung innerhalb der Anwendung erfolgt. Die Anwendung selbst ist mit einem Log-in-Prozess versehen. Werden Änderungen an der Komponente durchgeführt, ist der Weg zu dem Ergebnis länger als er sein sollte. Im schlimmsten Fall wird der Servlet-Container neu gestartet und der Entwickler navigiert manuell zu der Komponente, um das Ergebnis zu validieren. Das ist leider viel zu oft wirklich so realisiert, aber viel zu teuer und zeitaufwändig.

Der erste Schritt besteht darin, die Komponente zu isolieren und die Modifikationen mittels Komponententests zu validieren. Es ist also immer wieder derselbe Weg zu durchlaufen, bestehend aus vier Schritten:

  • Modifikation der Sourcen
  • Übersetzen der beteiligten Sourcen
  • Aktivieren der Änderungen
  • Überprüfung der Ergebnisse

Da es sich hier um rein visuelle Dinge handelt, ist es schwer mit jUnit das Ergebnis der Tests zu formulieren, die einem Zielbild entsprechen. Bei einer Berechnung ist das leicht, hat man – meistens zumindest – ein Ergebnis klar vor Augen. Wie aber ist das mit den grafischen Ergebnissen? Später kann man auf die Methode zurückgreifen, mittels der ein Vergleich gegen einen Referenz-Screenshot erfolgt. Soweit sind wir an dieser Stelle aber noch nicht.

Kurz zur Erinnerung. Das Balkendiagramm ist in der Komponenten CalcComponent zum Einsatz gekommen. Dort sollte die Häufigkeit sowohl der positiv als auch negativ beantworteten Rechenaufgaben angezeigt werden (Abb. 1).

Abb. 1: Auswertung der Häufigkeit der richtigen und falschen Antworten

Wenn man mit diesem Diagramm ein wenig experimentieren möchte, um das optimale Ergebnis für diesen einen Fall zu erreichen, ist der Workflow in der bisherigen Form viel zu aufwändig. Von Vorteil ist es, wenn von der Änderung des Quelltextes bis hin zur visuellen Kontrolle möglichst wenig Zeit vergeht.

Das Vorgehen dafür ist recht einfach. Die Komponente muss lediglich in der Instanz einer UI verwendet werden. Die Komponenten sollte so unabhängig sein, das sie keiner weiteren Grundgerüste der Anwendung selbst benötigt. Nur so kann man sicherstellen, das die Komponente auch wirklich unabhängig ist. Man kann das schon als eine Art ersten Test ansehen.

Bisher hatten wir in unserem Projekt die Initialisierung des Servlet-Containers im Modul microkernel direkt im Projekt selbst vorgehalten. Das werden wir nun ändern, da auch dieses Modul eine vollständig unabhängige Komponente sein sollte. In unserem Fall wird das Projekt Java Microservice verwendet, das sich auf GitHub befindet. Bei diesem Projekt handelt es sich um eine erweiterte Version der bisher hier verwendeten Quelltexte.

Da nun der Servlet-Container extrahiert ist und die eigenen Sourcen aus dem Projekt gelöscht wurden, kann damit begonnen werden, dessen Verwendung für die beiden bisher definierten Fälle zu konfigurieren: Zum einen ist es die Verwendung als Applikations-Servlet-Container und zum anderen kommt nun hinzu, den Servlet-Container für die Komponententests zu verwenden.

Komponentenentwicklung: In kleine Teile teilen

Um die Komponente, die bearbeitet werden soll, grafisch anzuzeigen, wurde in einem der vorherigen Teile eine minimale UI erzeugt, mit der die Komponente mittels ComboBox ausgewählt werden konnte. Dieses Verfahren werden wir auch weiterhin anwenden und erweitern. Um die Sourcen von den reinen jUnit-Tests ein wenig zu trennen, beginnen wir im Quelltextverzeichnis für die Testquelltexte (src/test/java) mit einem neuen Haupt-Package. Der Name ist dev, um zu verdeutlichen, dass es sich um Quelltexte handelt, die im aktiven Entwicklungsprozess verwendet werden. Man kann es als eine Art Werkzeug für den Entwickler sehen.

Um einen Servlet-Container zu starten erzeugen, wir eine Klasse DevMain und implementieren darin die Methode main. In dieser Methode initialisieren wir den Dependency-Injection-Mechanismus, um sicherzustellen, dass sowohl die produktiven Klassen als auch die Klassen aus dem Bereich dev im Depenency-Injection-Graphen enthalten sind. Alles aus dem Bereich der jUnit-Tests soll nicht aktiv sein.

public class DevMain implements HasLogger {
  public static void main(String[] args) {
    DI.activatePackages("org.rapidpm");
    DI.activatePackages("dev.org.rapidpm");

    final LoggingService logger = Logger.getLogger(DevMain.class);
    logger.warning("Starting the Vaadin Component Testing App");
    Main.deploy();
  }
}

Damit wir nicht die produktive Anwendung starten, exkludieren wir die Klasse, die normalerweise für das Erzeugen der Hauptkomponente verwendet wird. In unserem Fall ist es die Klasse MyUIComponentFactory.

public class MyUIComponentFactory implements JumpstartUIComponentFactory {
  private @Inject LoginComponent loginScreen;
  @Override
  public Component createComponentToSetAsContent(final VaadinRequest vaadinRequest) {
    return loginScreen;
  }
}

Diese Factory erzeugt normlerweise die Komponente, die für den Log-in-Prozess verwendet wird, und beginnt damit den Prozess für den Benutzer. Hier setzen wir an und aktivieren stattdessen die Factory zur Erzeugung der Komponente, die wir zur Anzeige unserer zu testenden Komponente verwenden möchten.

public class DevMyUIComponentFactory implements JumpstartUIComponentFactory {
  private @Inject DevComponent component;
  @Override
  public Component createComponentToSetAsContent(VaadinRequest vaadinRequest) {
    return component;
  }
}

Die Umsetzung dieses Umschalten ist abhängig vom DI-Framework, das in der Anwendung Verwendung findet. In unserem Projekt ist es das Framework Dynamic Dependency Injection. Das Projekt findet man hier und ist Open Source (Apache Lizenz). Die Quellen sind auf GitHub zu finden.

Um ein Umschalten zur Laufzeit zu erreichen, implementieren wir das Interface ClassResolver. Da es sich hier um eine statische Entscheindung handelt, ist die Implementierung einfach, da keine weiteren Informationen verarbeitet werden müssen.

@ResponsibleFor(JumpstartUIComponentFactory.class)
public class ComponentFactoryClassResolver 
             implements ClassResolver<JumpstartUIComponentFactory>{
  @Override
  public Class<? extends JumpstartUIComponentFactory> resolve(Class<JumpstartUIComponentFactory> interf) {
    return DevMyUIComponentFactory.class;
  }
}

Da diese Implementierung nicht im produktiven Quelltextpfad liegt, kann diese auch nicht versehentlich ausgeliefert werden. Dieser ClassResolver wird lediglich aktiv, wenn sich die Klasse in dem vom DI-Container aktivierten Klassenpfaden befindet.

Um nun einen Platz zu haben, um die zu testende Komponente anzuzeigen, realisieren wir eine minimale umhüllende Komponente als Zielumgebung. Sowohl das Servlet als auch die UI-Komponente selbst werden aus den Produktionsquelltexten verwendet. Die Hüllenkomponente bekommt den Namen DevComponent und erweitert die Klasse Composite. Benötigt werden unter anderem eine ComboBox zur Auswahl der zu testenden Komponente und einen Refresh-Button, der es dem Entwickler erlaubt, die Testkomponente gezielt erneut zu erzeugen.

Es gibt nun verschiedene Wege, wie der Inhalt der ComboBox erzeugt werden kann. Man kann natürlich den Inhalt manuell pflegen. Allerdings ist das meist nicht sonderlich komfortabel. Besser wäre es, wenn das System selbst erkennt, welche Komponenten überhaupt zur Verfügung stehen. Hier kommt es darauf an, welche Bordmittel in der Anwendung zur Verfügung stehen. Ein Weg kann es sein, dass alle Komponenten mit einer Annotation markiert werden, die dann mittels Reflection gesucht werden kann. Diese Aufgabe des Suchens kann oft der DI-Container übernehmen, da dieser in der Regel das Reflection-Modell vorhält. In diesem Fall ist dies mit dem DI-Container realisiert, jedoch ohne Annotation, da dieser explizit Methoden zur Verfügung stellt, um zum Beispiel eine Menge an Klassen zu erhalten, die ein bestimmtes Interface implementieren. Aus dieser Menge werden dann alle nicht benötigten Elemente herausgefiltert. In diesem Fall wird alles herausgefiltert, was sich nicht in den Produktionsquelltexten befindet. Somit ist bei jedem Start der Dev-App die Liste in der ComboBox aktuell.

public class DevComponent extends Composite {

  private final VerticalLayout  mainLayout         = new VerticalLayout();
  private final Panel           testComponentPanel = new Panel("Component Test Area");
  private final ComboBox<Class> classComboBox      = new ComboBox<>();
  private final Button          refresh            = new Button("refresh");

  public DevComponent() {
    classComboBox.setWidth(100, Unit.PERCENTAGE);

    classComboBox.addSelectionListener(event -> event.getSelectedItem()
                                                     .ifPresent(c -> {
                                                       Component o = (Component) DI.activateDI(c);
                                                       o.setSizeFull();
                                                       testComponentPanel.setContent(o);
                                                     }));

    refresh.setWidth(100, Unit.PERCENTAGE);
    refresh.addClickListener(e -> classComboBox.getSelectedItem().ifPresent(c -> {
      Component o = (Component) DI.activateDI(c);
      o.setSizeFull();
      testComponentPanel.setContent(o);
    }));

    testComponentPanel.setSizeFull();
    mainLayout.addComponents(classComboBox, refresh, testComponentPanel);
  }

  @PostConstruct
  private void postConstruct() {

    final List<Class> classStream = DI.getSubTypesWithoutInterfacesAndGeneratedOf(Composite.class)
                                      .stream()
                                      .filter(aClass -> aClass.getName().contains("org.rapidpm.vaadin"))
                                      .filter(aClass -> !aClass.getName().contains("dev.org.rapidpm.vaadin"))
                                      .collect(Collectors.toList());

    classComboBox.setItems(classStream);
    setCompositionRoot(mainLayout);
  }
}

So sieht der Ablauf im Ganzen dann aus (Abb. 2):

Abb. 2: Der Ganze Ablauf auf einen Blick

Prozessoptimierung: Schneller muss es gehen

Kommen wir nun zu einem praktischen Beispiel, in dem das verwendete Diagramm modifiziert werden soll. Dabei kann man sich gleich zu Beginn die Frage stellen, wie die Selektion in der ComboBox genau für diese eine Komponente entfallen kann. Immerhin kostet dies immer wieder unnötig viele Kilometer Mausbewegung und Klicks. Die Lösung ist einfach, bedarf es doch lediglich der Angabe eines einzigen Startparameters. In der Komponente DevComponent wird eine Konstante definiert, die nachfolgend als SystemProperty verwendet werden kann.

public static final String SELECTED_CLASS = "selected.class";

Bei der Initialisierung der ComboBox, welche die Selektion der zu testenden Klasse ermöglicht, wird einfach der über dieses Property übergebene Klassenname als Wert in Form einer Klasse übergeben. Somit sind immer noch die anderen Klassen in der ComboBox enthalten. Allerdings entfällt die erste
manuelle Selektion. Ein erneutes Laden der Seite führt nun zur Ansicht der gewünschten Komponente.

    try {
      final Class<?> aClass = Class.forName(System.getProperty(SELECTED_CLASS));
      classComboBox.setValue(aClass);
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    }

In der Kolumne Checkpoint Java befasse ich mich unter anderem mit funktionalen Aspekten mittels Core Java 9. Dort wird auch das Thema Exceptions behandelt und die Funktionsweise der CheckedFunctions erklärt. Damit kann zum Beispiel der trycatch-Block wie folgt umgeformt werden.

    ((CheckedFunction<String, Class>) Class::forName)
        .apply(getProperty(SELECTED_CLASS))
        .ifPresent(classComboBox::setValue);

Kommen wir nun zum Start der Anwendung selbst. Wird die Hauptanwendung auf meinem Laptop gestartet, dauert das ca 1,5 s. Man kann das ein wenig beschleunigen, da man beim Start auf der eigenen Maschine vieles nicht benötigt. Es ist zum Beispiel nicht notwendig, bei jedem Start den Bytecode zu verifizieren. Also kann man das auch einfach weglassen. Ebenfalls wissen wir, dass die Anwendung nicht ewig laufen wird, da sie sich ja noch in der aktiven Entwicklung und auf dem Entwicklungsrechner befindet. Also kann man auch die Optimierungen für eine lange Laufzeit einfach weglassen. Damit kommen wir zu den Parametern -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none. Seit Java 7 gibt es den Schalter TieredCompilation. Allerdings brachte ein explizites Ausschalten weniger Gewinn als die hier verwendete Variante der Reduktion.

Bei der Hauptanwendung führt das zu diesen Zahlen:

-Xmx128m

############ Startup finished = xxx ############
############ DDI = 473 [ms] ############
############ StartupActions = 24 [ms] ############
############ Undertow = 1029 [ms] ############
############ Complete = 1527 [ms] ############
############################### Enjoy ###############################

-Xmx128m -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none

############ Startup finished = xxx ############
############ DDI = 394 [ms] ############
############ StartupActions = 20 [ms] ############
############ Undertow = 771 [ms] ############
############ Complete = 1185 [ms] ############
############################### Enjoy ###############################

Bei der Testanwendung sind die Ergebnisse ähnlich:

-Xmx128m

############ Startup finished = xxx ############
############ DDI = 1 [ms] ############
############ StartupActions = 24 [ms] ############
############ Undertow = 916 [ms] ############
############ Complete = 941 [ms] ############
############################### Enjoy ###############################

-Xmx128m -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none

############ Startup finished = xxx ############
############ DDI = 0 [ms] ############
############ StartupActions = 16 [ms] ############
############ Undertow = 647 [ms] ############
############ Complete = 663 [ms] ############
############################### Enjoy ###############################

Backend meets Frontend

In der Artikelserie Backend meets Frontend stellt Sven Ruppert (Vaadin) Konzepte und Technologien rund um das UI-Framework Vaadin vor. Sein Fokus liegt dabei auf modernem Web-Design für Java-Backend-Entwickler.

Zum ersten Teil und damit dem Start der Tutorien rund um die UI-Entwicklung mit Java geht es hier entlang. Alle Teile der Serie Backend meets Frontend finden sich hier.

Fazit

Nachdem der Start ein wenig schneller abläuft, kann als nächstes das Laden der Klassen selbst
modifiziert werden. Oft reicht es aus, wenn kleinere Änderungen innerhalb einer Klasse einfach zu einem erneuten Laden der beteiligten Klassen führt, ohne dass die Anwendung vollständig neu gestartet werden muss. Ab wann sich dieser Aufwand lohnt, hängt von der absoluten Zeit ab, die ein erneuter Start der Anwendung benötigt. Bei etwas über einer Sekunde kann man da sehr unterschiedlicher Meinung sein. Wenn der Neustart der Anwendung allerdings einige Sekunden oder Minuten benötigt, stellt sich die Frage nach der Sinnhaftigkeit dieses Vorgehens meiner Meinung nach nicht mehr. Es besteht dann akuter Handlungsbedarf. Genau das werden wir im nächsten Teil im Detail ansehen. Den Quelltext findet ihr auf GitHub. Bei Fragen und Anregungen einfach melden unter sven@vaadin.com oder per Twitter @SvenRuppert.

Happy Coding!

Geschrieben von
Sven Ruppert
Sven Ruppert
Sven Ruppert arbeitet seit 1996 mit Java und ist Developer Advocate bei Vaadin. In seiner Freizeit spricht er auf internationalen und nationalen Konferenzen, schreibt für IT-Magazine und für Tech-Portale. Twitter: @SvenRuppert
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.