Presentation Logic vs Application Logic vs Domain Logic

Просто о NET | создано: 11.06.2024 | опубликовано: 14.06.2024 | обновлено: 26.06.2024 | просмотров: 307 | всего комментариев: 0

Разработчики и иже с ними часто упоминают термин "логика" и/или "бизнес-логика", а что это такое? Где находится логика? В каком слое находится бизнес-логика? Как определить в тексте программы тип логики? Попробую ответить на эти и подобные этим вопросы.

Виды логики приложения

Для начала стоит определиться с тем, что логика делится на frontend и backend уровни. Причем логика, которая находится на frontend имеет четкие рамки и задачи. Задача frontend логики управлять UI, то есть контролами и представлениями, которые "показывают" данные полученные от backend, а также "собирают" данные от пользователя для отправки их на backend.

Про рамки логики на frontend наверное и говорить не стоит, потому что итак понятно, что за пределы frontend данная логика не может и не должна "просачиваться". Тут всё понятно.

Логика управления формами и контролами - это frontend-логика. Она ни коим образом не является бизнес-логикой, особенно с точки зрения Domain Driven Design.

Если frontend-логика, которая кстати, также может иметь слои и достаточно сложные механизмы взаимодействия между этими слоями, не имеет отношения к Domain Driven Design, то мы не будем на ней останавливаться в этот раз. Давайте поговорим про backend-логику.

Backend Logic

Для начала я бы хотел выделить три вида логики, которые я вижу на backend:

  • Логика представления (Presentation logic)
  • Логика приложения (Application logic)
  • Бизнес-логика (Domain logic)

Теперь передо мной стоят следующие вопросы:

  • Как найти в коде эти типы логик?
  • Как отличить одну логику от другой?
  • Какие правила существуют для определения места, где классы, которые реализуют эту логику должны размещаться (например, в какой папке)?

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

Presentation Logic vs Application Logic vs Domain Logic

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

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

Presentation Logic

Это Controllers, Pages, Endpoints и т.д. и т.п., другими словами, это то, что непосредственно взаимодействует с клиентом (пользователь или клиентское приложение). Этот тип логики отвечает за валидацию данных, за трансформацию Entity в DTO, за протокол взаимодействия с клиентом и т.д. и т.п. Другими словами, делает всё, чтобы клиент получил данные в "красивом" виде и при этом, не "подсунул в обратку" данные, которые не соответствуют требованиям backend.

Задачи логики представления (presentation logic) отвечают на вопросы:

  • В каком виде и/или формате "отдать" данные на клиента? (JSON vs XML)
  • Валидны ли данные полученные от клиента? (validation)
  • Имеет ли клиент на получение и сохранение данных соответствующие разрешения? (authorization & authentication)

Application Logic

Это Services, Providers, Managers и т.д. и т.п., то есть то, что "знает", где взять данные для определенной сущности, с определенными критериями в какой Data Access Layer (DAL) направить запрос, чтобы получить требуемые данные. Обычно данный уровень сводят как раз к понятию DAL, но это не совсем верно, потому что понятие немного шире, чем только доспуп к БД.

Задачи логики приложения (application logic) отвечают на вопросы:

  • Как подключиться к БД? (EntityFrameworkCore, Microsoft SQL Client, PostgreSQL, MongoDb, ConnectionString и т.д. и т.п.)
  • Где и как взять объект(ы) из БД? Куда положить данные? (CRUD операции)
  • Как трансформировать или проецировать данные полученные от логики представления в DAL-объекты (mapping)?

Domain Logic

Это модели данных, то есть Model, Entity, а также другие сущности, управляющие вашей бизнес-логикой. Напоминаю, что в Domain Driven Design понятие бизнес-логика - одно из самых основополагающих понятий, вокруг которого "крутится" вся теория.

Задачи бизнес-логики (domain logic) отвечают на вопросы:

  • Как бизенс-логика трансформирует данные для логики представления (presentation logic)? (domain rules)
  • Как бизенс-логика трансформирует данные для логики приложения (application logic)? (domain rules)
  • Валидны ли данные полученные от логики приложения (application logic) для обеспечения правил бизнес-логики? (validation)
  • Какие процессы и как нужно запустить, чтобы соблюсти зависимости и консистентность данных (domain events)

Если с Presentation Logic и Application Logic более или менее всё понятно, тогда назревает вопрос "что за логика должна быть в Domain Logic"? Давайте на конкретном примере, с конкретным методом, конкретного кода. То есть, поговорим конкретно. :)

Логика приложения

Это пример неправильного размещения логики. В данном случае в Controller "запихано" всё, что можно было "запихать". Довольно часто встречаю подобные реализации. И, кстати, я настоятельно НЕ рекомендую вам так делать.

В контроллере (1) есть метод (2). Что делает этот метод (2)? Получает данные о пользователе User (3). Делает проверку на максимально допустимое количество заказов (4). Сохраняет данные о новом заказе (5). Правильно было бы вынести из контроллера в отдельный сервис все описанные действия. При этом в контроллере оставить вызов одного метода из этого сервиса. Кстати, это может быть и не сервис, а например, запрос через Mediatr или какой-нибудь другой класс абстракции более высокого уровня.

Давайте вынесем данный функционал из Controller в Service. Пусть данном случае для простоты будет именно сервис.

Новый сервис (1) также имеет метод (2) для размещения заказа, но в самом методе мы повторили действия которые были в контроллере. А это не совсем хорошо, потому что действия 3, 4 и 5 разные по своей сути имеют принадлежность к разным уровням логики. 

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

  • 3 - это получение данных о пользователе, что является Application Logic, потому что "Где и как взять объект(ы) из БД? Куда положить данные? (CRUD операции)".
  • 4 - это проверка на возможность размещение заказа, что уже точно Domain Logic, потому что "Валидны ли данные полученные от логики приложения (application logic) для обеспечения правил бизнес-логики? (validation)".
  • 5 - это размещение заказа, а это уже снова Application Logic, потому что "Где и как взять объект(ы) из БД? Куда положить данные? (CRUD операции)".

Исходя и того, что 3 и 5 находятся на "своём месте", то есть в логике сервиса, что определяет место для Application Logic, осталось определить, куда нужно поместить действие 4.

Ответ простой, конечно же в Domain Logic, потому что это и есть бизнес-логика. А что для этого нужно сделать? Нужно провести рефакторинг.

Обновим сущность ApplicationUser

Вынесем логику по определению возможности опубликовать новый заказ в сущность ApplicationUser. После рефакторинга класс выглядит так.

Мы добавили новый метод CanPlaceOrder (1) для определение возможности проверки. Честно говоря, этот метод (1) можно было и не делать, но заглядывая в будущее, он потребуется, потому что множество правил бизнес-логики должны иметь возможность оперировать более простыми (низкоуровневыми) проверками. Далее мы добавили метод AddOrder (2), которые используя проверку CanPlaceOrder добавляет объект в список заказов.

Если нам потребуется "выстрелить" событие домена (Domain event), которое скажет всем нуждающимся в этой информации о том, что событие произошло, то это можно и нужно будет сделать именно из методе AddOrder (2). "Выстреливание" заключается в создание объекта события в списке событий DomainEvents. Другими словами, событие домена "складывается" в список событий для сущности домена в свойство List<DomainEvent>. Списка DomainEvents нет в нашем примере, но если потребуется, его можно будет создать. Дальнейшая обратка каждого из списков у всех сущностей домена, для которых созданы DomainEvents осуществляется за пределами данного примера. Об этом в другой раз.

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

В методе сервиса метод (1) теперь просто вызывает метод от сущности домена (2).

Получилось всё именно так, как было задумано. Все уровни логики разделены и находятся в своих папках. Какие "плюшки" мы получили в результате написания дополнительного кода? Давайте их озвучим:

  • При необходимости разделить приложение на проекты, каждый из уровней логики можно без каких-либо проблем разложить на подпроекты. А это значит, что их можно будет разрабатывать разделать, доставлять до конечного потребителя независимо. Разделение на подпроекты должным образом дисциплинирует разработчиков, запретив менять направление зависимостей, чтобы не "нарваться" на рекурсию зависмостей в проектах.
  • Объекты бизнес-логики (Domain logic) использовать в других проектах, на других платформах.
  • Объекты уровня приложения (Application logic) могут послужить основой для других клиентов, в том числе на других платформах. Кстати, если использовать абстракции типа Mediatr, то есть полностью абстрагироваться от платформы ASP.NET Core, то можно создавать клиенты для других платформ (mobile, IoT, UWP, etc) гораздо быстрее.

Кстати, а что же в итоге? Где-то я уже видел подобные "круглишки". Ничего не напоминает реализованные направления зависимостей? Может быть это всё не просто так? :)

Заключение

В качестве заключения хочу сказать, что не всегда так просто и так чётко можно разделить логику на определенные типы. В данной статье в примере был использован Rich Domain Model подход. Но часто используется и другой подход - Anemic Domain Model. Сложности возникают при использовании Anemic Domain Model, когда управлением объектами моделей (Domain Model) используются сторонние классы. В любом случае, вам предстоить решать подобные задачи очень тщательно, чтобы максимально соблюдать концепцию Clean Architecture, которая позволит приблизить свой проект подходу Domain Driven Design. 

Также надо помнить, что Domain Driven Design - это про "долгосрочную перспективу" развития и "масштабирование" вашего приложения. Другими словами, "плюсами" можно будет воспользоваться в будущем. В настоящем же, только "минусы", потому что кода придется написать намного больше, чем предполагалось изначально. Но именно эти "плюсы" позволят вам масштабировать ваш проект долго и из без каких-либо глобальных проблем. Domain Driven Design своего рода гарантия того, что вы сможете внести нужные измения, правки, доработки и новые features в ваш проект и при это не упереться в "ведро костылей приправленных говнокодом".

Пишите правильно код!

Ссылки

Поблагодарить

Хотите тоже получать донаты? Тогда заходите на boosty.to и регистрируйтесь!

Кстати, я использую хостинг reg.ru. Подключайся с промокодом 9A17-953A-8591-CF98 чтобы получить скидку 5%

Пока нет комментариев

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