Организация бизнес-логики корпоративных приложений. Какие возможны варианты?

Оригинал статьи находится по адресу

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

Три типовых решения при работе с бизнес-логикой по Фаулеру

С одной стороны сложно писать об организации бизнес-логики в приложении. Получается очень абстрактная статья. Благо есть книги, где затронута эта тема и даже есть примеры кода. Мартин Фаулер в книге "Шаблоны корпоративных приложений" выделял три основных типовых решения. Сценарий транзакции (Transaction Script), модуль таблицы (Table Module) и модель предметной области (Domain Model). Самый элементарный из них - это сценарий транзакции. Не будем их здесь обсуждать подробно - они очень хорошо описаны в первоисточнике с примерами. Приведем для дальнейших рассуждений лишь схему все из той же книги:

На этом графике показана _приблизительная_ зависимость между сложностью доменной логики и стоимостью реализации для трех видов типовых решений. Сразу бросается в глаза очевидная схожесть между тем, как ведет себя сценарий транзакции и модуль таблицы. И совсем особняком стоит модель предметной области, которую применяют для сложной бизнес-логики. Как выбирать решение для вашего проекта? Очень просто, вы оцениваете, насколько сложная будет бизнес-логика. Например, расчет скидочной программы для клиентов. Какое типовое решение выбрать? Обычно при расчете скидок пользователи могут быть в разных категориях скидочной программы, в зависимости от категории можно получать скидки на разные группы товаров, причем товары могут входить в иерархические группы. Категории скидок распространяются на категории товаров. А еще есть число посещений заведения за заданный период. Периоды с разными характеристиками, заведения в сети заведений - различаются и т.д. и т.п. Если представить код - это приложение, в котором большое число классов с разнообразными свойствами и большое число связей между этими классами. А также разнообразные стратегии, которые оперируют всеми этими сущностями. В таком случае ответ очевиден - проектирование с использованием модели предметной области позволит вам совладать со всеми сложностями. Другой пример - у вас простое приложение, которое хранит свои данные в 3-х таблицах и никаких особенных операций с ними не делает. Рассылка сообщений по почте - список почтовых ящиков и список отправленных писем. Здесь нет смысла тащить какой-то сложный фреймворк. Простое приложение должно оставаться простым, и тут лучше выбрать модуль таблицы или даже сценарий транзакции. В зависимости от того на какой платформе вы собираетесь разрабатывать.

Сколько типовых решений на самом деле?

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

Понятно, что речь не идет о чистой реализации той или иной парадигмы. Всегда есть компромиссы, вызванные ограничениями используемых технологий. Если вы пишете на C# или Java, объектно-ориентированных языках по своей природе, то это совсем не значит, что ваш код автоматически становится объектно-ориентированным. Всю программу вполне могут поместить в один единственный класс, объявить все методы статическими и использовать их из любого фрагмента кода. Таким образом, в каждом конкретном приложении будет своя кривая. Нужно лишь понять, к какой из этих двух категорий она ближе.

Как влияют фреймворки и инструменты разработки на кривую стоимости?

Сразу обозначим, что в качестве хранилища данных может выступать не только реляционная СУБД, но и NoSQL хранилище, NewSQL и даже обычные файлы, сериализованные в json, бинарный формат и т.п. Мы смотрим на ситуацию в комплексе. Если говорить про работу с обычными SQL хранилищами, здесь также огромный выбор. Вы можете писать простые запросы, писать хранимые процедуры, можете использовать ORM, использовать Code First, либо DB First подходы - все это в конечном счете сказывается на стиле, в котором написана бизнес логика. В большей степени процедурном, либо в большей степени объектно-ориентированном. Ниже я примерно обозначил свое мнение о популярных схемах работы с БД.

Проблема в том, что используемые средства накладывают ограничения, которые не позволят вам реализовать тот или иной подход в полной мере. Например, с помощью Dapper не удобно работать со сложными ассоциациями внутри доменных сущностей. А при использовании ORM уровня Entity Framework вы добавляете код для отображения сущностей на таблицы. Если говорить о NoSQL СУБД, для примера Neo4j, то там очень выразительный и мощный для своих задач язык. Но опять же это приведет к использованию процедурной парадигмы.

Насколько легко сменить выбранное решение?

Давайте попробуем представить ситуацию, где мы решили кардинально изменить схему работы с хранилищем данных. С чем мы можем в таком случае столкнуться? До этого мы обсудили два важнейших аспекта - сложность кода и стоимость его сопровождения. Но на практике этого оказывается мало. Есть еще как минимум вопрос производительности - создаваемое приложение должно быть быстрым. И это сказывается на стиле написания кода. Чем жестче требования производительности, тем более процедурный код мы получаем на выходе. В каком-то экстремальном случае это может быть сервис или приложение, написанное с использованием полностью SQL, где вся логика скрыта в хитрообразных джойнах, оконных функция и обобщенных табличных выражениях. Работает быстро, но перевести его на ORM уже не так просто - все равно, что переписать с нуля. Развитие такого продукта также может столкнуться с сложностями, учитывая график выше и процедурный стиль. Еще один вопрос - консистентность данных. Например, реляционные СУБД предоставляют очень богатые возможности по работе с транзакциями. Разобравшись с ними один раз - можно легко писать код, где вы точно знаете, какие данные увидит пользователь, какие сможет изменить. С другой стороны, если вы пользуетесь ORM и выносите всю вашу бизнес-логику в классы, работать в терминах транзакций становится сложнее. Обычно происходит реорганизация структуры таблиц и даже бизнес-сценариев таким образом, что они начинают работать в стиле согласованности данных в конечном счете (eventual consistency). Очевидно, что это также затрудняет перевод с одной схемы на другую, если вы заранее не заложили такую возможность. Компетенцию команды также не следует сбрасывать со счетов. Часто разработчики знают хорошо либо SQL, либо ORM, и при переходе можно неожиданно столкнуться с проблемами.

Выводы

Из всего, что мы обсудили, можно сделать выводы:

  • При проектировании сервиса с нуля лучше сразу прогнозировать дальнейшее развитие этого сервиса и выбирать наиболее подходящее типовое решение.

  • Если дальнейшее развитие для сервиса не очевидно, то лучше сразу позаботиться о возможной смене парадигмы. Например, предпочитая eventual consistency. При добавлении нового функционала всегда держать в уме возможность смены решения.

  • Для доставшегося в "наследство" программного кода одно из первых действий - это оценка степени соответствия выбранного типового решения и объема уже реализованной логики. Как показывает практика - это наиболее частая причина технического долга.

Какие еще выводы вы могли бы предложить? Оставляйте ваши комментарии, давайте обсудим.

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 7 231 анкеты, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 22

    0
    Разработчикам платформы 1С бы показать график 2.
      0
      Тут стоило бы определиться что понимается под объектно-ориентированной парадигмой? Потому как она состоит из де-факто двух перпендикулярных вещей (формально трех, но первые две друг без друга бессмысленны): наследования / полиморфизма и инкапсуляции.
      Впрочем обе эти вещи в свою очередь перпендикулярны разрыву между процедурным (императивным, value-level, Java, C#, Python) и комбинаторным (декларативным, function-level, SQL) программированием (тут в разделе Немного теории про этот разрыв подробнее). И у вас отлично может использоваться объектно-ориентированная парадигма вместе с комбинаторной (аля SQL), как это сделано в lsFusion, впрочем как и с процедурной.

      Так что противопоставлять объектно-ориентированную парадигму с процедурной это как-то глупо на мой взгляд. Правильнее говорить о противопоставлении процедурной (C, Java) и комбинаторной парадигм (SQL).
        0
        Хотелось изложить свои мысли максимально локанично. Я даже пример кода не стал внедрять, а сослался на код из книги Мартина Фаулера. И да, рекомендовал бы всем освежить в памяти главу 2, прежде чем читать… ИМХО так удобнее. :) По поводу обьектно-ориентированной парадигмы, думаю, все в курсе. Банально — его придумали для того чтобы бороться со сложностью кода более эффективно, чем это делает процедурный подход. Ну вот вспомните был С, появился С++. С какой целью? Сложность кода увеличивалась…

        По поводу смешения процедурного и ООП на проекте. Да, это постоянно происходит. Мне просто хотелось бы рассказать про мое понимание влияния соотношения. Т.е. у вас бизнес-логика более процедурная, либо более ООПшная… со всеми вытикающими. И опять же это прямо очень хорошо видно на примере того кода из книги Фаулера.
          0
          Т.е. у вас бизнес-логика более процедурная, либо более ООПшная

          Нет. Это перпендикулярные понятия. Ну или у вас свое понимание процедурности.

          То есть У вас вполне может быть логика очень активно использующая и наследование и полиморфизм и даже инкапсуляцию, при этом все это делать в комбинаторной парадигме (то есть высокоуровневых операторах аля SELECT… GROUP BY ...), а не процедурной (if'ах, циклах, переменных и вот этом вот всем). Например так в lsFusion делается.
            0
            Хм… Такое ощущение, что вы говорите больше про структурное программирование. А это не равно процедурному. Чтож, в любом случае нужно максимально разделить бизнес-правила от логики хранения. Потому что бизнес-правила и безнес-логика и без того очень сложные и если их нагружать техническими деталями хранения (джойны, конвертации типов, условия фильтрации и обобщенные табличные выражения), то читать такой код сложнее. ООП в любом случае борется с такой сложностью очень хорошо… тут уж вне зависимости какой смысл вы вкладываете в понятие процедурного подходо. В идеале вы пишете логику на чистом C#, Java любом другом ООП языке без каких-то лишних вещей. И сериализуете это все в json. Вот в таком случае логика будет в чистом виде, без технических деталей… Но. Производительность. Поэтому и идут на компромисы.
              0
              Такое ощущение, что вы говорите больше про структурное программирование. А это не равно процедурному

              Так что вы тогда под процедурным понимаете? Абстрагирование? Так оно и в SQL есть, представления называется. И опять таки перпендикулярно во многом ООП. Хотя согласен, наверное термин структурное правильнее в данном случае.
              Чтож, в любом случае нужно максимально разделить бизнес-правила от логики хранения. Потому что бизнес-правила и безнес-логика и без того очень сложные и если их нагружать техническими деталями хранения (джойны, конвертации типов, условия фильтрации и обобщенные табличные выражения), то читать такой код сложнее

              Джойны, условия фильтрации содержат бизнес-логику (то есть essential complexity). Другое дело, что да SQL почему-то до сих пор завис на уровне таблиц, не поднявшись на уровень функций (почитайте ссылку выше, что я кидал) и поэтому привносит большую accidental complexity. Но в императивных языках с этим не сильно лучше.
              ООП в любом случае борется с такой сложностью очень хорошо

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

              Нет, как раз C#, Java очень императивные языки и там accidental complexity не сильно меньше чем в SQL. Если уж хотите уйти от императивности и «технических деталей» нужно идти в что-то куда более декларативное (аля того же lsFusion).
                0
                Очень много идей в комментарии… на которые хотелось бы ответить :) И я ранее планировал в отдельной статье с примерами на все ответить. :) Если меня не забанят на данно ресурсе, конечно.
                Но вот про джойны и условия фильтрации хотелось бы ответись тут. Содержат ли они бизнес-логику? И да и нет.
                С одной стороны если у нас есть сущность пользвателя и его адрес. Джой таблиц пользователя и адреса — да, содержит. Простая ассоциация.
                Но, если у нас наследование? И мы из базового класса должны вытащить набор полей… тут мы опять же прибегаем к джойну, но это чистые технические детали. Как проверить? Вы говорите человеку далекому от техники (продук оунер, владелец, иногда аналитик) и он воспринимает это как что-то про реализацию, если вообще понимает о чем речь.
                Далее давайте просто на синтаксис посмотрим. С одной стороны мы получаем user.Address с другой как минимум две строчки кода from user u… join adress a on u.address_id = a.id И еще про схему не забываем (тоже чистейшай техника). Очевидно, что на любом ООП языке код локаничение и можно проще сосредоточиться на самих правилах, чем на ньюансах хранения.
                Все это приводит к мысли — выгрести в оперативку все таблицы, замепить их и условия фильтраци, ассоцииации — все делать в коде. Но даже любой джун вам скажет, что работать такое не будет, вот и получаются хранимые процедуры, методы репозиториев с SQL — по сути одно и то же… И да, они содержат там бизнес-логику. И да, за это приходится платить…
                  0
                  Не понял примера вашего с join'ом. Join это чистая композиция (например в lsFusion):
                  habr.com/ru/company/lsfusion/blog/458376/#rest
                  Да в структурном программировании композиция проще и понятнее (так как оперирует функциями, а не таблицами), чем join. Но скажем цикл, разбиение или примитивная рекурсия в комбинаторном (SQL) проще.

                  В любом случае непонятно какое отношении это к ООП имеет. ООП это прежде всего про наследование и полиморфизм (ООП может быть как в структурном, так и в комбинаторном программировании), вы же рассуждаете про то, что SQL оперирует таблицами, а не функциями. Да это создает дополнительную accidental complexity, но опять-таки причем тут ООП?
                    0
                    Но, если у нас наследование? И мы из базового класса должны вытащить набор полей

                    А причем тут наследование? Из базового класса вытащить набор полей это про инкапсуляцию скорее.

                    И причем тут хранимые процедуры? Это такая же императивщина как и обычные структурные языки, только что данные между сервером приложений и сервером БД чуть меньше гуляют. Но там такая же N+1 проблема есть, как и у Java, C#, Python.
                      0
                      Например, есть иерархия сущностей: пользователь — базовый класс, оператор — наследник, диспетчер — наследник. Одна из стратегий хранения таких иерархий в реляционных СУБД — это по таблице не класс. Табличка для пользователя, табличка для оператора, табличка для диспетчера. И вот у вас есть некотороая бизнес логика по вычислению «должности с ФИО». Для пример предположим что ФИО в базовом классе, должность в наследнике как поля (только для примера). Вот как эта логика будет выглядеть в коде? Там будет джойн и вот он чисто технический.
                        0
                        Ну в lsFusion например это прозрачно. Вы задаете логику классов / наследования, свойств (полей) для классов, при этом вообще не парясь про таблицы. А дальше платформа сама уже решает (ну или администратор), все будет либо в таблице пользователь лежать и один join, либо все в разных и тогда несколько.

                        Тут скорее вопрос, что в современном SQL нормально наследование / полиморфизм не поддерживаются. Но это проблема не ООП парадигмы, а конкретных SQL реализаций.
                          0
                          Про lsFusion до этого не знал. Обязательно посмотрю, интересно…
            –1

            Там про другую бизнес-логику. В заметке по ссылке (спасибо за прекрасную заметку) — про бизнес-логику с точки зрения бизнес-аналитиков — декларативное описание системы или её части. В текущей заметке — про бизнес-логику в смысле реализованного разработчиками императивного набора команд для обработки запроса/данных. Одно напоминает другое меньше, чем известный демотиватор с деревом и качелями.

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

            Кроме того, надо предусматривать, чтобы это решение, позволяло добавлять логику из «разных источников» (плагины, расширения и т.п.). Тогда, описание этой логики в «едином месте», должно проверяться на непротиворечивость, с возможностью указания разрешения конфликтов в этой логике (на конкретной сборке).
              0
              Приведем для дальнейших рассуждений лишь схему все из той же книги:
              image

              Ужасно я люблю такие схемы, ничего не могу с собой поделать. Для тех, кому интересно, она расположена на странице 29 (для издания Addison Wesley, 2010). К ней там есть интересное примечание:


              Figure 2.4 is one of those nonscientific graphs that really irritate me because thy have utterly unqualified aces.

              И, что важно, для этой схемы нет никакого обоснования, источника данных — ничего. Проще говоря, Фаулер ее придумал, а вы ему верите просто на слово. Но строите на этой вере дальнейшие рассуждения.


              Нужно лишь понять к какой из этих двух категорий она ближе.

              … а почему вы считаете, что категорий всего две? Почему вы не рассматриваете, например, функциональное программирование?

                0
                Нет, я не просто верю Фаулеру. Эти соображения полезны на практике. И в данном случае мой опыт совпадает с опытом Фаулера — на практике все подтвердилось. Это раз. Второе — на самом деле все можно формализовать и реально подсчитать сложность и число действий. Сложность бизнес-логики сводится к числу доменных сущностей, числу связей между этими сущностямии сложности алгоритмов, необходимых для реализации логики. Теоретически такой инструмент сделать можно. Но на практике его нет. Писать его в одиночку? Ну это год или больше, я даже не берусь оценить…
                Поэтому и остается лишь одно — делиться знаниеми и идеями только так.
                Функциональное программирование — да, формально не рассмотрено. Это опять же тема на будущее… Но опять же, при написании кода с помощью ООП языков сейчас, как правило, используют элементы и идеи функционального подхода. Стараются делать side effect free функции, моанада maybe, цепочки вызовов, делегаты и т.п. Т.е. формально это не чистое ООП.
                И хоть у меня на текущий момент не нашлось места для функциональных языков на графике. Мне думается, что мысль, изложенная в статье полезна для анализа кода легаси проектов. И для формирования стратегии по развитии новых сервисов (т.е. как мы будем в случае чего отходить от модуля таблицы к модели предметной области — задаваться таким вопросом имеет смысл)
                  0
                  И в данном случае мой опыт совпадает с опытом Фаулера — на практике все подтвердилось.

                  Confirmation bias?


                  Второе — на самом деле все можно формализовать и реально подсчитать сложность и число действий.

                  Будет ли подсчитанная метрика коррелировать с реальной сложностью внесения изменений? Как это проверить?


                  Поэтому и остается лишь одно — делиться знаниеми и идеями только так.

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


                  Но опять же при написании кода с помощью ООП языков сейчас как правило используют элементы и идеи функционального подхода. Стараются делать side effect free функции, моанада maybe, цепочки вызовов, делегаты и т.п. Т.е. формально это не чистое ООП.

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


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


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

                  Имеет смысл задаваться вопросом "как мы будем реализовывать бизнес-логику", и "как мы будем переходить от существующей реализации к другой, если существующая перестанет удовлетворять". А ориентироваться на Очень Общие Слова вида "объектно-ориентированный стиль" и "модель предметной области" — не стоит, потому что это оказывается неконструктивно.

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

                      В том-то и дело, что руководствоваться на практике непроверенной гипотезой — плохо (надо пояснять, почему?).


                      Мне, кстати, интересно, а как конкретно формулируется ваша гипотеза? Потому что текста в посте много, а одной короткой формулировки — нет.


                      Мне подобного рода рассуждения не попадались

                      Гм. Мне казалось, рассуждений "как организовывать бизнес-логику" и, в частности, "как рефакторить код" — много. У Фаулера две книги (ровно по этим направлениям), у Эванса (DDD), у Мартина (Clean Architecture), Симан регулярно пишет про то, как он организует архитектуру с помощью функционального программирования, ну и так далее.


                      Или какие рассуждения вам не попадались?

                    0
                    Второе — на самом деле все можно формализовать и реально подсчитать сложность и число действий.

                    Я бы на это с удовольствием посмотрел. Кажется получится очередная вариация Колмогоровской сложности без какого-либо приложения к программированию.

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

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