Pull to refresh
0

Groovy vs Java для JavaFX

Reading time 10 min
Views 16K
image

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 понимать, что у классов есть эти динамические поля.

image

Теперь 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 вы можете взять в 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
Tags:
Hubs:
+24
Comments 37
Comments Comments 37

Articles

Information

Website
codeorchestra.com
Registered
Founded
Employees
2–10 employees
Location
Сербия