Статьи

JavaFX: использование шаблонов и чистого кода


Мы видим множество захватывающих демонстраций JavaFX, демонстрирующих приятные возможности языка и возможность простой интеграции классной графики. Но, как разработчик программного обеспечения, я не могу помешать себе увидеть, что в большинстве примеров мы видим раздутый код — нет хорошего разделения проблем и плохих приложений шаблона MVC. Это разумно, поскольку JavaFX был новым языком, и сначала нужно было научить людей синтаксису и возможностям; но теперь, когда, я полагаю, многие из нас познакомились с новым языком, давно пора задуматься о передовой практике, а также о написании кода для комментирования. Давайте помнить, что хорошие практики и хороший дизайн всегда важнее языка!

Итак, давайте представим очень простой проект — список контактов, характеристики которого:

  1. У нас есть список «контактов», где каждый элемент — это имя, фамилия, адрес электронной почты, телефон и т. Д.
  2. Пользовательский интерфейс показывает список контактов, которые можно отфильтровать по текстовому полю; если вы введете в него значение, будут показаны только контакты, имя которых начинается с введенного текста. Выборы применяются по мере ввода.
  3. При выборе элемента в списке контактов, детали отображаются в форме, где вы можете редактировать их.
  4. При любом изменении выбора форма анимируется (вращение и эффект размытия).

Вы можете получить код от:

svn co -r 37 https://kenai.com/svn/javafxstuff~svn/trunk/ContactList/src/ContactList

Модель

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

package it.tidalwave.javafxstuff.contactlist.model;

public class Contact
{
public var id: String;
public var firstName: String;
public var lastName: String;
public var phone: String;
public var email: String;
public var photo: String;

override function toString()
{
return "\{id: {id}, value: {firstName} {lastName}, phone: {phone}, email: {email}"
}
}

Затем небольшой сервис, который предоставляет кучу данных:

package it.tidalwave.javafxstuff.contactlist.model;

public abstract class ContactRegistry
{
public abstract function items() : Contact[];
}

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

Пока все хорошо — вполне нормально держать эти вещи отделенными от пользовательского интерфейса.

Контроллеры

Мы не увидим здесь классический контроллер; на самом деле, мы немного отходим от «чистого» MVC. Код, который я показываю вам, — это скорее «Модель представления» , модель, описанная Мартином Фаулером:

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

Этот класс представляет собой гибрид гибридной модели и контроллера. Это также фасад между представлением и моделью предметной области.

package it.tidalwave.javafxstuff.contactlist.model;

import it.tidalwave.javafxstuff.contactlist.model.ContactRegistry;
import it.tidalwave.javafxstuff.contactlist.model.ContactRegistryMock;

public class PresentationModel
{
public var searchText : String;

public var selectedIndex : Integer;

// The Business Delegate
def contactRegistry = ContactRegistryMock{} as ContactRegistry;

def allContacts = bind contactRegistry.items();

// The contacts filtered according the contents of the search field
public-read def contacts = bind allContacts[contact | "{contact.firstName} {contact.lastName}".startsWith(searchText)];

// The selected contact; the code also triggers a notification at each change
public-read def selectedContact = bind contacts[selectedIndex] on replace previousContact
{
onSelectedContactChange();
};

// Notifies a change in the current Contact selection
public-init var onSelectedContactChange = function()
{
}
}

Обратите внимание на чрезвычайную компактность, обеспечиваемую функциональным программированием. Практически нет необходимости в программировании, поскольку все достигается путем правильного использования функции привязки. Единственная обязательная часть — это функция onSelectedContactChange, которая просто слушает, чтобы уведомить об изменениях выбора для некоторого внешнего кода — она ​​будет использоваться только для запуска анимации.

Кстати, я бы хотел убрать его отсюда, но не смог. Может быть, это JavaFX, что я еще не понял, но я оставлю это для другого поста.

Теперь все в анимации заключено в определенный класс, который предоставляет только два свойства, управляющих анимацией: эффект и угол поворота. Для запуска анимации предусмотрена одна функция play ().

package it.tidalwave.javafxstuff.contactlist.view;

import javafx.animation.Interpolator;
import javafx.animation.Timeline;
import javafx.scene.effect.GaussianBlur;

public class AnimationController
{
public-read var rotation = 0;
public-read def effect = GaussianBlur{}

def timeline = Timeline
{
repeatCount: 1
keyFrames:
[
at(0s) { effect.radius => 20; rotation => 45 }
at(300ms) { effect.radius => 0 tween Interpolator.EASEBOTH;
rotation => 0 tween Interpolator.EASEBOTH }
]
}

public function play()
{
timeline.playFromStart();
}
}

Виды

Теперь компонент пользовательского интерфейса. То, как мы его проектируем, во многом зависит от процесса, поскольку может быть задействован графический дизайнер. В любом случае, я думаю, что весь пользовательский интерфейс не должен быть реализован в одном раздутом классе; довольно соответствующие части должны быть разделены. Например, CustomNode может моделировать «форму», которая отображает контактные данные (в приведенном ниже коде я опустил все атрибуты, связанные с рендерингом):

package it.tidalwave.javafxstuff.contactlist.view;

import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.Node;
import javafx.ext.swing.SwingLabel;
import javafx.ext.swing.SwingTextField;
import it.tidalwave.javafxstuff.contactlist.model.Contact;

public class ContactView extends CustomNode
{
public var contact : Contact;

public override function create() : Node
{
return Group
{
content:
[
VBox
{
content:
[
SwingLabel
{
text: bind "{contact.firstName} {contact.lastName}"
}
HBox
{
content:
[
SwingLabel { text: "First name: " }
SwingTextField { text: bind contact.firstName }
]
}
HBox
{
content:
[
SwingLabel { text: "Last name: " }
SwingTextField { text: bind contact.lastName }
]
}
HBox
{
content:
[
SwingLabel { text: "Email: " }
SwingTextField { text: bind contact.email }
]
}
HBox
{
content:
[
SwingLabel { text: "Phone: " }
SwingTextField { text: bind contact.phone }
]
}
]
}
]
};
}
}

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

 

Собираем все вместе

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

 

 

 

 

package it.tidalwave.javafxstuff.contactlist.view;

import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.ext.swing.SwingLabel;
import javafx.ext.swing.SwingList;
import javafx.ext.swing.SwingListItem;
import javafx.ext.swing.SwingTextField;
import it.tidalwave.javafxstuff.contactlist.controller.PresentationModel;

Stage
{
def animationController = AnimationController{};

def presentationModel = PresentationModel
{
onSelectedContactChange : function()
{
animationController.play();
}
};

scene: Scene
{
content: VBox
{
content:
[
HBox
{
content: SwingLabel { text: "Contact List" }
}
HBox
{
content:
[
SwingLabel { text: "Search: " }
SwingTextField { text: bind presentationModel.searchText with inverse }
]
}
HBox
{
content:
[
SwingList
{
items: bind for (contact in presentationModel.contacts)
{
SwingListItem { text:"{contact.firstName} {contact.lastName}" }
}
selectedIndex: bind presentationModel.selectedIndex with inverse
}
ContactView
{
contact: bind presentationModel.selectedContact
effect: bind animationController.effect
rotate: bind animationController.rotation
}
]
}
]
}
}
}

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

В конце концов, каждый класс в этом небольшом проекте делает простую, понятную вещь: представляет данные, инкапсулирует логику представления, управляет анимацией, отображает представления. Зависимости сведены к минимуму и имеют правильное направление: представления зависят от моделей (а не от противоположных) и AnimationController; AnimationController является независимым. Вы можете заменить компоненты представления, не затрагивая остальные классы, а также удалить или добавить другие анимации, правильно используя различные AnimationControllers. Такое хорошее разделение ролей и обязанностей является хорошим способом применения ОО.

Есть деталь, которую стоит обсудить. Обратите внимание на две привязки … с обратным . Они реализуют так называемую «двунаправленную привязку», где не только изменение в модели (например, PresentationModel.selectedIndex) отражается на атрибутах в пользовательском интерфейсе (например, SwingList.selectedIndex), но также происходит обратное. Действительно, обратная привязка является более важной в нашем примере, потому что она реализует ответственность контроллера (она захватывает жесты пользователя из представления и изменяет модель); прямая привязка PresentationModel к SwingList, напротив, бесполезна, так как в нашем случае PresentationModel никогда не является источником изменений.

Итак, почему бы не использовать простую прямую привязку в PresentationModel к SwingList? Такие как:

public class PresentationModel
{
var list : SwingList;

public def selectedIndex = bind list.selectedIndex;

...
}

Потому что это привело бы к зависимости от модели / контроллера к представлению, что совершенно неверно. Здесь, bind … with инверсия не только работает как ярлык для написания меньшего количества кода (то есть, явно объявляет привязку и ее инверсию), но также является важной функцией для лучшего дизайна.

Насколько я знаю — но я могу ошибаться — например, в ActionScript (язык Adobe Flex) нет двунаправленной привязки (необходимо поместить ключевые слова привязки на обоих концах ассоциации), что вводит ненужные или циклические зависимости. Я считаю, что это верно, по крайней мере, на уровне кода (насколько я понимаю, в ActionScript существуют разные способы связывания).