Comment découpler complètement le modèle de View / Controller dans Java Swing

10

Existe-t-il une collection de directives de conception communément acceptées pour séparer les classes Model des classes View / Controller dans une application Java Swing? Je ne suis pas tellement préoccupé par le fait que le View / Controller ne sait rien du modèle comme l'inverse: j'aimerais concevoir mon modèle pour n'avoir aucune connaissance de quoi que ce soit dans javax.swing. Idéalement, il devrait avoir une API simple lui permettant d'être piloté par quelque chose d'aussi primitif qu'une CLI. Ce devrait être, en gros, un "moteur".

La communication des événements GUI au modèle n'est pas trop difficile - les Action Performers peuvent appeler l'API du modèle. Mais qu'en est-il lorsque le modèle effectue ses propres changements d'état qui doivent être reflétés dans l'interface graphique? C'est à cela que sert «l'écoute», mais même «être écouté» n'est pas entièrement passif; cela nécessite que le modèle sache comment ajouter un écouteur.

Le problème particulier qui m'a fait réfléchir implique une file d'attente de fichiers. Du côté de l'interface graphique, il y a un DefaultListModelderrière un JList, et il y a des éléments graphiques pour choisir des fichiers dans le système de fichiers et les ajouter à la JList. Du côté du modèle, il souhaite extraire les fichiers du bas de cette "file d'attente" (les faisant disparaître de la JList) et les traiter d'une manière ou d'une autre. En fait, le code Model est déjà écrit - il maintient actuellement une ArrayList<File>et expose une add(File)méthode publique . Mais je ne sais pas comment faire fonctionner mon modèle avec le View / Controller sans quelques modifications lourdes spécifiques au Swing.

Je suis très nouveau dans la programmation Java et GUI, ayant toujours fait de la programmation "batch" et "back-end" jusqu'à présent - d'où mon intérêt à maintenir une division stricte entre le modèle et l'interface utilisateur, si c'est possible et si cela peut être enseigné.

Type
la source
Btw: Dans MVC, le modèle doit être découplé de la vue et du contrôleur par défaut. Vous devriez relire votre manuel, je pense que vous n'avez pas bien compris le concept. Et vous pouvez implémenter une collection personnalisée qui notifie quand elle change, un peu comme l'interface INotifyCollectionChanged de .NET.
Falcon
@Falcon: merci pour les liens. Je ne suis pas sûr de comprendre votre commentaire cependant .. "le modèle doit être découplé de la vue et du contrôleur par défaut". Pourriez-vous reformuler ou élaborer?
Chap

Réponses:

10

Il n'y a pas de directives de conception communément acceptées (c'est-à-dire de facto ) pour MVC. Ce n'est pas si difficile que ça de le faire vous-même, mais cela nécessite une certaine planification de vos cours et beaucoup de temps et de patience.

La raison pour laquelle il n'y a pas de solution définitive est qu'il existe plusieurs façons de faire du MVC, toutes avec leurs avantages et leurs inconvénients. Soyez donc intelligent et faites ce qui vous convient le mieux.

Pour répondre à votre question, vous souhaitez également dissocier le contrôleur de la vue (vous pouvez donc utiliser la même logique de règle métier pour une application Swing et dans l'application console). Dans l'exemple Swing, vous voulez découpler le contrôleur du JWindowwidget et de n'importe quel widget dans Swing. La façon dont je le faisais (avant d'utiliser les cadres réels) consiste à créer une interface pour la vue que le contrôleur utilise:

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());
    }

}

Pour cette solution lors du démarrage, vous devez enregistrer le contrôleur dans la vue.

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
    }

}

Ce pourrait être une bonne idée de créer un conteneur IoC pour faire toute la configuration pour vous à la place.

Quoi qu'il en soit, de cette façon, vous pouvez implémenter des vues console uniquement, en utilisant les mêmes contrôleurs:

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

}    

La partie amusante est de savoir comment gérer les événements. J'ai implémenté cela en laissant la vue s'enregistrer auprès du contrôleur à l'aide d'une interface, cela se fait en utilisant le modèle Observer (si vous utilisez .NET, vous utiliseriez des gestionnaires d'événements à la place). Voici un exemple d'un simple "observateur de documents", qui signale quand un document a été enregistré ou chargé.

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);
    }        
}

De cette façon, la vue peut se mettre à jour correctement car elle s'abonne aux mises à jour du document. Il suffit de mettre en œuvre l' DocumentObserverinterface:

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
    }

    // ...

}

J'espère que ces exemples motivants vous donneront quelques idées sur la façon de le faire vous-même. Cependant, je vous conseille fortement d'envisager d'utiliser des frameworks en Java qui font la plupart des choses pour vous, sinon vous finirez par avoir beaucoup de code standard qui prend beaucoup de temps à écrire. Il existe quelques plates-formes client enrichi (RCP) que vous pouvez utiliser pour implémenter certaines des fonctionnalités de base dont vous avez le plus probablement besoin, telles que la gestion des documents à l'échelle de l'application et une grande quantité de gestion des événements de base.

Il y a un couple auquel je peux penser de ma tête: Eclipse et Netbeans RCP.

Vous devez encore développer des contrôleurs et des modèles pour vous-même, mais c'est pourquoi vous utilisez un ORM. L'exemple serait Hibernate .

Les conteneurs IoC sont cool'n all, mais il existe également des cadres pour cela. Tels que Spring (qui gère également les données entre autres).

Spoike
la source
Merci d'avoir pris le temps d'écrire une réponse aussi longue. La plupart de cela est un peu au-dessus de ma tête à ce stade, mais je suis sûr que Wikipedia va aider. Je suis utilisant à la fois Eclipse et Netbeans, et se penchant à ce dernier. Je ne sais pas comment distinguer le contrôleur de la vue là-bas, mais votre exemple de pré-framework en haut aide.
Chap
0

Je pense que parfois nous devons faire des compromis

Comme vous le dites, ce serait formidable si nous pouvions propager la notification de changement implicitement sans que l'objet observé ait une infrastructure explicite pour cela. Pour les langages impératifs courants comme Java, C #, C ++, leur architecture d'exécution est trop légère, pour l'instant, de toute façon. J'entends par là que cela ne fait pas partie de la spécification linguistique pour le moment.

Dans votre cas particulier, je ne pense pas que ce soit exactement une mauvaise chose de définir / utiliser une interface générique comme INotifyPropertyChanged (comme en c #), car cela n'est pas automatiquement couplé à une vue de toute façon - cela dit simplement que si je change, Je vais te le dire .

Encore une fois, je conviens que ce serait formidable si nous n'avions pas à définir la notification de changement nous-mêmes, mais là encore, si elle était implicite pour toutes les classes, elle pourrait entraîner des frais généraux.

Max
la source
plus j'y pense, je me rends compte que vous avez raison - l'objet modèle doit être impliqué dans une certaine mesure dans le lancement de la notification à l'interface graphique qu'un changement s'est produit; sinon, l'interface graphique devrait interroger le modèle pour découvrir les changements - et c'est mauvais.
Chap