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

Например, стимулы кажутся бесконечно настраиваемыми.

Что такое стимул? Это преимущество, которое вы как клиент можете получить по одному или нескольким своим заказам на основе определенных критериев или условий. Назвать несколько:

В течение нескольких лет мы решали каждую соединительную линию на графике по мере их поступления в соответствии с требованиями клиентов. У нас уже были хорошие навыки применения набора стимулов для данного заказа. Тем не менее, условная часть оставалась спонтанной.

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

Мы все были там. Мы все видели это в той или иной форме. Но как бы грустно это ни звучало, если учесть достаточно времени и размышлений, обычно есть чистое и, возможно, даже умное решение проблемы. Не бойтесь - решения идут!

Так как же мы решили все эти проблемы, решив только одну? Мы сосредоточились на постановке проблемы. Делая это снова и снова, мы заметили, что все они следовали одному и тому же шаблону: При выполнении условия X примените поощрение Y.

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

Инструмент оказался настолько полезным, что мы извлекли его ядро ​​и начали кодировать различные функциональные исполнения на основе условий уровня приложения / бизнес-домена.

Вот как все началось.

Внутренние требования

  • Настраиваемый - с минимальным вводом мы получаем состояние и работаем
  • Расширяемость - новые Условия могут быть легко реализованы и подключены к платформе
  • Возможность повторного использования - существующее условие можно использовать несколько раз с разными конфигурациями.
  • Составной - Условие может состоять из комбинаций других Условий с использованием логических операторов (and, not, or)

Поскольку условие можно составить с помощью логического оператора, мы создали концепцию предпосылки. По своей сути, Условие строится из одного или нескольких Помещений. Например:

1: condition = And(Premise1, Or(Premise2, Not(Premise3)))

где Premise1, Premise2 и Premise3 - идентификаторы конфигурации Premise. Что-то, что мы можем использовать для получения этого экземпляра конфигурации Premise. Слаг, идентификатор и т. Д.

Чтобы настроить Premises, мы сохранили его как строку «And (Premise1, Or (Premise2, Not (Premise3)))» в качестве поля базы данных в модели Condition, описанной ниже. Где помещения - это поле, содержащее строку, представляющую условие. Вы можете выбрать место, которое лучше всего соответствует вашим потребностям, файл или базу данных.

Переход от буквальной строки к чему-то полезному

Если мы внимательно рассмотрим наше условие: And (Premise1, Or (Premise2, Not (Premise3))), мы увидим, что это не более чем несбалансированное дерево.

Нам нужен способ синтаксического анализа ключевых слов из нашего условия (And, Or и Not), где каждое ключевое слово является нетерминальным узлом в дереве, а идентификаторы помещения - конечными узлами.

К счастью для нас, есть пакет Python, который упрощает решение таких проблем: pyparsing.

Pyparsing предоставляет все строительные блоки для определения вашей грамматики. Он позволяет вам определять, как следует обрабатывать каждый термин в грамматике (например: вызывать функцию каждый раз, когда вы видите конкретный термин, приводить его к целому числу и т. Д.). В нашем случае мы заботимся о наших операторах (внутренних узлах), наших конечных узлах и о том, как должно быть построено каждое выражение.

Краткое из длинных:

  • Операторы: And, Or, Not, Condition
  • Терминальные узлы: Слаг, идентифицирующий помещение (Premise1, Premise2 и т. Д.)
  • Круглые скобки: левая и правая скобки. Мы не заботимся о них, но они являются частью конструкции
  • Аргументы: один или несколько конечных узлов или поддеревьев.
  • Выражение: оператор + lparen + args + rparen

В дальнейшем мы будем называть оператор условия C. C - это то, как мы определяем условие с одной предпосылкой (пример: C (Premise1)). Всякий раз, когда мы видим одну предпосылку, мы оборачиваем ее вокруг конструктора C.

Учитывая то, как мы определили наши термины, анализ нашего примера условия приведет к следующему списку терминов:

['and', <Premise1 Instance>, ',', 'or', <Premise2 Instance>, ',', 'not', <Premise3 Instance>]

Вы можете определить свои рабочие термины так, как считаете нужным, потому что Pyparsing чрезвычайно гибок.

Давайте продвинемся вперед и продолжим работу с логическими операторами и способом сокращения условий Condition до логического выражения:

До сих пор мы создали способ настройки условий, инструмент синтаксического анализа и способ представления нашей конфигурации в реальном коде Python. Соберем эти кусочки вместе. На основе проанализированных терминов из pyparsing нам нужно построить такое же представление с помощью ConditionInterface. Вот как мы это делаем:

Подача проанализированного результата в Grammar Interpreter даст нам то, что нам нужно. Выглядит это примерно так:

ConditionInterface.And(<Premise1 Instance>, ConditionInterface.Or(<Premise2 Instance>, ConditionInterface.Not(<Premise3 Instance>)))

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

Базовый класс типа помещения имеет следующее:

  • Предпосылка (первый аргумент) - это экземпляр конфигурации предпосылки (например, тип или местонахождение клиента), который является всем, что нам нужно для выполнения части «Когда выполняется критерий X» в формулировке проблемы.
  • Порядок (второй аргумент) - это проверка условий заказа для определения возможности применения стимулов. Из заказа у нас есть доступ к покупателю, элементам заказа и всему, что абстрактно представляет «вещь», которую помещения проверяют на предмет того, что им может понадобиться.
  • _get_values_from_premise получает эту информацию и сохраняет ее локально для дальнейшего использования.
  • _is_valid - это то место, где мы собираем все вместе и по значениям конфигурации можем определить, соблюдены критерии или нет.

Конкретный пример этого может выглядеть так:

Если углубиться в Premises, конфигурация Premise - это модель Django, которая выглядит следующим образом:

Все идет нормально. Тем не менее, в этой головоломке все еще есть недостающие части. Начиная с: Как перейти от идентификатора конфигурации помещения к фактическому экземпляру помещения?

Если вы помните, в нашем определении условия: And (Premise1, Or (Premise2, Not (Premise3))), Premise [1 | 2 | 3] были идентификаторами. Нам нужен способ сопоставить эти идентификаторы с реальным экземпляром Premise. Выше мы сказали, что можем указать pyparsing на выполнение функции всякий раз, когда мы видим определенный тип термина. У каждого помещения есть уникальный идентификатор (ярлык), который мы можем использовать. Давайте создадим фабрику для использования с pyparsing, чтобы всякий раз, когда он видит термин-слаг, мог вернуть нам экземпляр Premise. Вот как мы это делаем.

Некоторые части жестко запрограммированы для лучшей читаемости, но мы сохраняем сопоставление Premise.premise_type с реальным классом, который мы можем создать должным образом. Из записи конфигурации Premise у нас есть доступ к его type, и из сопоставления типов у нас есть фактический класс, который мы хотим создать.

И это в основном все. Нам удалось удовлетворить все наши внутренние требования: он настраиваемый, расширяемый, многоразовый и компонуемый.

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

Вот базовый класс:

А «конкретная» реализация выглядит так:

Мы подаем в службу Заказ, который нужно обработать, и соответствующий ему Предмет. Оттуда мы получаем все условия, перебираем их в цикле, затем анализируем их предпосылки, создавая наши логические блоки из разных посылок. Подавая заказ в каждый тип помещения, мы получаем необходимую информацию, чтобы сказать, является ли посылка верной или ложной для данного заказа. После оценки всех предпосылок мы сокращаем условие с его интерфейсом условий до одного логического значения. Если True, мы добавляем эти поощрения за состояние в возвращаемую коллекцию.

В начале, когда мы создавали модель Condition, мы определили поле ManyToMany для стимулов. А поскольку мы уже знали, как применить набор стимулов к заказу и / или товару, остальное уже история.

Заключение

Иногда правильное решение неясно. Иногда правильное решение не появляется, пока вы не повторите один и тот же тип проблемы несколько раз. Это как раз наш случай.

Продолжайте к этому стремиться. Внимательно изучите формулировку проблемы. Обдумайте условия постановки задачи. Посмотрите, есть ли в этих терминах повторяющийся образец. И не бойтесь - решения приходят!