Recherche…


Exemple MVP simple

Pour illustrer un exemple simple d'utilisation du modèle MVP, considérez le code suivant qui crée une interface utilisateur simple avec uniquement un bouton et une étiquette. Lorsque l'utilisateur clique sur le bouton, l'étiquette se met à jour avec le nombre de clics sur le bouton.

Nous avons 5 classes:

  • Model - Le POJO pour maintenir l'état (M dans MVP)
  • View - La classe avec code UI (V dans MVP)
  • ViewListener - Interface fournissant des méthodes pour répondre aux actions dans la vue
  • Présentateur - Répond aux entrées et met à jour la vue (P dans MVP)
  • Application - La classe "principale" pour tout rassembler et lancer l'application

Une classe "modèle" minimale qui ne fait que maintenir une variable de count unique.

/**
 * A minimal class to maintain some state 
 */
public class Model {
    private int count = 0;

    public void addOneToCount() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Une interface minimale pour notifier les auditeurs:

/**
 * Provides methods to notify on user interaction
 */
public interface ViewListener {
    public void onButtonClicked();
}

La classe de vue construit tous les éléments de l'interface utilisateur. La vue, et uniquement la vue, doit faire référence à des éléments de l'interface utilisateur (par exemple, aucun bouton, champ de texte, etc. dans le présentateur ou dans d'autres classes).

/**
 * Provides the UI elements
 */

import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.WindowConstants;

public class View {
    // A list of listeners subscribed to this view
    private final ArrayList<ViewListener> listeners;
    private final JLabel label;
    
    public View() {
        final JFrame frame = new JFrame();
        frame.setSize(200, 100);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLayout(new GridLayout());

        final JButton button = new JButton("Hello, world!");

        button.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(final ActionEvent e) {
                notifyListenersOnButtonClicked();
            }
        });
        frame.add(button);

        label = new JLabel();
        frame.add(label);

        this.listeners = new ArrayList<ViewListener>();

        frame.setVisible(true);
    }

    // Iterate through the list, notifying each listner individualy 
    private void notifyListenersOnButtonClicked() {
        for (final ViewListener listener : listeners) {
            listener.onButtonClicked();
        }
    }

    // Subscribe a listener
    public void addListener(final ViewListener listener) {
        listeners.add(listener);
    }

    public void setLabelText(final String text) {
        label.setText(text);
    }
}

La logique de notification peut également être codée comme ceci dans Java8:

        ...
        final Button button = new Button("Hello, world!");
        // In order to do so, our interface must be changed to accept the event parametre
        button.addActionListener((event) -> {
            notifyListeners(ViewListener::onButtonClicked, event);
            // Example of calling methodThatTakesALong, would be the same as callying:
            // notifyListeners((listener, long)->listener.methodThatTakesALong(long), 10L)
            notifyListeners(ViewListener::methodThatTakesALong, 10L);
        });
        frame.add(button);
        ...

/**
 * Iterates through the subscribed listeneres notifying each listener individually.
 * Note: the {@literal '<T>' in private <T> void} is a Bounded Type Parametre. 
 *
 * @param <T>      Any Reference Type (basically a class).
 * 
 * @param consumer A method with two parameters and no return, 
 *                 the 1st parametre is a ViewListner, 
 *                 the 2nd parametre is value of type T.
 * 
 * @param data     The value used as parametre for the second argument of the
 *                 method described by the parametre consumer.
 */
private <T> void notifyListeners(final BiConsumer<ViewListener, T> consumer, final T data) {
    // Iterate through the list, notifying each listener, java8 style 
    listeners.forEach((listener) -> {

        // Calls the funcion described by the object consumer.
        consumer.accept(listener, data);

        // When this method is called using ViewListener::onButtonClicked
        // the line: consumer.accept(listener,data); can be read as:
        // void accept(ViewListener listener, ActionEvent data) {
        //     listener.onButtonClicked(data);
        // }
        
    });
}

L’interface doit être restructurée pour que l’ActionEvent devienne un paramètre:

public interface ViewListener {
    public void onButtonClicked(ActionEvent evt);
    // Example of methodThatTakesALong signature
    public void methodThatTakesALong(long );
}

Ici, une seule méthode notify est nécessaire, la méthode d'écoute réelle et son paramètre sont transmis en tant que paramètres. Dans le cas où cela est nécessaire, cela peut aussi être utilisé pour quelque chose d'un peu moins astucieux que le traitement des événements, tout fonctionne tant qu'il y a une méthode dans l'interface, par exemple:

        notifyListeners(ViewListener::methodThatTakesALong, -1L);

Le présentateur peut prendre la vue et s’ajouter en tant qu’auditeur. Lorsque le bouton est cliqué dans la vue, la vue avertit tous les écouteurs (y compris le présentateur). Maintenant que le présentateur est averti, il peut prendre les mesures appropriées pour mettre à jour le modèle (c'est-à-dire l'état de l'application), puis mettre à jour la vue en conséquence.

/**
 * Responsible to responding to user interaction and updating the view
 */
public class Presenter implements ViewListener {
    private final View view;
    private final Model model;

    public Presenter(final View view, final Model model) {
        this.view = view;
        view.addListener(this);
        this.model = model;
    }

    @Override
    public void onButtonClicked() {
        // Update the model (ie. the state of the application)
        model.addOneToCount();
        // Update the view
        view.setLabelText(String.valueOf(model.getCount()));
    }
}

Pour tout rassembler, la vue peut être créée et injectée dans le présentateur. De même, un modèle initial peut être créé et injecté. Bien que les deux puissent être créés dans le présentateur, leur injection dans le constructeur permet des tests beaucoup plus simples.

public class Application {
    public Application() {
        final View view = new View();
        final Model model = new Model();
        new Presenter(view, model);
    }

    public static void main(String... args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Application();
            }
        });
    }
}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow