Lex Kravetski (lex_kravetski) wrote,
Lex Kravetski
lex_kravetski

Categories:
  • Music:

Концепция «Авто-всё»: построение авто-формы на основе объекта с данными

Перед прочтением данной статьи рекомендуется прочитать предыдущие по теме.

 

Ломанёмся, как говорится, с места в карьер.

Если уж у нас имеется объекто-ориентированная программа, то наверняка имеются и объекты, представляющие собой данные, с которыми она работает. То есть, миновать создание объектов с данными, — пусть даже мега-универсальных объектов, — нам не удастся. Объекты данных, как ни крути, есть, и это, простите за каламбур, — наша данность.

Чтобы лучше понимать, о чём речь, возьмём какой-то условно-синтетический объект для примера. Ну, положим, некоторые сведения о человеке. Объект выглядит примерно так:

public class Person {
        
        private String name;
        
        private boolean male;
        
        private int yearOfBirth;
        
        public String getName() {
                return name;
        }
        
        public void setName( String name ) {
                this.name = name;
        }
        
        public int getYearOfBirth() {
                return yearOfBirth;
        }
        
        public void setYearOfBirth( int yearOfBirth ) {
                this.yearOfBirth = yearOfBirth;
        }
        
        public boolean isMale() {
                return male;
        }
        
        public void setMale( boolean male ) {
                this.male = male;
        }
}

Тут, конечно, всё предельно просто и прямолинейно, однако всё ведь примерно так и делается. Реальные переменные спрятаны от посторонних глаз, вместо них используются геттеры и сеттеры (То бишь, методы, названия которых начинаются с «get» и «set», а далее содержат имя реальной переменной. Для булевских пропертей иногда вместо «get» используется префикс «is»). Сам объект настолько шаблонен, что скорее всего создавался просто генератором кода.

Между делом, стоит чуть подробнее сказать о концепции «пропертей» — «свойств» класса. Давать публичный доступ к полям — моветон. Это осмысленно только для классов-структур, то есть, таких, которые содержат только лишь поля и ни одного метода. В остальных же случаях реальные поля скрываются, а вместо них наружу выводится пара методов — геттер и сеттер. Преимущество такого подхода в том, что все изменения полей легко отслеживаются прямо внутри класса. Второе преимущество — отвязка от реального поля и возможность при необходимости выполнить ещё какие-то операции, кроме изменения значения поля или его получения. Кстати, реального поля класса вообще может и не быть, но для внешнего наблюдателя это совершенно незаметно.

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

В данном случае у нас их три — имя, год рождения и флаг принадлежности к мужскому полу. Код, благодаря пропертям, несколько разросся, но нас это не должно волновать, поскольку в любом случае мы бы что-то такое сделали. Тем более, настолько стандартный код легко генерируется при помощи всевозможных средств автоматизации среды.

Итак, есть объект. Рано или поздно нам захочется дать пользователю возможность его заполнять. Поскольку мы не звери, заполнение для пользователя будет дано в виде графического диалога. И выглядеть он будет как-то так:

Всё вроде просто и логично, однако прикинем, сколько времени нам потребуется на создание такого диалога. При этом, от щедрот, мы разрешим себе визуальный редактор, где можно всё это быстро нарисовать.

В редакторе нам придётся куда-то клинкуть, чтобы создалось само окно диалога. После этого добавим лэйаут к основному композиту. Для лэйаута зададим поведение — занимать всю доступную площадь окна. После этого создадим композит внутри основного композита. В нём, на табличном лэйауте — две ячейки для названий полей и ещё две для эдит боксов. Вставим лэблы для названий и эдит-боксы для ввода данных. Зададим поведение каждого из добавленных элементов — выравнивание, реакцию на растяжение и т.п. Потом добавим чек-бокс снизу. Чекбоксу и лэблам зададим имена («Name», «YearOfBirth», «Male»). Красную звёздочку, означающую, что поле не может быть пустым, пока проигнорируем.

Если всё получилось с первого раза и мы хорошо помнили, как и что делать, мы потратили минут десять. У нас на данный момент есть часть, отвечающая за название диалога. И всё.

Теперь нам надо сделать его заполнение — то есть, передачу значений из пропертей объекта в отвечающие за них виджеты. Имя можно передавать прямо сразу (оно — класса String), а вот год уже надо перевести из числа в строку. Ну, положим, мы парни быстрые и за пять минут с пересылкой/переконвертацией управились. Ещё есть процесс обратной передачи. Из виджетов — в пропертя. Тут снова та же проблема: год надо для начала распарсить из строки в число и проверить результат на валидность. Если введена какая-то хрень, то надо выдать окно с предупреждением и каким-то образом пресечь срабатывание кнопки «ОК». Не исключено, имя тоже надо каким-то образом проверить. Ну да ладно, забьём пока на проверку. Ещё пять минут потратили. Далее надо описать процесс создания диалога и связывания его с объектом. Без готового прототипа ещё минут пять.

В общем, примерно полчаса на такой вот максимально простой диалог. Для сравнения создание самого объекта заняло меньше минуты. Спросим себя, а чего такого особого мы тут понаписали, что на это дело ушло в тридцать раз больше времени, чем на сам объект? Было ли тут что-то сильно содержательное и уникальное? Ну, какие-то новые сущности или что-то типа того?

Было, но мало. Содержательным и невычислимым по самому объекту, вообще говоря, оказалось только название диалога и программная констатация факта, что такой диалог в принципе нужен. Мы ведь даже имена полей диалога просто взяли из имён пропертей. Остальное же…

Ну посмотрим на объект. В нём три проперти. Что мы можем по ним сказать? Одно из них — строка. По-видимому, для её редактирования нам понадобится эдит бокс. Второе имеет тип int. Тут, как видно, тоже эдит-бокс, но с одним отличием: значение не может быть null и должно проверяться на допустимость — только числа могут быть в эдит-боксе, остальные значения ошибочны. Третье проперти — булевское. Наверно подойдёт чек-бокс. Всё. Информация о диалоге получена прямо из самого объекта. Мы потратили полчаса на то, что в общем-то уже написали. Нам было бы достаточно написать что-то вроде

AutoFormHelper.createAutoFormOKCancelDialog( new Person(), "Данные о человеке" ).open();

и этого было бы уже достаточно. Ну, с поправкой на то, что Person наверно не надо прямо тут создавать — он должен откуда-то быть взят. Нам даже визуальный редактор не нужен — мы ведь вид диалога не описываем. Оно всё само.

Многие спросят: «а вдруг нам встретится более сложный случай?». На что я отвечу: вот когда встретится, тогда мы и опишем его особость. Причём, именно особость, не вообще всё вместе. Собственно, концепция авто-всё про это и говорит: описываются только отличия от наиболее частого сценария. Причём, чем чаще встречается такое отличие, тем меньше текста нужно для его описания.

В нашем случае описывать не надо вообще ничего. Даже если мы вдруг решим, что нам нужен не год рождения, а прямо сразу дата рождения, то и тут от нас требуется только изменить тип проперти, остальное уже — дело авто-формы. Она сама поймёт, что теперь вместо эдит-бокса надо использовать виджет с датой.

Но, постойте, а как же связывание интерфейса с данными? Диалог-то мы автоматически создали, догадавшись, как он должен выглядеть, но где описание результатов его работы? А оно всё там же — в строке создания диалога. Связка диалога и объекта произошла где-то за пределами нашей видимости. Для нас важно лишь то, что они связались. При валидных изменениях пользователем значений полей диалога меняются и проперти нашего объекта. По кнопке «Cancel» в диалоге отменяются и изменения в объекте тоже. Всё само. Куча времени сэкономлена. В простом случае интерфейс требует полминуты на своё создание. Не появляется никаких дополнительных классов. Тишь да гладь.

А если диалог более сложный, то счёт сэкономленного времени пойдёт на месяцы. Ведь в этих случаях нам надо кроме вида диалога (визуальное создание которого, к слову, тоже будет куда как более сложным) надо ещё описать взаимодействие контролей, проверку кучи возможных ошибок и вывод сообщений о них. Даже если для всего этого мы напишем некоторые вспомогательные методы, всё равно по крайней мере вызов этих методов нам придётся написать явно. И большая часть написанного будет бессодержательна — то есть, нести минимум уникальной информации. Мы это всё когда-то уже делали. Причём, неоднократно.

Если нам уж и вводить какие-то дополнительные фрагменты кода, то разве что как раз для её описания. Точнее, для описания только её. И такая информация имеется. Например, имена пропертей в качестве имён полей диалога в общем подходят, но смотрятся недружелюбно. Хотелось бы, в частности, вместо «Name» видеть «Имя», вместо «YearOfBirth» — «Год рождения» и так далее. Но из контекста объекта вычислить такие эти имена вроде как нельзя — их там попросту нет. Возникает вопрос, а как их туда вписать?

Ну, очевидных путей два: во-первых, можно сделать метод getFieldName(String property), который поочерёдно вызовется для каждого проперти в процессе создания диалога, и откуда мы вернём имена полей. Во-вторых, можно откуда-то извне при создании диалога выдать связку имени проперти и имени поля диалога. Однако оба этих метода не особо удобны для использования. Как с точки зрения удобочитаемости, так и с точки зрения поиска возможных ошибок и проблем. По краткости кода они тоже не ахти. И при расширении диалога эти места не так-то просто отслеживать.

По счастью, в языке Java есть довольно удобный механизм аннотаций, позволяющий нам приписать всё необходимое прямо к тому месту, где объявляется метод. Например, чтобы задать имя поля диалога для проперти getYearOfBirth мы напишем что-то вроде


        @Visible(name = "Год рождения")
        public int getYearOfBirth() {
                return yearOfBirth;
        }

И читается легко и сразу понятно, где в случае чего искать. Более того, остальные отличия поведения от дефолтного имеет смысл описывать точно так же — через аннотации. И лишь совсем уж сложные случаи придётся обрабатывать через коллбэки в объекте (то есть, через некие функции нашего объекта, вызывающиеся библиотекой при возникновении какого-либо описанного в ней события).

Однако тут мы встречаем первые грабли: объект-то используется не только в интерфейсе. А в него при этом попадает информация о каких-то там диалогах.

Ну, для начала, ничего особо страшного тут нет. Это, как говорится, не бага, это — фича. Объект, как мы видим, всё равно ведь не содержит информацию непосредственно о реализации интерфейса, в нём только некоторые рекомендации по его, интерфейса созданию. Вполне возможно, что библиотека некоторые из них проигнорирует или трактует по-своему. Не исключено даже, библиотеки будут разные, по разному обрабатывающие рекомендации — так не вопрос, ссылок на реализацию ведь нет. Наши аннотации никому не мешают.

Тут прослеживается параллель с языком гипертекстовой разметки: тэги этого языка тоже помещаются прямо в данные, однако в общем случае не содержат явных ссылок на систему визуализации. Браузер волен трактовать тэг, положим, <b> не как указание на жирный шрифт, а, например, как выделение цветом. Или же вообще это дело проигнорировать. Хотя такой подход и нарушает принцип разделения данных с методами их отображения, он таки оказался крайне удобным в использовании. Куда удобнее остальных вариантов.

Вообще говоря, нам не следует смешивать данные именно с методами их обработки. С рекомендациями же по обработке их весьма полезно смешивать. Например, при использовании авто-формам из громоздкой системы «данные — абстрактная логика диалога — описание визуального вида диалога» последние два пункта бесследно и без проблем для нас исчезают. Остаются только сами данные и небольшой набор рекомендаций в них, не оказывающий никакого влияния на остальные части программы.

Правда, иногда нам достаётся объект, в код которого мы не можем внести никаких изменений, однако в целях правильной визуализации диалога дать некоторый набор рекомендаций всё-таки должны. В этом случае в качестве хранилища рекомендаций и, возможно, некоторых коллбэков мы можем использовать «враппер» — объект-обёртку над объектом с данными. Этот враппер просто-напросто инкапсулирует в себя объект с данными и делегирует ему свои проперти. Делегирование, обратно же, — процесс, без проблем позволяющий авто-генерацию кода.

Упомянутый выше враппер, мы будем называть «моделью», подразумевая под ним высокоуровневую модель логики диалога, заданную через рекомендации в аннотациях и некоторый набор методов-коллбэков.

Хотя речь везде идёт про диалог, по сути мы имеем дело с чуть менее крупной сущностью — формой, коих на диалоге может быть несколько, да и быть они могут не только на диалоге.

Процесс же создания диалога в конечном счёте выглядит так: для некоторого объекта создаётся модель, инкапсулирующая в себя этот объект. При помощи кодогенератора организуется делегирование методов модели в инкапсулированный объект. К сгенерированным методам добавляются аннотации, описывающие отличия в поведении по сравнению с наиболее стандартным случаем, и определяющие те нюансы, которые не могут быть вычислены просто по коду. Далее одной строкой создаётся форма, соответствующая модели. Такой подход позволяет получить полностью рабочий диалог за считанные минуты. В дальнейшем, конечно, потребуется некоторая его рихтовка до идеального вида, но по трудоёмкости она несравнима с традиционным методом создания интерфейсов.


public class PersonWrapper {
        
        private Person person;
        
        private PersonWrapper(Person person) {
                super();
                this.person = person;
        }
        
        @Visible(name = "ФИО")
        public String getName() {
                return person.getName();
        }
        
        @Visible(name = "Год рождения")
        public int getYearOfBirth() {
                return person.getYearOfBirth();
        }
        
        @Visible(name = "Пол мужской")
        public boolean isMale() {
                return person.isMale();
        }
        
        public void setMale( boolean male ) {
                person.setMale( male );
        }
        
        public void setName( String name ) {
                person.setName( name );
        }
        
        public void setYearOfBirth( int yearOfBirth ) {
                person.setYearOfBirth( yearOfBirth );
        };
        
        public static void main( String[] args ) {
                Person person = new Person();
                Dialog dialog = AutoFormHelper.createAutoFormOKCancelDialog( new PersonWrapper(person),
                                "Данные о человеке" );
                dialog.setBlockOnOpen( true );
                dialog.open();
        }

}

Тут, собственно, показано, как выглядит модель, созданная на основе класса Person. Всё, кроме содержимого метода main создано при помощи генератора кода за пять секунд. Сам код создания диалога специально слегонца избыточен — чтобы показать полную идентичность того, что создано в методе createAutoFormOKCancelDialog с обычным диалогом.

 

Примечание для альтернативно одарённых читателей:

Тут пока ещё, мягко говоря, не всё изложено. Тут только про самое-самое введение. Всё более сложное будет потом. Включая ответы на пришедшие вам в голову типа коварные вопросы «а как же списки?!!», «а вдруг не эдит-бокс?!!» и «а как же быстродействие?!!».

Ну и обычный в таких случаях комментарий «такое не будет работать», можете засунуть себе в жопу. Описанное тут уже полтора года работает.

Как легко догадаться, альтернативно одарённые комментаторы будут оперативно забанены.

 

Всё про авто-всё

Tags: авто-всё, программирование, философия
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 135 comments
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →
Previous
← Ctrl ← Alt
Next
Ctrl → Alt →