Статьи

Домен-специфический язык для JavaFX 2.0 и Scala

Две недели назад на конференции JavaOne 2010 в Сан-Франциско было объявлено, что Oracle больше не будет поддерживать JavaFX Script в качестве языка. Фактически, Sun.Oracle преобразует текущую реализацию в API Java и среду 2.0.

Как уже сообщали многие (и, очевидно, неправильно), Oracle приняла деловые и технологические решения о будущем JavaFX . В этом блоге я упомянул мой разговор с Брайаном Гетцем, простой вопрос которого заключался в том, что инструментальные средства вокруг Java слишком велики для того, чтобы язык мог достичь JavaFX Script с тем же высоким стандартом качества. Решение Oracle о прекращении собственной разработки JavaFX Script было временем выхода на рынок, а также отражением опыта, заключающегося в том, что создание платформы JavaFX значительно дороже, чем язык.

Наиболее важным нововведением в языке JavaFX Script является синтаксис привязки. Выглядит невероятно просто, но под ледяной айсберг навязывается много сил. Язык JavaFX Script — это вершина айсберга, легкая цель, белая неподвижная гора, которая находится над морским котиком. Sun.Oracle строятся ниже этого и существуют уже несколько лет. Это сложно спроектировать, особенно привязки и обновления свойств.

Преимущества концентрации на дне айсберга:

  • Графическая архитектура и конвейер с аппаратным ускорением (временно объединенные Java2D и Prism)
  • Унифицированная архитектура графа сцены для детального 2D и 3D просмотра
  • Новый апплет на основе Prism и веб-приложения JNLP, которые можно развернуть с самого начала
  • Позвольте авторам и энтузиастам языка JVM создавать DSL на своих любимых языках программирования, которые (повторно) используют предстоящий API JavaFX для Java 2.0

Именно этот последний пункт, который я концентрирую здесь. Начиная с JavaOne 2010, вы действительно серьезно размышляете над возможным языком, специфичным для домена Scala для JavaFX 2.0 для Java API , особенно зная, что Sun.Oracle предоставит эту функциональность через 6-8 месяцев, как и было обещано. Плохие старые времена, когда Sun Microsystem потеряла фокус на широком спектре продуктов пробной монетизации, действительно прошли навсегда, особенно в открытом мире Oracle Ларри Эллисиона.

Это мой умозрительный вклад в API JavaFX 2.0. Я считаю, что Sun.Oracle выберет шаблон компоновщика для Java API, а не устаревшие JavaBeans, сеттеры и геттеры. Вот Node.java по моему личному мнению:

package uk.co.xenonique.javafx;

/**
 * Research JavaFX for Java API 2.0
 * @author Peter Pilgrim, 26 September 2010
 */
public abstract class Node {

    private boolean blocksMouse;
    private Bounds boundsInLocal = new Bounds();
    private Bounds boundsInParent = new Bounds();
    private Node clip;
    private boolean focused;
    private boolean hover;
    private String id;
    private Bounds layoutBounds = new Bounds();
    private float layoutX;
    private float layoutY;
    private boolean managed;
    private float opacity;
    private boolean pressed;
    private Parent parent;
    private boolean visible;

    public Node() { super(); }

    public boolean isBlocksMouse() {
        return blocksMouse;
    }

    public Node setBlocksMouse(boolean blocksMouse) {
        this.blocksMouse = blocksMouse;
        return this;
    }

    public Bounds getBoundsInLocal() {
        return boundsInLocal;
    }

    public Bounds getBoundsInParent() {
        return boundsInParent;
    }
    ...

    public boolean isFocused() {
        return focused;
    }

    public boolean isHover() {
        return hover;
    }

    public String getId() {
        return id;
    }

    public Node setId(String id) {
        this.id = id;
        return this;
    }

    public Bounds getLayoutBounds() {
        return layoutBounds;
    }

    public float getLayoutX() {
        return layoutX;
    }

    public float getLayoutY() {
        return layoutY;
    }

    public boolean isManaged() {
        return managed;
    }

    public Node setManaged(boolean managed) {
        this.managed = managed;
        return this;
    }

    public float getOpacity() {
        return opacity;
    }

    public Node setOpacity(float opacity) {
        this.opacity = opacity;
        return this;
    }

    public Parent getParent() {
        return parent;
    }

    public Node setParent(Parent parent) {
        this.parent = parent;
        return this;
    }

    public boolean isPressed() {
        return pressed;
    }

    public boolean isVisible() {
        return visible;
    }

    public Node setVisible(boolean visible) {
        this.visible = visible;
        return this;
    }

    ...

    @Override
    public String toString() {
        return "Node{" + "blocksMouse=" + blocksMouse + ", boundsInLocal=" + boundsInLocal + ", boundsInParent=" + boundsInParent + ", clip=" + clip +
                ", focused=" + focused + ", hover=" + hover + ", id=" + id + ", layoutBounds=" + layoutBounds +
                ", layoutX=" + layoutX + ", layoutY=" + layoutY + ", managed=" + managed + ", opacity=" + opacity +
                ", pressed=" + pressed + ", parent=" + parent + ", visible=" + visible + '}';
    }
}

Узел является базовым классом, который представляет сценограф. Он может иметь родителя, который выглядит следующим образом:

Node — это базовый абстрактный класс, который представляет один узел графа сцены в новой Prism (айсберг).
Каждый узел может иметь родительский узел:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package uk.co.xenonique.javafx;

import java.util.*;

/**
 * @author Peter Pilgrim
 */
public abstract class Parent extends Node {

    protected List<Node> children = new ArrayList<Node>();
    protected boolean needsLayout;

    public Parent() {
    }

    public List<Node> getChildren() {
        return children;
    }

    protected Parent setChildren(List<Node> children) {
        this.children = children;
        return this;
    }

...
    @Override
    public String toString() {
        return "Parent{" + "children=" + children + ", needsLayout=" + needsLayout + '}';
    }

}

Parent and Node are abstract classes. Group is the simplest container type of Parent and it is a concrete type. A group represents a collection of scene graph nodes, which is not the same of a container. So here is the Java API version.

package uk.co.xenonique.javafx;

import java.util.*;

public class Group extends Parent {

    public Group() {
    }

    public List<Node> getContent() {
        return getChildren();
    }

    public void setContent(List<Node> content) {
        this.setContent( content );
    }
    ...
}

Let us look at the Shape as a Java class, or at the very least the basics of it.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package uk.co.xenonique.javafx;

public abstract class Shape extends Node {

    protected Color fill   = new Color( 0x999966);
    protected Color stroke = new Color(0xFF9900);
    protected float strokeWidth = 1.0F;

    public Shape() {
        super();
    }

    public Color getFill() {
        return fill;
    }

    public Shape setFill(Color fill) {
        this.fill = fill;
        return this;
    }
     ...
}

Finally, here is the outline of the remaining JavaFX for Java API classes that we need. Namely, they are the Rectangle, Stage and the Scene classes:

 

package uk.co.xenonique.javafx;

/**
 * @author Peter Pilgrim
 */
public class Rectangle extends Shape {
    private float x;
    private float y;
    private float width;
    private float height;

    public Rectangle() {
    }

    public float getHeight() {
        return height;
    }

    public Rectangle setHeight(float height) {
        this.height = height;
        return this;
    }

    ...
}

The Stage:

package uk.co.xenonique.javafx;

public class Stage {

    private String title;
    private float width;
    private float height;
    private float x;
    private float y;
    private Bounds stageBounds;
    private Bounds screenBounds;
    private Scene scene;
    private boolean visible;
    private float opacity;

    public Stage() {
    }

    public float getWidth() {
        return width;
    }

    public Stage setWidth(float width) {
        this.width = width;
        return this;
    }

    public Scene getScene() {
        return scene;
    }

    public Stage setScene(Scene scene) {
        this.scene = scene;
        return this;
    }
    ...
}

Finally, the Scene:

package uk.co.xenonique.javafx;

import java.util.*;

public class Scene extends Parent {

    public Scene() {
    }

    public List<Node> getContent() {
        return getChildren();
    }

    public Scene setContent(Node content) {
        this.setContent( content );
        return this;
    }
    ...

}

So how on earth do we call this Java API from Scala? What could it look like? Here is a work-in-progress Domain Specific Language example based on my own attempts of modelling these Java API classes. So far it looks like this:

object TestDSL {
  def main(args: Array[String]) {
    val builder = new FXBuilder()
    import builder._

    val s1 = stage("Demo") { s =>
      val s2 = scene { s =>
        group { g =>
          rectangle() { n =>
            n.setX(50)
            .setY(50)
            .setWidth(100)
            .setHeight(200)
            .setFill( new Color( 0xFFCC00 ) )
            .setStrokeWidth(2.0F)
            .setStroke(new Color( 0xCC9900) )
          }
        }
      }
    }
    s1.setVisible(true)
  }
}
object TestDSL {
  def main(args: Array[String]) {
    val builder = new FXBuilder()
    import builder._

    val s1 = stage("Demo") { s =>
      val s2 = scene { s =>
        group { g =>
          rectangle() { n =>
            n.setX(50)
            .setY(50)
            .setWidth(100)
            .setHeight(200)
            .setFill( new Color( 0xFFCC00 ) )
            .setStrokeWidth(2.0F)
            .setStroke(new Color( 0xCC9900) )
          }
        }
      }
    }
    s1.setVisible(true)
  }
}

Writing a Scala DSL is not easy to tell the truth. Having attend the Scala LiftOff USA after JavaOne 2010 I sought some advice, and the best advice I was given was to take things slowly. Build slowly and deliberately, the DSL is an internal one. Here are some observations:

  • The Scala DSL at this stage is not replacement for JavaFX Script or Visage project. Scala does not support declarative instantiation of objects in 2.8.0-final.
  • The closures in Groovy have an implicit “it” parameter that are passed to them, in this way Groovy Builder can hide an implicit context. See examples of code that use Groovy’s XML, Ant and Swing builders and you will see this. In Scala we cannot simulate this. Therefore note the explicity closure parameter inside each of the closures.
  • Each closure parameter represents the JavaFX Java object in question. Hence we can chain the operations together in the form n.setX(10).setY(10).setWidth(100).setHeight(100) etc
  • Due to way Scala is calling Java classes it is not possible to support in-fix operations directly. So the following does not cut it: n setX 10 setY 10 setWidth 100 setHeight 100.
  • The advantage of using an FXBuilder and an explicit closure parameter should make binding using the official Java APIs easier rather than harder. If you need to call a specific API routine your closure has the object instance.
  • The FXBuilder DSL is a lightweight wrapper around the JavaFX for Java API.
  • The FXBuilder also knows how to add a Node to Parent. It would have to be extended to handle collections of nodes, namely List[Node].

Of course, Scala is a statically typed language and there is great type safety in there. Getting the code with the mock JavaFX API classes to compile required some enduration as my knowledge of Scala does not quite match my long Java experience just yet. It is the first cut and writing any DSL is a long road and an investment. The equivalent JavaFX Script/ Visage looks this:

def s1:Stage = Stage {
   scene: Scene {
       content: Group {
            content: Rectangle {
                x: 50 y: 50 width: 200 height: 200
                fill: Color.web("#FFCC00")
                strokeWidth: 2.0 stroke: Color.web("#CC9900")
            }
       }
}

All the fun of the fair. This is Peter Pilgrim. Out.

PS: I am all ears for your opinion by the way.

From http://www.xenonique.co.uk/blog/?p=55