Изучение взаимосвязи и различий между бизнес-логикой и сервисами.

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



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

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

Общий подход к многоуровневой архитектуре

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

Была общая тема: многоуровневая архитектура и сервисы. Эти две вещи практически определили целые проекты. Буквально каждый класс, который не вписывался в структуру папок по умолчанию, был службой и располагался в папке с именем services. Каждый из.

Существовало только 3 уровня: уровень представления, сохранение позже и уровень обслуживания. Сервисный уровень обрабатывал буквально все. Файловая структура выглядела примерно так (слегка упрощена, чтобы не включать вещи, специфичные для фреймворка):

Одна из проблем со структурой папок заключается в том, что она не объясняет, о чем приложение, что может затруднить поиск материала, когда файлов много.

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

Вот как выглядело содержимое папки services:

На следующем изображении показан поток выполнения через слои:

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

Для этого будет создан класс с именем что-то вроде FinanceService или SpendingsService, с методом в нем, называемым calculateFurnitureSpendings, или методом, называемым calculateSpendings, который принимает какую-то категорию, но суть не в категории, дело в том, что должен быть класс обслуживания с методом расчета итоговой суммы.

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

Что было бы, если бы существовала третья функция, связанная с расходами? Еще один метод на службе. Я думаю, вы понимаете, к чему все идет — начинает формироваться божественный объект в форме SpendsService.

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

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

Почему все является услугой?

Этот способ создания многоуровневой архитектуры широко распространен. Однако я ни в чем из этого не виню многоуровневую архитектуру.

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

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

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

Проблемы, возникающие из-за отношения ко всему как к услуге

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

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

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

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

Многие люди могут подумать: «Ничего, это просто модульное тестирование, ничего страшного, мы все равно их не пишем».

Суть модульных тестов в том, что они показывают, насколько сложно или легко модифицировать код. Если вы не можете написать модульный тест для своего кода, вы не сможете легко его модифицировать.

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

Отсутствие юнит-тестов не означает, что вы не только не можете быть уверены, что не сломали что-то неожиданно, это также указывает на трудно модифицируемую кодовую базу.

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

Сервисный уровень не является бизнес-логикой, и бизнес-логика не должна существовать на сервисном уровне в виде божественных объектов.

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

Конечно, это вызывает очевидный вопрос.

Что я думаю о многоуровневой архитектуре

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

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

Что такое услуга?

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

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

По этому определению все можно рассматривать как услугу, как это делают люди, верно? Теоретически да. Я расскажу об этом чуть позже.

Что такое сервисный уровень?

Он не должен быть воротами в бизнес-логику. Что я подразумеваю под «шлюзом»? Шлюз — это сквозной прокси, который не является абстракцией, он ничего не скрывает, он просто передает запрос чему-то другому. Например, шлюз может выглядеть так:

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

Итак, какова цель сервисного уровня? Цель та же, что и у службы — действовать как абстракция. Слой представляет собой набор абстракций.

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

Все слои должны скрывать то, как они работают, от других слоев.

Если подумать, поскольку все уровни уже абстрагируются от других слоев, теоретически они сами являются сервисами.

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

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

Слою сервиса нет смысла скрывать что-то, что уже скрывают другие слои. Например, уровень сохраняемости уже скрывает подробности о хранении данных, так что это не работа уровня сервиса.

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

Уровень обслуживания не место для бизнес-логики. Сервисный уровень != бизнес-уровень. Если ваша бизнес-логика находится в классе с именем SomethingService, я думаю, вам следует серьезно пересмотреть свои действия.

Что вообще такое многоуровневая архитектура?

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

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

Многоуровневая архитектура не зависит от фреймворка. Дело не в контроллерах, не в DAO, не в веб-приложениях и HTTP-запросах и не в службах. У него не обязательно должен быть сервисный уровень, если в нем нет необходимости.

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

Не имеет значения, если приложение не следует стандартному шаблону представления/бизнеса/постоянства, оно все равно может использовать многоуровневую архитектуру до тех пор, пока существует основная идея.

Собираем все вместе

В качестве примера возьмем веб-приложение (поскольку это то, с чем знакомо большинство людей), которое отслеживает поставки пользователей.

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

Имея все в виду, наши слои могут выглядеть так:

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

Давайте посмотрим на структуру папок:

Во-первых, это структура папок. Как видите, я использовал вертикальное структурирование. Вертикальное структурирование означает, что мы структурируем вещи по функциям, к которым они принадлежат, а не по слою, к которому они принадлежат.

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

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

Вот расширенные представления каждой папки функций:

Давайте обратимся к слону в комнате. Я просто использовал здесь название папки «BusinessLogic», чтобы было понятно, так быть не должно.

Вот код для всех файлов в примере (имейте в виду, что это псевдокод, а не какой-то конкретный язык программирования или фреймворк):

Объяснение

Давайте распакуем пример и попробуем понять, что он означает.

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

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

Уровень представления

На уровне представления есть только те вещи, которые связаны с отображением вещей и получением инструкций от пользователя. Очевидно, что уровень представления вызывает бизнес-уровень (невозможно иметь пользовательский интерфейс, полностью независимый от бизнес-домена), но он не знает, как выполняются бизнес-правила, и это важная часть.

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

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

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

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

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

Короче говоря, некоторые зависимости от бизнес-уровня должны существовать на уровне представления. Создание сервисного уровня между ними ничего не решит, поскольку потребует такого же объема обслуживания, а сервисный уровень даже нельзя будет использовать повторно, потому что он будет очень тесно связан с вариантом использования обслуживания уровня представления.

Бизнес-уровень

Бизнес-уровень имеет только наши бизнес-правила, а именно:

  • пользователь может пройти аутентификацию
  • пользователь может получить доступ к своим статусам отправки
  • поставщик доставки получает уведомление о доступе к статусу доставки

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

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



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

Сервисный уровень

Сервисный уровень ни к чему не ведет, он просто служит абстракцией для внешней системы, которая является системой уведомлений для провайдера доставки. Он служит абстракцией для набора функций, которые не являются частью приложения и не принадлежат никакому другому уровню.

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

Бизнес-логика не должна зависеть от технических деталей.

Постоянный слой

Уровень постоянства довольно прост, он просто извлекает некоторые данные, а также сохраняет информацию о вошедшем в систему пользователе в хранилище сеансов.

Слои не знают, как работают другие слои

Самое главное, что слои не знают, что происходит внутри других слоев.

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

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

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

Сервисный уровень также не знает подробностей о других слоях.

Еще одна важная вещь заключается в том, что каждый слой можно легко протестировать изолированно.

Последнее, и самое главное, это то, что понятия сервиса и бизнес-логики не смешиваются вместе.

Что вы можете или не можете делать

Пример, который я привел, иллюстрирует идеи и образ мышления, которые возникают у меня при подходе к многоуровневой архитектуре.

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

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

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

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

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

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

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

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу