So entkoppeln Sie Model vollständig von View / Controller in Java Swing

10

Gibt es eine Sammlung von gemeinsam vereinbarten Entwurfsrichtlinien zum Trennen der Modellklassen von den View / Controller-Klassen in einer Java Swing-App? Ich bin nicht so besorgt, dass der View / Controller nichts über das Modell weiß, als umgekehrt: Ich möchte mein Modell so gestalten, dass es nichts über javax.swing weiß. Idealerweise sollte es eine einfache API haben, die es ermöglicht, von etwas so Primitivem wie einer CLI gesteuert zu werden. Es sollte lose gesagt ein "Motor" sein.

Die Kommunikation von GUI-Ereignissen mit dem Modell ist nicht allzu schwierig. Die Aktionsausführenden können die API des Modells aufrufen. Aber was ist, wenn das Modell seine eigenen Statusänderungen vornimmt, die in der GUI wiedergegeben werden müssen? Dafür ist "Zuhören" gedacht, aber selbst "Zuhören" ist nicht ganz passiv. Das Modell muss über das Hinzufügen eines Listeners informiert sein.

Das besondere Problem, das mich zum Nachdenken gebracht hat, betrifft eine Warteschlange mit Dateien. Auf der GUI-Seite befindet sich ein DefaultListModelhinter a JList, und es gibt einige GUI-Elemente, mit denen Sie Dateien aus dem Dateisystem auswählen und zur JList hinzufügen können. Auf der Modellseite möchte es die Dateien aus dem unteren Bereich dieser "Warteschlange" ziehen (wodurch sie aus der JList verschwinden) und auf irgendeine Weise verarbeiten. Tatsächlich ist der Modellcode bereits geschrieben - er verwaltet derzeit eine ArrayList<File>und macht eine öffentliche add(File)Methode verfügbar . Ich bin jedoch nicht in der Lage, mein Modell mit dem View / Controller zum Laufen zu bringen, ohne dass einige schwere, Swing-spezifische Änderungen am Modell vorgenommen werden müssen.

Ich bin sowohl in der Java- als auch in der GUI-Programmierung sehr neu, da ich bisher immer "Batch" - und "Back-End" -Programmierung durchgeführt habe - daher mein Interesse daran, eine strikte Trennung zwischen Modell und Benutzeroberfläche beizubehalten, wenn dies möglich ist und wenn dies möglich ist unterrichtet werden.

Kerl
quelle
Übrigens: In MVC sollte das Modell standardmäßig von der Ansicht und dem Controller entkoppelt sein. Sie sollten Ihr Lehrbuch noch einmal lesen, ich denke, Sie haben das Konzept nicht ganz verstanden. Sie können auch eine benutzerdefinierte Sammlung implementieren, die benachrichtigt, wenn sie sich ändert, ähnlich wie die INotifyCollectionChanged-Schnittstelle von .NET.
Falcon
@ Falcon: Danke für die Links. Ich bin mir jedoch nicht sicher, ob ich Ihren Kommentar verstehe. "Das Modell sollte standardmäßig von der Ansicht und dem Controller entkoppelt sein." Könnten Sie umformulieren oder näher erläutern?
Kap

Antworten:

10

Es gibt keine allgemein vereinbarten (dh defacto ) Designrichtlinien für MVC. Es ist nicht ganz so schwer, es selbst zu tun, erfordert aber etwas Planung für Ihren Unterricht und viel Zeit und Geduld.

Der Grund, warum es keine definitive Lösung gibt, ist, dass es mehrere Möglichkeiten gibt, MVC durchzuführen, alle mit ihren Vor- und Nachteilen. Seien Sie also schlau und tun Sie, was am besten zu Ihnen passt.

Um Ihre Frage zu beantworten, möchten Sie den Controller auch von der Ansicht entkoppeln (sodass Sie dieselbe Geschäftsregellogik sowohl für eine Swing-App als auch für die Konsolen-App verwenden können). Im Swing-Beispiel möchten Sie den Controller vom JWindowWidget und jedem anderen Widget in Swing entkoppeln . Früher habe ich (vor der Verwendung der tatsächlichen Frameworks) eine Schnittstelle für die Ansicht erstellt, die der Controller verwendet:

public interface PersonView {
    void setPersons(Collection<Person> persons);
}

public class PersonController {

    private PersonView view;
    private PersonModel model;

    public PersonController(PersonView view, PersonModel model) {
        this.view = view;
        this.model = model;
    }
    // ... methods to affect the model etc. 
    // such as refreshing and sort:

    public void refresh() {
        this.view.setPersons(model.getAsList());
    }

    public void sortByName(boolean descending) {
       // do your sorting through the model.
       this.view.setPersons(model.getSortedByName());
    }

}

Für diese Lösung müssen Sie beim Start den Controller in der Ansicht registrieren.

public class PersonWindow extends JWindow implements PersonView {

    PersonController controller;
    Model model;

    // ... Constructor etc.

    public void initialize() {
        this.controller = new PersonController(this, this.model);

        // do all the other swing stuff

        this.controller.refresh();
    }

    public void setPersons(Collection<Person> persons) {
        // TODO: set the JList (in case that's you are using) 
        // to use the given parameter
    }

}

Es könnte eine gute Idee sein, einen IoC-Container zu erstellen, um stattdessen das gesamte Setup für Sie durchzuführen.

Auf diese Weise können Sie jedoch nur Konsolenansichten mit denselben Controllern implementieren:

public class PersonConsole implements PersonView {

    PersonController controller;
    Model model;

    public static void main(String[] args) {
        new PersonConsole().run();
    }

    public void run() {
        this.model = createModel();
        this.controller = new PersonController(this, this.model);

        this.controller.refresh();
    }

    public void setPersons(Collection<Person> persons) {
        // just output the collection to the console

        StringBuffer output = new StringBuffer();
        for(Person p : persons) {
            output.append(String.format("%s%n", p.getName()));
        }

        System.out.println(output);
    }

    public void createModel() {
        // TODO: create this.model
    }

    // this could be expanded with simple console menu with keyboard
    // input and other console specific stuff

}    

Der lustige Teil ist, wie man Event-Handling macht. Ich habe dies implementiert, indem ich die Ansicht über eine Schnittstelle beim Controller registrieren ließ. Dies erfolgt mithilfe des Observer-Musters (wenn Sie .NET verwenden, verwenden Sie stattdessen Ereignishandler). Hier ist ein Beispiel eines einfachen "Dokumentbeobachters", der signalisiert, wann ein Dokument gespeichert oder geladen wurde.

public interface DocumentObserver {
    void onDocumentSave(DocModel saved);
    void onDocumentLoad(DocModel loaded);
}

// in your controller you implement register/unregister methods
private List<DocumentObserver> observers;

// register observer in to the controller
public void addObserver(DocumentObserver o) {
    this.observers.add(o);
}

// unregisters observer from the controller
public void removeObserver(DocumentObserver o) {
    this.observers.remove(o);
}

public saveDoc() {
    DocModel model = model.save();
    for (DocumentObserver o : observers) {
        o.onDocumentSave(model);
    }
}

public loadDoc(String path) {
    DocModel model = model.load(path);
    for (DocumentObserver o : observers) {
        o.onDocumentLoad(model);
    }        
}

Auf diese Weise kann sich die Ansicht ordnungsgemäß selbst aktualisieren, da sie die Dokumentaktualisierungen abonniert. Alles was es tun muss, ist die DocumentObserverSchnittstelle zu implementieren :

public class DocumentWindow extends JWindow 
        implements DocView, DocumentObserver {

    //... all swing stuff

    public void onDocumentSave(DocModel saved) {
        // No-op
    }

    public void onDocumentLoad(DocModel loaded) {
        // do what you need with the loaded model to the
        // swing components, or let the controller do it on
        // the view interface
    }

    // ...

}

Ich hoffe, diese motivierenden Beispiele geben Ihnen einige Ideen, wie Sie es selbst machen können. Ich rate Ihnen jedoch dringend, die Verwendung von Frameworks in Java in Betracht zu ziehen, die die meisten Dinge für Sie erledigen, da Sie sonst viel Boilerplate-Code haben, dessen Schreiben lange dauert. Es gibt einige Rich Client Platforms (RCP), die Sie verwenden können, um einige der grundlegenden Funktionen zu implementieren, die Sie höchstwahrscheinlich benötigen, z. B. die anwendungsweite Dokumentenbehandlung und viele grundlegende Ereignisbehandlungen.

Es gibt ein paar, die ich mir vorstellen kann: Eclipse und Netbeans RCPs.

Sie müssen noch Controller und Modelle für sich selbst entwickeln, aber deshalb verwenden Sie ein ORM. Beispiel wäre Ruhezustand .

IoC-Container sind cool, aber es gibt auch Frameworks dafür. Zum Beispiel Spring (das unter anderem auch das Datenhandling übernimmt).

Spoike
quelle
Vielen Dank, dass Sie sich die Zeit genommen haben, eine so lange Antwort zu schreiben. Das meiste davon ist zu diesem Zeitpunkt etwas über meinem Kopf, aber ich bin sicher, Wikipedia wird helfen. Ich bin unter Verwendung von sowohl Eclipse und Netbeans, und stützte sich auf den letzteren. Ich bin mir nicht sicher, wie ich den Controller von der dortigen Ansicht unterscheiden soll, aber Ihr Beispiel vor dem Framework oben hilft.
Kap
0

Ich gehe davon aus, dass wir manchmal Kompromisse eingehen müssen

Wie Sie sagen, wäre es großartig, wenn wir Änderungsbenachrichtigungen implizit verbreiten könnten, ohne dass das beobachtete Objekt eine explizite Infrastruktur dafür hat. Für gängige Imperativsprachen wie Java, C #, C ++ ist ihre Laufzeitarchitektur ohnehin ab sofort zu leicht. Damit meine ich, dass es derzeit nicht Teil der Sprachspezifikation ist.

In Ihrem speziellen Fall halte ich es nicht für eine schlechte Sache, eine generische Schnittstelle wie INotifyPropertyChanged (wie in c #) zu definieren / zu verwenden , da diese ohnehin nicht automatisch an eine Ansicht gekoppelt wird. Ich werde es dir sagen .

Auch hier stimme ich zu, dass es großartig wäre, wenn wir die Änderungsbenachrichtigung nicht selbst definieren müssten, aber wenn sie für alle Klassen implizit wäre, könnte dies zu einem Overhead führen.

Max
quelle
Je mehr ich darüber nachdenke, desto mehr wird mir klar, dass Sie Recht haben. Das Modellobjekt muss in gewissem Maße an der Initiierung einer Benachrichtigung der GUI über eine Änderung beteiligt sein. Andernfalls müsste die GUI das Modell abfragen, um Änderungen festzustellen - und das ist schlecht.
Kap