Рубрики
Без рубрики

Чистая архитектура с пружинной загрузкой

Автор оригинала: 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 .