Всем известно, что в Erlang нет привычных объектов из “классического” ООП, но они, по сути, и не нужны. Под словом объект, в данном случае, будет пониматься связка структура (record) и модуль, в котором описаны все функции для работы с этой структурой.
Приведу пример, который буду рассматривать на протяжении всех своих рассуждений:
Классический объект – User с полями login, email, password и salt (куда без нее). Его можно описать так (приставка db_ – необходима, т.к. в erlang уже есть модуль с таким именем).
1 2 3 4 5 6 7 8 9 10 11 |
|
Я хочу описать два своих проекта tq_transform и tq_db. Которые суммарно генерируют объект позволяющий:
- Прятать реализацию, т.е. объекты за пределами модуля db_user не должны работать с record-ом напрямую. Это позволит в случае необходимости с легкостью поменять record, к примеру, на map.
- Задавать значения по умолчанию.
- Работать с “внешним миром”. Существует возможность указать какие поля и доступны для отправки внешнему пользователю ( к примеру через REST api ) и в каком виде, какие нет, какие только для чтения, какие только для записи.
- Конвертировать содержимое полей в/из proplist.
- Выводить список измененных полей.
- Задавать валидаторы как отдельных полей, так и всей модели в целом.
- Проводить загрузку модели в/из БД, притом представление полей в БД и Erlang может отличаться.
- При обновлении модели, подставлять в SQL запрос только обновленные поля.
- Задавать хуки на события:
before_save
,after_save
,before_delete
,after_delete
tq_record_transform
Начну с того, что tq_record_transform – это базовый плагин tq_transform для генерации функций, назначение которых будет описано далее, по описанию заданному через атрибуты -field и -model. Сначала я постараюсь описать, как решается проблема в общем виде в Erlang, следом – как это реализовано или работает через tq_transform и почему именно так.
Прячем реализацию
Для того чтобы нормально работать с полями структуры не обращаясь к ним напрямую нужно задать getter-ы и setter-ы
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
Как можно заметить имя record-а совпадает с именем модуля. Я уже много раз писал об этом в том числе на habre. Это позволяет использовать объект как классическим способом
1 2 3 4 5 |
|
так и более емким
1 2 3 4 5 |
|
Теперь как то же делается с помощью tq_transform.
1 2 3 4 5 6 7 8 |
|
Т.е. надо в модуль добавить указание компилятору использовать parse_transform. Далее определяются поля, имеющие отношение к данной модели через атрибут -field()
и кортеж {FieldName, Opts}
. FieldName – имя поля, а про Opts поговорим чуть далее.
tq_transform сгенерирует record, геттеры, сеттер и еще много вспомогательных функций.
Значения по умолчанию.
Для того чтобы задать default-ные значения для модели надо модифицировать метод new/2
. В нашем примере это не требуется, но, к примеру, если бы у нас было поле group, которое указывало бы имя группы в какой находится пользователь и по умолчанию оно было бы равно “main”, то это бы выглядело так:
1 2 3 4 5 6 7 8 |
|
В tq_record_transform этот же функционал достигается через опции. Их две: {default, Value}
– задает значение по умолчанию. {default_fun, Fun}
– значение, если его необходимо вычислить.
Примеры:
1 2 3 4 |
|
или
1 2 3 4 5 6 7 |
|
Сразу хочу рассказать о Fun. Везде, где в опциях упоминается функция, а это: deafult_func, валидаторы, хуки, Fun; может быть:
F::atom()
– это равносильно простому вызову функции в том же модуле{F::atom(), Args::list()}
– вызов F в текущем модуле с дополнительными аргументами Args (всегда идут вначале){M::atom(), F::atom()}
– равносильно вызову F в модуле M{M::atom, F::atom(), Args::list()}
– вызов F в модуле M с дополнительными аргументами Args.
Т.е. если get_default_group/0 значение по-умолчанию можно всеми ниже перечисленными способами:
1 2 3 4 |
|
Функция определена таким образом в виду того, что в атрибуте нельзя сделать вызов или указать лямбду.
Ограничение доступа к полям.
Поля в модели могут иметь различные параметры доступа:
- разрешения для чтения/записи
- разрешения для сериализации/десериализации
В текущем примере особыми параметрами отличаются поля password и salt. salt – никогда не видна конечному пользователю, она нужна только для внутренних проверок. password – можно только писать, т.к. при сохранении в БД он солится и хэшируется, то для клиента он более не несет никакой смысловой нагрузки.
В обычной версии пока нечего писать, т.к. еще не определены функции для сериализации/десериализации, но к ним еще вернемся в следующем пункте.
В tq_record_transform существует опция {mode, AccessMode}
, которая отвечает за все правила доступа к полям.
AccessMode :: r | w | rw | sr | sw | srw | rsw | srsw
r
– в атоме означает, что поле можно читатьw
– писать
Приставка s
к r
или w
(образована от слова system) означает – только системный доступ. Исходя из вышесказанного получается, что:
r
– может читать и пользователь и systemsr
– может читать только systemw
– может писать и пользователь и systemsw
– может писать только system
Если в атоме отсутствует r
– то вовсе не будет сгенерирован getter, если w
– setter.
По умолчанию mode
задан как rw
.
Применимо к рассматриваемой модели:
1 2 3 4 5 6 7 8 9 10 |
|
Получается, внешний пользователь может писать, но не читать пароль и вовсе никак не может влиять на соль.
Сериализация
Если необходимо задать модели сразу много полей – это не всегда удобно делать через сеттеры, т.к. приходится много раз копировать объект. для этого лучше предусмотреть функции from_proplist/1
и from_proplist/2
которые принимает proplist полей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Пример:
1 2 3 4 5 6 |
|
Для того, чтобы получить поля модели “извне”, надо реализовать еще одну функцию – from_ext_proplist
(ext от external) т.к. во-первых в ключах proplist-а уже не атомы, а бинари, а во-вторых для поля могут требоваться дополнительные трансформации.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Обратите внимание, что from_ext_proplist не реализует преобразование из поля <<“salt”>>, т.к. внешнему пользователю не разрешено писать в это поле.
Еще одна полезная функция to_proplist/1
, но тут все проще:
1 2 3 4 5 6 7 |
|
А теперь вспомним про mode
, сколько еще предстоит писанины… Так что оставлю реализацию to_ext_proplist
на ваше усмотрение.
Теперь про tq_record_transform.
Прежде чем говорить про *_ext_*
функции сразу расскажу про опции поля from_ext
и to_ext
.
{from_ext, Fun}
– Fun применяется к данным, если Fun вернет{ok, Data}
, то будет вызван сеттер с полученными данными, если{error, Reason}
то, к списку ошибок трансформации будет добавлена{FieldName, Reason}
.{to_ext, Fun}
– Fun применяется к данным перед их отправкой пользователю. Функция должна вернуть простоData
.
А также полю можно указать опцию {type, Type}
. Пока она используется только для указания parse_transform-у какую from_ext
функцию и какие валидаторы по умолчанию использовать, но в планах использовать ее для построения спецификации функций. Поддерживаемые на данный момент типы – binary
, non_empty_binary
, non_neg_integer
, non_neg_float
, integer
, float
, boolean
, date
, time
и datetime
.
Функции from_proplist
и from_ext_proplist
реализованы несколько по-другому. Они отличаются тем, что дополнительно принимают опции safe | unsafe
и ignore_unknown
, и возвращает не первую ошибку, а все ошибки, которые возникли по ходу преобразования полей.
ignore_unknown
– говорит продолжать выполнение, если встретилось неизвестное поле, а не выкидывать ошибку.safe
– запрещает задавать поля которые помечены как системные,unsafe
– разрешает задание системных полей
to_proplist
и to_ext_proplist
реализованы сходным образом.
Помимо вышеперечисленных функций генерируются fields/[2,3]
и ext_fields/[2,3]
. На вход они принимают, помимо модели, список полей, которые надо вернуть. Т.е. работают почти как to_proplist
аналоги, но возвращают только часть запрошенных полей. Опции для них:
binary_key
– означает, что список полей, которые надо вернуть содержит не атомы, а binaryunsafe
– вернуть и системные поля тожеignore_unknow
– игнорировать неизвестные имена полей
Список измененных полей
Довольно часто встречается такая необходимость, например, при сохранении в БД или валидации. Есть два способа определить, что поле изменилось:
- Хранить старую версию и новую
- Ставить флаг об изменении в record-е через setter
Первый способ заставляет хранить 2 версии одних и тех же данных, что порой слишком накладно по памяти. Так что реализуем 2-й. Для каждого record-а поля зададим еще одно с пометкой об изменении.
1 2 3 4 5 6 7 8 9 10 11 |
|
Все сеттеры меняются по аналогичному правило, например
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Не стоит забывать про значения по умолчанию. Если они есть, то в них тоже стоит поменять маркер changed, например:
1 2 3 4 5 6 7 8 9 |
|
Теперь 2 функции is_changed/2
и get_changed_field/2
для проверки поменялось ли поле, и получения списка измененных полей.
1 2 3 4 5 |
|
Я использую throw
вместо простого “let it fail”, мне кажется, что при таком подходе проще понять ошибку по логам.
1 2 3 4 5 6 7 8 9 |
|
Код, сгенерированный tq_record_transform, особо ничем не отличается от приведенного выше, так что просто приведу пример использования.
1 2 3 4 5 6 7 8 9 10 |
|
Валидаторы
Валидатор – эта функция, которая принимает на вход данные и возвращает ok
или {error, Reason}
.
Определим валидаторы для нашей модели:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Логику самой функции valid я писать не буду, просто упомяну, что проблема достаточно сложная, требуется учитывать обязательное ли это поле, получено он из БД или нет (is_new/1
обсудим в следующей части), валидаторы могут выстраиваться в цепочки, могут быть заданы не только для поля, но и для модели в целом.
В tq_transform_utils определено несколько валидаторов для типов. A tq_record_transform позволяет задать опции поля required
, {validators, Fun | [Fun]}
и опции модели {validators, Fun | [Fun]}
. Про опции модели я пока не говорил, но они задаются через атрибут -model([Opt])
.
В связи с новой информацией изменим модель так чтобы она удовлетворяла следующим критериям:
login
,password
иsalt
– обязательные поляlogin
иemail
не могут быть пустыми и должны соответствовать определенным критериямpassword
не меньше 6 символов.
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 31 32 33 34 35 36 37 38 |
|
Валидаторы:
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 |
|
Пример:
1 2 3 4 |
|
Итог
Если всю логику, которая описывается в декларативном виде благодаря tq_transform каждый раз описывать вручную, то потребуется огромное количество boilerplate кода а значит, количество требуемого времени и вероятность возникновения ошибок сильно увеличатся.
В следующей части я расскажу о втором плагине для tq_transtorm – tq_db, который позволяет сохранять модели в БД, о моей маленькой надстройке для sql, и том, где и как хранится мета информация о полях и модели.
P.S. Не так давно начал заниматься причесыванием кодовой базы для этих проектов, по ним пока мало документации ( на самом деле эта статья на данный момент наиболее полный справочник ). Так что, если будут вопросы, не стесняйтесь – задавайте, если пожелания или информация об ошибках и недочетах то слать их лучше или на github, или на мой email.
P.P.S Если интересно посмотреть сгенерированный код, то для этого есть специальная функция
1
|
|