Saturday, 21 September 2013

Type-safe bindings (reflection) for Arena

Arena

Arena is UI framework strongly based on a pure MVC pattern through bindings and the ApplicationModel idea.

The UI elements are connected to the model through bindings, which are in charge of propagating changes between the two parties. If the model changes, then it will update the ui (view), and of course if the view changes because of user interaction (like changing a textbox value) then it will also change the model.
This simple idea reduces a lot of UI code, and introduces a new way for developing UI's.
Plus the fact that Arena makes your model object observables, transparently by means of AOP.

Sample Distance Converter Application


Here's a sample really small app whose functionality is to convert miles to kilometers.


Here's the model (in xtend language):

@Observable
class DistanceConverter {
    @Property Double miles
    @Property Double kilometers
   
    def convert() {
        kilometers = miles * 1.60934
    }
}


And here's the View, in terms of control objects.

class DistanceConverterWindow extends MainWindow<DistanceConverter> {
   
   
new() { super(new DistanceConverter) }

   
override createContents(Panel mainPanel) {
        title = "Miles To Kilometer Converter"

        mainPanel.layout = new VerticalLayout       
       
new Label(mainPanel).text = "Input in miles"
       
new TextBox(mainPanel)

       
new Button(mainPanel) => [
            caption = "Convert"
        ]

       
new Label(mainPanel) => [
             background = Color.ORANGE
        ]
       
new Label(mainPanel).text = " km"
    }
   

}


As you are probably thinking or maybe wondering, this is not the actually functional View implementation.
It is not actually connected to the model. If we start this app it will indeed show pretty much as the one on the screenshot but clicking on the button won't do anything. Changing the textbox neither. And we'll never actually see any converted kilometers.

That's because we are missing the Bindings. The stuff that glues together view + model.

Bindings


As you can see the UI is a direct view of the domain. Whenever the user changes the value of the textbox it will modify the "miles" property. When he presses the button it will call the "convert" method which doesn't receive any parameter (the converter already knows the input miles), and also doesn't return anything (because the converter also "remembers" the converted km value, it's one of its responsabilities).

All this wiring between the UI and the model is done through bindings.
Here's a sample  drawing:


Now lets change the UI code in order to express this bindings.

The textbox value will be sync'd with the "millas" property from the model

new TextBox(mainPanel) => [
            bindValueToProperty("miles")
        ]


The background colored label is also bound to the "kilometers" property. This time as the label is a "read-only" ui control it won't ever change the value from the model, it will just display it.

        new Label(mainPanel) => [
            background = Color.ORANGE
            bindValueToProperty("kilometers")
        ]


And at last the button's on click should execute the model's "convert" method.

        new Button(mainPanel) => [
            caption = "Convert"
            onClick [ |
this.modelObject.convert ]
        ]


Here we sue the power of xtend blocks/closures :)

Bindings, Reflection & Strings

Probably you are familiar with this kind of code. Frameworks are generic pieces of software, that provide some sort of functionality or lifecycle. But they don't do anything along. You "use" them. Or actually they "use" your code/objects. But anyway, eventually you need to instruct them in order to use your objects.
Because they know how to handle any object, but none in particular.

So they use metaprogramming techniques, and specifically Reflection API's in order to manipulate your objects.
Some examples of this are hibernate where you instruct it by means of mappings, for example in xml, or in the form of annotations. Or spring framework's xml, etc.

In our example Arena uses reflection for the bindings, in particular for reading and modifying the model's properties.

One of the big drawbacks of reflection in java is that there's no way to refer to a property in a safe way. The only way is to use a string, whose value is the "name" of property or method.

bindValueToProperty("miles")

So, what's the problem with it ?
Well, that you are in a statically compile-time checked language, but the compiler won't be able to help you on this one.
For it the "miles" is just that, a string. So it won't check if it's actually a valid property name, or if it exists a getMiles() method in the model class.

If you are building a middle/big sized application, and all the windows, panels and controls binds themselves in this way then you'll face a maintenance complexity.
For example:
  • if you change the name of the model's property in a refactor, then the IDE won't be able to help you changing this strings. And this will blow-up in runtime.
  • if you want to lookup references to a given method or property, then again the IDE won't help you. Then you might think that a property is not used by anyone, and go ahead and delete it. Again.. booom! only at runtime. Lucky you if you catch it before your user =)
So well, this is pretty bad ! But indeed we don't want to lose reflection. It's a very powerful mechanism !

Avoiding String problems when using reflection using constants

There are some tricks to tackle this problem list having the string declared as a constant string in only one place.
For example:

class DistanceConverter {
   
public static String MILES = "miles"
   
public static String KILOMETERS = "kilometers"
   
public static String CONVERT = "convert"
    ...

And now from the UI:

        new TextBox(mainPanel) => [
            bindValueToProperty(DistanceConverter.MILES)
        ]


So now we can look up references to the constant for usages. Also if we change the name of the property we'll only have to change the string value in on place, the constant.

But this still not perfect.
There's a better way :)
But first we need to see some other idea.

What's the mocking frameworks approach

Mocking frameworks face some similar problem. By nature they need to work with our own defined classes, but they cannot be coupled to them, of course. They don't know anything about our DistanceConverter for example.
But also they don't know what to expect and assert, so you need to instruct them.

But instead of using reflection with strings or a complicated API for expressing the expected behavior they use a quite interesting idea.
For example Mockito...

What's interesting here is the line:

when(mockedList.get(0)).thenReturn("first");

We are not actually interested in executing the "mockedList.get(0)". Instead we are "instructing" mockito telling him "when some calls the "get" method with a "0" as parameter, then you need to return the "first" string).

A normal string based reflection approach would be:

whenSomeoneCalls("get").on(mockedList).with(0).thenReturn("first")

(and this is still actually a good fluent API, could be really worst).

So, in conclusion, they found a way of actually using the type information and therefore the compiler in a statical way, to instruct a framework.

So the whole idea of this post was to get here. Can we do that same thing for Arena bindings ?

Type-Safe Arena Bindings

Yes we can !
This is just a really draft proof of concept that was done just in 30 minutes, so there's still a lot of work to be done, and many things to think about.
Let's get directly to the code:

        new TextBox(mainPanel) => [
            bindValue(
this, [ miles ])
        ]


        ...

        new Label(mainPanel) => [
            background = Color.ORANGE
            bindValue(
this, [kilometers])
        ]


Compare to the previous version. This one doesn't have any string for "miles" and "kilometers".

The new "bindValue" method receives an xtend block whose first and only parameter is the window's model object (that gets bound by means of generic types).

So [ miles ] here can be read as

[ it.miles]

Or

[ converter | converter.miles ]

Or even in the longest way

[ DistanceConverter converter | converter.miles ]

So in case you didn't notice this is code. We have a line of code that calls the "miles" property, and therefore it gets checked by the compiler.

But it's actually a trick. Pretty similar to the one that Mockito uses.
This code is not actually executed to get the property. We are not writting "[ miles ]" to execute it right away, but just to "instruct" arena "miles is the property I want you to bind to".

And it works flawlessly ! :)

In case you are interested in the implementation, what the "bindValue" does is:
  • receives your block
  • it gets the actual model (the converter) and with its class, it creates a new proxy instance. Kind of a "ghost" object of the same class.
  • it then calls your block with that proxy.
  • Your code sends the message "miles" (actually getMiles()).
  • The proxy "records" all the methods you called into it.
  • So, after the block finishes up, the next thing the bindValue does is to ask the proxy "hey, what property the guy accessed  ?".
  • In this case the proxy will tell "the miles property",
  • "Ah.. ok, then we need to bind to that one.
It's not as black magic as one thinks initially looking at Mockito. And it's all there. Just enter the "when" method and explore some code !

Further work

We still need to work on this, because we will like to add this feature to Arena's new version.
There are still many things to think about, like what if the developer writes some side-effect weird or long lines of code in the block ?.
How to support nested properties ? Like [ customer.address.street.number ]
Etc.
We should also remove the first parameter "this" on the bindValue method, but that's not easy because we will lose the typing of the closure (now bound to the window's generic type, the model class).

More about Arena

If you are interested in Arena framework here are a couple of links:

No comments:

Post a comment