• Как мы написали еще один алгоритм RFM-анализа

      КДПВ Недавно мы рассказали, почему придумали свой RFM-сегментатор, который помогает сделать RFM-анализ за 20 секунд, и показали, как использовать его результаты в маркетинге.


      Теперь рассказываем, как он устроен.


      Читать дальше →
    • RFM-анализ одной кнопкой или как мы облегчили клиентам жизнь

        image
        С тех пор как в компании Mindbox впервые произнесли Machine Learning, общей целью стала Большая Зеленая Кнопка. Это такая кнопка во весь экран, при нажатии на которую всё работает само и приносит прибыль.


        В аналитическом проекте «RFM» цель менее амбициозная — Маленькая зеленая кнопка. Нажимаешь, и база автоматически делится на сегменты, по которым запускается отправка писем (например).




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


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

        Читать дальше →
      • Немного о ретроспективе

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

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

          • на ретро тратили по 2 часа и сильно выматывались
          • обсуждения периодически уходили в затяжные бесполезные споры
          • некоторые мелкие проблемы не успевали решить и постоянно переносили на следующее ретро
          • отдельным членам команды надоедали ретроспективы из-за однообразия

          Первую и вторую проблему мы считали данностью, что делать с третьей не понимали, а о последней я даже не знал.

          Прошлой весной я сходил на Okademy от ScrumTrek. Это обширный курс, включающий в себя много полезного для скрам-мастера, но для меня самым полезным оказалась часть о том, как эффективнее проводить ретроспективы. Хочу рассказать, как это нам помогло.

          image
          Читать дальше →
        • Одних тестов недостаточно, нужна хорошая архитектура

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

            В статье я постарался описать одну из проблем, которую может решить хорошая архитектура: связанные участки кода могут разъезжаться между собой, это может приводить к багам, и тесты тут не спасут. А грамотный дизайн может помочь.
            Читать дальше →
          • Проектирование и разработка шаблонного движка на C# и ANTLR

              Предыстория


              Уже много лет мы помогаем нашим клиентам отправлять потребителям хорошие, информативные и человеческие письма. В них вместо сухого “Добрый день” мы пишем “Здравствуйте, Никита!”, а вместо “Ваш баланс пополнился” сообщаем “вы получили 25 баллов”. Но маркетологи становятся все изобретательнее, и современное письмо от интернет-магазина должно выглядеть так:


              В реальной жизни всего этого на порядок больше в каждом письме


              И мы хотим уметь генерировать такие письма автоматически.

              Читать дальше →
            • Про опыт обучения в OKademy

                Вместо вступления


                Всем привет. Делюсь впечатлениями от обучения в школе скрам-мастеров от ScumTrek, под хабракатом шесть страниц текста моих мыслей и впечатлений по этому поводу. Велкам.

                Читать дальше →
              • Трансляция запросов в SQL с использованием LinqToSql в тестах

                  Мы уже несколько лет делаем наш продукт автоматизации маркетинга, и пилить фичи с высокой скоростью нам помогает CI, а точнее — большое количество автоматических тестов.

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

                  Практически каждая транзакция в нашей системе связана с работой с MS SQL с использованием LinqToSql. Да, технология старенькая, но мигрировать с неё нам довольно сложно, и по бизнесу она нас вполне устраивает. Более того, как я уже писал раньше, у нас даже есть свой форк LinqToSql, где мы чуть-чуть чиним его баги и добавляем кое-какой функциональности.

                  Для того, чтобы делать запросы к БД, используя LinqToSql, нужно использовать интерфейс IQueryable. В момент получения Enumerator’а или выполнения Execute у QueryProvider’а построенное дерево выражений с помощью Extension-методов к IQueryable транслируется в SQL, который и выполняется на SQL Server.

                  Так как наша бизнес-логика сильно завязана на сущностях в базе данных, наши тесты много работают с базой данных. Однако в 95% тестов мы не используем реальную базу, так как это очень дорого по времени, а довольствуемся InMemoryDatabase. Она является частью нашей тестовой инфраструктуры, о которой можно написать отдельную статью, и на самом деле представляет из себя просто Dictionary<Type, List> для каждого существующего типа сущности. В тестах наш UnitOfWork прозрачно работает с такой базой, давая доступ к EnumerableQueryable, который просто получить из любого IEnumerable, вызвав у него AsQueryable().

                  Покажу пример теста для понимания происходящего:
                  Читать дальше →
                • Как оценивать большие задачи

                    Существует множество способов оценить пользовательские истории. Мы используем собственную методологию, чтобы оценить и проработать задачи перед тем, как писать код. Как мы до этого дошли и почему наш подход лучше, чем Planing Poker, читайте под катом.

                    image
                    Читать дальше →
                  • Webpack в Visual Studio для больших солюшенов

                      КПДВ У нас в солюшене 51 проект. В 10 из них используется TypeScript. Объем минимизированного JavaScript-кода ~1 MB. TypeScript-код одних проектов зависит от кода других проектов. Для многих React-компонентов используются глобальные переменные.


                      Все вместе это приводит к долгим часам отладки front-end кода. Чтобы упростить себе жизнь, мы внедрили Webpack. А по пути отловили грабли.


                      TL;DR


                      1. Устанавливаем node 7 + npm
                      2. Выполняем в консоли npm i -g webpack typescript
                      3. Устанавливаем Webpack Task Runner
                      4. Добавляем webpack.config.js
                        в папку "основного" проекта
                      5. Добавляем webpack.config.part.js
                        в папку каждого зависимого проекта
                      Читать дальше →
                    • Как мы перестали бояться тикетов на UI

                        Всем привет.
                        Прошло уже больше года с тех пор, как мы начали использовать ReactJS в разработке. Наконец пришел момент для того, чтобы поделиться тем, насколько счастливее стала наша компания. В статье я собираюсь рассказать о причинах, которые побудили нас использовать эту библиотеку и о том, как мы это делаем.

                        Зачем всё это


                        Мы — маленькая компания, наш штат составляет порядка 50 человек, 20 из которых разработчики. Сейчас у нас 4 команды разработки, в каждой из которых сидит по 5 fullstack разработчика. Но одно дело называть себя fullstack-разработчиком, а другое — действительно хорошо разбираться в тонкостях работы SQL Server'а, ASP.NET, разработке на C#, OOP, DDD, знать HTML, CSS, JS и уметь этим всем разумно пользоваться. Конечно каждый разработчик тяготеет к чему-то своему, но все мы, так или иначе, специалисты именно в разработке на .NET и 90% кода мы пишем на C#.
                        Наш продукт — система автоматизации маркетинга, — подразумевает большой объем настроек для каждого конкретного клиента. Для того, чтобы наши менеджеры могли заниматься настройкой продукта под клиентов, есть административный сайт, в котором можно заводить рассылки, создавать триггеры и другие механики, кастомизировать сервисы и многое другое. Этот административный сайт содержит много различного нетривиального UI'а, и чем более тонкие моменты мы даём настраивать, чем большее количество фич мы выпускаем в продакшн, тем более интересным UI становится.
                        Читать дальше →
                      • 40 ключевых концепций информационных технологий доступно и понятно

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

                        Важные замечания:
                        • Пункты с неуказанным источником написаны мной самостоятельно. Поправьте меня, если вы заметите какие-то неточности. Предложите лучшую аналогию, если это возможно.
                        • Заголовки ссылаются на соответствующие им статьи в Wikipedia. Пожалуйста, читайте эти статьи для более серьезных и детальных объяснений.
                        • Аналогии — отличный способ объяснить материал, но они не идеальны. Если вы хотите по-настоящему понять перечисленные концепции, вам следует начать с фундаментальных азов и рассуждать, исходя из них.

                        Также зацените эту инфографику (вариант на русском), если вы просто начинающий программист.
                        Читать дальше →
                      • Типичные взаимные блокировки в MS SQL и способы борьбы с ними

                          Чаще всего deadlock описывают примерно следующим образом:
                          Процесс 1 блокирует ресурс А.
                          Процесс 2 блокирует ресурс Б.
                          Процесс 1 пытается получить доступ к ресурсу Б.
                          Процесс 2 пытается получить доступ к ресурсу А.
                          В итоге один из процессов должен быть прерван, чтобы другой мог продолжить выполнение.
                          Но это простейший вариант взаимной блокировки, в реальности приходится сталкиваться с более сложными случаями. В этой статье мы расскажем с какими взаимными блокировками в MS SQL нам приходилось встречаться и как мы с ними боремся.


                          Читать дальше →
                        • Распределенные транзакции между RabbitMQ и MS SQL

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

                            Однако, как только у нас появляется отдельная система для хранения очереди сообщений (мы используем RabbitMQ), сразу возникают проблемы с транзакционностью. Например, если мы хотим сохранить в БД отметку о том, что мы отправили сообщение в Rabbit, не так уж и просто гарантировать, чтобы отметка была сохранена только в случае успешной отправки сообщения. О том, как мы справлялись с этой проблемой читайте под катом.
                            Читать дальше →
                          • Слой кэширования поверх Linq to SQL

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

                              Однако, за перенос настроек в базу данных пришлось расплачиваться. Об архитектуре, позволяющий кэшировать редко меняющиеся Linq to SQL сущности, смотрите под катом.
                              image
                              Читать дальше →
                            • Интересные моменты работы LINQ to SQL. Опять

                                С моего предыдущего поста прошёл месяц, по-моему самое время продолжить. В этот раз поговорим об Inheritance Mapping’е, ну а особо интересующихся в конце статьи ждёт сюрприз.

                                Итак, начнём.

                                Проблемы с дискриминатором


                                Разумеется, мы храним в нашей базе данных полиморфные сущности. Например, есть сущность CustomerOperation, которая отражает некоторую операцию, которую можно совершать над потребителем. Операции совершаются в основном через сервисы, поэтому есть наследник CustomerServiceOperation, а так же у нас есть механизм WebTracking’а, для которого есть WebTrackingOperation. Но довольно слов, лучше покажу код:
                                Читать дальше →
                                • +13
                                • 12,2k
                                • 4
                              • Триггерные рассылки

                                  Последнее время в Email-маркетинге все чаще используются автоматические рассылки определенным группам потребителей. Типичные задачи:
                                  • поздравить с днем рожденья
                                  • позвать на сайт, если потребитель на него долго не заходил
                                  • сделать персонализированное предложение (делим потребителей на сегменты и рассылаем каждому сегменту свое письмо)

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

                                  Первые реализации

                                  3 года назад подобные задачи возникали крайне редко и мы каждый раз реализовывали их с нуля. При этом возникали одни и те же вопросы:
                                  1. Как помечать потребителей, которым мы уже отправили это письмо?
                                  2. Как максимально быстро обработать всех потребителей и при этом не тормозить работу сайтов (которые обращаются к тем же записям в БД)?

                                  На первый вопрос ответ для нас был очевиден: в нашей системе сохраняется информации о всех значимых действиях, выполняемых потребителем (вход на сайт, изменение персональных данных) или над ним (розыгрыш приза, отправка уведомления). Кроме того, мы используем действия для разнообразных технических пометок потребителей. Так что при отправке автоматической рассылки, мы также решили выдавать потребителю особое действие-маркер, в качестве пометки, что эта автоматическая рассылка ему уже была отправлена. Чтобы повторно не отправлять рассылку, к условию рассылки всегда добавляется условие “у потребителя нет действия-маркера”.

                                  На втором вопросе мы набили множество шишек, связанных с блокировками в БД, и в итоге пришли к следующему шаблону:
                                  1. Отправка рассылок идет из windows-сервиса, который периодически проверяет не появилось ли новых потребителей, подходящих под условия.
                                  2. В сервисе первым шагом делается один запрос к БД с уровнем изоляции Read Uncommitted. Этот запрос вытаскивает Id всех потребителей, которым надо отправить письмо. Из-за низкого уровня изоляции такой запрос не накладывает блокировок на записи в БД и, как следствие, крайне слабо влияет на работу сайта. Однако он не гарантирует чистоту данных и их надо повторно проверить с более высоким уровнем изоляции.
                                  3. После того, как мы вытащили Id потребителей, для каждого потребителя мы выполняем отдельную транзакцию с уровнем изоляции Serializable. В этой транзакции мы заново проверяем подходит ли потребитель под условия и если да, отправляем ему письмо и выдаем действие-маркер. Так как мы обрабатываем каждого потребителя в отдельной транзакции, блокировки накладываются только на данные одного потребителя и на работу остальных потребителей не влияют. Так как такая транзакция очень короткая, у потребителя, которому отправляют письмо, также не будет особых проблем, если он в это время ходит по сайту. Уровень изоляции транзакции должен быть именно Serializable, чтобы ненароком не отправить одно письмо дважды, или не отправить письмо тому потребителю, который внезапно перестал подходить под условия. Хотя, если мы гарантируем, что отправка одной и той же рассылки может идти только из одного потока и с одного сервера, а также забьем на небольшую вероятность того, что одному потребителю могут отправится две рассылки с взаимоисключающими условиями, то можно использовать и Read Committed транзакцию.

                                  Само собой после реализации нескольких рассылок по этому шаблону, мы решили вынести шаблонный код. Для этого был создан класс BatchMailing, и для каждой новой рассылки мы создавали и регистрировали в специальном реестре его наследника. В наследнике необходимо было перегрузить следующие свойства и методы:
                                  • шаблон действия-маркера (раньше мы называли шаблон типом действия: думаю, для разработчиков это более понятный термин), которое выдается при отправке письма
                                  • метод, отправляющий письмо
                                  • метод, выполняющий дополнительные действия (например, вместе с отправкой поздравления с днем рожденья, мы можем выдавать потребителю баллы на счет)
                                  • метод, который формирует Expression<Func<Customer, bool>>, проверяющий, что потребитель подходит под условие

                                  Свойство и первые два метода никогда никаких проблем не вызывали, но вот составить Expression было довольно таки не просто. Этот Expression использовался два раза — сначала в Read Uncommitted запросе, чтобы вытащить Id потребителей, а затем в Serializable транзакции, чтобы повторно проверить подходит ли потребитель под условие. Его было нужно написать так, чтобы Linq to SQL смог его транслировать в T-SQL. Условия могли быть довольно сложными и в них всегда возникали проблемы. Ни одну рассылку нельзя было завести не написав на нее кучку тестов. Кроме того, для отправки СМС и email мы завели разных промежуточных наследников от BatchMailing. Когда же нам надо было отправить и email и СМС, приходилось копипастить. У меня были идеи как это исправить, но так как автоматические рассылки клиенты просили не так уж и часто, это была низкоприоритетная задача.

                                  Замена наследования композицией

                                  2 года назад при разработке очередной рекламной кампании клиент попросил сделать ему сразу 8 разных автоматических рассылок. При этом частично условия в рассылках повторялись. Тут уже не оставалось сомнений, что больше так жить нельзя, и я взялся за переписывание нашей архитектуры. Для того чтобы справится со всеми описанными выше проблемами достаточно было применить наш любимый прием: замену наследования композицией. Этот прием настолько много раз нам помогал, что я советую использовать композицию вместо наследования везде, где это возможно (ну или как минимум рассматривать такой вариант). Если вы создаете базовый абстрактный класс с мыслью “для каждой конкретной задачи у меня будет наследник перегружающий методы и свойства”, сразу спрашивайте себя “а почему бы мне вместо этого не регистрировать для каждой задачи экземпляр класса, передавая ему разные настройки”. И только если вы уверены, что композиция здесь не подходит, используйте наследование. Если подходит и то и то, всегда склоняйтесь к композиции — так получается гораздо более гибкая и понятная архитектура.

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

                                  Так как кроме отправки рассылок эта сущность теперь может выполнять любые произвольные операции над потребителем, рассылкой ее называть некорректно. Фактически это класс который делает какую-то абстрактную работу над заданной выборкой потребителей. Не придумав ничего лучше, мы стали называть это триггерами (в маркетинге их примерно так и называют, так что название неплохое). Меня, честно говоря, немного пугало то, что я ввел в систему крайне абстрактную сущность, которую можно назвать DoSomeWorkOnSomeCustomers. Но никакого смысла в специализации триггеров не было, так что я решил над этим не заморачиваться, и в принципе больших проблем с пониманием, что такое триггер, у клиентов не возникает.

                                  Регистрация триггера выглядела примерно следующим образом:
                                  Add(new Trigger(“Приглашение на сайт для пришедших через канал one-to-one”)
                                  {
                                  	MarkerActionTemplateSystemName = “InvitationMarker”,
                                  	TriggerAction = new TriggerActionCombination(
                                  		new GeneratePasswordForCustomerTriggerAction(),
                                  		new SendEmailTriggerAction(“InvitationMailing”)),
                                  	TriggerCondition = new AndTriggerConditionSet(
                                  		new CustomerHasSubscripionCondition(),
                                  		new CustomerHasEmailTriggerCondition(),
                                  		new CustomerHadFirstActionOverChannelCondition(“OneToOne”)),
                                  });
                                  

                                  Интерфейс TriggerAction’а крайне прост:
                                  public interface ITriggerAction
                                  {
                                  	void Execute(
                                  		ModelContext modelContext, // класс для работы с БД 
                                  		Customer customer);
                                  }
                                  

                                  Базовый класс для условий триггера выглядит следующим образом:
                                  public class TriggerCondition
                                  {
                                  	private readonly Func<ModelContext, Expression<Func<Customer, bool>>> triggerExpressionBuilder;
                                  
                                  	public TriggerCondition(Func<ModelContext, Expression<Func<Customer, bool>>> triggerExpressionBuilder)
                                  	{
                                  		if (triggerExpressionBuilder == null)
                                  			throw new ArgumentNullException("triggerExpressionBuilder");
                                  
                                  		this.triggerExpressionBuilder = triggerExpressionBuilder;
                                  	}
                                  
                                  	public Expression<Func<Customer, bool>> GetExpression(ModelContext modelContext)
                                  	{
                                  		return triggerExpressionBuilder(modelContext, brand);
                                  	}
                                  
                                  	// Используется в Read Uncommitted транзакции для получения спиcка Id потребителей, подходящих под условие
                                  	public IQueryable<Customer> ChooseCustomers(ModelContext modelContext, IQueryable<Customer> customers)
                                  	{
                                  		if (modelContext == null)
                                  			throw new ArgumentNullException("modelContext");
                                  		if (customers == null)
                                  			throw new ArgumentNullException("customers");
                                  
                                  		var expression = GetExpression(modelContext);
                                  		return customers.Where(expression).ExpandExpressions();
                                  	}
                                  
                                  	// Используется в Serializable транзакции, для проверки, что потребитель все еще подходит под условие
                                  	public bool ShouldTrigger(ModelContext modelContext, Customer customer)
                                  	{
                                  		if (modelContext == null)
                                  			throw new ArgumentNullException("modelContext");
                                  		if (customer == null)
                                  			throw new ArgumentNullException("customer");
                                  
                                  		var expression = GetExpression(modelContext);
                                  		// Можно бы было просто вызывать expression.Evaluate(customer),
                                  		// но тогда для сложных условий выполнилось бы несколько запросов в БД вместо одного
                                  		return modelContext.Repositories.Get<CustomerRepository>().Items
                                  			.Where(aCustomer => aCustomer == customer)
                                  			.Where(aCustomer => expression.Evaluate(aCustomer))
                                  			.ExpandExpressions()
                                  			.Any();
                                  	}
                                  }
                                  
                                  Для часто используемых условий мы создавали наследников от TriggerCondition, в которых строился конкретный Expression в зависимости от переданных в конструктор параметров.

                                  Все, надоело, сами заводите свои триггеры

                                  С использованием архитектуры, описанной выше, мы заводили триггер менее чем за пол часа, за счет комбинирования уже написанных условий и TriggerAction’ов. Однако и этого нам было мало. Следующим шагом мы захотели полностью исключить разработчиков из процесса заведения триггеров. Причем как это делать в общих чертах я понял уже через пару месяцев после реализации предыдущей версии архитектуры. Условия триггеров были один в один похожи на фильтры, которые мы используем в админке. Наша система фильтров позволяет описывать сложные условия, включая запросы к связанным сущностям, а также позволяет комбинировать их через И/ИЛИ. Фильтр формирует Expression, с помощью которого уже можно отфильтровывать сущности в БД. И для всего этого уже был написан UI и сериализация. Оставалось лишь добавить пару фильтров, которые часто нужны для триггеров, но не имели смысла при обычной работе со списком потребителей (например: “с действия прошло N дней”). Для TriggerAction’ов надо было написать UI и структуру для хранения их в БД, но тут тоже в общем все было понятно. Однако оставались еще небольшие вопросы, над которым пришлось поломать голову:
                                  • отсылку любого письма мы к этому времени стали регистрировать как действие, и действие-маркер стало лишним — мы и так могли определить, кому мы отправляли письмо, и вообще хотелось бы избавиться от выдачи лишних действий везде, где это было возможно
                                  • кроме простых триггеров, которые выполняли определенный набор операций один раз над каждым потребителем, у нас появились периодические триггеры. Надо было придумать как это все перенести в БД и при этом позволить использовать произвольные маркеры
                                  • маркетологи придумывают триггеры не отдельно друг от друга, а в качестве цепочек, в которых есть как триггеры так и операции, выполняемые потребителем на сайте (письмо с предложением зайти на сайт и что-то сделать → потребитель выполняет несколько операций на сайте → начисляются бонусные баллы и посылается письмо об этом). Хотелось бы если и не реализовать это сразу, то оставить задел на будущее, чтобы было не сложно описывать зависимости между триггерами и операциями
                                  Все эти три проблемы связаны с тем, как мы определяем выполнился ли триггер над потребителем или нет. Если заводить для каждого триггера и операции на сайте свой маркер, задача сильно упрощается, но плодить лишние действия в системе очень не хотелось. Была даже идея заставлять менеджеров составлять фильтр таким образом, чтобы он полностью отвечал за то, можно ли сейчас выполнить действие над потребителем (и соответственно частота повторения триггера описывалась бы условием в фильтре), однако данный подход слишком уж располагает к ошибкам. После долгих мучительных размышлений мне все-таки пришла идея, как отслеживать выполнение триггеров без дополнительных сущностей и без усложнения работы менеджера.

                                  Нужно больше Expression’ов

                                  Так как триггер выполняет абстрактный шаг операции (бывший TriggerAction) над потребителем, причем почти всегда этот шаг операции уникален (например, определенное письмо отсылается или определенный приз выдается только из этого триггера), то в этот шаг можно вынести логику проверяющую выполнился ли он. Так как в триггере может быть несколько шагов операции, то менеджеру надо будет выбрать какой из них является маркером (проверять выполнение каждого шага не имеет смысла). Однако просто, реализовать в шаге операции метод, возвращающий Expression<Func<Customer,bool>> нельзя, так как пришлось бы в каждом шаге операции формировать один Expression для одноразовых триггеров, другой для периодических. Тут нас спасает то, что практически любая операция над пользователем в нашей системе выдает ему действие. Соответственно шаг операции может отфильтровать те действия, которые были выдан им. Большинство шагов операции выдают конкретное действие и для них метод, формирующий Expression для фильтрации действий, выглядит вот так:
                                  public sealed override Expression<Func<CustomerAction, bool>> GetIsMarkerExpression(ModelContext modelContext)
                                  {
                                  	return action => action.ActionTemplateId == ActionTemplateId;
                                  }
                                  

                                  Но, например, у шага, выдающего приз, он выглядит следующим образом:
                                  public override Expression<Func<CustomerAction, bool>> GetIsMarkerExpression(ModelContext modelContext)
                                  {
                                  	IQueryable
                                  Читать дальше →
                                  • +11
                                  • 8,2k
                                  • 5

                                Самое читаемое