Мы рассмотрели варианты структуры корпоративного проекта, теперь подробнее изучим конкретную структуру проекта. Предполагая, что мы моделируем корпоративную систему небольшого размера с незначительной функциональностью, преобразуем задачи проекта в кодовые структуры.
Мы уже знаем, что такое вертикальные и горизонтальные структуры модулей. Именно это нужно в первую очередь изучить при структурировании проекта.
Ситуация в корпоративных проектах
Типичный корпоративный проект обычно состоит из трех технически обоснованных слоев — уровней представления, бизнес-логики и данных. Это означает, что проект имеет горизонтальную структуру в виде трех подмодулей, или пакетов.
Идея состоит в том, чтобы разделить по уровням данные, бизнес-логику и представления. Таким образом, функциональность нижнего уровня не может зависеть от более высокого — только наоборот. На уровне бизнес-логики не может быть задействована функциональность уровня представления, а уровень представления может использовать функционал бизнес-логики. То же самое верно для уровня данных, который не может зависеть от уровня бизнес-логики.
У каждого технически обоснованного уровня или модуля есть свои внутренние зависимости, которые нельзя задействовать извне. Например, использовать базу данных может только уровень данных, прямые вызовы с уровня бизнес-логики недопустимы.
Еще одна причина разделения на уровни заключается в возможности передавать детали реализации, не влияя на другие уровни. Если будет принято решение изменить технологию базы данных, то теоретически это не повлияет на два остальных уровня, поскольку эти детали инкапсулированы на уровне данных. Аналогично изменение технологии представления никак не повлияет на остальные уровни. В сущности, можно создать даже несколько уровней представления, использующих на уровне бизнес-логики одни и те же компоненты, — в случае если эти уровни представлены в виде отдельных модулей.
Мы были свидетелями горячих дискуссий между высококвалифицированными архитекторами о необходимости организации и разделения обязанностей в соответствии с техническими задачами. Однако у этого подхода есть ряд недостатков.
Структурирование по горизонтали и по вертикали
Чистый код — это такой код, который должны понимать люди, а не машины. То же самое касается области разработки и разделения ответственности. Мы хотим построить структуру, по которой инженеры легко разобрались бы в том, что собой представляет проект.
Проблема структурирования по техническим признакам на высоких уровнях абстракции заключается в том, что при этом назначение и область применения программного обеспечения искажаются и скрываются на более низких уровнях абстракции. Когда человек, незнакомый с проектом, смотрит на структуру кода, первое, что он видит, — это три технических уровня (иногда их названия и количество могут быть иными). С одной стороны, это будет выглядеть знакомо, но с другой — ничего не скажет о фактической области применения программного продукта.
Инженеры-программисты стремятся разобраться в области применения модулей, а не в технических уровнях. Например, в функционале учетных записей они найдут только то, что связано с областью учетных записей, а не все подряд классы для доступа к базе данных. Более того, программисту, скорее всего, нужны не все классы доступа к базе данных, а только тот единственный класс, который обрабатывает эту логику в своей предметной области.
То же самое касается изменений, вносимых в систему. Изменения функциональности, скорее всего, влияют на все технические уровни одной или нескольких областей бизнес-логики, но вряд ли сразу на все классы одного технического уровня. Например, изменение одного поля в учетной записи пользователя повлияет на модель последнего, доступ к базе данных, бизнессценарии и даже логику представления, но не обязательно на все остальные классы моделей.
Для того чтобы лучше объяснить, какие характеристики больше всего интересуют программистов, позвольте привести еще один пример. Представьте, что все члены семьи хранят свою одежду в одном большом шкафу. Они могли бы разместить в одном ящике все брюки, в другом — все носки, а в третьем — все рубашки. Но мало кому захочется, одеваясь, перебирать все брюки, чтобы найти свои. Каждого интересует только его одежда, будь то брюки, рубашки, носки или что-то еще. Поэтому целесообразно сначала разделить шкаф на несколько зон, по одной на каждого члена семьи, а затем структурировать каждую зону по техническим признакам одежды, в идеале следуя аналогичной структуре. То же самое верно и в отношении программного обеспечения.
Структура, продиктованная бизнес-логикой
Роберт Мартин, он же дядюшка Боб, однажды описал «кричащую» архитектуру, которая должна сообщить инженеру прежде всего о том, что представляет собой корпоративный проект в целом. Чтобы, глядя на чертежи зданий, видя структуру и детализированный интерьер, сразу можно было сказать: вот это дом, это библио тека, а это — железнодорожная станция. То же самое справедливо и для программных систем. Взглянув на структуру проекта, программист должен сразу понять: это система учета, это система инвентаризации книжного магазина, а это — система управления заказами. Так ли это в большинстве существующих проектов? Или же, глядя на самый высокий уровень модулей и пакетов, мы можем лишь сказать: это приложение Spring, у этой системы есть уровень представления, бизнес-логики и данных, а в этой системе используется кэш Hazelcast?
Техническая реализация, безусловно, важна для нас, программистов. Однако первое, на что мы обращаем внимание, — бизнес-логика. Соответственно, эти аспекты должны отражаться в структуре проекта и модулей.
Но самое главное, это означает, что предметная область будет отражена в структуре приложения. Уже имена пакетов верхнего уровня иерархии должны давать представление о том, для чего предназначено это программное обеспечение. Поэтому разделение на уровни должно определяться прежде всего задачами бизнеса и только потом — деталями реализации.
Чертежи зданий тоже создаются по такому принципу: сначала нужно показать, что представляет собой здание, как разделены комнаты, где находятся двери и окна. И только потом могут быть указаны используемые материалы, сорта кирпича и тип бетона.
В качестве обзора микросервисов рассмотрим следующее: проектирование вертикальных модулей позволяет команде разработчиков легко представить приложение как систему из нескольких приложений. Исследуя зависимости между модулями — например, посредством анализа статического кода, — можно наметить точки интеграции между системами. Эти точки проявятся в какой-то форме коммуникации между приложениями. Теоретически можно создать один модуль, добавив к нему минимальные связи, и упаковать его в виде отдельного самодостаточного приложения.
Уточним терминологию: модулями мы сейчас называем бизнес-модули, реализованные в виде пакетов и подпакетов Java, а не модули проекта. Поэтому термином «модуль» описывается скорее концепция, чем строгая техническая реализация.
Рациональное проектирование модулей
А теперь перейдем к практике: как построить структуру модулей разумного размера?
Поскольку на первом месте у нас требования бизнеса, лучше всего начать с обзора всех задач и сценариев использования приложения. Это можно сделать во время мозгового штурма, в идеале при участии специалистов по бизнесу, если это не было предпринято раньше. Каковы задачи приложения? Какие бизнес-сценарии следует реализовать? Какой подходящий функционал можно применить? Ответив на эти вопросы, вы составите представление о том, какие модули, скорее всего, будут присутствовать в проекте, не концентрируясь на внешних системах, деталях реализации или фреймворках.
На этом этапе мы уже учитываем зависимости между бизнес-задачами. Такие зависимости — полезные индикаторы того, следует модули разделить или же объединить, особенно если обнаружены круговые зависимости. Построение обзорных диаграмм, начиная с самого высокого уровня и постепенно спускаясь, за несколько итераций позволяет составить более четкое представление о том, какую бизнеслогику должно реализовывать приложение. Вообще говоря, идентифицированные модули должны соответствовать бизнес-логике, созданной экспертами предметной области.
Например, приложение для интернет-магазина может состоять из модулей, отвечающих за пользователей, рекомендации, статьи, платежи и доставку. Их основная структура может выглядеть так, как показано на рис. 1.
Рис. 1. Структура интернет-магазина
Идентифицированные модули представляют собой базовые пакеты Java создаваемого приложения.
Имеет смысл потратить время на эту разработку. Однако, как обычно, любая определенная структура или реализация, независимо от уровня — кода или модулей, — должна иметь возможность изменяться. Могут появиться новые требования или сформироваться лучшее понимание уже существующих, когда программисты тщательнее изучат предметную область. Итеративное перепроектирование, на каком бы уровне оно ни делалось, всегда улучшает качество системы.
Далее в моем блоге будут показаны аналогичные причины и методологии проектирования систем с распределенными приложениями. В частности, мы рассмотрим принципы проблемно-ориентированного проектирования взаимосвязанных контекстов и карты контекстов.
Реализация пакетных структур
Предположим, что базовая структура Java-пакетов создана. Как теперь реализовать внутреннюю структуру пакетов? Другими словами, какие подпакеты использовать?
Содержимое пакетов
Для начала рассмотрим содержимое модуля с вертикальной структурой. Поскольку его модель соответствует бизнес-логике, то в состав модуля будет входить все необходимое для реализации определенного функционала.
Прежде всего в модуле будут технические точки входа для таких сценариев, как конечные точки HTTP, контроллеры представления фреймворков или конечные точки JMS. В этих классах и методах обычно применяются такие принципы Java EE, как инверсия управления, чтобы при обращении к приложению можно было осуществлять вызов прямо из контейнера.
Следующей не менее важной задачей является создание функциональности, реализующей различные сценарии использования. Обычно они отличаются от технических конечных точек тем, что не содержат коммуникационной логики. Бизнес-сценарии являются точкой входа в логику предметной области. Они реализуются как управляемые компоненты, обычно как Stateless Sessions Beans — другими словами, EJB или управляемые компоненты CDI.
Эти компоненты очерчивают пределы, в которых реализуется бизнес-логика. В тех случаях, когда логика сценария состоит всего из нескольких шагов, она вполне может быть ограничена лишь бизнес-методом или приватными методами в определении класса. В этом случае никакие другие делегаты не нужны. В подавляющем большинстве бизнес-сценариев за логику отвечают соответствующие службы, и именно эти делегаты выполняют более мелкие операции. В зависимости от предметной области это означает реализацию детальной бизнес-логики или доступ к внешним системам, таким как базы данных. В соответствии с языком проблемно-ориентированного проектирования эти классы включают в себя службы, сценарии транзакций, фабрики и хранилища данных.
Следующий тип объектов — это все классы, которые обычно принято считать моделями содержимого, такого как сущности, объекты-значения и объекты переноса данных. Эти классы описывают сущности из предметной области, но также могут и должны реализовывать бизнес-логику. Примерами являются сущностные объекты, которыми оперирует база данных, другие POJO-объекты и перечисления.
В некоторых случаях в состав пакета может входить также сквозная функциональность, такая как методы-перехватчики бизнес-логики или технических функций. Теперь остается разместить и упорядочить все эти компоненты в рамках модуля.
Горизонтальное структурирование пакетов
Если бы перед нами стояла задача структурировать содержимое модуля, то сначала мы попытались бы, вероятно, спроектировать внутреннюю структуру пакета путем технического разделения на уровни. Сперва разделить по коммерческим задачам, а затем по техническим функциям — по крайней мере, это звучит разумно.
Например, в пакете users это означало бы предусмотреть такие подпакеты, как controller, business (или core), model, data и client. Придерживаясь этого подхода, далее разделяем функционал внутри пакета users по техническим категориям. Действуя последовательно, мы добьемся того, что все остальные модули и пакеты в проекте будут иметь аналогичную структуру в зависимости от их содержимого. Эта идея похожа на реализацию трехуровневой архитектуры, но только внутри модулей, соответствующих предметной области.
Один из подпакетов, например controller, будет считаться технической точкой входа. Этот пакет будет содержать коммуникационные конечные точки, инициирующие логику сценариев использования, и служить точкой входа извне приложения. На рис. 2 приведена структура горизонтально упорядоченного пакета users.
На рис. 3 эта структура реализована в виде Java-пакетов.
Рис. 2. Структура горизонтально упорядоченного пакета users
Рис. 3. Структура пакета users на языке Java
Плоская структура модулей
Есть более простой и прямолинейный способ структурировать содержимое модуля — разместить все связанные классы в модульном пакете в виде плоской (горизонтальной) иерархии. В частности, для пакета users это означает включить в него все классы, относящиеся к пользователям, в том числе точки доступа для пользовательских сценариев, параметры доступа к базе данных пользователей, потенциальный функционал для внешних систем и классы пользовательских сущностей.
В зависимости от сложности модулей таким способом можно получить либо ясную и простую структуру, либо такую, которая со временем превратится в хаотичную. В частности, сущности, объекты-значения и объекты переноса данных могут расшириться до нескольких классов. Если все эти классы находятся в одном пакете, то его структура становится запутанной и непонятной. Тем не менее имеет смысл начать с такой структуры и при необходимости позже ее реорганизовать.
На рис. 4 показана структура рассмотренного в примере пакета users.
Рис. 4. Структура пакета users
Преимущество такого подхода заключается в том, что он хорошо поддерживается языком Java. По умолчанию классы и методы Java имеют видимость только в пределах пакета. С учетом этого обстоятельства размещение всех классов в одном пакете улучшает инкапсуляцию и видимость. Компоненты, к которым желательно обеспечить доступ извне пакета, становятся общедоступными. Все классы и методы, которые должны быть доступны только внутри пакета, имеют такую видимость по умолчанию. Таким образом, в пакете инкапсулируются все внутренние задачи.
Подход «Сущность — управление — граница»
Для того чтобы можно было справиться с таким большим количеством классов в пакете модуля, предусмотрен другой подход, аналогичный техническому разделению на слои, но более логичный и подразумевающий использование меньшего количества пакетов. Смысл его состоит в том, чтобы создать структуру модулей, соответствующую контурам (границам) бизнес-сценариев, которые затем станут компонентами бизнес-логики и классами сущностей.
При этом основное внимание уделяется структурированию пакетов модулей в соответствии с их задачами, но с меньшим, по сравнению с горизонтальным структурированием, количеством технических деталей на верхнем уровне пакета. В контуре содержатся инициаторы бизнес-сценариев и классы-разграничители, доступные извне системы. Эти классы обычно представляют собой конечные точки HTTP, управляемые сообщениями объекты, контроллеры клиентских интерфейсов и обычные серверные компоненты Java. Они реализуют бизнессценарии и, возможно, делегируют какие-то операции подчиненным классам из дополнительных пакетов управления. Пакет сущности содержит все имена модуля, сущности предметной области и объекты переноса данных.
Айвар Джекобсон (Ivar Jacobson) сформулировал термин «сущность — управление — граница» (Entity Control Boundary, ECB), описывающий показанный на рис. 5 способ организации модулей.
Рис. 5. Модуль users в модели "сущность — управление — граница"
Пакеты
Рассмотрим подробнее пограничные пакеты, соответствующие контурам. Суть в том, чтобы инициировать в таких пакетах все бизнес-сценарии, вызываемые из клиентского интерфейса или извне системы. Эти пакеты принимают обращения для создания, обновления или удаления пользователей, им же принадлежат классы верхнего уровня. В зависимости от сложности бизнес-сценариев контур либо полностью обрабатывает логику, либо делегирует ее элементу управления, прежде чем это станет слишком сложным.
В корпоративном приложении Java классы пограничного пакета реализованы в виде управляемых объектов. Как уже отмечалось, в данном случае обычно применяется EJB.
Если логика в контуре становится слишком сложной и трудноуправляемой в рамках одного класса, то ее передают задействованным в контуре делегатам.
Эти делегаты, или элементы управления, размещаются в пакете управления. Они обычно реализуют более детальные бизнес-сценарии, обеспечивают доступ к базе данных или внешним системам, действуя в рамках технических транзакций, которые инициируются контуром.
В такой структуре повышаются согласованность и возможность повторного использования, а также соблюдается принцип разделения обязанностей. После того как введены эти уровни абстракции, структура бизнес-сценариев становится более читаемой. Сначала можно рассматривать контур как точку входа в бизнессценарий, а затем по очереди выполнять все делегированные операции.
В языке проблемно-ориентированного проектирования пакет управления включает в себя службы, сценарии транзакций, фабрики и хранилища данных. Однако наличие пакета управления для реализации бизнес-сценариев не является обязательным.
Основа нашего продукта — сущности и объекты-значения. В совокупности с объектами передачи данных они образуют модель модулей предметной области — объектов, которыми обычно оперирует бизнес-сценарий. Они объединены в пакет сущностей в соответствии с шаблоном ECB.
А как быть с компонентами, связанными с презентацией, и сквозными задачами, такими как перехватчики или фреймворки со связующей логикой? К счастью, в современных проектах Java EE все требуемые связующие функции спрятаны в пределах фреймворка. Всего несколько необходимых вещей, таких как самонастройка JAX-RS с классом активатора приложения, помещаются в корневой пакет проекта или в специальный пакет platform. То же самое касается и сквозных задач, таких как технически обусловленные методы- перехватчики, которые не привязаны к определенному модулю, а относятся к приложению в целом. Обычно таких классов не очень много, имеет смысл выделить их в особый пакет. Опасность добавления пакета platform заключается в том, что программистам, естественно, захочется добавить туда и другие компоненты. Однако он предназначен лишь для нескольких классов платформы — все остальное должно находиться в соответствующих пакетах бизнес-логики.
На рис. 6 приведен пример модуля users с использованием шаблона ECB.
Рис. 6. модуль users с использованием шаблона ECB
Доступ к пакетам
В шаблоне ECB далеко не каждое обращение из каждого пакета имеет смысл и, соответственно, считается допустимым. В общем случае логический поток начинается с контура и заканчивается на пакете управления или пакете сущностей. Таким образом, у пограничного пакета есть зависимости от пакета управления (если он существует) и пакета сущностей. Использование пограничных пакетов других модулей не допускается и не имеет смысла, так как подобный пакет представляет собой отдельный бизнес-сценарий. Доступ к другому пограничному пакету означал бы обращение к тому, что должно быть отдельным, автономным бизнес-сценарием. Поэтому пограничные пакеты могут обращаться только вниз по иерархии к элементам управления.
Однако иногда зависимости и вызовы от пограничных пакетов к элементам управления других модулей допустимы и имеют смысл. Программистам нужно следить за тем, чтобы при обращении к компонентам из других модулей правильно выбиралась область транзакций. При доступе к элементам управления из других модулей также нужно проследить, чтобы объекты, с которыми они работают или которые возвращают, принадлежали соответствующим «чужим» модулям. Так бывает в нестандартных бизнес-сценариях, и это не вызывает проблем, если позаботиться о разделении обязанностей и правильном использовании элементов управления и сущностей.
Элементы управления одного модуля могут обращаться к элементам управления других модулей, а также к их собственным и внешним сущностям. Как и в случае с пограничными пакетами, нет смысла в том, чтобы элемент управления вызывал функции контура. Это означало бы появление нового бизнес-сценария верхнего уровня в рамках уже существующего сценария.
Сущности могут зависеть только от других сущностей. Иногда приходится импортировать элементы управления, например, если есть приемник сущностей JPA или преобразователи типа JSON-B, реализующие сложную логику. Такие технически обоснованные случаи, когда следует разрешить импортировать классы, являются исключением из общего правила. В идеале поддерживающие сущности компоненты, такие как приемники или преобразователи сущностей, должны находиться непосредственно в пакете сущностей. Из-за наличия других зависимостей и использования делегирования данное правило не всегда выполняется, но это не должно приводить к реализации чрезмерно сложных технических решений.
Все это подводит нас еще к одной важной теме.
Не перегружайте архитектуру
Какой бы архитектурный шаблон вы ни выбрали, основным приоритетом приложения должна оставаться его бизнес-логика. Это справедливо как при поиске приемлемых модулей, соответствующих предметной области, так и при структурировании пакетов внутри модуля, чтобы программисты могли работать с ним с минимальными усилиями.
Важно: программисты должны работать над проектом, не задействуя слишком сложных или чрезмерно перегруженных структур и архитектур. Нам встречалось слишком много примеров того, как намеренно использовались уровни, где выполнялись чисто технические задачи, или слишком строгие шаблоны — лишь бы сделать все по книге или учесть заданные ограничения. Но эти ограничения часто ничем не обоснованы и не помогают достичь какой-либо важной цели. Необходимо рационально все пересмотреть и отделить то, что действительно необходимо, от того, что лишь раздувает процесс разработки. Поинтересуйтесь на досуге, что означает термин «карго-культ» в программировании, — вы услышите интересную реальную историю о том, что бывает, если следовать правилам и ритуалам, не задумываясь об их смысле.
Поэтому не стоит чрезмерно усложнять или перегружать архитектуру. Если существует простой и понятный способ достичь того, что требуется в настоящее время, — просто воспользуйтесь им. Это относится не только к преждевременному рефакторингу, но и к проектированию архитектуры. Если при объединении нескольких классов в один пакет с понятным названием будет решена поставленная задача и обеспечено создание понятной документации, почему бы не ограничиться этим? Если пограничный класс для бизнес-сценария сам выполняет всю простую логику, зачем создавать пустые делегаты?
Если архитектурный шаблон не всегда нужен, то необходимо искать компромисс между последовательностью и простотой. Если все пакеты, модули и проекты соответствуют одним и тем же шаблонам и имеют одинаковую структуру, то у программистов быстро складывается представление об архитектуре. Однако, при ближайшем рассмотрении согласованность — это цель, которой вряд ли удастся достичь в рамках всей организации или даже отдельных проектов. Во многих случаях выгоднее разработать что-то более простое и, следовательно, более гибкое, чем сохранить единообразие.
То же самое верно и для чрезмерной инкапсуляции с использованием технических уровней. Действительно, именно модули и классы должны инкапсулировать детали реализации, обеспечивая ясные и понятные интерфейсы. Однако эти задачи могут и должны решаться с помощью единых, в идеале самодостаточных пакетов и классов. Упаковывать в модуль всю ключевую функциональность неправильно, поскольку, с технической точки зрения, в таком случае мы открываем доступ к деталям в остальной части модуля, например, выдаем, какую базу данных мы используем или через какой клиент общаемся с внешней системой. Построение структуры системы в первую очередь в соответствии с задачами предметной области позволяет инкапсулировать функционал в единых точках ответственности, прозрачных для остальных модулей и приложения в целом.
Во избежание ошибок при выборе способа упаковки следует воспользоваться самым простым и прозрачным вариантом — выполнить статический анализ кода. Пакеты, импортированные в классы, и сами пакеты можно сканировать и анализировать с целью обнаружения и предотвращения нежелательных зависимостей. Такое определение степени безопасности, похожее на использование тестовых сценариев, позволяет избежать случайных ошибок. Статический анализ кода обычно выполняется как дополнительная процедура в процессе сборки на сервере непрерывной интеграции, поскольку сборка занимает определенное время.
Резюме
Главным приоритетом при разработке корпоративного программного обеспечения должна быть реализация бизнес-логики. Это приводит к построению приложений и технологий на базе бизнес-сценариев, а не технологических решений. В конечном счете именно бизнес-сценарии принесут прибыль.
Насколько возможно, корпоративные приложения должны разрабатываться в рамках одного проекта сборки для каждого артефакта в системе контроля версий. Разделение проекта на несколько независимых модулей сборки, которые в итоге сводятся к одному артефакту, не приносит особой пользы. На верхних уровнях структуры проекта рекомендуется располагать программные модули вертикально, а не горизонтально. Это означает строить структуру приложения в соответствии с бизнесом, а не с техническими задачами. С первого взгляда на структуру проекта программист должен понимать, к какой предметной области относится приложение и какие задачи оно выполняет.
В самом простом случае отдельный модуль приложения может представлять собой единый Java-пакет. Это удобно, если количество классов в модуле невелико. Для более сложных модулей имеет смысл создать дополнительный иерархический уровень, используя такой шаблон, как ECB.
Инженерам-программистам следует помнить, что не нужно чрезмерно усложнять архитектуру программного продукта. Хорошо продуманная и задокументированная структура, безусловно, способствует созданию высококачественного программного обеспечения. Тем не менее всегда есть золотая середина между разумной архитектурой и чрезмерным техническим усложнением.
Рассмотрев основную структуру корпоративных проектов и способы разработки модулей, спустимся на один уровень и разберем, как создаются модули проекта. В следующей статье поговорим о том, что нужно для реализации корпоративных приложений на базе Java EE.