LogicComposer - логика в динамике
Еще с пол-года назад приходила ко мне мысль - подчинить логику динамике или просто говоря создавать сложные древовидные цепочки условий динамически, тоесть на основе опять-таки каких-либо других логических условий, полученных данных, генерируя циклически и т.п. Первые попытки я даже не выносил на всеобщее рассмотрение. Было создание Коллектора-Декоратора WHERE и HAVING условий для Zend_Db_Select. Смысл был прост - создать наборы условий в зависимости , например, от открытого сейчас раздела сайта, а далее продекорировать ими объект Zend_Db_Select в зависимости от полученных из запроса (Request) данных. Тогда работа велась с Zend_Datagrid (привет Baziak & Bashmach) и данные было удобно выгребать одним, пусть объемным(это не значит медленным), достаточно оптимизированным SQL запросом, это реально работало, но было довольно узкоспециализированным решением, так как такой подход к построению запросов не всегда себя оправдывает. На этом предистория заканчивается и начинается повествование о LogicComposer :-).
Сама идея состоит в том, чтобы создавать древовидные цепочки логических условий (AND/OR) с неограниченной степенью вложенности, ну например:
(a < b) AND ((c == 1) OR ((n < 6) AND (h > 0)) OR ((m < 0) OR ((j == 4) AND (((l == 'abc') AND (k == 'sc')) OR ((s < 0) AND (i > 6))))))
Запутанно блин, запутанно и со скобками можно париться долго, считая где они начинаются, где заканчиваются, а тут еще и нифига не понятно для чего каждая ветка условий и куда к ней добавить условие. А когда еще и эти условия собираются для, например, SQL запроса на основе данных других SQL запросов и данных HTTP запроса в разных частях исходника, написанного каким-то “талантом”, а потом еще и компонуются неправильно, потому что он также как и вы с моим примером ошибся в скобках, создавая эти условия кусочно. Вот где-то в таких условиях у меня появилось желание написать LogicComposer.
Что я сделал - создал класс на PHP, который релизует GOF паттерн композит (composite) и научил его правильно компоновать на выходе, те условия которые он в себя насобирал. Начнем с примера, построим условие, написанное выше, с помощью LogicComposer:
$myLogic = new LogicComposer(); $myLogic->start('a < b') ->addAND()->mark('myLogic1') ->start('c == 1') ->addOR()->start('n < 6')->addAND('h > 0') ->jump('myLogic1') ->addOR() ->start('m < 0') ->addOR() ->start('j==4') ->addAND()->mark('mySubLogic') ->start() ->start("l == 'abc'")->addAND("k == 'sc'") ->jump('mySubLogic') ->addOR() ->start('s < 0')->addAND('i > 6'); echo $myLogic->toText();
Мне стало понятнее, о скобках и синтаксических операторах логики позаботиться сам класс, но глупо было бы изобретать свой псевдоязык описание сабжа только для этого. В участке кода, что я привел в пример, видны 2 не особо понятных метода - mark() и jump(). Эти методы позволяют маркировать любую ветку логических условий и, соответственно, возвращаться к любой из промаркированных веток для добавления в нее новых условий или веток.
Представим, что нам понадобилось добавить условие в ветку которая прибавлена ANDом после (j == 4), в нашем примере мы промаркировали ее как mySublogic, для этого нужно снова вернуться в эту ветку и добавить условие:
$myLogic->jump('mySubLogic')->addOR('f > 9'); echo $myLogic->toText();
Результатом будет:
(a < b) AND ((c == 1) OR ((n < 6) AND (h > 0)) OR ((m < 0) OR ((j==4) AND (((l == 'abc') AND (k == 'sc')) OR ((s < 0) AND (i > 6)) OR (f > 9)))))
Теперь постараюсь прояснить, то что возможно еще не всем понятно в использовании класса. Любой метод задающий условие - start(), addOR(), addAND(), вызванный без аргументов, создает новую ветку условий а вы выходе вернет уже объект этой ветки, при вызове данного метода с аргументом условия, то это условие будет добавлено в текущую ветку условий, а на выходе будет объект текущего условия. Подробнее разобрав пример вы быстро все поймете, а составив пару простеньких условий, так я думаю вы уже четко представите, как использовать этот класс в своих целях.
В процессе реализации класса я несколько раз искоренял костыли и пытался сделать использование класса логичнее и проще, но сохранить гибкость. Скажу сразу сам класс прошел испытания, так сказать “в бою” - создавал набор логических условий в поисковом sql запросе, притом переделывать логику этого поиска было нельзя, а вот компановку - пришлось, так как были явные ошибки, приводящие в тому что с обязательными условиями компановались ORом условия, которые очень часто давали true…
Также в классе учтены несколько нужных штучек:
- Пустые группы(без стартовых условий и подусловий) рендериться не будут;
- Если не задано стартовое условие/группа условий, то первое заданное в группе условие станет стартовым, а при определении стартового встанет на свое место с заданной для него логикой;
- метод reset(), который позволяет полностью сбросить содержимое группы, включая стартовое условие;
- методы addANDUnless() и addORUnless(), которые добавят условие только при выполнении условия поданного в них :) (удобно, когда сам не знаешь какая логика нужна OR или AND);
- magic method __to String(), echo $myLogic выведет в браузер сформированное условие;
- возможность использования адаптеров синтаксических едениц, определяющих условия. Например, использовать вместо OR ||, а вместо AND &&.
Сама идея и код класса открыты для всеобщего использования и критики.
Скачать:
http://www.feodosian.com/downloads/logiccomposer/logiccomposer-0.1.3b.zip
svn:
http://anonymous:anonymous@svn.yomoyo.net/ideas/LogicComposer/
Поглядеть исходники через web:
http://yomoyo.feodosian.com/srcFiles/LogicComposer/
Очень хотелось бы услышать или прочитать критику.
Tags: php, разработка
Крези мен :)
Мне не понравилось именование методов - оставь OR(), AND()
:), изначально тоже хотел методы OR() и AND(), но это зарезервированные слова и потому таким образом именовать методы нельзя :(. Я думаю можно закрутить через __call(), но мне показалось это излишним джедайством, которое снизит читабельность кода.
омг. не нравится реализация.
твоя задача переформулируется как “надо построить дерево выражения, для последующего его представления в виде php/sql/whatever-кода”
в итоге тебе надо как-то хранить структуру типа
expression
|
or
/ \
> and
/ \ / \
a 4 == <=
/ \ / \
b 5 c 4
на лабах в институте это советуют делать в польской нотации или S-expression:
(expression (or (> (a 4))(and (== (b 5)(<= (с 4))))
если чуток отформатировать, получается:
(expression
(or
(> (a 4))
(and
(== (b 5)
(<= (с 4)
)
)
)
ничего не напоминает? а если заменить круглые скобки на угловые?
а вот тем, что получается с угловыми скобками очень удобно манипулировать, лазать по нодам, добавлять способ представления через xslt и, кстати, расширяемость бесконечная :) как бонус оно уже сериализованно.
повторюсь - идея правильная :) щас вон Мартин Фаулер бредит DSL’ями… но вот реализация - имхо - не очень.
метпрограммирование, ёпт. о правильных вещах задумываешься, товарисч!
Очень ждал такого комментария, сразу появилось несколько идей и тем для прочтения. Буду работать над алгоритмом.
Сейчас всплыл внезапный интерес к метапрограммированию (нашел для себя BNF, Python и Pyparser), главное не перегореть как у меня это часто бывает :). Спасибо за содержательный комментарий.