Хныканье про UI биндинги
Мне по долгу службы приходится иметь дело с UI компонентами и их API. Я постоянно разрабатываю их, расширяю и интегрирую существующие, пишу к ним тесты и документацию. За 5 лет я видел много разных подходов и хотел бы обсудить одну существенную деталь из мира UI разработки.
Связь с данными — довольно болезненный вопрос при работе с различными полями ввода. Мы постоянно используем различные сложные абстракции — Property, DataSet, DataSource, Binding, Adapter и ещё в довесок кучу костылей вроде ручной реализации реакции на изменение данных в модели и в компоненте. При этом основных подходов для подключения источника данных к UI компоненту всего два:
- Реализация интерфейса
Property { get; set; }
- Подключение свойства объекта при помощи рефлексии по имени
Первый вариант довольно громоздок и требует реализации интерфейса доступа к данным в каждом месте, где требуется биндинг. Второй вариант очень часто применяется, но у него есть существенный недостаток — он плохо поддаётся рефакторингу, очень легко пропустить строку с именем свойства в дебрях UI при переименовании самого свойства.
Давайте помечтаем. Ах вот если бы у нас был такой волшебный оператор, который бы в статически типизированной форме вернул нам по члену класса его имя.
Мы бы смогли использовать его так:
1 |
textField.bind(user, nameOf(user.name)); |
Это было бы идеально! Рефакторинг такого кода очень прост, компилятор проверяет существование свойства и его модификатор доступа — просто подарок для UI фреймворков!
Но, к сожалению, будущее пока не наступило и поддержку такого синтаксиса сложно встретить в популярных языках программирования. Хотя можно с некоторым скрипом эмулировать такое поведение.
Groovy
Groovy — динамический язык для платформы Java, интересен хорошей поддержкой Мета-программирования. В нём есть возможность получения ссылки на метод при помощи специального оператора &. Его можно использовать для получения имени свойства по соглашениям (к сожалению в мире Java по-прежнему нет свойств).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import org.codehaus.groovy.runtime.MethodClosure class Test { static class User { String name } public static String nameOf(Closure c) { ((MethodClosure)c).method } public static void main(String[] args) { def u = new User() println nameOf(u.&getName) } } |
Это всё хорошо, за исключением маленькой проблемы. Groovy — динамический язык. Компилятор даже не заметит вашей опечатки в имени свойства.
C#
Начиная с версии 3.5 в C# появилась поддержка деревьев выражений в рамках технологии LINQ. Вкратце — это возможность во время исполнения вместо переданного выражения манипулировать его синтаксическим деревом. Как если бы компилятор превратил выражение в строку и подставил её вместо исходного кода (только развернул бы ещё все вызовы).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using System; using System.Linq.Expressions; namespace NameOfMagic { public class User { public String Name { get; set; } } public class Nameof<T> { public static string Property<TProp>(Expression<Func<T, TProp>> expression) { var body = expression.Body as MemberExpression; if (body == null) throw new ArgumentException("'expression' should be a member expression"); return body.Member.Name; } } class Program { static void Main(string[] args) { var propName = Nameof<User>.Property(u => u.Name); Console.WriteLine(propName); } } } |
Использование несколько громоздко, но ведь это замечательно работает! Есть небольшой минус в виде больших накладных расходов, но это того стоит. Такой трюк с замыканием позволяет получить типобезопасную ссылку на свойство Name. В некоторых C# фреймворках этот подход принят и биндинги записываются именно в такой форме.
Неужто в Java мире всё настолько хуже?
Scala
В Scala пришлось изрядно попотеть. API макросов и квазицитат «quasiquotes» (да, они реально так безумно назвали свою фичу) является экспериментальным и в нём есть ряд ограничений. Первое — макрос и код с его использованием должны компилироваться независимо, их нельзя поместить в один Jar / модуль. Второе — IDE впадает в ступор и плачет горючими слезами при использовании квазицитат. Хотя Intellij Idea 14 вела себя терпимо.
Будем использовать вот такой макрос:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import scala.language.experimental.macros import scala.reflect.macros.whitebox object NameOf { def apply[T](x: (T) => AnyRef): (Class[T], String) = macro impl def impl(c: whitebox.Context)(x: c.Tree) = { import c.universe._ val q"((${_: TermName}:${a: Type}) => ${_: TermName}.${p: TermName})" = x val typeDef = a.typeSymbol val propertyDef = p.toString q"(classOf[$typeDef], $propertyDef)" } } |
Здесь происходит примерно то же самое, что в C# версии, за одним исключением. Код макроса исполняется во время компиляции и не требует больших накладных расходов. По сути макрос пытается разбить переданное замыкание на объявление типа и возвращаемое поле. Метод возвращает кортеж из класса и имени свойства.
Использовать очень просто:
1 2 3 4 5 |
class User(val name: String) object Test extends App { println(NameOf((u: User) => u.name)) } |
Будущее уже здесь
На самом деле, разработчики языков программирования уже начали реализацию концепции nameof(). Такой оператор доступен в новой версии языка C# 6.0.
С нетерпением ждём языковой поддержки от Java и Scala!