Jak całkowicie oddzielić model od widoku / kontrolera w Java Swing

10

Czy istnieje zbiór wspólnie uzgodnionych wytycznych projektowych dotyczących oddzielania klas Model od klas View / Controller w aplikacji Java Swing? Nie martwię się tak bardzo, że Widok / Kontroler nic nie wie o Modelu, jak na odwrót: Chciałbym zaprojektować mój Model tak, aby nic nie wiedział o javax.swing. Idealnie powinien mieć prosty interfejs API umożliwiający sterowanie nim przez coś tak prymitywnego jak CLI. Luźno mówiąc, powinien to być „silnik”.

Przekazywanie zdarzeń GUI do modelu nie jest zbyt trudne - Wykonawcy akcji mogą wywoływać interfejs API modelu. Ale co z tym, kiedy model wprowadza własne zmiany stanu, które muszą zostać odzwierciedlone z powrotem w GUI? Do tego właśnie służy „słuchanie”, ale nawet „słuchanie” nie jest całkowicie bierne; wymaga, aby model wiedział o dodaniu detektora.

Szczególny problem, który skłonił mnie do myślenia, dotyczy kolejki plików. Po stronie GUI znajduje się DefaultListModelza JList, a niektóre elementy GUI do wyboru plików z systemu plików i dodania ich do JList. Po stronie modelu chce usunąć pliki z dolnej części tej „kolejki” (powodując ich zniknięcie z listy JList) i przetworzyć je w jakiś sposób. W rzeczywistości kod modelu jest już napisany - obecnie utrzymuje ArrayList<File>i udostępnia add(File)metodę publiczną . Ale nie mam pojęcia, jak sprawić, by mój model działał z View / Controller bez pewnych ciężkich, specyficznych dla Swinga modyfikacji modelu.

Jestem bardzo nowy zarówno w programowaniu w Javie, jak i GUI, zawsze do tej pory programowałem „wsadowo” i „back-end” - stąd moje zainteresowanie utrzymaniem ścisłego podziału między modelem a interfejsem użytkownika, jeśli jest to możliwe i czy może być nauczonym.

Facet
źródło
Btw: W MVC model powinien być domyślnie oddzielony od widoku i kontrolera. Powinieneś ponownie przeczytać swój podręcznik, myślę, że nie do końca zrozumiałeś tę koncepcję. I możesz zaimplementować niestandardową kolekcję, która powiadamia o jej zmianie, podobnie jak interfejs INotifyCollectionChanged .NET.
Falcon
@Falcon: dzięki za linki. Nie jestem jednak pewien, czy rozumiem twój komentarz. „Model powinien być domyślnie oddzielony od widoku i kontrolera”. Czy mógłbyś przeformułować lub rozwinąć?
Rozdz.

Odpowiedzi:

10

Nie ma powszechnie uzgodnionych (tj. Defacto ) wytycznych projektowych dla MVC. Nie jest to wcale takie trudne, ale wymaga planowania na zajęciach oraz dużo czasu i cierpliwości.

Powodem, dla którego nie ma określonego rozwiązania, jest to, że istnieje wiele sposobów na MVC, wszystkie z ich zaletami i wadami. Bądź więc mądry i rób to, co najbardziej ci odpowiada.

Aby odpowiedzieć na twoje pytanie, faktycznie chcesz również oddzielić kontroler od widoku (abyś mógł używać tej samej logiki reguł biznesowych zarówno dla aplikacji Swing, jak i konsoli). W przykładzie Swing chcesz oddzielić kontroler od JWindowi dowolnego widgetu w Swing. Sposób, w jaki kiedyś to robiłem (przed użyciem rzeczywistych frameworków), to stworzenie interfejsu dla widoku używanego przez kontroler:

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

}

W przypadku tego rozwiązania podczas uruchamiania należy zarejestrować kontroler w widoku.

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
    }

}

Dobrym pomysłem może być utworzenie kontenera IoC w celu wykonania całej konfiguracji.

W każdym razie w ten sposób możesz zaimplementować widoki tylko na konsolę, używając tych samych kontrolerów:

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

}    

Zabawne jest to, jak obsługiwać zdarzenia. Zaimplementowałem to, pozwalając widokowi zarejestrować się w kontrolerze za pomocą interfejsu, odbywa się to za pomocą wzorca Observer (jeśli używasz platformy .NET, zamiast tego używałbyś programów obsługi zdarzeń). Oto przykład prostego „obserwatora dokumentów”, który sygnalizuje zapisanie lub załadowanie dokumentu.

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

W ten sposób widok może się poprawnie zaktualizować, ponieważ subskrybuje aktualizacje dokumentu. Wystarczy zaimplementować DocumentObserverinterfejs:

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
    }

    // ...

}

Mam nadzieję, że te motywujące przykłady podadzą kilka pomysłów, jak to zrobić samodzielnie. Jednak zdecydowanie radzę rozważyć użycie frameworków w Javie, które wykonują większość rzeczy za Ciebie, w przeciwnym razie skończy się dużo kodu, który zajmuje dużo czasu, aby napisać. Istnieje kilka bogatych platform klienta (RCP), które można wykorzystać, które implementują niektóre podstawowe funkcje, które najprawdopodobniej będą potrzebne, takie jak obsługa dokumentów w całej aplikacji i wiele podstawowych czynności związanych z obsługą zdarzeń.

Z głowy mogę wymyślić kilka: RCP Eclipse i Netbeans .

Nadal musisz opracować dla siebie kontrolery i modele, ale właśnie dlatego używasz ORM. Przykładem może być Hibernacja .

Kontenery IoC są fajne, ale istnieją też ramy dla tego. Takich jak Spring (który zajmuje się także przetwarzaniem danych, między innymi).

Łup
źródło
Dziękujemy za poświęcenie czasu na napisanie tak długiej odpowiedzi. W tym momencie większość z nich jest trochę ponad moją głową, ale jestem pewien, że Wikipedia pomoże. Ja jestem przy użyciu zarówno Eclipse i Netbeans, a opierając się na tych ostatnich. Nie jestem pewien, jak odróżnić kontroler od widoku tam, ale twój przykład wstępnego frameworka u góry pomaga.
Rozdz.
0

Uważam, że czasami musimy iść na kompromis

Jak mówisz, byłoby wspaniale, gdybyśmy mogli propagować powiadomienia o zmianach w sposób dorozumiany bez obserwowanego obiektu posiadającego jawną infrastrukturę do tego. W przypadku popularnych języków imperatywnych, takich jak Java, C #, C ++, ich architektura uruchomieniowa jest na razie zbyt lekka. Rozumiem przez to, że nie jest to obecnie część specyfikacji języka.

W twoim konkretnym przypadku nie wydaje mi się, aby zdefiniowanie / użycie jakiegoś ogólnego interfejsu, takiego jak INotifyPropertyChanged (jak w c #), było złym posunięciem , ponieważ i tak nie jest ono automatycznie łączone z widokiem - po prostu mówi, że jeśli zmienię, powiem wam .

Ponownie zgadzam się, że byłoby wspaniale, gdybyśmy nie musieli sami definiować powiadomienia o zmianie, ale z drugiej strony, jeśli byłoby to domniemane dla wszystkich klas, mogłoby to spowodować obciążenie ogólne.

Max
źródło
im więcej o tym myślę, zdaję sobie sprawę, że masz rację - obiekt modelowy musi być w pewnym stopniu zaangażowany w inicjowanie powiadomienia GUI o zaistnieniu zmiany; w przeciwnym razie GUI musiałby sondować model, aby wykryć zmiany - i to źle.
Rozdz.