Erweiterbare Java-Anwendungen programmieren

Hier beschreibe ich, wie Java-Programme implementiert werden können, die leicht zu erweitern sind. In diesem Artikel ist ein ZIP-Archiv mit vollständigem Java-Quellcode, nach einem Build + Run erscheint ein GUI.

Leicht zu erweitern

Unter leicht zu erweitern verstehe ich:

  • Die Anwendung besteht aus verschiedenen Modulen. Jedes Modul erledigt Aufgaben einer bestimmten Kategorie.
  • Die Module hängen so wenig wie möglich voneinander ab. Abhängigkeit bedeutet: Wird ein Modul benutzt, sind ebenso alle anderen Module erforderlich, die das benutzte Modul benötigt, unabhängig davon, ob der Nutzer des Moduls diese auch verwendet.
  • Ein Modul definiert nur zu jenen Modulen Abhängigkeiten, die es benötigt, je weniger das sind, desto unabhängiger/eigenständiger ist es
  • Module hängen nicht gegenseitig voneinander ab
  • Die Module benutzen bevorzugen Schnittstellen (Interfaces) anstelle Implementierungen. So lässt sich eine Implementierung austauschen, ohne dass Code an anderen Stellen anzupassen ist und bis aufwändig zu implementierende Objekte fertiggestellt sind, können an deren Stelle Mock-Objekte („Dummys“) benutzt werden.

Beispielsanwendung mit Quellcode

In der Beispielsanwendung sollen Filme verwaltet werden. Sie können die Quellcode-Dateien als ZIP-Archiv herunteraden. Zum Erstellen benutzte ich ich die NetBeans-IDE. Nach Entpacken des ZIP-Archivs öffnen Sie mit der NetBeans-IDE das Projekt Suite und kreuzen an, dass erforderliche Projekte ebenfalls geöffnet werden sollen sowie dass das Projekt Suite das Hauptprojekt sein soll. Wählen Sie im Navigator-Fenster das Projekt Suite aus, öffnen mit Rechtsklick das Kontextmenü, erstellen die Anwendung mit Clean and Build und starten diese mit F6.

Für die Datenspeicherung benutze ich eine Mock-Implementierung, die sich leicht austauschen lässt gegen eine Datenbank-Implementierung, deren Implementierung ist natürlich aufwändiger. Mit anderen Worten: Nach Neustart sind eingegebene Daten nicht mehr vorhanden.

Sie benötigen außerdem das Java Development Kit 1.7 (Java 7). Falls NetBeans dies nicht kennt, kann es (falls installiert) hinzugefügt werden über Tools > Java Platforms.

Vorerst wird die Verwaltung von DVDs implementiert. Die Anwendung soll so strukturiert sein, dass später „elegant“ weitere Film-Medien hinzu kommen können wie Blu-rays oder weitere Funktionalität wie der Upload von Videodateien oder der Ausdruck einer Liste mit Filmen. Unter elegant verstehe ich: Funktionalität einer anderen Kategorie wird in einem neuen Modul (Projekt) implementiert, die bisherigen Module (Projekte) sollten möglichst nicht zu modifizieren sein, lediglich was generell anderswo benötigt werden kann, könnte ein API werden oder ein existierendes erweitern.

Module

Module sind im Beispiel Java-Projekte. In NetBeans erzeuge ich sie via: File > New Project, Categories: Java, Projects: Java Class Library. Die Abängigkeiten eines Projekts zu anderen definiere ich in dessen Properties-Dialog über Categories: Libraries, Add Project.

API

Das API (Application Programming Interface) besteht in der Regel aus verschiedenen Modulen, beispielsweise einem, das ein Anwendungsfenster zur Verfügung stellt mit Methoden zum Hinzufügen von Menüs und Unterfenstern. Ein anderes API-Modul könnte den Zugriff auf das Dateisystem regeln, ein weiteres das Speichern von Einstellungen usw.

Für den Fall, dass Sie wenig Zeit haben, können Sie das API des Beispiels als Ausgang für ein eigenes Projekt benutzen. Soll Ihre Anwendung über viele Jahre wachsen, sparen Sie bezogen auf die gesamte Arbeit viel Zeit und sind flexibler, auch wenn die Einarbeitung Monate dauern kann, falls Sie eine Rich Client Platform benutzen, ich kann die NetBeans Platform empfehlen.

Module der Filme-Verwaltung

Die Minimalversion der Filme-Verwaltung besteht aus folgenden Modulen:

  • Window System API: Stellt Programmfenster zur Verfügung
  • Domain: Entities, z.B. Movie (ein Film) und Geschäftsobjekte, die die Filme-Verwaltung benutzt
  • Persistence: Schnittstellen für die dauerhafte Speicherung der Entities
  • JPAPersistence: Implementierung der Persistence-Schnittstelle mittels des Java Persistence API (im Beispiel durch eine Mock-Implementierung ersetzt)
  • DVD: Verwaltet DVDs: Anzeigen, Erzeugen, Bearbeiten und Löschen von DVDs
  • EventBus: Benachrichtigt Beobachter über Ereignisse
  • Lookup: Wird hier benutzt für die komfortable Nutzung des Service Provider Interface, siehe hierzu Modulare Java-Programmierung via Services und Das NetBeans-Lookup für Nicht-RCP-Projekte benutzen
  • Suite: Fasst alle Module zusammen, startet die Anwendung mit Hilfe des API

Abhängigkeiten

Sind die Abhängigkeiten gering, kann ich einzelne Module leichter unabhängig voneinander entwickeln (es sind für die Entwicklung weniger Module erforderlich) und Änderung eines Moduls betrifft nur jene, die davon abhängen, nicht die gesamte Anwendung. Ich erreiche geringe Abhängigkeit unter anderem durch:

  • Aufteilen der Anwendung in Module (Projekte) mit unterschiedlichen Zuständigkeiten
  • Module hängen nur ab von (anderen) Modulen, die sie benötigen
  • Schnittstellen benutzen anstelle Implementierungen, die Implementierungen werden über das Service Provider Interface geliefert und können so leicht ausgetauscht werden

Die Abhängigkeiten der Filme-Verwaltung skizziert:

Gehen viele Pfeile von einem Modul aus, hat es viele Abhängigkeiten. Implizit bestehen Abhängigkeiten zu den Abhängigkeiten des Moduls, von dem ein Modul abhängig ist: Wer das Modul Window System API benutzt, hat implizt auch eine Abhängigkeit zu Lookup, da Window System davon abhängt (unabhängig davon, ob das Modul das Modul Lookup direkt benutzt).

Window System API

Über das Interface EditWindowProvider kann jedes Modul dem Bearbeiten-Bereich des Programmfensters automatisch ein Fenster hinzufügen:

public interface EditWindowProvider {
    Component getEditWindow();
    String getDisplayName();
    int getPosition();
}

Eine in anderen Packages nicht sichtbare Implementierung holt die Fenster auf diese Weise ab:

final class MainWindow extends javax.swing.JFrame {

    MainWindow() {
        initComponents();
        lookupEditWindowProviders();
    }

    private void lookupEditWindowProviders() {
        List<EditWindowProvider> editWindowProviders =
                new LinkedList<>(Lookup.getDefault().lookupAll(EditWindowProvider.class));
        Collections.sort(editWindowProviders, EditWindowProviderAscendingSortComparator.INSTANCE);
        for (EditWindowProvider editWindowProvider : editWindowProviders) {
            String displayName = editWindowProvider.getDisplayName();
            Component editWindow = editWindowProvider.getEditWindow();
            editorAreaTabbedPane.add(displayName, editWindow);
        }
    }

    ...
}

Ein Modul kann dem Anwendungsfenster eigene Fenster dynamisch hinzufügen über einen WindowManager:

public final class WindowManager {
    public static void dockIntoEditArea(Component component) {
        ...
    }
}

Domain

Exemplarisch sei hier ein Film-Entity skizziert:

public final class Movie implements Serializable {
    private Medium medium;
    private String name;
    ...
}

Persistence

Persistence definiert Interfaces, wie Filme dauerhaft gespeichert werden können und Ereignisse:

public interface MoviePersistence {
    void persist(Movie movie);
    Movie merge(Movie movie);
    void remove(Movie movie);
    Movie findById(long id);
    Collection<? extends Movie> findAll();
    boolean isPersisted(Movie movie);
}

public final class MoviePersistedEvent {...}
public final class MovieRemovedEvent {...}

JPAPersistence

Eine typische Code-Stelle in der Implementierung könnte so aussehen:

entityManager.persist(movie);
EventBus.publish(new MoviePersistedEvent(this, movie));

Im Beispiel benutze ich eine Mock-Implementierung. So kann ich das GUI und die restliche Anwendung programmieren und testen und mich später um die aufwändigere Implementierung via JPA kümmern.

DVD

Das Modul DVD fügt dem Anwendungsfenster eine Oberfläche zum Bearbeiten von DVDs hinzu durch implementieren von EditWindowProvider und Veröffentlichung als Service, um letzteres kümmert sich die @ServiceProvider-Annotation des Moduls Lookup:

@ServiceProvider(service = EditWindowProvider.class)
public final class EditDVDPanel extends JPanel implements EditWindowProvider {
    ...
    @Override
    public Component getEditWindow() {
        return this;
    }
}

Das EditDVDPanel hat eine Liste mit DVDs, deren ListModel Persistence-Ereignisse beobachtet und sich selbst aktualisiert, falls neue/modifizierte Filme gespeichert werden oder Filme gelöscht werden. So können überall, auch in anderen Modulen, DVDs hinzugefügt, modifiziert und gelöscht werden, ohne dass den Modifizierern zu interessieren braucht, wer von der Modifikation wissen sollte. Jede JList mit diesem Model spiegelt sofort den aktuellen Status wieder.

public final class DVDsListModel extends DefaultListModel<Movie> {

    private static final long serialVersionUID = 1L;

    public DVDsListModel() {
        addMovies();
        listen();
    }

    private void listen() {
        AnnotationProcessor.process(this);
    }

    private void addMovies() {
        MoviePersistence moviePersistence = Lookup.getDefault().lookup(MoviePersistence.class);
        for (Movie movie : moviePersistence.findAll()) {
            if (isDVD(movie)) {
                addElement(movie);
            }
        }
    }

    private boolean isDVD(Movie movie) {
        return movie.getMedium() == Medium.DVD; // == preferred for enums
    }

    @EventSubscriber(eventClass = MoviePersistedEvent.class)
    public void moviePersisted(MoviePersistedEvent evt) {
        Movie movie = evt.getMovie();
        if (isDVD(movie)) {
            if (contains(movie)) {
                update(movie);
            } else {
                addElement(movie);
            }
        }
    }

    private void update(Movie movie) {
        int index = indexOf(movie);
        set(index, movie);
    }

    @EventSubscriber(eventClass = MovieRemovedEvent.class)
    public void movieRemoved(MovieRemovedEvent evt) {
        Movie movie = evt.getMovie();
        if (isDVD(movie)) {
            removeElement(movie);
        }
    }
}

Über AnnotationProcessor.process(this) meldet sich das Model an als Beobachter beim EventBus. Dieser benachrichtigt es im Event Dispatch Thread beim Auftreten von Ereignissen, die mit @EventSubscriber annotiert sind, also falls Filme hinzugefügt/modifiziert wurden oder gelöscht (MoviePersistedEvent, MovieRemovedEvent). Der EventBus umhüllt Beobachter mit einer Weak Reference, sodass Abmelden nicht erforderlich ist: Falls ein ListModel nicht mehr referenziert wird, entfernt es der Garbage Collector von den Beobachtern.

Zum Synchronisieren zwischen einer in der Liste ausgewälten DVD und den Eingabefeldern würde ich Beansbinding benutzen, für das Beispiel verzichtete ich darauf, damit ist ein JAR weniger erforderlich und es sind weniger Vorkenntnisse nötig zum Verstehen der Implementierung.

Eine Speichern-Aktion könnte so implementiert werden:

public final class PersistMovieAction extends AbstractAction {

    private static final long serialVersionUID = 1L;
    private Movie movie;

    public PersistMovieAction() {
        setEnabled(false);
    }

    public void setMovie(Movie movie) {
        this.movie = movie;
        setEnabled(movie != null);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        persistMovie(movie);
    }

    private void persistMovie(Movie movie) {
        MoviePersistence moviePersistence = Lookup.getDefault().lookup(MoviePersistence.class);
        try {
            if (moviePersistence.isPersisted(movie)) {
                moviePersistence.merge(movie);
            } else {
                moviePersistence.persist(movie);
            }
        } catch (Throwable t) {
            Logger.getLogger(PersistMovieAction.class.getName()).log(Level.SEVERE, null, t);
            if (confirmRetry()) {
                persistMovie(movie);
            }
        }
    }

    private boolean confirmRetry() throws HeadlessException {
        return JOptionPane.showConfirmDialog(null, "Error saving movie, try again?", "Error", JOptionPane.YES_NO_OPTION)
                == JOptionPane.YES_OPTION;
    }
}

Suite

Am schnellsten und einfachsten implementiert ist das Modul Suite: Es ruft nur die init-Methode des Window System API auf, die das Programmfenster startet:

public final class Suite {

    public static void main(String[] args) {
        Main.init();
    }

    private Suite() {
    }
}

Erweitern

Will ich auch Videodateien verwalten, füge ich ein neues Modul (Projekt) hinzu, das ähnlich strukturiert ist wie das Modul DVD. Der gesamte sonstige Code bleibt wie er ist, aus neuen Modulen resultiert keine monolithische Anwendung.

Im Projekt Suite ist eine Abhängigkeit zu jedem neuen Modul hinzuzufügen.

Auch zusätzliche Speichermöglichkeiten ließen sich realisieren, beispielsweise in ein WebDAV-Verzeichnis. Sollen mehrere Persistence-Implementierungen existieren, müssten die Actions dies berücksichtigen, da sie mehr als eine (einzige) Speicher-Implementierung erhalten können.

Ich könnte ein Report-Modul hinzufügen, das mir ausgewählte oder alle Filme ausdruckt oder in eine XML-/HTML-Datei schreibt usw.

Weitere Vorteile der Modularisierung

Durch die Modularisierung lässt sich ohne großen Aufwand eine Anwendung nur teilweise ausliefern, beispielsweise, falls ich keine Blu-rays verwalten will oder auf Reports verzichten kann.

Will ich das ermöglichen, könnte ich das API erweitern um eine Benutzeraus-/-abwahl der gewünschten Module.

Da dies und vieles mehr in beispielsweise der NetBeans RCP implementiert ist, benutze ich diese für neue Projekte.

Stichwörter: , , , ,

Zu diesem Artikel können keine Kommentare mehr geschrieben werden.