Как построить современную чистую архитектуру

kak postroit sovremennuyu chistuyu arhitekturu

Чистая архитектура – ​​термин, введенный Робертом К. Мартином. Основная идея состоит в том, что сущности и варианты использования не зависят от фреймворков, пользовательского интерфейса, базы данных и внешних служб.

Стиль чистой архитектуры положительно влияет на ремонтопригодность, поскольку:

  • Мы можем тестировать объекты домена и варианты использования без рамки, пользовательского интерфейса или инфраструктуры.
  • Технологические решения могут изменяться, не влияя на код домена и наоборот. Можно даже перейти на новый фреймворк с ограниченными усилиями.

Моя цель – выровнять кривую обучения и уменьшить усилия, необходимые для внедрения чистой архитектуры. Поэтому я создал современную чистую архитектуру библиотеки.

В этой статье я покажу вам, как создать приложение с современной чистой архитектурой, от интерфейса HTML/JavaScript до сервера Spring Boot. Акцент будет сделан на задней части.

Начнем с примера программы – вечной классики, программы TODO.

Образец программы для списка задач

А список дел является коллекцией Задача. Задание имеет a имяи есть или завершено или нет. Как пользователь вы можете:

  • Создайте единый список дел и храните его
  • Добавьте задачу
  • Выполните задание или «не выполните» его
  • Удалить задание
  • Перечислите все задачи
  • Фильтровать выполненные/незавершенные задания

Вот как выглядит список дел с 1 незавершенной и 2 выполненными задачами:

график-1

Мы начнем с ядра программы, сущностей домена. Затем мы пройдемся наружу к переднему концу.

Субъекты домена

Центральными объектами домена являются TodoList и Task.

The Список дел субъект содержит:

  • уникальный идентификатор,
  • список задач,
  • доменные методы для добавления, выполнения и удаления задач

The TodoList сущность не содержит общедоступных установщиков. Сеттера нарушают должную инкапсуляцию.

Вот часть сущности TodoList. Аннотации Lombok сокращают код.

public class TodoList implements AggregateRoot<TodoList, TodoListId> {
	private final TodoListId id;
	private final List<Task> tasks;
	
	@Value(staticConstructor = "of")
	public static class TodoListId implements Identifier {
		@NonNull
		UUID uuid;
	}

	@Override
	public TodoListId getId() {
		return id;
	}
	...
	public TaskId addTask(String taskName) {
		if (taskName == null || isWhitespaceName(taskName)) {
			throw new IllegalTaskName("Please specify a non-null, non-whitespace task name!");
		}
		TaskId taskId = add(TaskId.of(UUID.randomUUID()), taskName, false);
		return taskId;
	}
  	...
	public void deleteTask(TaskId task) {
		Optional<Task> foundTask = findTask(task);
		foundTask.ifPresent(tasks::remove);
	}
 	...
}

Что AggregateRoot интерфейс подходит для? Агрегатный корень – это термин с Domain Driven Design (DDD) Эрика Эванса:

Агрегат – это кластер связанных объектов, которые мы рассматриваем как единицу для изменения данных. Каждая совокупность имеет корень и предел. Предел определяет, что находится внутри агрегата. Корень – это единственная конкретная сущность, содержащаяся в совокупности.

Мы можем изменить состояние агрегата только из-за корня агрегата. В нашем примере это означает: мы всегда должны использовать TodoList для добавления, удаления или изменения задач.

Это позволяет TodoList для соблюдения ограничений. Например, мы не можем добавить задачу с пустым названием в список.

The AggregateRoot Интерфейс является частью библиотеки jMolecules. Эта библиотека делает концепции DDD очевидными в коде домена. Во время сборки плагин ByteBuddy сопоставляет инструкции с инструкциями Spring Data.

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

The Задача класс подобен, но он реализует jMolecules Субъект интерфейс вместо этого:

public class Task implements Entity<TodoList, TaskId> {
	private final TaskId id;
	private final String name;
	private final boolean completed;
	
	@Value(staticConstructor = "of")
	public static class TaskId implements Identifier {
		@NonNull
		UUID uuid;
	}

	Task(@NonNull TaskId id, @NonNull String name, boolean completed) {
		this.id = id;
		this.name = name;
		this.completed = completed;
	}
}

Конструктор по Задача пакет является приватным. Поэтому мы не можем создать экземпляр Задача вне пакета домена. И Задача класс неизменен. Из-за пределов агрегата невозможны изменения его состояния.

Нам нужен репозиторий для хранения Список дел. Чтобы соблюдать условия домена в коде домена, его вызывают TodoLists:

public interface TodoLists extends Repository<TodoList, TodoListId> {
	TodoList save(TodoList entity);
	Optional<TodoList> findById(TodoListId id);
	Iterable<TodoList> findAll();
}

Опять же код использует аннотацию jMolecues: Репозиторий. Во время сборки плагин ByteBuddy переводит его в репозиторий Spring Data.

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

Поведение программы (и варианты использования)

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

  1. Пользовательский интерфейс отправляет a запрос.
  2. Бекенд реагирует, выполняя a обработчик запросов. Обработчик запроса делает все необходимое для выполнения запроса:
    — доступ к базе данных
    — вызвать внешние службы
    — вызвать методы сущности домена
  3. Обработчик запросов может вернуть а ответ.

Мы реализуем а обработчик запросов с функциональным интерфейсом Java 8

Возвращающий обработчик a ответ реализует java.util.Function интерфейс. Вот код AddTask обработчик. Этот обработчик

  • извлекает идентификатор списка дел и название задания из файла AddTaskRequest,
  • находит список дел в хранилище (или создает исключение),
  • добавляет задание с названием из запроса в список,
  • возвращает an AddTaskResponse с идентификатором добавленной задачи.
@AllArgsConstructor
class AddTask implements Function<AddTaskRequest, AddTaskResponse> {
	@NonNull
	private final TodoLists repository;

	@Override
	public AddTaskResponse apply(@NonNull AddTaskRequest request) {
		final UUID todoListUuid = request.getTodoListUuid();
		final String taskName = request.getTaskName();
		
		final TodoList todoList = repository.findById(TodoListId.of(todoListUuid))
			.orElseThrow(() -> new TodoListNotFound("Repository doesn't contain a TodoList of id " + todoListUuid));

		TaskId taskId = todoList.addTask(taskName);
		repository.save(todoList);

		return new AddTaskResponse(taskId.getUuid());
	}
}

Ломбок создает конструктор с помощью TodoLists интерфейс репозитория как аргумент конструктора. Мы передаем любую внешнюю зависимость как интерфейс конструктору обработчику.

Запросы и ответы являются постоянными объектами:

@Value
public class AddTaskRequest {
	@NonNull
	UUID todoListUuid;

	@NonNull
	String taskName;
}

Библиотеки Modern Clean Architecture (де)сериализируют их из/в JSON.

Далее, пример обработчика, не возвращающий ответ. The DeleteTask обработчик получает a DeleteTaskRequest. Поскольку обработчик не возвращает ответ, он реализует Потребитель интерфейс.

@AllArgsConstructor
class DeleteTask implements Consumer<DeleteTaskRequest> {
	@NonNull
	private final TodoLists repository;

	@Override
	public void accept(@NonNull DeleteTaskRequest request) {
		final UUID todoListUuid = request.getTodoListUuid();
		final UUID taskUuid = request.getTaskUuid();
		
		final TodoList todoList = repository.findById(TodoListId.of(todoListUuid))
			.orElseThrow(() -> new TodoListNotFound("Repository doesn't contain a TodoList of id " + todoListUuid));

		todoList.deleteTask(TaskId.of(taskUuid));
		repository.save(todoList);
	}
}

Остается один вопрос: кто создает эти обработчики?

Ответ: реализующий класс Модель поведения интерфейс. Модель поведения отображает каждый запрос класс до обработчик запросов для такой просьбы.

Вот часть из TodoListBehaviorModel:

@AllArgsConstructor
public class TodoListBehaviorModel implements BehaviorModel {
	@NonNull
	private final TodoLists todoLists;
	...
	@Override
	public Model model() {
		return Model.builder()
			.user(FindOrCreateListRequest.class).systemPublish(findOrCreateList())
			.user(AddTaskRequest.class).systemPublish(addTask())
			.user(ToggleTaskCompletionRequest.class).system(toggleTaskCompletion())
			...
			.build();
	}

	private Function<FindOrCreateListRequest, FindOrCreateListResponse> findOrCreateList() {
		return new FindOrCreateList(todoLists);
	}

	private Function<AddTaskRequest, AddTaskResponse> addTask() {
		return new AddTask(todoLists);
	}

	private Consumer<ToggleTaskCompletionRequest> toggleTaskCompletion() {
		return new ToggleTaskCompletion(todoLists);
	}
	...
}

The user(...) операторы определяют классы запросов. Мы используем systemPublish(...) для возвращающих обработчиков a ответи system(...) для обработчиков, которые этого не делают.

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

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

Веб-уровень приложений (адаптеры)

Веб-слой в современной чистой архитектуре может быть очень тонким. В самом простом виде он состоит только из 2 классов:

  • Один класс для настройки зависимостей
  • Один класс для обработки исключений

Вот TodoListConfiguration класс:

@Configuration
class TodoListConfiguration {
	@Bean
	TodoListBehaviorModel behaviorModel(TodoLists repository) {
		return new TodoListBehaviorModel(repository);
	}
}

Весна инжектирует исполнение TodoLists интерфейс репозитария в модель поведения(…) метод. Этот метод создает a модель поведения реализация как bean.

Если приложение использует внешние службы, класс конфигурации является местом для создания конкретных экземпляров как beans. И ввести их в модель поведения.

Итак, где же все контролеры?

Ну нет таких, которые вам нужно создать. По крайней мере, если вы обрабатываете только запросы POST. (Для обработки запросов GET см. вопросы и ответы позже.)

The spring-behavior-web библиотека является частью Современная чистая архитектура библиотеки. Мы определяем единую конечную точку запросов. Мы указываем URL-адрес этой конечной точки в файле app.properties:

behavior.endpoint = /todolist

Если это свойство существует, spring-behavior-web настраивает контроллер для конечной точки в фоновом режиме. Этот контроллер получает запросы POST.

Нам не нужно писать код для Spring, чтобы добавить новое поведение. И нам не нужно добавлять или изменять контроллер.

Вот что происходит, когда конечная точка получает запрос POST:

  1. spring-behavior-web десериализирует запрос,
  2. spring-behavior-web передает запрос к поведению, настроенному моделью поведения,
  3. поведение передает запрос к соответствующему обработчику запроса (если он есть),
  4. spring-behavior-web сериализирует ответ и передает его обратно в конечную точку (если она есть).

По умолчанию spring-behavior-web обращает каждый вызов обработчика запроса в транзакции.

Как отправлять запросы POST

После запуска программы Spring Boot мы можем посылать запросы POST в конечную точку.

Мы включаем а @type свойства в содержимом JSON, чтобы spring-behavior-web мог определить правильный класс запроса при десериализации.

Например, это действительно curl команду программы Список дел. Он посылает a FindOrCreateListRequest к конечной точке.

curl -H "Content-Type: application/json" -X POST -d '{"@type": "FindOrCreateListRequest"}'

И это подходящий синтаксис для использования в Windows PowerShell:

iwr -Method 'POST' -Headers @{'Content-Type' = 'application/json'} -Body '{"@type": "FindOrCreateListRequest"}'

Обработка исключений

Обработка исключений с помощью spring-behavior-web ничем не отличается от «обычных» приложений Spring. Мы создаем класс с аннотациями @ControllerAdvice. И мы размещаем аннотацию методов @ExceptionHandler в этом.

См. TodoListExceptionHandling например:

@ControllerAdvice
class TodoListExceptionHandling {
	@ExceptionHandler({ Exception.class })
	public ResponseEntity<ExceptionResponse> handle(Exception e) {
		return responseOf(e, BAD_REQUEST);
	}
	...
}

Заметьте, что в реальном приложении разные типы исключений требуют разной обработки.

Передняя часть программы

Передняя часть программы «Список дел» состоит из:

Здесь мы сосредоточимся на main.js. Он отправляет запросы и обновляет веб-страницу.

Вот часть его содержания:

// URL for posting all requests,
// Must be the same as the one in application.properties
// (See 
const BEHAVIOR_ENDPOINT = "/todolist";

//variables
var todoListUuid;
...

// functions
function restoreList(){
	const request = {"@type":"FindOrCreateListRequest"};
	
	post(request, function(response){
		todoListUuid = response.todoListUuid;
		restoreTasksOf(todoListUuid);
	});
}

function restoreTasksOf(todoListUuid) {
	const request = {"@type":"ListTasksRequest", "todoListUuid":todoListUuid};

	post(request, function(response){	
		showTasks(response.tasks);
	});
}
...
function post(jsonObject, responseHandler) {	
	const xhr = new XMLHttpRequest();
	xhr.open("POST", BEHAVIOR_ENDPOINT);

	xhr.setRequestHeader("Accept", "application/json");
	xhr.setRequestHeader("Content-Type", "application/json");
	
	xhr.onreadystatechange = function() {
		if (xhr.readyState === 4) {
			response = xhr.responseText.length > 0? JSON.parse(xhr.response) : "";
			if(response.error){
				alert('Status ' + response.status + ' "' + response.message + '"');
			} else{
				responseHandler(response);
			}
		}	
	};
	
	const jsonString = JSON.stringify(jsonObject);
	xhr.send(jsonString);
}

Так, например, это объект JSON для a ListTasksRequest:

const request = {“@type”:”ListTasksRequest”, “todoListUuid”:todoListUuid};

The post(…) метод посылает запрос к бэкенду и передает ответ к обработчику ответа (функция обратного вызова, которую вы передали как второй параметр).

Вот и все о программе «Список дел».

А если…

… Я хочу отправлять запросы GET вместо запросов POST?

… Я хочу, чтобы веб-уровень развивался отдельно от поведения?

… Я хочу использовать другой фреймворк, чем Spring?

… У меня есть гораздо большая программа, чем образец списка дел. Как мне это структурировать?

Вот ответы.

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

Моя цель – уменьшить усилия на построение чистой архитектуры и выровнять кривую обучения.

Чтобы достичь этого, библиотеки Modern Clean Architecture обеспечивают следующие функции:

  • Сериализация постоянных запросов и ответов без сериализации конкретных инструкций.
  • Нет необходимости в DTO. Вы можете использовать те же неизменные объекты для запросов/ответов в веб-уровне и вариантах использования.
  • Общая конечная точка который получает и передает запросы POST. Новое поведение и логику домена можно добавлять и использовать без необходимости писать код, специфичный для фреймворка.

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

Я приглашаю вас посетить страницу GitHub Modern Clean Architecture.

Просмотрите пример программы «Список дел».

И пожалуйста, поделитесь со мной любыми отзывами. Что вы думаете об этом?

Если вы хотите быть в курсе того, что я делаю, или напишите мне заметку, следите за мной на LinkedIn или Twitter.

Спасибо Surya Shakti за публикацию оригинального интерфейса только для кода списка.

Спасибо Оливеру Дротбому за то, что он указал мне на замечательную библиотеку jMolecules.

Добавить комментарий

Ваш адрес email не будет опубликован.