Cómo desacoplar completamente el modelo de View / Controller en Java Swing

10

¿Existe una colección de pautas de diseño comúnmente acordadas para separar las clases Modelo de las clases Ver / Controlador en una aplicación Java Swing? No estoy tan preocupado de que la Vista / Controlador no sepa nada sobre el Modelo al revés: me gustaría diseñar mi Modelo para que no tenga conocimiento de nada en javax.swing. Idealmente, debería tener una API simple que le permita ser conducida por algo tan primitivo como una CLI. Debería ser, en términos generales, un "motor".

Comunicar eventos de GUI al modelo no es demasiado difícil: los Action Performers pueden llamar a la API del modelo. Pero, ¿qué pasa cuando el modelo realiza sus propios cambios de estado que deben reflejarse en la GUI? Para eso sirve "escuchar", pero incluso "ser escuchado" no es del todo pasivo; requiere que el modelo sepa sobre agregar un oyente.

El problema particular que me hizo pensar implica una cola de archivos. En el lado de la GUI hay un DefaultListModeldetrás JList, y hay algunas cosas de GUI para elegir archivos del sistema de archivos y agregarlos a la JList. En el lado del Modelo, quiere extraer los archivos del final de esta "cola" (haciendo que desaparezcan de la JList) y procesarlos de alguna manera. De hecho, el código del modelo ya está escrito: actualmente mantiene ArrayList<File>y expone un add(File)método público . Pero no sé cómo hacer que mi Modelo funcione con la Vista / Controlador sin algunas modificaciones pesadas y específicas de Swing en el Modelo.

Soy muy nuevo tanto en la programación Java como en la GUI, ya que siempre he realizado programación "por lotes" y "back-end", de ahí mi interés en mantener una división estricta entre el modelo y la interfaz de usuario, si es posible y si puede ser enseñado.

Cap
fuente
Por cierto: en MVC, el modelo debe estar desacoplado de la vista y del controlador de forma predeterminada. Deberías leer tu libro de texto nuevamente, creo que no has entendido bien el concepto. Y podría implementar una colección personalizada que notifique cuándo cambia, al igual que la interfaz INotifyCollectionChanged de .NET.
Falcon
@ Falcon: gracias por los enlaces. Sin embargo, no estoy seguro de entender su comentario ... "el modelo debe estar desacoplado de la vista y del controlador por defecto". ¿Podrías reformular o elaborar?
Cap

Respuestas:

10

No existen pautas de diseño comúnmente acordadas (es decir, de facto ) para MVC. No es tan difícil hacerlo usted mismo, pero requiere un poco de planificación en sus clases y mucho tiempo y paciencia.

La razón por la que no hay una solución definitiva es porque hay varias formas de hacer MVC, todas con sus ventajas y desventajas. Así que sé inteligente y haz lo que más te convenga.

Para responder a su pregunta, en realidad también desea desacoplar el controlador de la vista (para que pueda usar la misma lógica de reglas de negocio tanto para una aplicación Swing como en la aplicación de consola). En el ejemplo de Swing, desea desacoplar el controlador del JWindowy cualquier widget en Swing. La forma en que solía hacerlo (antes de usar marcos reales) es crear una interfaz para la vista que usa el controlador:

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

}

Para esta solución durante el inicio, debe registrar el controlador en la vista.

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
    }

}

Puede ser una buena idea crear un contenedor de IoC para hacer toda la configuración por usted.

De todos modos, de esta manera puede implementar vistas solo de consola, utilizando los mismos controladores:

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 parte divertida es cómo manejar eventos. Implementé esto permitiendo que la vista se registre en el controlador usando una interfaz, esto se hace usando el patrón Observador (si está usando .NET, en su lugar usaría controladores de eventos). Aquí hay un ejemplo de un simple "observador de documentos", que señala cuándo se ha guardado o cargado un documento.

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 esa manera, la vista puede actualizarse correctamente ya que se está suscribiendo a las actualizaciones de documentos. Todo lo que tiene que hacer es implementar la DocumentObserverinterfaz:

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
    }

    // ...

}

Espero que estos ejemplos motivadores te den algunas ideas sobre cómo hacerlo tú mismo. Sin embargo, le recomiendo encarecidamente que considere usar frameworks en Java que hagan la mayoría de las cosas por usted, o de lo contrario terminará teniendo una gran cantidad de código repetitivo que lleva mucho tiempo escribir. Hay un par de plataformas de cliente enriquecido (RCP) que puede usar que implementan algunas de las funciones básicas que probablemente necesite, como el manejo de documentos en toda la aplicación y una gran cantidad de manejo básico de eventos.

Puedo pensar en un par de mi cabeza: Eclipse y Netbeans RCP.

Aún necesita desarrollar controladores y modelos para usted, pero es por eso que usa un ORM. El ejemplo sería Hibernate .

Los contenedores de IoC son geniales, pero también hay marcos para esto. Como Spring (que también maneja datos, entre otras cosas).

Spoike
fuente
Gracias por tomarse el tiempo para escribir una respuesta tan larga. La mayor parte está un poco sobre mi cabeza en este momento, pero estoy seguro de que Wikipedia ayudará. Estoy usando Eclipse y Netbeans, y me estoy inclinando por lo último. No estoy seguro de cómo distinguir el Controlador de la Vista allí, pero su ejemplo previo al marco en la parte superior ayuda.
Capítulo
0

Mi opinión sobre esto es que a veces necesitamos comprometernos

Como usted dice, sería genial si pudiéramos propagar la notificación de cambio implícitamente sin que el objeto observado tenga una infraestructura explícita para esto. Para lenguajes imperativos comunes como Java, C #, C ++, su arquitectura de tiempo de ejecución es demasiado ligera, a partir de ahora, de todos modos. Con eso quiero decir que no es parte de la especificación del lenguaje en este momento.

En su caso particular, no creo que sea exactamente malo definir / usar alguna interfaz genérica como INotifyPropertyChanged (como en c #), ya que de todos modos eso no se acopla automáticamente a una vista, solo dice que si cambio, yo te diré .

Una vez más, estoy de acuerdo en que sería genial si no tuviéramos que definir la notificación de cambio nosotros mismos, pero, de nuevo, si fuera implícito para todas las clases, podría incurrir en gastos generales.

Max
fuente
cuanto más pienso en ello, me cuenta que tienes razón - el objeto del modelo tiene que estar involucrado con alguna medida en el inicio de la notificación a la interfaz gráfica de usuario que se ha producido un cambio; de lo contrario, la GUI tendría que sondear el modelo para descubrir cambios, y eso es malo.
Cap