Buscar..


Ejemplo de MVP simple

Para ilustrar un ejemplo simple del uso del patrón MVP, considere el siguiente código que crea una IU simple con solo un botón y una etiqueta. Cuando se hace clic en el botón, la etiqueta se actualiza con el número de veces que se ha hecho clic en el botón.

Tenemos 5 clases:

  • Modelo - El POJO para mantener el estado (M en MVP)
  • Vista: la clase con el código UI (V en MVP)
  • ViewListener - Interfaz que proporciona métodos para responder a las acciones en la vista
  • Presentador: responde a la entrada y actualiza la vista (P en MVP)
  • Aplicación: la clase "principal" para reunir todo y lanzar la aplicación.

Una clase de "modelo" mínima que solo mantiene una única variable de count .

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

    public void addOneToCount() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

Una interfaz mínima para notificar a los oyentes:

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

La clase de vista construye todos los elementos de la interfaz de usuario. La vista, y solo la vista, debe hacer referencia a los elementos de la interfaz de usuario (es decir, no hay botones, campos de texto, etc. en el presentador u otras clases).

/**
 * 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 lógica de notificación también puede codificarse así en 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);
        // }
        
    });
}

La interfaz debe ser rediseñada para tomar el ActionEvent como un parámetro:

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

Aquí solo se necesita un método de notificación, el método de escucha real y su parámetro se pasan como parámetros. En caso necesario, esto también puede usarse para algo menos ingenioso que el manejo de eventos reales, todo funciona siempre que haya un método en la interfaz, por ejemplo:

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

El presentador puede ver la vista y agregarse a sí mismo como un oyente. Cuando se hace clic en el botón en la vista, la vista notifica a todos los oyentes (incluido el presentador). Ahora que se notifica al presentador, puede tomar las medidas adecuadas para actualizar el modelo (es decir, el estado de la aplicación) y luego actualizar la vista en consecuencia.

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

Para poner todo junto, la vista se puede crear e inyectar en el presentador. Del mismo modo, un modelo inicial puede ser creado e inyectado. Si bien ambos pueden crearse en el presentador, inyectarlos en el constructor permite realizar pruebas mucho más 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
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow