Groovy vs Java для JavaFX

от автора

JavaFX хороша!

Сначала пару слов об JavaFX. Чем нам понравилось с ней работать.

Современный API. Даже без «билдеров», все выглядит очень современно.

Тотальный Data Driven Development. Обожаем это. Логика основанная на связке данных очищает код от хлама, гетеры/сеттеры — «долой!». События обработки изменения данных, дву-направленный «биндинг».

FXML. Отличная вещь для прототипирования. Понятна дизайнеру, имеется хороший визуальный инструмент, от Oracle — «JavaFX Scene Builder». Отмечу, что потом все же нам потом захотелось переписать FXML в виде обычного кода. Просто поддерживать FXML будет получается сложнее — всегда нужно менять два файла, код и FXML. Плюс когда используется код легче пользоваться наследованием.

Nodes. Структура компонентов. Можно бегать по дереву. Можно искать по lookup(). Как в DOM. Прям jQuery пиши.

CSS. Это действительно вещь. «Скинуем» компоненты через один общий css-файл. ID-шники, CSS-классы, селекторы и псевдоселекторы.

Text Engine. Очень хороший движок для сложных текстов.

WebView. Реализуем навороченные компоненты на движке Webkit. Об этом читать предыдущую статью.

Что не очень хорошо

Это хорошее. Что плохо? JavaFX скрипт в свое время не просто так придумали. Создавать поля для доступа к Bindable данным через гетеры и сеттеры — это какой-то шаг назад и вчерашний день. Java тут не очень хороша. В Java 8 есть лямбда-выражения, но появление их тоже ответ на вопрос, что с Java что-то нужно делать и повод задуматься о более кардинальном решении.

Groovy!

Мы решили все эти проблемы для себя выбрав Groovy. Лаконичен, в хорошем смысле стар (вызрел) и хорошо поддерживается в IDEA. Groovy позволил нам сократить объем кода раз в десять точно. Работает, выглядит и читается почти как Java, но как же он хорош с точки зрения компактности!

Есть еще куча хороших и красивых языков для JVM, но так сложилось что Groovy нас устраивает. Да и любим мы скобочки, аннотации и не хотим мы что-то тут в себе ломать. Плюс у меня лично был семи-летний опыт использования Groovy, а когда есть эксперт в команде, то лучше воспользоваться, а не брать что то совсем неведомое.

Кстати Groovy по популярности языков (по данным TIOBE) занимает 18 место.

Наши практики

Теперь давайте поглядим примеры. Копируем из нашего проекта, код реальный.

Конфигурирование компонентов

Просто создаем экземпляр компонента через код и его конфигурируем.
На Java нам приходилось пошагово, строчка за строчкой, присваивать значения.

Button button = new Button(); button.setFocusTraversable(false); button.setLayoutX(23); button.setPrefHeight(30); button.setPrefWidth(30); button.setText("ADD"); 

Как выглядит тоже самое если переписать на Groovy?

Button button = new Button(focusTraversable: false, layoutY: 23, prefHeight: 30, prefWidth: 30, text: "Add")   

Груви, напомню, кто не знает, позволяет обращаться к методам доступа (геттерам, сеттерам) без приставки set/get. То есть если в классе есть метод setText — то его вызов производится через простое присвоение значения text = «Add». Плюс при компиляции Groovy классов, публичным полям добавляются геттеры и сеттеры автоматически. Поэтому из груви не прянято вызывать метод set/get если для этого нет реальной необходимости.

А в параметры конструктора можно передавать пары — имя: значение (на самом деле это обычных HashMap и синтаксис тут используется Groovy Maps — [key1:value1, key2:value]).

Причем важно, что IDEA нам все это подсказывает, валидирует тип данных и ограничение доступа.

Такой способ конфигурации компонентов, сразу наводит на мысль на нельзя ли конфигурирвать сразу структуру компонентов?

Можно!

menus.addAll(         new Menu(text: "File", newItems: [                 new MenuItem(                         text: "New Window",                         onAction: { t ->                             ApplicationUtil.startAnotherColtInstance()                         } as EventHandler<ActionEvent>,                         accelerator: new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN)                 ),                 new Menu(text: "New Project", newItems: [                         newAs = new MenuItem(                                 text: "New AS Project",                                 id: "new-as",                                 onAction: { t ->                                     ProjectDialogs.newAsProjectDialog(scene, false)                                 } as EventHandler<ActionEvent>                         ),                         newJs = new MenuItem(                                 text: "New JS Project",                                 id: "new-js",                                 onAction: { t ->                                     ProjectDialogs.newJsProjectDialog(scene, false)                                 } as EventHandler<ActionEvent>                         )                 ]),                 new SeparatorMenuItem(),                 new MenuItem(                         text: "Open Project",                         onAction: { t ->                             ProjectDialogs.openProjectDialog(scene, false)                         } as EventHandler<ActionEvent>,                         accelerator: new KeyCodeCombination(KeyCode.O, KeyCombination.SHORTCUT_DOWN)                 ),                 recentProjectsSubMenu = new Menu(text: "Open Recent", newItems: [                         clearRecentProjects = new MenuItem(                                 text: "Clear List",                                 onAction: { t ->                                     RecentProjects.clear()                                 } as EventHandler<ActionEvent>                         ),                 ]),                 new SeparatorMenuItem(),                 save = new MenuItem(                         text: "Save Project",                         id: "save",                         onAction: { t ->                             ProjectDialogs.saveProjectDialog()                         } as EventHandler<ActionEvent>,                         accelerator: new KeyCodeCombination(KeyCode.S, KeyCombination.SHORTCUT_DOWN),                         disable: true                 ),                 saveAs = new MenuItem(                         text: "Save As...",                         onAction: { t ->                             ProjectDialogs.saveAsProjectDialog(scene)                         } as EventHandler<ActionEvent>,                         accelerator: new KeyCodeCombination(KeyCode.S,                                 KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN),                 ),                 new MenuItem(                         text: "Close Project",                         onAction: { t ->                             ProjectDialogs.closeProjectDialog()                         } as EventHandler<ActionEvent>,                         accelerator: new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN),                 ),                 new SeparatorMenuItem(),                 new MenuItem(                         text: "Exit",                         onAction: { t ->                             ApplicationUtil.exitColt()                         } as EventHandler<ActionEvent>                 ),         ]),         new Menu(text: "Help", newItems: [                 new MenuItem(                         text: "Open Demo Projects Directory",                         onAction: { t ->                             ProjectDialogs.openDemoProjectDialog(scene)                         } as EventHandler<ActionEvent>                 ),                 new MenuItem(                         text: "Open Welcome Screen",                         onAction: { t ->                             ProjectDialogs.openWelcomeScreen(scene)                         } as EventHandler<ActionEvent>                 ),         ]) ) 

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

Динамические свойства и методы

Внимательный читатель спросить, а что за поле «newItems» у Menu? Да, такого метода у класса Menu нет. А добавили мы такой метод потому что items" мы можем только читать, не можем присваивать. У него нет метода «setItems()», а есть только «getItems()» и присваивать новое значение нельзя. Read-only. Чтобы конфигурировать Menu в виде структуры, мы добавили динамическое поле.

Добавить такое поле очень просто, но наша Java сущность долго сопротивлялась такой крамоле как динамические методы. Мы много напридумывали велосипедов, пока не смирились с фактом необходимости воспользоваться динамикой. А все оказалось просто и нестрашно.

Добавление динамического полей мы вынесли в отдельный класс GroovyDynamicMethods. Вот его код:

class GroovyDynamicMethods {      private static inited = false      static void init() {         if(inited)return         inited = true          addSetter(javafx.scene.Node, "newStyleClass", { String it ->             styleClass.add(it)         })         addSetter(Parent, "newChildren", {List<MenuItem> it ->             children.addAll(it)         })         addSetter(Menu, "newItems", {List<MenuItem> it ->             items.addAll(it)         })     }      private static void addSetter(Class clazz, String methodName, Closure methodBody) {         addMethod(clazz, "set" + methodName.capitalize(), methodBody)     }      private static void addMethod(Class clazz, String methodName, Closure methodBody) {         ExpandoMetaClass exp = new ExpandoMetaClass(clazz, false)         exp."$methodName" = methodBody         exp.initialize()         clazz.metaClass = exp     } } 

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

Плюс мы научили IDEA понимать что это динамический класс.

(img dynamic properties)

Теперь IDEA знает о существовании таких полей, как будто они есть в API JavaFX.

Работа с Bindable свойствами

Связывание данных, замечательна штука. У нас в команде используется такая мантра — «Если что-то можно сделать через байндидинг, делай через байндинг». "… чтобы потом не переделывать".

Бандинг позволяет связать модель данных, и компоненты. UI-компоненты сами имеют байндинг свойства, которые можно связывать с данными модели или строить на изменении этих свойств логику — подписываться на события изменения данных.

Простой пример CheckBox:

CheckBox checkBox = new CheckBox(); checkBox.selectedProperty().bindBidirectional(selectedProperty); 

А тут мы реагируем на событие нажатия на чекбокс:

CheckBox checkBox = new CheckBox(); checkBox.selectedProperty().addListener(new ChangeListener<Boolean>() {     @Override     public void changed(ObservableValue<? extends Boolean> value, Boolean before, Boolean after) {         System.out.println("value = " + value);     } }); 

Использовать удобно. Не очень удобно такие свойства описывать.

Java предлагает такой вот сценарий (код создан IDEA автоматически).

private StringProperty name = new SimpleStringProperty(); // создали свойство  //даем ссылку на свойство наружу (но не даем его изменять внешне) public StringProperty nameProperty() {     return name; }  // можно взять значение public String getName() {     return name.get(); }  // даем возможность присвоить свойству новое значение public void setName(String name) {     this.name.set(name); } 

Все бы хорошо, да и IDE для нас такой код генерирует. Ну тупость ли? Почему нам все это нужно видеть? За всем этим хламом мы не видим нашей логики.

Решение! Берем AST трансформацию, которая для нас этот код генерирует. При компиляции.

Наше свойство (которое мы описали в Java 10 строками) превращается в Groovy в одну строку и выглядит так:

@FXBindable String name; 

@FXBindable вы можете взять добавив Jar-ку GroovyFX в свой проект.
Мы сделали форк такой аннотации и вы можете ее взять у нас на гитхабе.

Плюс в этом же проекте вы обнаружите файл с расширением .gdsl, который научит IDEA пользоваться этой аннотацией — автокомлит и т.д.

Такая трасформация, так же создает методы setName, getName, getNameProperty. Плюс к этому еще добавляется метод name() который позволяет получить доступ к полю написав еще меньше букв. Вкусовщина, но мы чаще всего пользуемся именно этим методом.

this.nameInput.textProperty().bindBidirectional(this.name()) // this.name() - это наше строковое поле name 

Долой анонимные классы

В примере с Menu мы подписываемся на события через анонимные классы. На примере структуры меню видно, что обработчиком событий выступает «кложура».

onAction: { t ->     ProjectDialogs.newAsProjectDialog(scene, false) } as EventHandler<ActionEvent> 

Вся магия в «as EventHandler» — тело кложуры перемещается в тело метода handle, класса EventHandler. Использование такой краткой записи для обработки событий делает код чище. Кстати умная IDEA предлагает квикфис «Change to dynamic instantiation». Так же можно использовать другую записать — через Map ([handler1: {}, handler2: []]), если класс обработчик требует перегрузить несколько методов.

Работа с XML

В нашем проекте нам нужно было сериализовать модель данных в XML и брать ее с диска. Сначала хотели по привычке воспользоваться XStream, но нам нужна была более управляемая структура — Bindable свойства JavaFX они большие, а конвертеры писать лень. Посмотрели JAXB, тоже плохо. Тоже и с Groovy XML-сериализацией.

Подошел встроенный в Groovy SDK XmlSlurper.

Каждый Bean модели реализует два метода — buildXml и buildModel — сериализация и десериализация

Closure buildXml(Project project) {    return { 		  'launcher'(launcherType)       'browser-path'(browserPath)       'nodejs-path'(nodejsPath)       'console-value'(console)     } }  @Override void buildModel(Object node) {     launcherType = node.'launcher'     browserPath = node.'browser-path'     nodejsPath = node.'nodejs-path'     console = node.'console-value' } 

Метод buildXml возвращает структуру в виде кложуры. Магия тут в вызове, и присвоении несуществующих методов и свойств. Если вызывается несуществующий метод — то создается свойство в виде дочерней ноды, если присваивается значение несуществующему полю — создается аттибут XML, если вызывается несуществующий метод и ему как пораметр передается кложура — то создается вложенная структура XML нод.

Метод buildModel принимает аргумент node, и через динамические запросы ноду разбирает.

Работа с файлами

Наша программа много работает с файловой системой. Используя Groovy мы смогли сильно сократить код IO. Нам не нужно было экономить каждую наносекунду, у нас не нагруженный web-сервер, и то что Groovy делал за нас кучу работы нас устраивало.

Groovy SDK предлагает множество полезных расширений для классов Java в том числе File. Например, возможность писать/читать содержимое файла просто через поле «text», или же работа со строками файла с помощью «splitEachLine».

Кроме этого нам понравился AntBuilder, который можно использовать также для поиска и фильтрации файлов.

Следующий пример копирует файлы:

def ant = new AntBuilder() ant.sequential {     myDir = "test/to/"     mkdir(dir:myDir)     copy(todir:myDir) {         fileset(dir:"text/from/") {             include(name:"**/*.*")         }     } } 

Вы можете искать файлы по шаблону с помощью fileScaner

def ant = new AntBuilder() def scanner = ant.fileScanner {     fileset(dir: file) {         include(name: "**/*.jpg")     } } scanner.each{ printlt(it) } 

И конечно же AntBuilder — это полноценный ANT, со всеми его расширениями и возможностями. Здесь еще изучать и изучать. Gradle тоже использует AntBuilder, и то что там можно «наворотить» нас впечатляет.

Использование GPath для работы с Nodes

Так структура компонентов в JavaFX, мы использовали запросы к нодам как к коллекциям. При таком подходе, избавившись от большого количества циклов, мы сильно сократили наш код.

Например, чтобы убрать скролы на Java:

webView.getChildrenUnmodifiable().addListener(new ListChangeListener<Node>() {     @Override     void onChanged(ListChangeListener.Change<? extends Node> change) {         Set<jNode> scrolls = webView.lookupAll(".scroll-bar");         for (Node  scroll : scrolls) {             scroll.setVisible(false);         }     } }); 

Тоже самое на Groovy:

webView.childrenUnmodifiable.addListener({ change ->     webView.lookupAll(".scroll-bar")*.visible = false } as ListChangeListener) 

Боремся с NPE

Оператор «?.» — по нашему мнению, только он один, может заставить задаться о переходе с Java на Groovy.

model?.projectSettings?.projectPaths?.livePaths?.each{ println(it) }  

Переводим это в Java и получаем как минимум двадцать строк кода.

Заключение

Вот и пожалуй все, что мы смогли вспомнить. Конечно же в нашем проекте мы использовали и другие и другие «вкусности» Groovy, но если все перечислить, то мы выйдем за рамки статьи, да и учебников по Groovy можно найти много.

Но хочется поговорить и о том, что нам не подходит из Groovy. Первое — мы избегали лишней динамики. В нашей команде мы договорились о том, что обязательно нужно указывать тип при создании любой переменной или поля (кроме параметров кложур — тут теряется половина удовольствия от них). Так-же, мы не использовали mixins и перегруженные операторы. Жонглирование кодом мы считаем вредной практикой — нам важен не только компактный, но и контролируемый, поддерживаемый код. Вот пожалуй и все. Groovy очень похож на Java и мы использовали его именно в этом контексте — мы знаем что за нас при компиляции производятся AST трансформации и мы при написании кода предполагаем, что к какой-то конструкции за нас что-то добавляется автоматом. Такая вот Java с автогенерацией. И больше ничего нам не нужно.

Сайт проекта codeorchestra.com

ссылка на оригинал статьи http://habrahabr.ru/company/codeorchestra/blog/199050/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *