Автор оригинала: Gilvan Ornelas.
1. Обзор
Когда мы разрабатываем долгосрочные системы, мы должны ожидать изменчивой среды.
В целом, наши функциональные требования, фреймворки, устройства ввода-вывода и даже наш дизайн кода могут меняться по разным причинам. Имея это в виду, чистая архитектура является руководством к высокому обслуживаемому коду, учитывая все неопределенности вокруг нас .
В этой статье мы создадим пример API регистрации пользователей, следуя Чистой архитектуре Роберта С. Мартина . Мы будем использовать его исходные слои – сущности, варианты использования, интерфейсные адаптеры и фреймворки/драйверы.
2. Обзор чистой архитектуры
Чистая архитектура компилирует множество конструкций кода и принципов, таких как SOLID , стабильные абстракции и другие. Но, основная идея состоит в том, чтобы разделить систему на уровни, основанные на стоимости бизнеса . Следовательно, на самом высоком уровне есть бизнес-правила, причем каждый нижний уровень приближается к устройствам ввода-вывода.
Кроме того, мы можем перевести уровни в слои. В данном случае все наоборот. Внутренний слой равен самому высокому уровню, и так далее:
Имея это в виду, мы можем иметь столько уровней, сколько требует наш бизнес. Но, всегда учитывая правило зависимости – более высокий уровень никогда не должен зависеть от более низкого .
3. Правила
Давайте начнем определять системные правила для нашего API регистрации пользователей. Во-первых, бизнес-правила:
- Пароль пользователя должен содержать более пяти символов
Во-вторых, у нас есть правила подачи заявок. Они могут быть в разных форматах, как примеры использования или истории. Мы будем использовать фразу повествования:
- Система получает имя пользователя и пароль, проверяет, существует ли пользователь, и сохраняет нового пользователя вместе со временем создания
Обратите внимание, что нет упоминания о какой-либо базе данных, пользовательском интерфейсе или подобном. Потому что наш бизнес не заботится об этих деталях , как и наш код.
4. Уровень Сущности
Как подсказывает чистая архитектура, давайте начнем с нашего бизнес-правила:
interface User { boolean passwordIsValid(); String getName(); String getPassword(); }
И, a Пользовательская фабрика :
interface UserFactory { User create(String name, String password); }
Мы создали пользовательский заводской метод по двум причинам. Придерживаться принципа стабильных абстракций и изолировать создание пользователя.
Далее, давайте реализуем и то, и другое:
class CommonUser implements User { String name; String password; @Override public boolean passwordIsValid() { return password != null && password.length() > 5; } // Constructor and getters }
class CommonUserFactory implements UserFactory { @Override public User create(String name, String password) { return new CommonUser(name, password); } }
Если у нас сложный бизнес, то мы должны построить наш доменный код как можно более понятным . Итак, этот слой-отличное место для применения шаблонов дизайна . В частности, следует принимать во внимание | управляемый доменом дизайн .
4.1. Модульное тестирование
Теперь давайте протестируем наш Общий пользователь :
@Test void given123Password_whenPasswordIsNotValid_thenIsFalse() { User user = new CommonUser("Baeldung", "123"); assertThat(user.passwordIsValid()).isFalse(); }
Как мы видим, модульные тесты очень понятны. В конце концов, отсутствие насмешек является хорошим сигналом для этого слоя .
В общем, если мы начнем думать о насмешках здесь, возможно, мы смешиваем наши сущности с нашими вариантами использования.
5. Уровень Прецедентов Использования
Примеры использования-это правила, связанные с автоматизацией нашей системы . В Чистой архитектуре мы называем их Взаимодействующими.
5.1. Интерфейс Регистрации Пользователей
Во-первых, мы создадим наш Интерфейс регистрации пользователей , чтобы мы могли видеть, куда мы идем. Затем мы создадим и обсудим все используемые детали:
class UserRegisterInteractor implements UserInputBoundary { final UserRegisterDsGateway userDsGateway; final UserPresenter userPresenter; final UserFactory userFactory; // Constructor @Override public UserResponseModel create(UserRequestModel requestModel) { if (userDsGateway.existsByName(requestModel.getName())) { return userPresenter.prepareFailView("User already exists."); } User user = userFactory.create(requestModel.getName(), requestModel.getPassword()); if (!user.passwordIsValid()) { return userPresenter.prepareFailView("User password must have more than 5 characters."); } LocalDateTime now = LocalDateTime.now(); UserDsRequestModel userDsModel = new UserDsRequestModel(user.getName(), user.getPassword(), now); userDsGateway.save(userDsModel); UserResponseModel accountResponseModel = new UserResponseModel(user.getName(), now.toString()); return userPresenter.prepareSuccessView(accountResponseModel); } }
Как мы можем видеть, мы выполняем все шаги прецедента. Кроме того, этот слой отвечает за управление танцем сущности. Тем не менее, мы не делаем никаких предположений о том, как работает пользовательский интерфейс или база данных. Но мы используем Use rDs Gateway и User Presenter . Так как же мы можем их не знать? Потому что, наряду с границей ввода пользователем , это наши границы ввода и вывода.
5.2. Границы ввода и вывода
Границы-это контракты, определяющие, как компоненты могут взаимодействовать. | Входная граница предоставляет наш вариант использования внешним слоям:
interface UserInputBoundary { UserResponseModel create(UserRequestModel requestModel); }
Далее, у нас есть наши выходные границы для использования внешних слоев . Во-первых, давайте определим шлюз источника данных:
interface UserRegisterDsGateway { boolean existsByName(String name); void save(UserDsRequestModel requestModel); }
Во-вторых, ведущий представления:
interface UserPresenter { UserResponseModel prepareSuccessView(UserResponseModel user); UserResponseModel prepareFailView(String error); }
Примечание мы используем принцип инверсии зависимостей , чтобы сделать наш бизнес свободным от таких деталей, как базы данных и Is .
5.3. Режим развязки
Прежде чем продолжить, обратите внимание, как границы являются контрактами, определяющими естественные подразделения системы . Но мы также должны решить, как будет доставлено наше приложение:
- Монолитный – вероятно, организованный с использованием некоторой структуры пакета
- С помощью модулей
- С помощью Сервисов/Микросервисов
Имея это в виду, мы можем достичь целей чистой архитектуры с любым режимом развязки . Следовательно, мы должны подготовиться к изменению этих стратегий в зависимости от наших текущих и будущих бизнес-требований . После выбора нашего режима развязки разделение кода должно происходить на основе наших границ.
5.4. Модели запросов и ответов
До сих пор мы создавали операции между слоями с использованием интерфейсов. Далее давайте посмотрим, как передавать данные через эти границы.
Обратите внимание, что все наши границы имеют дело только с строкой или моделью объектами:
class UserRequestModel { String login; String password; // Getters, setters, and constructors }
В принципе, только простые структуры данных могут пересекать границы . Кроме того, все Модели имеют только поля и методы доступа . Кроме того, объект данных принадлежит внутренней стороне. Таким образом, мы можем сохранить правило зависимости.
Но почему у нас так много похожих объектов? Когда мы получаем повторяющийся код, он может быть двух типов:
- Ложное или случайное дублирование – сходство кода является случайным, так как у каждого объекта есть разные причины для изменения. Если мы попытаемся удалить его, мы рискуем нарушить принцип единой ответственности .
- Истинное дублирование – код изменяется по тем же причинам. Следовательно, мы должны удалить его
Поскольку у каждой модели своя ответственность, мы получили все эти объекты.
5.5. Тестирование Интерактора регистрации пользователей
Теперь давайте создадим наш модульный тест:
@Test void givenBaeldungUserAnd12345Password_whenCreate_thenSaveItAndPrepareSuccessView() { given(userDsGateway.existsByIdentifier("identifier")) .willReturn(true); interactor.create(new UserRequestModel("baeldung", "123")); then(userDsGateway).should() .save(new UserDsRequestModel("baeldung", "12345", now())); then(userPresenter).should() .prepareSuccessView(new UserResponseModel("baeldung", now())); }
Как мы видим, большая часть теста вариантов использования связана с управлением сущностями и запросами границ. И наши интерфейсы позволяют нам легко издеваться над деталями.
6. Интерфейсные Адаптеры
На этом мы закончили все наши дела. Теперь давайте начнем подключать наши данные.
Наш бизнес должен иметь дело только с наиболее удобным для него форматом данных, и наши внешние агенты, такие как DBs или Is. Но этот формат обычно отличается . По этой причине уровень адаптера интерфейса отвечает за преобразование данных .
6.1. Пользователь Регистрирует Шлюз Ds С Помощью JPA
Во-первых, давайте использовать JPA для отображения нашей таблицы user :
@Entity @Table(name = "user") class UserDataMapper { @Id String name; String password; LocalDateTime creationTime; //Getters, setters, and constructors }
Как мы видим, цель Mapper состоит в том, чтобы отобразить наш объект в формат базы данных.
Далее, JpaRepository используя нашу сущность :
@Repository interface JpaUserRepository extends JpaRepository{ }
Учитывая, что мы будем использовать spring-boot, это все, что нужно для сохранения пользователя.
Теперь пришло время реализовать наш шлюз User Register Ds:
class JpaUser implements UserRegisterDsGateway { final JpaUserRepository repository; // Constructor @Override public boolean existsByName(String name) { return repository.existsById(name); } @Override public void save(UserDsRequestModel requestModel) { UserDataMapper accountDataMapper = new UserDataMapper(requestModel.getName(), requestModel.getPassword(), requestModel.getCreationTime()); repository.save(accountDataMapper); } }
По большей части код говорит сам за себя. Помимо наших методов, обратите внимание на имя UserRegisterDsGateway. Если вместо этого мы выберем Использовать шлюз rDs|/, то у других Пользователей возникнет соблазн нарушить принцип разделения интерфейса .
6.2. API регистрации пользователей
Теперь давайте создадим наш HTTP-адаптер:
@RestController class UserRegisterController { final UserInputBoundary userInput; // Constructor @PostMapping("/user") UserResponseModel create(@RequestBody UserRequestModel requestModel) { return userInput.create(requestModel); } }
Как мы видим, единственная цель здесь-получить запрос и отправить ответ клиенту.
6.3. Подготовка ответа
Прежде чем ответить, мы должны отформатировать наш ответ:
class UserResponseFormatter implements UserPresenter { @Override public UserResponseModel prepareSuccessView(UserResponseModel response) { LocalDateTime responseTime = LocalDateTime.parse(response.getCreationTime()); response.setCreationTime(responseTime.format(DateTimeFormatter.ofPattern("hh:mm:ss"))); return response; } @Override public UserResponseModel prepareFailView(String error) { throw new ResponseStatusException(HttpStatus.CONFLICT, error); } }
Наш User Register Interactor заставил нас создать презентатора. Тем не менее, правила представления касаются только внутри адаптера. Кроме того, w всякий раз, когда что-то трудно проверить, мы должны разделить его на проверяемый и скромный объект . Итак, Форматирование ответов пользователей легко позволяет нам проверить наши правила представления:
@Test void givenDateAnd3HourTime_whenPrepareSuccessView_thenReturnOnly3HourTime() { UserResponseModel modelResponse = new UserResponseModel("baeldung", "2020-12-20T03:00:00.000"); UserResponseModel formattedResponse = userResponseFormatter.prepareSuccessView(modelResponse); assertThat(formattedResponse.getCreationTime()).isEqualTo("03:00:00"); }
Как мы видим, мы проверили всю нашу логику, прежде чем отправить ее в представление. Следовательно, только скромный объект находится в менее проверяемой части .
7. Драйверы и фреймворки
По правде говоря, мы обычно не кодируем здесь. Это связано с тем, что этот уровень представляет собой самый низкий уровень связи с внешними агентами . Например, драйвер H2 для подключения к базе данных или веб-фреймворку. В этом случае мы будем использовать spring-boot в качестве web и инъекции зависимостей framework . Итак, нам нужна его начальная точка:
@SpringBootApplication public class CleanArchitectureApplication { public static void main(String[] args) { SpringApplication.run(CleanArchitectureApplication.class); } }
До сих пор мы не использовали никаких весенних аннотаций в нашем бизнесе. За исключением адаптеров spring-specifics , как наш UserRegisterController . Это потому что мы должны относиться к spring-boot как к любой другой детали .
8. Ужасный Основной Класс
Наконец-то, последняя часть!
До сих пор мы следовали принципу стабильных абстракций . Кроме того, мы защитили наши внутренние слои от внешних агентов с помощью инверсии управления . Наконец, мы отделили все создание объекта от его использования. На данный момент мы должны создать наши оставшиеся зависимости и внедрить их в наш проект :
@Bean BeanFactoryPostProcessor beanFactoryPostProcessor(ApplicationContext beanRegistry) { return beanFactory -> { genericApplicationContext( (BeanDefinitionRegistry) ((AnnotationConfigServletWebServerApplicationContext) beanRegistry) .getBeanFactory()); }; } void genericApplicationContext(BeanDefinitionRegistry beanRegistry) { ClassPathBeanDefinitionScanner beanDefinitionScanner = new ClassPathBeanDefinitionScanner(beanRegistry); beanDefinitionScanner.addIncludeFilter(removeModelAndEntitiesFilter()); beanDefinitionScanner.scan("com.baeldung.pattern.cleanarchitecture"); } static TypeFilter removeModelAndEntitiesFilter() { return (MetadataReader mr, MetadataReaderFactory mrf) -> !mr.getClassMetadata() .getClassName() .endsWith("Model"); }
В нашем случае мы используем spring-boot инъекцию зависимостей для создания всех наших экземпляров. Поскольку мы не используем @Component , мы сканируем наш корневой пакет и игнорируем только объекты Model /.
Хотя эта стратегия может показаться более сложной, она отделяет наш бизнес от структуры DI. С другой стороны, основной класс получил власть над всей нашей системой . Именно поэтому чистая архитектура рассматривает ее в особом слое, охватывающем все остальные:
9. Заключение
В этой статье мы узнали, как чистая архитектура дяди Боба построена на основе многих шаблонов и принципов проектирования|/. Кроме того, мы создали прецедент, применив его с помощью Spring Boot.
И все же мы оставили в стороне некоторые принципы. Но все они ведут в одном направлении. Мы можем подвести итог, процитировав его создателя: “Хороший архитектор должен максимизировать количество не принятых решений .”, и мы сделали это, защитив наш бизнес-код от деталей, используя границы .
Как обычно, полный код доступен на GitHub .