2. Паттерн MVC

Паттерн MVC можно описать следующим образом

Шаблон проектирования MVC предполагает разделение данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента: Модель, Представление и Контроллер – таким образом, что модификация каждого компонента может осуществляться независимо.

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

Архитектурная концепция (архитектурный паттерн) MVC позволяет разделить программу на три отдельных компонента, которые могут быть реализованы следующим образом:

1. Пользователь взаимодействует с представлением. Представление – «окно», через которое пользователь воспринимает модель. Когда вы делаете что-то с представлением (скажем, щелкаете на кнопке воспроизведения), представление сообщает контроллеру, какая операция была выполнена. Контроллер должен обработать это действие.

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

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

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

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

Модель (Model) – содержит бизнес-логику приложения и включает методы выборки, обработки и предоставления конкретных данных, что зачастую делает ее очень «толстой», что вполне нормально. Модель не должна напрямую взаимодействовать с пользователем. Модель:

  • предоставляет (представлению и контроллеру):

    • данные;

    • методы работы с данными:

      • запросы к базам данных;

      • валидация данных;

      • бизнес-логика (если модель «активна»).

  • нуждается в следующем:

    • в представлении (не может самостоятельно демонстрировать данных и результаты их обработки);

    • в контроллере (не имеет точек взаимодействия с пользователем).

  • может иметь множество различных представлений и контроллеров;

  • отвечает на запросы изменением состояния. При этом, в модель может быть встроено автоматическое оповещение «наблюдателей».

Представление (View) – используется для задания внешнего вида отображения данных, полученных из контроллера и модели. Представление не должно обращаться к базе данных, этим должны заниматься модели. Также, представление не должно работать с данными, полученными из запроса пользователя. Эту задачу должен выполнять контроллер. Представление:

  • отвечает за получение необходимых данных от модели и отправку их пользователю;

  • не обрабатывает введенные данные пользователя;

  • может влиять на состояние модели через отправку ей сообщений (вызовы методов).

Контроллер (Controller) – связующее звено, соединяющее модели, представления и другие компоненты в рабочее приложение. Контроллер отвечает за обработку действий пользователя. В хорошо спроектированном MVC-приложении контроллеры обычно очень «тонкие» и содержат только несколько десятков строк кода. Логика контроллера довольно типична и большая ее часть выносится в базовые классы. Модели, наоборот, очень «толстые» и содержат большую часть кода, связанную с обработкой данных, так как структура данных и бизнес-логика, содержащаяся в них, обычно довольно специфична для конкретного приложения. Контроллер:

  • обеспечивает «связь» между пользователем и системой, контролирует и направляет:

    • данные от пользователя к системе;

    • реакцию системы – пользователю.

  • использует модель и представление для необходимого действия;

  • в случае «пассивной» модели – реализует бизнес-логику.

Рассмотрим пример реализации паттерна MVC на примере просто приложения. Приложение выводит на экран список студентов, добавляет нового студента и удаляет выделенного студента из списка.

Для начала, создадим новый JavaFX проект. Шаблон JavaFX проекта содержит три файла. Давайте определимся, к какой категории относится тот или иной файл.

Файл sample.fxml, очевидно, относится к представлению (View). Он задает внешний вид приложения.

Класс Main (файл Main.java) относится к контроллеру. Этот класс можно назвать контроллером приложения. Он содержит метод Application.launch(), который запускает JavaFX приложение, настраивается окно, создается объект сцены, устанавливается нужный fxml файл для генерации графа сцены для данного окна.

Класс Controller (файл Controller.java) очевидно является контроллером. Это контроллер окна, который содержит код для обработки событий, связанный с данным окном.

Создадим соответствующие пакеты и распределим файлы по своим пакетам. Не забудьте изменить путь к файлу fxml.

Теперь создадим класс Student, который будет содержать данные и логику для объекта студента. Очевидно, что этот класс относится к классу модели.

Student.java
public class Student {

    private String firstName;
    private String lastName;
    private String group;

    public Student(String firstName, String lastName, String group) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.group = group;
    }

    @Override
    public String toString() {
        return "Student{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", group='" + group + '\'' +
                '}';
    }
}

Спроектируем UI для нашего приложения. Для этого отредактируем файл sample.fxml. За вывод списка студентов будет отвечать элемент ListView. Кроме него предусмотрены две кнопки для добавления и удаления студента.

Добавим обработчики нажатий на кнопки в контроллер окна Controller.java.

Controller.java
public class Controller implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
    }

    @FXML
    public void add(ActionEvent event) {
        // Обработчик кнопки "Добавить"
    }

    @FXML
    public void delete(ActionEvent event) {
        // Обработчик кнопки "Удалить"
    }
}

Теперь вернемся к классам модели. Очевидно, что в данном приложении объекты класса Student должны быть организованы в некоторую коллекцию. Кроме того, мы должны предоставить метод добавления и удаления студентов из коллекции.

Так как «сборка» отдельных частей приложения осуществляется в контроллере – организаторе и посреднике между различными компонентами приложения, самым простым вариантом было бы просто создать пустую коллекцию студентов, в которую можно было бы напрямую добавлять или удалять студентов.

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

Таким образом, первый кандидат на хранении коллекции со студентами – контроллер приложения, класс Main. Код для такого варианта будет примерно следующим:

Main.java
public class Main extends Application {

    private List<Student> list;

    @Override
    public void init() throws Exception {
        list = new ArrayList<>();
    }

    public void addStudent(Student student) {
        list.add(student);
    }

    public void deleteStudent(Student student) {
        list.remove(student);
    }


    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }
}

Методы addStudent() и deleteStudent() будут вызываться из контроллера окна вместе с объектом для добавления или удаления.

Такой подход оправдан только, если мы точно знаем, что программа дальше не будет расширяться и наша модель будет, фактически, состоять из одного класса Student. Но мы хотим оставить задел на будущее, когда у нас может появиться класс студенческой группы, класс с оценками, класс учебной дисциплины, данные могут записываться и считываться из файлов, передаваться по сети и так далее.

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

Поэтому, лучшим вариантом будет применение паттерна проектирования «Фасад», который позволит перенести весь будущий функционал в слой модели.

Принцип работы паттерна «Фасад» можно объяснить следующим образом: фасад – это объект некоторого класса, который предоставляет простой (но урезанный) интерфейс работы со сложной системой объектов. Таким образом, если наша модель будет усложняться, и будут добавляться новые классы, то будет усложняться внутреннее строение фасада, а внешне он будет предоставлять те же самые простые методы для работы со сложной моделью.

Создадим класс StudentsFacade. Класс будет содержать коллекцию со студентами, а также предоставлять публичные методы добавления и удаления студента.

StudentFacade.java
public class StudentFacade {
    private List<Student> studentList;

    public StudentFacade() {
        studentList = new ArrayList<>();
    }

    public void addStudent(Student student) {
        studentList.add(student);
    }

    public void deleteStudent(Student student) {
        studentList.remove(student);
    }
}

Теперь создадим объект фасада в контроллере приложения и создадим методы, которые будут вызывать методы фасада.

Main.java
public class Main extends Application {

    private StudentFacade facade;

    @Override
    public void init() throws Exception {
        facade = new StudentFacade();
    }

    public void addStudent(Student student) {
        facade.addStudent(student);
    }

    public void deleteStudent(Student student) {
        facade.deleteStudent(student);
    }
    
    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }
    
    public static void main(String[] args) {
        launch(args);
    }
}

Следующий шаг – необходимо написать обработчик кнопки «Добавить». При нажатии на кнопку необходимо создать объект класса Student и вызвать метод Main.addStudent(), которому необходимо передать созданный объект.

Так как обработчик нажатия на кнопку находится в контроллере окна, мы должны из контроллера окна (Controller.java) вызвать метод контроллера окна (Main.java). Чтобы это сделать, мы должны передать контроллеру окна ссылку на контроллер приложения.

Давайте разберемся, как это сделать. Для начала, откроем контроллер приложения и немного изменим загрузку fxml файла.

Main.java
    @Override
    public void start(Stage primaryStage) throws Exception {

        FXMLLoader loader = new FXMLLoader(getClass().getResource("../view/sample.fxml"));
        Parent root = loader.load();

        primaryStage.setTitle("Студенты");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }

Если ранее мы использовали статический метод FXMLLoader.load(), то теперь мы создадим объект класса FXMLLoader, укажем в конструкторе ресурс fxml файла и вызовем метод load() у созданного объекта.

Эти манипуляции нужны нам для того, чтобы вызвать у объекта метод getController(), который вернет нам ссылку на созданный объект класса Controller. Таким образом, мы получим ссылку на объект контроллера окна.

Далее, в контроллере окна необходимо добавить поле типа Main и публичный метод, который будет записывать в поле переданную ссылку на объект контроллера приложения.

Controller.java
public class Controller implements Initializable {

    private Main main;
    
    public void getMainController(Main main) {
        this.main = main;
    }
    
    ...
}
Main.java
    @Override
    public void start(Stage primaryStage) throws Exception {

        FXMLLoader loader = new FXMLLoader(getClass().getResource("../view/sample.fxml"));
        Parent root = loader.load();

        Controller controller = loader.getController();
        controller.getMainController(this);
        
        primaryStage.setTitle("Студенты");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }

Таким образом, контроллер окна будет иметь ссылку на объект контроллера приложения. Теперь мы можем приступать к написанию кода для обработчика кнопки «Добавить». Для упрощения кода, будем генерировать значения полей для объекта студента.

Controller.java
    @FXML
    public void add(ActionEvent event) {
        // Создаем объект студента
        Student student = new Student(
                String.valueOf(new Random().nextInt(100)),
                String.valueOf(new Random().nextInt(100)),
                String.valueOf(new Random().nextInt(100))
        );

        // Вызываем метод добавления студента
        main.addStudent(student);
    }

Итак, на данный момент мы реализовали этапы 1 – 3 схемы работы паттерна MVC: пользователь взаимодействует с представлением (нажимает на кнопку «Добавить»), контроллер обращается к модели с запросами об изменении состояния. Но нам необходимо оповестить представление о том, что модель изменилась, чтобы представление обновилось (на экране появилась новая запись о студенте).

Для начала, реализуем взаимодействие модели и представления без использования встроенных средств JavaFX. Для этого нам необходимо разобраться с паттерном «Наблюдатель» (Observer).

Паттерн «Наблюдатель» (Observer)

Паттерн Observer определяет зависимость "один-ко-многим" между объектами так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически.

Паттерн определяет объект Subject, хранящий данные (модель) и объекты Observer, которые реализуют функциональность представлений. При создании наблюдатели Observer регистрируются у объекта Subject. Когда объект Subject изменяется, он извещает об этом всех зарегистрированных наблюдателей. После этого каждый обозреватель запрашивает у объекта Subject ту часть состояния, которая необходима для отображения данных.

Итак, реализуем этот паттерн в нашем исходном коде. В качестве Subject у нас выступает объект класса StudentsFacade, который хранит нужные данные. Укажем в исходном коде, что класс StudentsFacade наследуется от класса java.util.Observable. Класс Observable содержит готовый функционал для регистрации наблюдателей и для их оповещения об изменении модели. В классе StudentsFacade есть два метода, которые меняют состояние объекта – метод addStudent() добавляет новый объект студента в коллекцию, метод deleteStudent() извлекает из коллекции объект студента. Таким образом, при вызове этих методов, мы должны оповестить всех наблюдателей о том, что произошло изменение данных и оповестить их об этом и передать нужные данные для отображения.

StudentFacade.java
public class StudentFacade extends Observable {
    private List<Student> studentList;

    public StudentFacade() {
        studentList = new ArrayList<>();
    }

    public void addStudent(Student student) {
        studentList.add(student);
        setChanged();
        notifyObservers(studentList);
    }
    public void deleteStudent(Student student) {
        studentList.remove(student);
        setChanged();
        notifyObservers(studentList);
    }
}

Далее необходимо указать, что используемый для вывода студентов элемент ListView реализует интерфейс java.util.Observer, а также необходимо запрограммировать реакцию на извещение об изменении модели.

Когда в классе StudentsFacade будет выполняться метод addStudent() или deleteStudent() и выполнится команда notifyObservers(), у объекта класса ListViewObservers будет вызван метод update(), в котором будет передано два параметра – ссылка на объект, который вызвал метод и переданные данные, если они есть (иногда необходимо передать в Observer сам факт изменений, тогда вызывается метод notifyObservers() без параметров).

public class ListViewObserver extends ListView<Student> implements Observer {
    @Override
    public void update(Observable o, Object arg) {
        if (o instanceof StudentFacade) {
            // В параметре arg передан список для отображения
            List<Student> list = (List<Student>) arg;
            getItems().clear();
            getItems().addAll(list);
        }
    }
}

Мы проверяем – от кого произошло событие, и обновляем список с помощью пришедших данных (очищаем список от существующих элементов и добавляем новые).

Теперь необходимо заменить класс ListView на ListViewObserver. Регистрация Observer будет происходить в классе контроллера приложения

Main.java
    public void bindObserverToFacade(Observer o) {
        facade.addObserver(o);
    }
Controller.java
public class Controller implements Initializable {

    private Main main;

    @FXML
    private ListViewObserver list;

    public void getMainController(Main main) {
        this.main = main;
        main.bindObserverToFacade(list);
    }
    
    ...
}

Нам осталось только реализовать функционал для кнопки «Удалить» и наше небольшое приложение теперь полностью готово.

Тестируем работу приложения.

Мы реализовали паттерн «Наблюдатель» вручную, но Java FX уже имеет готовый функционал для реализации этого паттерна. Для этого в JavaFX предусмотрен целый набор классов Observable*, которые предоставляют различные значения и коллекции уже со встроенной поддержкой паттерна Observer. Давайте реализуем 4-5 этап паттерна MVC с помощью встроенных классов Observable.

Для начала перейдем в класс StudentsFacade и изменим класс коллекции с List на ObservableList. Классу StudentsFacade теперь нет нужды наследоваться от класса java.util.Observable, так как этот функционал теперь реализует непосредственно коллекция со студентами. Кроме того, добавим геттер для коллекции (хотя это не очень согласуется с принципом инкапсуляции).

В классе Main заменим метод bindObserverToFacade() на setItemsForListView(). Обратите внимание, что элемент ListView содержит метод setItems(), который принимает на вход объект класса ObservableList. При вызове метода регистрация и обновление спискового элемента происходит без нашего участия – мы просто должны вызвать метод setItems() и передать ему ObservableList.

Заменим использование ListViewObserver на использование стандартного элемента ListView и изменим содержимое класса Controller.

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

Last updated