Сайт вебмастера

Реализация паттерна Observer в MVC (Model View Controller) для JavaFX

17-07-2018Время чтения ~ 5 мин.Java 10215

В продолжении предыдущей статьи «MVC (Model View Controller) в JavaFX», я решил-таки реализовать паттерн Наблюдатель (Observer), чтобы довести работу до логического завершения. За основу я взял код Java с Википедии. Этот пример из книги «Паттерны проектирования» Эрика и Элизабет Фримен, поэтому наверняка все его разбирали.

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

В конце статьи вы найдете исходные коды для загрузки.

В отличие от предыдущего варианта программы, я добавил ещё одну метку, которая показывает текущую операцию. Она демонстрирует работу второго оповещения.

Вид программы

Общее описание паттерна «Наблюдатель»

Есть некий объект, который выполняет роль наблюдателя (Observable). Слушатель (Observer) — это те объекты, которые регистрируются у наблюдателя. Наблюдатель отправляет уведомления всем слушателям. Те его получают и дальше уже сами решают что с ним делать.

В проекте создаётся файл Observer.java:

package org.maxsite;
/*
* Это интерфейы для организации паттерна Наблюдатель
* */
 
// слушатель
interface Observer {
    void notification(String message);
}
 
// наблюдатель
interface Observable {
    void registerObserver(Observer o);
    void notifyObservers(String message);
}

Это обычные интерфейсы, где у наблюдателя будут функции регистрации и уведомления, а у слушателей — функция, которая будет срабатывать при поступлении уведомления.

Model

Модель будет Наблюдателем.

package org.maxsite;
/*
* здесь вычисления и внутреннее хранение данных
* а также регистрация и уведомление слушателей
* */
 
import java.util.LinkedList;
import java.util.List;
 
class Model implements Observable {
 
    private List<Observer> observers; // список слушателей
 
    private String op = "-"; // операция
    private String num1 = "1"; // первое число
    private String num2 = "2"; // второе число
    private String result = "1-2"; // результат
 
    // конструктор: создаем новый список для слушателей
    Model() {
        observers = new LinkedList<>();
    }
 
    // регистрация слушателя
    @Override public void registerObserver(Observer o) {
        observers.add(o);
    }
 
    // уведомление слушателей
    @Override public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.notification(message);
        }
    }
 
    // выставляет операцию
    void setOp(String s) {
        op = s;
        notifyObservers("op");
    }
 
    // выставляет num1
    void setNum1(String s) {
        num1 = s;
    }
 
    // выставляет num2
    void setNum2(String s) {
        num2 = s;
    }
 
    // вычисляем результат
    void go() {
        result = num1 + op + num2;
        notifyObservers("go");
    }
 
    // отдаем результат
    String getResult() {
        return result;
    }
 
    String getOp() {
        return op;
    }
}

Слушатели хранятся в переменной observers, которая представляет собой список, который создается в конструкторе автоматически.

Уведомление рассылается с помощью notifyObservers() с параметром, где можно задать название сообщения. В этом варианте Модель может уведомить о нескольких событиях. В нашем случае идёт уведомление «go» при получении готового результата, и «op», когда меняется операция.

При этом Модель, опять же, ничего не знает о том, кто её использует.

Кто слушатель: View или Controller?

С ходу решить эту задачку у меня не получилось, поэтому пришлось повозиться. :-) Проблема в том, что для Контролёра в JavaFX мы не можем просто так вызвать конструктор. Это как-то связано с FXMLLoader, который сам его создает, а значит у нас нет возможности его переопределить, если она и есть (я не в курсе), то неясно насколько это окажется плохой идеей.

Можно было бы выделить класс Контролёра в main-файле и там уже зарегистрироваться в качестве слушателя, но мне такая идея не понравилась.

Поэтому в качестве слушателя может быть суперкласс View. При этом придется перенести в него создание Модели, иначе не получится зарегистрироваться. Соответственно Контролёр получает доступ к Модели автоматически, поскольку расширяет Представление.

View

Код View.java.

package org.maxsite;
/*
* это представление: то, что выводится на форму
* */
 
import javafx.fxml.FXML;
import javafx.scene.control.Label;
 
class View implements Observer {
 
    // поля вывода
    @FXML private Label LabelResult;
    @FXML private Label LabelOp;
 
    // создаем модель здесь, поскольку это суперкласс для контролера
    Model model = new Model();
 
    // конструктор: регистрация на сообщения модели
    View() {
        model.registerObserver(this);
    }
 
    // уведомления от модели
    @Override public void notification(String message) {
        // разные уведомления под разный вывод
        if (message.equals("go")) {
            this.displayLabel(model.getResult());
        }
        else if (message.equals("op")) {
            this.displayOp(model.getOp());
        }
    }
 
    // выводит текст в LabelResult (результат)
    private void displayLabel(String s){
        LabelResult.setText("Результат: " + s);
    }
 
    // выводит текст в LabelOp (операция)
    private void displayOp(String s) {
        LabelOp.setText("Операция: " + s);
    }
}

Регистрация в качестве слушателя происходит автоматически в конструкторе. При получении уведомления (функция notification()), выполняется либо вывод результата, либо вывод операции. Такое разделение позволяет без проблем добавить любой другой вывод на форму.

Controller

Теперь файл Controller.java.

package org.maxsite;
/*
* это контролер, он жестко завязан на форму
* */
 
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
 
public class Controller extends View {
 
    // форма: поля ввода
    @FXML private TextField tf1;
    @FXML private TextField tf2;
 
    // контролер знает модель из View
 
    // нажатите кнопки BtnPlus
    @FXML public void onActionBtnPlus() {
        model.setOp("+");
    }
 
    // нажатите кнопки BtnMinus
    @FXML public void onActionBtnMinus() {
        model.setOp("-");
    }
 
    // нажатите кнопки BtnGo - получение результата
    @FXML public void onActionBtnGo() {
        this.go();
    }
 
    // функция для получения и вывода результата
    private void go() {
        // отдаем модели нужные данные
        model.setNum1(tf1.getText());
        model.setNum2(tf2.getText());
 
        // вычисляем результат
        model.go();
    }
}

Он стал ещё проще. За получение результата теперь отвечает Модель, которая автоматом доступна из View. То есть Контролёр только отправляет Модели данные при получении событий с кнопок.

Код достаточно простой, да ещё и с комментариями, поэтому добавить больше нечего.

Желающие могут скачать исходные файлы.

Похожие записи