Чистая прагматичная архитектура. Мозговой штурм

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

Успокойтесь, начальство всё равно не даст ничего переписать. Остаётся рефакторить. На что лучше всего потратить свои невеликие ресурсы? Как именно рефакторить, где проводить чистки?

Название этой статьи — в том числе отсылка к книге Дяди Боба «Чистая Архитектура», а сделана она на основе замечательного доклада Victor Rentea (твиттер, сайт) на JPoint (под катом он начнёт говорить от первого лица, но пока дочитайте вводную). Чтения умных книжек эта статья не заменит, но для такого короткого описания изложено весьма хорошо.

Идея в том, что популярные в народе вещи вроде «Clean Architecture» действительно являются полезными. Сюрприз. Если нужно решить вполне конкретную задачу, простой изящный код не требует сверхусилий и оверинжиниринга. Чистая архитектура говорит, что нужно защищать свою доменную модель от внешних эффектов, и подсказывает, как именно это можно сделать. Эволюционный подход к наращиванию объема микросервисов. Тесты, которые делают рефакторинг менее страшным. Вы ведь уже знаете всё это? Или знаете, но боитесь даже подумать об этом, ведь это же ужас что тогда делать придётся?

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

Меня зовут Виктор, я из Румынии. Формально я являюсь консультантом, техлидом и ведущим архитектором в Румынском IBM. Но если бы меня попросили самому дать определение своей деятельности, то я евангелист чистого кода. Обожаю создавать красивый, чистый, поддерживаемый код — об этом, как правило, и рассказываю на докладах. Даже больше, меня вдохновляет преподавание: обучение разработчиков в областях Java EE, Spring, Dojo, Test Driven Development, Java Performance, а также в области упомянутого евангелизма — принципов чистоты паттернов кода и их разработки.

Опыт, на котором строится моя теория — в основном разработка корпоративных приложений для крупнейшего клиента IBM в Румынии — банковского сектора.

План на эту статью таков:

  • Моделирование данных: структуры данных не должны становиться нашими врагами;
  • Организация логики: принцип «декомпозиции кода, которого слишком много»;
  • «Onion» — самая чистая архитектура философии Transaction Script;
  • Тестирование как способ борьбы со страхами разработчика.

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

Принцип единственной ответственности

353dd0cd58012749b70e379e37231265.jpg

Иначе говоря, количество vs качество. Как правило, чем больше функциональности содержит ваш класс, тем хуже она оказывается в качественном отношении. Разрабатывая большие классы, программист начинает путаться, ошибаться в построении зависимостей, а большой код, помимо всего прочего, сложнее отладить. Лучше разбить такой класс на несколько более мелких, каждый из которых будет отвечать за некоторую подзадачу. Пусть лучше у вас будет несколько сильносвязанных модулей, чем один — крупный и неповоротливый. Модульность также даёт возможность повторного использования логики.

Слабое связывание модулей

f721c80f7b3184be2218a01b66f7440a.jpg

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

Не повторяйтесь

2b99de5c00257d6123553c7bfaed0519.jpg

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

Простота и лаконичность

7d48d4c85408c620ddcbe644c4acb6f9.jpg

На мой взгляд, это главный принцип, который нужно соблюдать в инженерных и программных разработках. «Преждевременная инкапсуляция — корень зла», — говорил Адам Биен. Иначе говоря, корень зла заключается в «пере-инженерии». Автор цитаты Адам Биен одно время занимался тем, что брал легаси-приложения и, полностью переписывая их код, получал кодовую базу объемом в 2–3 раза меньше исходного. Откуда берётся столько лишнего кода? Он ведь возникает не просто так. Его порождают испытываемые нами страхи. Нам кажется, что, нагромождая в большом количестве паттерны, плодя косвенность и абстракции, мы обеспечиваем нашему коду защиту — защиту от неизвестностей завтрашнего дня и завтрашних требований. Ведь на самом-то деле сегодня ничего из этого нам не нужно, изобретаем мы всё это только ради каких-то «будущих нужд». И не исключено, что эти структуры данных впоследствии будут мешать. Скажу честно, когда ко мне подходит какой-нибудь мой разработчик и озвучивает, что он придумал кое-что интересное, что можно добавить в продакшн-код, я отвечаю всегда одинаково: «Парень, тебе это не пригодится».

Кода не должно быть много, а тот, что есть, должен быть простым — только так с ним можно будет нормально работать. Это забота о ваших разработчиках. Вы должны помнить, что именно они являются ключевыми фигурами для вашей системы. Постарайтесь снизить их энергозатраты, уменьшить те риски, с которыми им придётся работать. Это не значит, что вам придётся создать свой собственный фреймворк, более того, я бы не советовал вам это делать: в своём фреймворке всегда будут баги, всем нужно будет его изучать и т.д. Лучше пустите в ход существующие средства, коих сегодня имеется масса. Это должны быть простые решения. Пропишите глобальные обработчики ошибок, примените технологию аспектов, генераторы кода, расширения Spring или CDI, настройте области действия Request/Thread, используйте манипуляцию и генерацию на лету байткода и пр. Всё это будет вашим вкладом в поистине важнейшую вещь — в комфорт вашего разработчика.

В частности, я бы хотел продемонстрировать вам применение областей Request/Thread. Я не раз наблюдал, как эта вещь невероятным образом упрощала корпоративные приложения. Суть в том, что она даёт вам возможность, будучи залогиненным пользователем, сохранять данные RequestContext. Таким образом, RequestContext будет в компактном виде хранить данные о пользователе.

3416c54740aca3b8f514696bf9cf7f3d.jpg

Как видите, реализация занимает всего пару строк кода. Прописав запрос в нужную аннотацию (несложно делается, если вы используете Spring или CDI), вы навсегда освободите себя от необходимости передавать методам пользовательский логин и что бы то ни было ещё: хранимые внутри контекста метаданные запроса будут прозрачным образом перемещаться по приложению. Scoped proxy же позволит вам в любой момент получить доступ к метаданным текущего запроса.

Регрессионные тесты

5c5edfd7a14d760bc335d8e65f0d5666.jpg

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

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

Где реализовывать бизнес-логику?

4fb8a208dc6d89279a663747de74369f.jpg

Начиная реализацию любой системы (или компоненты системы), мы задаём себе вопрос: где лучше реализовать логику предметной области, то есть функциональные аспекты нашего приложения? Есть два противоположных подхода.
Первый из них основывается на философии Transaction Script. Здесь логика реализуется в процедурах, работающих с анемичными сущностями (то есть со структурами данных). Такой подход хорош тем, что в ходе его реализации можно опираться на сформулированные бизнес-задачи. Работая над приложениями для банковского сектора, я не раз наблюдал перевод бизнес-процедур в софт. Могу сказать, что это действительно очень естественно — соотносить сценарии с софтом.

Альтернативный подход — использовать принципы Domain-Driven Design. Здесь вам потребуется соотнести спецификации и требования с объектно-ориентированной методологией. Важно и тщательно продумать объекты, и обеспечить хорошую вовлеченность со стороны бизнеса. Плюс спроектированных таким образом систем в том, что в дальнейшем они легко поддерживаются. Однако по моему опыту, осваивать данную методологию достаточно непросто: более-менее смело вы почувствуете себя не раньше, чем через полгода её изучения.

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

Моделирование данных

Сущности

Как же нам моделировать данные? Как только приложение принимает более-менее приличные размеры, обязательно появляются персистентные данные. Это такие данные, которые вам требуется хранить дольше остальных — они являются доменными сущностями (domain entities) вашей системы. Где их хранить — в базе данных ли, в файле или напрямую управляя памятью — не имеет значения. Важно то, как вы будете их хранить — в каких структурах данных.

0a856b127ec938a8c17eb345610b0ee8.jpg

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

c73e567da2fb69ceb3d627fb408530cd.jpg

Давайте посмотрим, чем я снабдил сущность Customer. Во-первых, я реализовал синтетический геттер getFullName(), который будет возвращать мне конкатенацию firstName и lastName. Также я реализовал метод activate() — для контроля состояния моей сущности, таким образом инкапсулируя его. В этот метод я поместил, во-первых, операцию по валидации, и, во-вторых, присвоение значений полям status и activatedBy, благодаря чему не нужно прописывать для них сеттеры. Также я добавил в сущность Customer методы isActive() и canPlaceOrders(), осуществляющие внутри себя лямбда-валидацию. Это так называемая инкапсуляция предикатов. Такие предикаты пригодятся, если вы используете фильтры Java 8: можно передавать их в качестве аргументов фильтрам. Этими хелперами советую пользоваться.

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

c557fe273cc1f2658573f9e78a70ca1d.png

Как видите, это вполне обычная сущность. Но внутрь неё заложена доменная логика. Такие сущности не должны быть скудными и поверхностными, но и не должны быть переполненными логикой. Переполненность логикой возникает чаще: если уж вы решили реализовать всю логику в домене, то для каждого use-case возникнет соблазн реализовать какой-нибудь специфический метод. А use-case-ов, как правило, бывает много. Вы получите не сущность, а одну большую кучу всевозможной логики. Старайтесь соблюдать здесь меру: в домен помещается только переиспользуемая логика и только в небольшом количестве.

Объекты-значения

Помимо сущностей вам, скорее всего, также понадобятся объекты-значения (object values). Это не что иное, как способ сгруппировать доменные данные, чтобы потом вместе перемещать их по системе.

Объект-значение должен быть:

  • Небольшим. Никаких float для денежных переменных! Крайне осторожно подходите к выбору типов данных. Чем компактнее ваш объект, тем легче в нём разберётся новый разработчик. Это основа основ для комфортной жизни.
  • Неизменяемым. Если объект действительно иммутабельный, то разработчик может быть спокоен, что ваш объект не изменит своего значения и не сломается после создания. Это закладывает основу для спокойной, уверенной работы.

a0e76528432de8c55dc146805a8b286d.jpg

А если добавить в конструктор вызов метода validate(), то разработчик сможет быть спокоен за валидность созданной сущности (при передаче, скажем, несуществующей валюты или отрицательной денежной суммы конструктор не отработает).

Отличие сущности от объекта-значения

Объекты-значения отличаются от сущностей тем, что у них нет постоянного ID. В сущностях всегда будут поля, связанные с внешним ключом какой-нибудь таблицы (или другого хранилища). У объектов-значений таких полей нет. Возникает вопрос: отличаются ли процедуры проверки на равенство двух объектов-значений и двух сущностей? Поскольку у объектов-значений нет поля ID, чтобы заключить, что два таких объекта равны, придётся попарно сравнить значения всех их полей (то есть осмотреть всё содержимое). При сравнении же сущностей достаточно провести одно единственное сравнение — по полю ID. Именно в процедуре сравнения заключается главное отличие сущностей от объектов-значений.

Data Transfer Objects (DTOs)

72ad31681509cdef20509d4514b02139.jpg

В чём заключается взаимодействие с пользовательским интерфейсом (UI)? Ему вы должны передать данные для отображения. Неужели понадобится ещё одна структура? Так и есть. А всё потому, что пользовательский интерфейс вам совсем не друг. У него свои запросы: ему нужно, чтобы данные хранились в соответствии с тем, как они должны отображаться. Это так чудно — что именно порой требуют от нас пользовательские интерфейсы и их разработчики. То им нужно достать данные для пяти строк; то им приходит в голову завести для объекта булево поле isDeletable (может ли в принципе у объекта быть такое поле?), чтобы знать, делать ли активной кнопку «Удалить» или нет. Но возмущаться нечего. У пользовательских интерфейсов попросту другие требования.

Вопрос в том, можно ли вверить им в пользование наши сущности? Вероятней всего, они их изменят, причем самым нежелательным для нас образом. Поэтому предоставим им кое-что другое —  Data Transfer Objects (DTO). Они будут приспособлены специально под внешние требования и под логику, отличную от нашей. Некоторые примеры структур DTO: Form/Request (поступают из UI), View/Response (отправляются в UI), SearchCriteria/SearchResult и пр. Можно в некотором смысле назвать это API-моделью.

Первый важный принцип: DTO должно содержать минимум логики.
Перед вами пример реализации CustomerDto.

4ec3bb716c630f3b39ca82f8fe4c5e33.jpg

Содержимое: private-поля, public-геттеры и сеттеры для них. Вроде бы всё супер. ООП во всей красе. Но одно плохо: в виде геттеров и сеттеров я реализовал слишком большое количество методов. В DTO же логики должно быть как можно меньше. И тогда какой мой выход? Я делаю поля публичными! Вы скажете, что такое плохо работает с method references из Java 8, что возникнут ограничения и пр. Но верите или нет, все свои проекты (10–11 штук) я делал вот с такими DTO. Брат жив. Теперь, поскольку мои поля — публичные, я имею возможность запросто присваивать значение dto.fullName, просто ставя знак равно. Что может быть прекраснее и проще?

Организация логики

Маппинг

Итак, у нас есть задача: нам нужно преобразовать наши сущности в DTO. Реализуем преобразование так:

995dfdca8c1945fb92a64cee09851259.jpg

Как видите, объявив DTO, мы переходим к операциям маппинга (присвоения значений). Нужно ли быть senior developer, чтобы писать в таком количестве обычные присвоения? Для некоторых это настолько непривычно, что они начинают переобуваться на ходу: например, копировать данные при помощи какого-нибудь фреймворка для маппинга, используя рефлекшн. Но они упускают главное — то, что рано или поздно произойдёт взаимодействие UI с DTO, в результате которого сущность и DTO разойдутся в своих значениях.

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

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

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

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

Проиллюстрирую на примере. Создадим на основе имеющегося DTO сущность Customer.

ef39e8b82a023a2fad6afe8e3c22b5ec.png

Для маппинга нам необходимо добыть из базы данных ссылку на группу покупателя. Поэтому я запускаю метод getReference(), и он возвращает мне некоторую сущность. Запрос, скорей всего, уйдёт в базу данных (в некоторых случаях этого не происходит, и отрабатывает функция-заглушка).

Но неприятность нас ожидает не здесь, а в методе, выполняющем обратную операцию — преобразование сущности в DTO.

e6ae277ff6b22588b5bc5aaffabf9d0e.png

При помощи цикла мы проходим все адреса, ассоциированные с имеющимся Customer, и преобразуем их в адреса DTO. Если вы используете ORM, то, вероятно, при вызове метода getAddresses() выполнится ленивая загрузка. Если вы не используете ORM, то это будет открытый запрос ко всем детям данного родителя. И здесь вы рискуете вляпаться в «проблему N+1». Почему?

e540012bbb4a242d84e55a2dffa5ca11.jpg

У вас есть набор родителей, у каждого из которых есть дети. Для всего этого вам нужно создать свои аналоги внутри DTO. Вам понадобится выполнить один SELECT-запрос для обхода N родительских сущностей и далее — ещё N SELECT-запросов, чтобы обойти детей каждой из них. Итого N+1 запрос. Для 1000 родительских сущностей Customer такая операция займёт 5–10 секунд, что, конечно, долго.

Предположим, что, всё же, наш метод CustomerDto() вызывается внутри цикла, преобразуя список объектов Customer в список CustomerDto.

58c9c32b3e8a01a96d500920dfe5ef07.jpg

У проблемы с N+1 запросами есть простые типовые решения: в JPQL для извлечения детей вы можете воспользоваться FETCH по customer.addresses и затем соединить их при помощи JOIN, а в SQL вы можете применить обход IN и оператор WHERE.

Но я бы сделал по-другому. Можно узнать, какова максимальная длина списка детей (это можно сделать, например, на основе поиска с пагинацией). Если в списке всего 15 сущностей, то нам потребуется всего 16 запросов. Вместо 5 мс мы потратим на всё, скажем, 15 мс — пользователь не заметит разницы.

Об оптимизации

Я бы не советовал вам оглядываться на производительность системы уже на начальном этапе разработки. Как сказал Дональд Кнуд: «Преждевременная оптимизация — корень зла». Нельзя оптимизировать с самого начала. Это именно то, что нужно оставить на потом. И что особенно важно: никаких предположений — только измерения и оценка измерений!

Так ли вы уверены, что вы компетентны, что вы настоящий эксперт? Будьте скромны в оценке себя. Не думайте, что поняли работу JVM, пока не прочтёте хотя бы пару книг о JIT-компиляции. Бывает, лучшие программисты из нашей команды подходят ко мне и говорят, что, как им кажется, они нашли более эффективную реализацию. Оказывается же, что они снова изобрели что-то, что только усложняет код. Поэтому я раз за разом отвечаю: YAGNI. Нам это не понадобится.

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

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

Предпочитайте композицию наследованию

Вернёмся к нашим DTO. Предположим, мы определили такую DTO:

18039df91fd1cc76a2413b7f373e7c0b.png

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

Как вариант, можно создать копии избыточно большого DTO (в количестве имеющихся use-case-ов) и далее убрать из каждой копии лишние для неё поля. Но многим программистам, в силу ума и грамотности, действительно больно нажимать Ctrl+V. Аксиома гласит, что копипастить — плохо.

Можно прибегнуть к известному в теории ООП принципу наследования: просто определим некий базовый DTO и для каждого use-case создадим наследника.

6f706977fec8625d720323b64fe87ad8.png

Известный принцип гласит: «Предпочитайте композицию наследованию». Прочитайте, что там написано: «extends». Вроде бы мы должны были «расширить» исходный класс. Но если вдуматься, то, что мы сейчас понаделали — вовсе не «расширение». Это самое настоящее «повторение» — тот же копипаст, вид сбоку. Поэтому наследование мы использовать не будем.

Но как же тогда нам быть? Как перейти к композиции? Сделаем так: пропишем в CustomerView поле, которое будет указывать на объект базового DTO.

1f2d49831b9b286b8bc069c824ac2871.jpg

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

Используем ли мы наследование или решаем вопрос композицией — это всё частности, тонкости, возникшие глубоко в ходе нашей реализации. Они очень хрупкие. Что значит хрупкие? Посмотрите внимательно на этот код:

921eb1ef20effd43b129ad5d28b88253.png

Большинство разработчиков, которым я это показал, сразу выпалили, что число »2» повторяется, поэтому его нужно вынести в виде константы. Они не обратили внимание, что двойка во всех трёх случаях имеет совершенно разный смысл (или «бизнес-значение») и что её повторение — не более чем совпадение. Вынести двойку в константу — правомерное решение, однако очень хрупкое. Старайтесь не допускать в домен хрупкую логику. Никогда не работайте из него со внешними структурами данных, в частности, с DTO.

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

Фасады

be731c9e4cfe002cb3b59c11a708f623.jpg

Вернёмся к общей картине приложения. Я бы советовал вам реализовывать доменную логику через паттерн Facade, расширяя фасады доменными сервисами по необходимости. Доменный сервис создаётся тогда, когда в фасаде накапливается слишком большое количество логики, и удобнее её вынести в отдельный класс.
Ваши доменные сервисы должны обязательно говорить на языке вашей доменной модели (её сущностей и объектов-значений). Они ни в коем случае не должны работать с DTO, потому как DTO, как вы помните, — это структуры, постоянно изменяемые с клиентской стороны, слишком хрупкие для домена.

3adeb86570a92f804ba491836c99b9e8.jpg

Каково назначение фасада?

  1. Преобразование данных. Если мы имеем сущности с одного конца и DTO с другого, необходимо проводить преобразования из одного в другое. И это первое, для чего нужны фасады. Если процедура преобразования разрослась в объёме — применяйте классы-мапперы.
  2. Реализация логики. В фасаде вы начнёте писать основную логику приложения. Как только её становится много — выносите части в доменный сервис.
  3. Валидация данных. Помните, что любые поступающие от пользователя данные по определению являются некорректными (содержащими ошибки). В фасаде есть возможность провести валидацию данных. Эти процедуры при превышении объёма принято выносить в валидаторы.
  4. Аспекты. Можно пойти дальше и сделать так, чтобы каждый use-case проходил через свой фасад. Тогда получится надстроить на методы фасада такие вещи, как транзакции, логирование, глобальные обработчики исключений и др. Отмечу, очень важно иметь в любом приложении глобальные обработчики исключений, которые бы ловили все ошибки, не пойманные другими обработчиками. Они очень помогут вашим программистам — дадут им спокойствие и свободу действий.

Декомпозиция кода, которого много

604a9c8e859ff1dc3fb38208d6bf836d.jpg

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

Пример:

81ffe382b74f6bbd4a91626d83249a97.jpg

В исходном фасаде CustomerFacade часть методов связана непосредственно с покупателем, некоторые же — с предпочтениями покупателя. На основе этого я смогу расколоть класс на две части, когда он достигнет критических размеров. Получу два фасада: CustomerFacade и CustomerPreferencesFacade. Плохо только то, что оба этих фасада принадлежат одному уровню абстракции. Разделение же по уровням абстракции предполагает несколько другое.

Ещё один пример:

2ac84e788af60fb038091aed273a390e.jpg

Предположим, в нашей системе есть класс OrderService, в котором мы реализовали механизм уведомлений по электронной почте. Теперь мы создаём DeliveryService и хотели бы использовать здесь тот же самый механизм уведомлений. Копипаст — исключён. Сделаем так: извлечём функциональность уведомлений в новый класс AlertService и пропишем его в качестве зависимости для классов DeliveryService и OrderService. Здесь, в отличие от предыдущего примера, разделение произошло именно по уровням абстракции. DeliveryService более абстрактен, чем AlertService, так как использует его как составляющую своего рабочего потока.

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

Задача извлечения не всегда даётся просто. Она также может повлечь за собой некоторые сложности и потребовать какого-нибудь рефакторинга unit-тестов. Тем не менее, по моим наблюдениям, искать какой бы то ни было функционал по огромной монолитной кодовой базе приложения разработчикам ещё тяжелее.

Парное программирование

6255b23467294d6224107235443379f1.jpg

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

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

Рефакторинг в формате парного программирования — искусство, требующее опыта и тренировки. Здесь полезны, например, практики агрессивного рефакторинга, проведение хакатонов, катов, Coding Dojos и др.

Парное программирование хорошо работает в случаях, когда нужно решать задачи высокой сложности. Сам процесс работы вдвоём не всегда прост. Зато он гарантирует вам, что вы избежите «переинженерии» — напротив, получите реализацию, которая адресует поставленные требования с минимальной сложностью.

a5a5aa666cb29c58dc9bb5b7d5bc6651.jpg

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

«Я архитектор. По определению, я всегда прав».

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

Архитектура «луковица»

«Луковица» — самая чистая архитектура философии Transaction Script. Строя её, мы руководствуемся целью обеспечить защиту того кода, который считаем критичным, и для этого перемещаем его в доменный модуль.

a45bb494a51b04ecfe92606b744c0857.jpg

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

Внимание вот на эту зависимость:

c7d2736bdc016b732c5c431299bf32ea.jpg

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

Тем не менее, домену может понадобиться взаимодействовать с каким-нибудь внешним сервисом. С внешним — значит, с недружестве

© Habrahabr.ru