Cohesion in Enterprise Applications

в 7:50, , рубрики: architecture, cohesion, enterprice, java, patterns, архитектура приложений, Программирование, проектирование, Проектирование и рефакторинг

Введение

Структура кода, структура проекта, дизайн проекта, архитектура проекта — эти понятия могут иметь различные значения, сложность или глубину для архитектора, разработчика, руководителя проекта или консультанта. Дальше должно идти долгое копание в терминологии, однако позвольте мне быть ленивым и считать, что в рамках этой статьи все эти понятия выражают примерно одно и то же, а именно набор шаблонов, правил, которые говорят, каким образом нужно писать код, правильно реагируя на приходящие требования. К примеру, если для доступа к базе данных мы используем DAO (Data Access Object), то вместе с созданием новой структуры в базе данных, нужно будет создать новый DAO или расширить существующий, но никак не писать SQL, скажем, на уровне презентации.

Что бы стало еще понятнее, добавлю, что речь пойдет о том же, о чем писал «классик» — Patterns of enterprise application architecture by M. Fowler. Речь в книге идет в общем о том, как делить функционал, т.е. какой метод какому классу должен принадлежать.

Проблема

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

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

class VisaRequest{
 Collection<Applicant> applicants
}

class Applicant{
 Contact contact
 WokrInfo workInfo
}
…

Не забываем, что и язык программирования магический (т. е. без магии не компилируется), хотя и похож на groovy.

Сам функционал в этом случае может выглядеть как-то так:

class VisaRequestService {
 def create(def visaRequest){...}
 def update(def visaRequest){...}
 def get(def requestId){}
 def submit(def requestId){...}
 def getDecision(def requestId, def applicantName){...}
 def generateReport(def requestId, def applicantName){...}
}
 

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

Cohesion in Enterprise Applications

Что же пишет по-этому поводу Мартин Фаулер? Я не буду пересказывать книгу, скажу лишь, что наравне со всем прочим, он обращает внимание на тот факт, что с добавлением функциональности класс типа VisaRequestService будет расти, а переиспользовать написанные методы будет все сложнее и сложнее, так как каждый метод представляет собой фактически скрипт, специфичный для конкретного сценария. Не будем с ним спорить, а обратим внимание на понятие из заголовка статьи — cohesion. Cohesion это свойство объекта / класса, определяющие насколько объект / класс занят своим делом. Если cohesion низкое, то у класса слишком много ответственности, класс делает слишком много различных операций, отчего становится большим, а большой класс тяжело читать, тяжело расширять и т. д.

В нашем тривиальном случае мы фактически создали God Сlass, который заведует всем. Безусловно, это не единственное решение и мы могли бы создать отдельный DecisionService:

class DecisionService{
 def getDecision(def requestId, def applicantName){...}
}

или отдельный SubmitService:

class SubmitService{
 def submit(def requestId){...}
}

или отдельный ApplicantService:

class ApplicantService{
 def getDecision(def requestId, def applicantName){...}
 def generateReport(def requestId, def applicantName){...}
}

или еще что-нибудь (все-таки страна Ос).
Во всех этих примерах cohesion новых классов кажется довольно хорошим, но не во всех мы улучшаем cohesion существующих. В первых двух случаях, если мы ограничиваемся созданием только одного из этих классов, VisaRequestService все равно продолжает делать слишком много. Кроме того, остаются неясными причины, почему мы разбили класс именно таким образом.

Решение

Классическое

Фаулер решает это проблему перекладывая ее целиком и полностью на плечи ООП. Он вводит понятие Rich Data Model, которое является ни чем иным, как честным совмещением данных и методов в одном классе, что и есть одна из основ ООП. Rich Data Model для нашей системы будет выглядеть следующим образом:

class VisaRequest{
 Collection<Applicant> applicants
 def create(){...}
 def update(){...}
 def get(){}
 def submit(){...}
}

class Applicant{
 Contact contact
 WokrInfo workInfo
 def getDecision(){...}
 def generateReport(){...}
 def create(){...}
 def update(){...}
 def get(){}
}

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

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

Первая из них — такая модель statefull, т. е. объект имеет некоторое состояние. До тех пор, пока состояние имели только структуры данных, это не представляло никакой проблемы, однако теперь объекты, несущие функциональность системы, тоже имеют состояние. На практике это приводит к усложнению процесса создания таких объектов. Фактически, это делает весьма проблематичным использование популярных DI (Dependency Injection) контейнеров, таких как spring. Кроме того, если говорить о J2EE или WEB, никто не отменял необходимости создавать фасады (actions в spring), что приводит к новому слою функциональности, который непонятно как структурировать.

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

Не классическое

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

Cohesion in Enterprise Applications

Следуя этому правилу, мы получим следующие сервисы для нашей системы:

class VisaRequestCRUDService {
 def create(def visaRequest){...}
 def update(def visaRequest){...}
 def get(def requestId){...}
 def getDecision(def requestId, def applicantName){...}
}

class VisaRequestActionService {
 def submit(def requestId){...}
}

class ApplicantCRUDService{
 def create(def Applicant){...}
 def update(def Applicant){...}
 def get(def applicantId){...} 
def create(def Applicant){...}
}
class ApplicantInfoService{
 def getDecision(def requestId, def applicantName){...}
 def generateReport(def requestId, def applicantName){...}
}

Примерно так будет выглядеть диаграмма классов:

Cohesion in Enterprise Applications

Преимущества такого подхода достаточно очевидны. Во-первых, мы имеем практически все плюсы, что и при использовании Rich Data Model, только вместо одного класса у нас будет два, что уменьшает связность, что есть хорошо. Во-вторых, мы явно указываем механизм (правило) дальнейшего улучшения cohesion, что сводит к минимуму загрязнения системы.
Есть еще один, не совсем очевидный плюс. Модель данных не всегда совпадает с доменной моделью, при этом операции всегда принадлежат доменной модели. Приведенное выше решение позволяет отразить различия между этими двумя моделями на координате функционала. Для примера давайте вспомним про летающих обезьян. Исходя из требований, у них нет никаких характеристик, т. е. нам не надо хранить о них никакой информации и соответственно им нечего делать в модели данных. С другой стороны, они есть в доменной модели, что мы может отразить в сервисе, зависящим от класса VisaRequest:

class FlyApesRequestDeliveryService{
 def deliver(def Request){...}
}

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

Вместо заключения

Я думаю с cohesion все более или менее понятно, а что же с enterprise? Корпоративные приложения интересны тем, что над ними, как правило, работают несколько команд, возможно разбитых географически. Много команд — много людей, много людей — много вопросов. Где и как реализовать новый функционал? Есть ли в системе похожий? Новый разработчик — новый подход.

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

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

Автор: ashalitkin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js