Шаблон MVC и SWING

Одним из шаблонов проектирования, который мне наиболее трудно понять в реальной жизни SWING, является шаблон MVC. Я прочитал немало постов на этом сайте, в которых обсуждается шаблон, но я до сих пор не чувствую, что у меня есть четкое понимание того, как использовать шаблон в моем приложении (Java SWING).

Допустим, у меня есть JFrame, который содержит таблицу, пару текстовых полей и несколько кнопок. Я бы, вероятно, использовал TableModel, чтобы "связать" JTable с базовой моделью данных. Однако все функции, отвечающие за очистку полей, проверку полей, блокировку полей и действия кнопок, обычно выполняются непосредственно в JFrame. Однако разве это не смешивает Контроллер и Вид шаблона?

Насколько я вижу, мне удается "правильно" реализовать шаблон MVC, когда я смотрю на JTable (и модель), но все становится мутным, когда я смотрю на весь JFrame в целом.

Мне бы очень хотелось услышать, как другие относятся к этому. Как вы поступаете, когда вам нужно отобразить таблицу, пару полей и несколько кнопок для пользователя (используя шаблон MVC)?

7 ответов

Решение

Книга, которую я бы настоятельно рекомендовал вам для свинга MVC, будет "Head First Design Patterns" Фримена и Фримена. У них очень подробное объяснение MVC.

Краткое содержание

  1. Вы пользователь - вы взаимодействуете с представлением. Вид - это ваше окно в модель. Когда вы что-то делаете с представлением (например, нажимаете кнопку Play), тогда представление сообщает контроллеру, что вы сделали. Это работа контроллера, чтобы справиться с этим.

  2. Контроллер просит модель изменить свое состояние. Контролер принимает ваши действия и интерпретирует их. Если вы нажмете кнопку, то задача контроллера - выяснить, что это значит и как следует манипулировать моделью на основе этого действия.

  3. Контроллер также может попросить изменить вид. Когда контроллер получает действие от представления, ему, возможно, потребуется сообщить представлению об изменении в результате. Например, контроллер может включать или отключать определенные кнопки или элементы меню в интерфейсе.

  4. Модель уведомляет представление, когда его состояние изменилось. Когда что-то меняется в модели, основываясь на каком-либо действии, которое вы предприняли (например, нажав кнопку), или на каком-либо другом внутреннем изменении (например, началась следующая песня в списке воспроизведения), модель уведомляет представление о том, что ее состояние изменилось.

  5. Представление просит модель для государства. Представление получает состояние, которое оно отображает непосредственно из модели. Например, когда модель уведомляет представление о начале воспроизведения новой песни, представление запрашивает название песни у модели и отображает его. Представление может также запрашивать у модели состояние в результате того, что контроллер запрашивает некоторое изменение в представлении.

Источник (Если вам интересно, что такое "кремообразный контроллер", подумайте о печенье Oreo, где контроллер является кремовым центром, вид - верхнее печенье, а модель - нижнее печенье).

Хм, если вам интересно, вы можете скачать довольно интересную песню о паттерне MVC отсюда!

Одна из проблем, с которой вы можете столкнуться при программировании на Swing, заключается в объединении потоков SwingWorker и EventDispatch с шаблоном MVC. В зависимости от вашей программы вашему представлению или контроллеру может потребоваться расширить SwingWorker и переопределить doInBackground() метод размещения ресурсоемкой логики. Это можно легко объединить с типичным шаблоном MVC и типично для приложений Swing.

РЕДАКТИРОВАНИЕ № 1:

Кроме того, важно рассматривать MVC как своего рода смесь различных шаблонов. Например, ваша модель может быть реализована с использованием шаблона Observer (требуется, чтобы представление было зарегистрировано в качестве наблюдателя модели), в то время как ваш контроллер может использовать шаблон Strategy.

РЕДАКТИРОВАТЬ № 2:

Я также хотел бы ответить конкретно на ваш вопрос. Вы должны отобразить кнопки таблицы и т. Д. В представлении, которое, очевидно, будет реализовывать ActionListener. В вашем actionPerformed() метод, вы обнаруживаете событие и отправляете его связанному методу в контроллере (помните - представление содержит ссылку на контроллер). Таким образом, когда кнопка нажата, событие обнаруживается представлением, отправленным методу контроллера, контроллер может напрямую попросить представление отключить кнопку или что-то в этом роде. Затем контроллер будет взаимодействовать с моделью и изменять ее (которая в основном будет содержать методы получения и установки, а также некоторые другие для регистрации и уведомления наблюдателей и т. Д.). Как только модель будет изменена, она вызовет обновление для зарегистрированных наблюдателей (это будет представление в вашем случае). Следовательно, представление теперь обновится само.

Мне не нравится идея, что представление является тем, которое уведомляется моделью, когда ее данные изменяются. Я бы делегировал эту функциональность контроллеру. В этом случае, если вы измените логику приложения, вам не нужно вмешиваться в код представления. Задача представления только для компонентов приложения + компоновка не более, не более того. Разметка в свинге - это уже сложная задача, почему она мешает логике приложений?

Моя идея MVC (с которой я сейчас работаю, пока что хороша) такова:

  1. Вид является самым тупым из трех. Он ничего не знает о контроллере и модели. Его заботит только простетика и расположение компонентов качелей.
  2. Модель тоже тупая, но не такая тупая, как вид. Он выполняет следующие функции.
    • а. когда один из его установщиков вызывается контроллером, он запускает уведомление своих слушателей / наблюдателей (как я уже сказал, я бы переназначил эту роль на контроллер). Я предпочитаю SwingPropertyChangeSupport для достижения этой цели, так как он уже оптимизирован для этой цели.
    • б. функциональность взаимодействия с базой данных.
  3. Очень умный контроллер. Знает вид и модель очень хорошо. Контроллер имеет две функции:
    • а. Он определяет действие, которое будет выполнять представление, когда пользователь взаимодействует с ним.
    • б. Слушает модель. Как я уже говорил, когда вызывается установщик модели, модель отправляет уведомление контроллеру. Задача контроллера - интерпретировать это уведомление. Возможно, потребуется отразить изменение в представлении.

Пример кода

Вид:

Как я уже сказал, создание представления уже многословно, поэтому просто создайте свою собственную реализацию:)

interface View{
    JTextField getTxtFirstName();
    JTextField getTxtLastName();
    JTextField getTxtAddress();
}

Это идеальное решение для сопряжения трех объектов в целях тестирования. Я только представил свою реализацию модели и контроллера.

Модель:

public class MyImplementationOfModel implements Model{
    ...
    private SwingPropertyChangeSupport propChangeFirer;
    private String address;
    private String firstName;
    private String lastName;

    public MyImplementationOfModel() {
        propChangeFirer = new SwingPropertyChangeSupport(this);
    }
    public void addListener(PropertyChangeListener prop) {
        propChangeFirer.addPropertyChangeListener(prop);
    }
    public void setAddress(String address){
        String oldVal = this.address;
        this.address = address;

        //after executing this, the controller will be notified that the new address has been set. Its then the controller's
        //task to decide what to do when the address in the model has changed. Ideally, the controller will update the view about this
        propChangeFirer.firePropertyChange("address", oldVal, address);
    }
    ...
    //some other setters for other properties & code for database interaction
    ...
}

Контроллер:

public class MyImplementationOfController implements PropertyChangeListener, Controller{

    private View view;
    private Model model;

    public MyImplementationOfController(View view, Model model){
        this.view = view;
        this.model = model;

        //register the controller as the listener of the model
        this.model.addListener(this);

        setUpViewEvents();
    }

    //code for setting the actions to be performed when the user interacts to the view.
    private void setUpViewEvents(){
        view.getBtnClear().setAction(new AbstractAction("Clear") { 
            @Override
            public void actionPerformed(ActionEvent arg0) {
                model.setFirstName("");
                model.setLastName("");
                model.setAddress("");
            }
        });

        view.getBtnSave().setAction(new AbstractAction("Save") { 
            @Override
            public void actionPerformed(ActionEvent arg0) {
                ...
                //validate etc.
                ...
                model.setFirstName(view.getTxtFName().getText());
                model.setLastName(view.getTxtLName().getText());
                model.setAddress(view.getTxtAddress().getText());
                model.save();
            }
        });
    }

    public void propertyChange(PropertyChangeEvent evt){
        String propName = evt.getPropertyName();
        Object newVal = evt.getNewValue();

        if("address".equalsIgnoreCase(propName)){
            view.getTxtAddress().setText((String)newVal);
        }
        //else  if property (name) that fired the change event is first name property
        //else  if property (name) that fired the change event is last name property
    }
}

Главный, где находится MVC:

public class Main{
    public static void main(String[] args){
        View view = new YourImplementationOfView();
        Model model = new MyImplementationOfModel();

        ...
        //create jframe
        //frame.add(view.getUI());
        ...

        //make sure the view and model is fully initialized before letting the controller control them.
        Controller controller = new MyImplementationOfController(view, model);

        ...
        //frame.setVisible(true);
        ...
    }
}

Шаблон MVC - это модель того, как может быть структурирован пользовательский интерфейс. Поэтому он определяет 3 элемента Model, View, Controller:

  • Модель Модель - это абстракция чего-то, что представлено пользователю. В разгаре у вас есть дифференциация моделей графического интерфейса и моделей данных. Модели GUI абстрагируют состояние компонента пользовательского интерфейса, такого как ButtonModel. Модели данных абстрагируют структурированные данные, которые пользовательский интерфейс представляет пользователю, например, TableModel.
  • Представление Представление представляет собой компонент пользовательского интерфейса, который отвечает за представление данных пользователю. Таким образом, он отвечает за все зависящие от пользовательского интерфейса вопросы, такие как макет, рисование и т. Д. Например, JTable.
  • Контроллер Контроллер инкапсулирует код приложения, который выполняется для взаимодействия с пользователем (движение мыши, щелчок мыши, нажатие клавиши и т. Д.). Контроллерам может потребоваться ввод для их выполнения, и они производят вывод. Они читают свои данные из моделей и обновляют модели в результате выполнения. Они также могут реструктурировать пользовательский интерфейс (например, заменить компоненты пользовательского интерфейса или показать полностью новый вид). Однако они не должны знать о компонентах пользовательского интерфейса, потому что вы можете инкапсулировать реструктуризацию в отдельный интерфейс, который только вызывает контроллер. В свинге контроллер обычно реализуется ActionListener или Action.

пример

  • Красный = модель
  • Зеленый = вид
  • Синий = контроллер

Когда Button по щелчку вызывает ActionListener, ActionListener зависит только от других моделей. Некоторые модели используются в качестве входных данных, а другие - в качестве результата или вывода. Это как аргументы метода и возвращаемые значения. Модели уведомляют пользовательский интерфейс, когда они обновляются. Таким образом, логике контроллера не нужно знать компонент пользовательского интерфейса. Объекты модели не знают пользовательского интерфейса. Уведомление делается по схеме наблюдателя. Таким образом, объектам модели известно только, что есть кто-то, кто хочет получить уведомление, если модель изменится.

В Java Swing есть некоторые компоненты, которые также реализуют модель и контроллер. Например, javax.swing.Action. Он реализует модель пользовательского интерфейса (свойства: enablement, маленький значок, имя и т. Д.) И является контроллером, поскольку расширяет ActionListener.

Подробное объяснение, пример приложения и исходный код: https://www.link-intersystems.com/blog/2013/07/20/the-mvc-pattern-implemented-with-java-swing/.

Основы MVC менее чем в 240 строках:

public class Main {

    public static void main(String[] args) {
        JFrame mainFrame = new JFrame("MVC example");
        mainFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        mainFrame.setSize(640, 300);
        mainFrame.setLocationRelativeTo(null);

        PersonService personService = new PersonServiceMock();

        DefaultListModel searchResultListModel = new DefaultListModel();
        DefaultListSelectionModel searchResultSelectionModel = new DefaultListSelectionModel();
        searchResultSelectionModel
                .setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        Document searchInput = new PlainDocument();

        PersonDetailsAction personDetailsAction = new PersonDetailsAction(
                searchResultSelectionModel, searchResultListModel);
        personDetailsAction.putValue(Action.NAME, "Person Details");

        Action searchPersonAction = new SearchPersonAction(searchInput,
                searchResultListModel, personService);
        searchPersonAction.putValue(Action.NAME, "Search");

        Container contentPane = mainFrame.getContentPane();

        JPanel searchInputPanel = new JPanel();
        searchInputPanel.setLayout(new BorderLayout());

        JTextField searchField = new JTextField(searchInput, null, 0);
        searchInputPanel.add(searchField, BorderLayout.CENTER);
        searchField.addActionListener(searchPersonAction);

        JButton searchButton = new JButton(searchPersonAction);
        searchInputPanel.add(searchButton, BorderLayout.EAST);

        JList searchResultList = new JList();
        searchResultList.setModel(searchResultListModel);
        searchResultList.setSelectionModel(searchResultSelectionModel);

        JPanel searchResultPanel = new JPanel();
        searchResultPanel.setLayout(new BorderLayout());
        JScrollPane scrollableSearchResult = new JScrollPane(searchResultList);
        searchResultPanel.add(scrollableSearchResult, BorderLayout.CENTER);

        JPanel selectionOptionsPanel = new JPanel();

        JButton showPersonDetailsButton = new JButton(personDetailsAction);
        selectionOptionsPanel.add(showPersonDetailsButton);

        contentPane.add(searchInputPanel, BorderLayout.NORTH);
        contentPane.add(searchResultPanel, BorderLayout.CENTER);
        contentPane.add(selectionOptionsPanel, BorderLayout.SOUTH);

        mainFrame.setVisible(true);
    }

}

class PersonDetailsAction extends AbstractAction {

    private static final long serialVersionUID = -8816163868526676625L;

    private ListSelectionModel personSelectionModel;
    private DefaultListModel personListModel;

    public PersonDetailsAction(ListSelectionModel personSelectionModel,
            DefaultListModel personListModel) {
        boolean unsupportedSelectionMode = personSelectionModel
                .getSelectionMode() != ListSelectionModel.SINGLE_SELECTION;
        if (unsupportedSelectionMode) {
            throw new IllegalArgumentException(
                    "PersonDetailAction can only handle single list selections. "
                            + "Please set the list selection mode to ListSelectionModel.SINGLE_SELECTION");
        }
        this.personSelectionModel = personSelectionModel;
        this.personListModel = personListModel;
        personSelectionModel
                .addListSelectionListener(new ListSelectionListener() {

                    public void valueChanged(ListSelectionEvent e) {
                        ListSelectionModel listSelectionModel = (ListSelectionModel) e
                                .getSource();
                        updateEnablement(listSelectionModel);
                    }
                });
        updateEnablement(personSelectionModel);
    }

    public void actionPerformed(ActionEvent e) {
        int selectionIndex = personSelectionModel.getMinSelectionIndex();
        PersonElementModel personElementModel = (PersonElementModel) personListModel
                .get(selectionIndex);

        Person person = personElementModel.getPerson();
        String personDetials = createPersonDetails(person);

        JOptionPane.showMessageDialog(null, personDetials);
    }

    private String createPersonDetails(Person person) {
        return person.getId() + ": " + person.getFirstName() + " "
                + person.getLastName();
    }

    private void updateEnablement(ListSelectionModel listSelectionModel) {
        boolean emptySelection = listSelectionModel.isSelectionEmpty();
        setEnabled(!emptySelection);
    }

}

class SearchPersonAction extends AbstractAction {

    private static final long serialVersionUID = 4083406832930707444L;

    private Document searchInput;
    private DefaultListModel searchResult;
    private PersonService personService;

    public SearchPersonAction(Document searchInput,
            DefaultListModel searchResult, PersonService personService) {
        this.searchInput = searchInput;
        this.searchResult = searchResult;
        this.personService = personService;
    }

    public void actionPerformed(ActionEvent e) {
        String searchString = getSearchString();

        List<Person> matchedPersons = personService.searchPersons(searchString);

        searchResult.clear();
        for (Person person : matchedPersons) {
            Object elementModel = new PersonElementModel(person);
            searchResult.addElement(elementModel);
        }
    }

    private String getSearchString() {
        try {
            return searchInput.getText(0, searchInput.getLength());
        } catch (BadLocationException e) {
            return null;
        }
    }

}

class PersonElementModel {

    private Person person;

    public PersonElementModel(Person person) {
        this.person = person;
    }

    public Person getPerson() {
        return person;
    }

    @Override
    public String toString() {
        return person.getFirstName() + ", " + person.getLastName();
    }
}

interface PersonService {

    List<Person> searchPersons(String searchString);
}

class Person {

    private int id;
    private String firstName;
    private String lastName;

    public Person(int id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public int getId() {
        return id;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

}

class PersonServiceMock implements PersonService {

    private List<Person> personDB;

    public PersonServiceMock() {
        personDB = new ArrayList<Person>();
        personDB.add(new Person(1, "Graham", "Parrish"));
        personDB.add(new Person(2, "Daniel", "Hendrix"));
        personDB.add(new Person(3, "Rachel", "Holman"));
        personDB.add(new Person(4, "Sarah", "Todd"));
        personDB.add(new Person(5, "Talon", "Wolf"));
        personDB.add(new Person(6, "Josephine", "Dunn"));
        personDB.add(new Person(7, "Benjamin", "Hebert"));
        personDB.add(new Person(8, "Lacota", "Browning "));
        personDB.add(new Person(9, "Sydney", "Ayers"));
        personDB.add(new Person(10, "Dustin", "Stephens"));
        personDB.add(new Person(11, "Cara", "Moss"));
        personDB.add(new Person(12, "Teegan", "Dillard"));
        personDB.add(new Person(13, "Dai", "Yates"));
        personDB.add(new Person(14, "Nora", "Garza"));
    }

    public List<Person> searchPersons(String searchString) {
        List<Person> matches = new ArrayList<Person>();

        if (searchString == null) {
            return matches;
        }

        for (Person person : personDB) {
            if (person.getFirstName().contains(searchString)
                    || person.getLastName().contains(searchString)) {
                matches.add(person);
            }

        }
        return matches;
    }

}

Для правильного разделения у вас обычно будет класс контроллера, которому делегируется класс Frame. Существуют различные способы установить отношения между классами - вы можете реализовать контроллер и расширить его классом основного представления или использовать отдельный класс контроллера, который Frame вызывает при возникновении событий. Представление обычно получает события от контроллера, реализуя интерфейс слушателя.

Иногда одна или несколько частей шаблона MVC являются тривиальными или настолько "тонкими", что это добавляет ненужную сложность для их разделения. Если ваш контроллер полон однострочных вызовов, наличие его в отдельном классе может привести к запутыванию основного поведения. Например, если все события, которые вы обрабатываете, относятся к TableModel и являются простыми операциями добавления и удаления, вы можете выбрать реализацию всех функций манипулирования таблицами в этой модели (а также обратные вызовы, необходимые для отображения в JTable). Это не настоящий MVC, но он избегает добавления сложности там, где это не нужно.

Как бы вы это ни реализовывали, не забывайте JavaDoc о ваших классах, методах и пакетах, чтобы компоненты и их отношения были правильно описаны!

Вы можете создать модель в отдельном простом Java-классе, а контроллер - в другом.

Тогда у вас могут быть компоненты Swing. JTable будет одним из представлений (и табличная модель де-факто будет частью представления - она ​​будет переводить только из "общей модели" в JTable).

Всякий раз, когда таблица редактируется, ее табличная модель сообщает "главному контроллеру" что-то обновить. Тем не менее, контроллер не должен ничего знать о таблице. Таким образом, вызов должен выглядеть так: updateCustomer(customer, newValue)не updateCustomer(row, column, newValue),

Добавьте интерфейс слушателя (наблюдателя) для совместно используемой модели. Некоторые компоненты (например, ваша таблица) могут реализовать это напрямую. Другим наблюдателем может быть контроллер, который координирует доступность кнопок и т. Д.


Это один из способов сделать это, но, конечно, вы можете упростить или расширить его, если это является излишним для вашего варианта использования.

Вы можете объединить контроллер с моделью и иметь те же обновления процесса класса и поддерживать доступность компонента. Вы даже можете сделать "общую модель" TableModel (хотя, если он используется не только таблицей, я бы рекомендовал, по крайней мере, предоставить более удобный API, который не пропускает абстракции таблицы)

С другой стороны, вы можете иметь сложные интерфейсы для обновлений (CustomerUpdateListener, OrderItemListener, OrderCancellationListener) и выделенный контроллер (или посредник) только для согласования разных точек зрения.

Это зависит от того, насколько сложна ваша проблема.

Я нашел несколько интересных статей о реализации шаблонов MVC, которые могут решить вашу проблему.

Если вы разрабатываете программу с графическим интерфейсом, шаблон mvc почти не существует, но размыт.

Разобрать код модели, вида и кода контроллера сложно, и обычно это не только задача рефакторинга.

Вы знаете, что он у вас есть, когда ваш код можно использовать повторно. Если вы правильно внедрили MVC, должно быть легко реализовать TUI, CLI, RWD или мобильную конструкцию с той же функциональностью. Легко увидеть, что это сделано, чем сделать это на самом деле, более того, на существующем коде.

Фактически, взаимодействие между моделью, представлением и контроллером происходит с использованием других шаблонов изоляции (таких как Observer или Listener).

Я предполагаю, что этот пост объясняет это подробно, от прямого не-MVC шаблона (как вы будете делать в Q & D) до окончательной многоразовой реализации:

http://www.austintek.com/mvc/

Другие вопросы по тегам