Мы видим множество захватывающих демонстраций JavaFX, демонстрирующих приятные возможности языка и возможность простой интеграции классной графики. Но, как разработчик программного обеспечения, я не могу помешать себе увидеть, что в большинстве примеров мы видим раздутый код — нет хорошего разделения проблем и плохих приложений шаблона MVC. Это разумно, поскольку JavaFX был новым языком, и сначала нужно было научить людей синтаксису и возможностям; но теперь, когда, я полагаю, многие из нас познакомились с новым языком, давно пора задуматься о передовой практике, а также о написании кода для комментирования. Давайте помнить, что хорошие практики и хороший дизайн всегда важнее языка!
Итак, давайте представим очень простой проект — список контактов, характеристики которого:
- У нас есть список «контактов», где каждый элемент — это имя, фамилия, адрес электронной почты, телефон и т. Д.
- Пользовательский интерфейс показывает список контактов, которые можно отфильтровать по текстовому полю; если вы введете в него значение, будут показаны только контакты, имя которых начинается с введенного текста. Выборы применяются по мере ввода.
- При выборе элемента в списке контактов, детали отображаются в форме, где вы можете редактировать их.
- При любом изменении выбора форма анимируется (вращение и эффект размытия).
Вы можете получить код от:
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 существуют разные способы связывания).