Желательно-ориентированное программирование
— Как засунуть верблюда в холодильник в 4 приёма?
— Очень просто:
- открыть холодильник;
- достать оттуда слона;
- засунуть на его место верблюда;
- закрыть холодильник.
Школьный фольклор, «Загадка».
Содержание
В рамках Eludia.pm реализован мини-framework, позволяющий программировать в терминах желаний.
Описание
Желание — это требование к состоянию системы (как правило, к содержимому БД), характеризующееся следующими свойствами:
- на момент формулировки желание может быть частично или полностью исполненным;
- выяснить, исполнено ли желание, проще, чем пытаться выполнить его вслепую.
Кроме того, характерным (хотя необязательным) свойством желаний является возможность их группировки: исполнять несколько однотипных желаний вместе проще, чем последовательно.
Для исполнения желаний предусмотрена процедура
wish ($type, $items, $options);
где
- $type
- тип желаний (строка);
- $items
- (ссылка на) список желаний;
- $options
- (ссылка на) хэш с разнообразными опциями.
Алгоритм
Код процедуры в основном сводится к вызовам других процедур, имена которых зависят от $type. Таким образом, framework сам по себе весьма абстрактный, хотя предлагает достаточно удобную декомпозицию подобных задач (как можно убедиться на примере желаний типа table_data). Ниже описаны шаги реализуемого алгоритма.
Уточнение опций
Прежде всего, вызывается
&{"wish_to_adjust_options_for_$type"} ($options);
этот шаг предусмотрен для подстановки всевозможных значений по умолчанию, которые в дальнейшем должны считаться обязательными.
В частности, после этого вызова $options -> {key} должен быть ссылкой на список строк, являющихся именами некоторых компонент в хэшах, из которых состоит @$items. В дальнейшем этот набор полей рассматривается как первичный ключ при сопоставлении желаемого с действительным.
Прояснение требований
Далее аналогичная операция производится с каждым из желаний
&{"wish_to_clarify_demands_for_$type"} ($i, $options)
уже в контексте уточнённых опций. Характерный пример: приведение имени типа данных к каноническому виду в wish table_columns: замена NUMERIC на DECIMAL для MySQL, INT на INT4 для PostgreSQL и т. п.
Исследование текущего состояния
Теперь система выясняет, что из затребованного (или относящегося к нему) уже имеет место в реальности:
$existing = &{"wish_to_explore_existing_$type"} ($options);
Результирующий хэш $existing должен быть проиндексирован по $options -> {key}.
Составление плана действий
Имея в руках текущее и желаемое, система для определяет, какие (минимальные) операции требуется произвести.
Не было ли такого ранее?
Вначале для каждого требования $new ищется его прототип в прошлом:
my $old = delete $existing -> {@$new {@{$options -> {key}}}};
Если такового не находится, то планируется операция с фиксированным именем create. "Планирование операции" означает прописывание в список действий: хэш $todo, о котором рассказано ниже.
push @{$todo -> {create}}, $new
Если было, нужно ли его менять?
Если же прототип найден, то сначала желание уточняется в его контексте:
&{"wish_to_update_demands_for_$type"} ($old, $new, $options);
Необходимость этого шага обусловлена тем, что существующее состояние может отличаться от требуемого в бОльшую сторону — тогда необходимо подправить $new так, чтобы он содержал бОльшие значения параметров из $old. Скажем, если в wish table_columns требуется тип VARCHAR(255) при имеющемся VARCHAR(4000), то $new можно приравнять к $old — тогда никаких действий не потребуется.
Если менять, то как именно?
Когда уточнённое желание всё-таки отличается от прототипа (сравниваются значения функции Dumper от $old и $new), то производится планирование необходимых операций:
&{"wish_to_schedule_modifications_for_$type"} ($i, $existing, $todo, $options);
При отладке функций, связанных с wish, часто возникают ситуации, когда производятся лишние действия. Это связано с тем, что $old и $new, в принципе обозначая одно и то же, имеют небольшие отличия: лишние компоненты, пустые строки или 0 вместо undef и т. п. Выявляются такие ситуации вставкой отладочной печати в wish_to_schedule_modifications_for_$type. Определив различие, обычно следует модифицировать либо wish_to_explore_existing_$type (которая обеспечивает значения $old), либо wish_to_clarify_demands_for_$type (соответственно, $new).
Бывают также различия, обусловленные наличием лишних компонент в $new — они могут вообще не иметь смысла в рамках текущего диалекта SQL или вообще содержать какую-либо информацию, специфичную для приложения. В этой связи бывает полезно обеспечить совпадение наборов компонент уже на выходе из wish_to_update_demands_for_$type:
foreach my $i ($old, $new) { %$i = map {$_ => $i -> {$_}} qw (name field_1 field_2 ... field_n); }
Возвращаясь к wish_to_schedule_modifications_for_$type, отметим, что хэш $todo как на входе в процедуру, так и на выходе имеет следующий вид:
{ action1 => [args11, args12,... ], action2 => [args21, args22,... ], ... }
Процедура wish_to_schedule_modifications_for_$type может приписывать элементы в список и для действия с предопределённым именем create. Это, в частности, имеет смысл в ситуациях, когда соответствующие DDL-команды для создания и изменения имеют одинаковый вид: CREATE OR REPLACE ..., COMMENT ON... и т. п.
То, что не запрошено — может, удалить?
Далее в раках этого же этапа вызывается
&{"wish_to_schedule_cleanup_for_$type"} ($existing, $todo, $options);
которая может спланировать дополнительные действия по "сборке мусора" (типичный пример из wish table_data: удаление того, что было в $existing, но не упомянуто ни в одном $i). Здесь следует проявлять большую осторожность: в большинстве случаев каждое желание покрывают только часть общего требуемого состояния, так что удалять не следует вообще ничего.
Реализация плана
И, наконец, содержимое $todo реализуется в виде вызовов
&{"wish_to_actually_${action}_${type}"} ($todo -> {$action}, $options)
Порядок действий не определён, всегда следует предполагать возможность асинхронного исполнения.
Разное
Идея "Желательно-ориентированного программирования" не претендует на всеобщность и даже на принципиальную новизну. Весьма похожими категориями приходится мыслить, скажем, при использовании утилиты make (типичный Makefile — огромное желание) и её суррогатов. Однако там всё в основном привязано к файловой системе, а wish получился из нескольких процедур для работы с СУБД.
В области же БД wish можно условно считать далеко идущим обобщением инструкций типа ALTER и CREATE OR REPLACE с одной стороны и REPLACE INTO / MERGE INTO — с другой.