Реализация паттерна Observer в MVC (Model View Controller) для JavaFX
17-07-2018Время чтения ~ 5 мин.Java 11089
В продолжении предыдущей статьи «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. То есть Контролёр только отправляет Модели данные при получении событий с кнопок.
Код достаточно простой, да ещё и с комментариями, поэтому добавить больше нечего.
Желающие могут скачать исходные файлы.