Русские Блоги
Гексагональная архитектура — это стиль дизайна, который изолирует основную логику от внешних объектов через слои. Основная логика — это бизнес-модули, а внешние элементы — это точки интеграции, такие как базы данных, внешние API и интерфейсы. Он делит программное обеспечение на внутреннее и внешнее. Внутренний содержит основную бизнес-логику и уровень домена (так называемая многоуровневая архитектура), а внешний содержит интерфейс, базу данных, обмен сообщениями и другой контент. Внутренний и внешний обмениваются данными друг с другом через порты и адаптеры.
* Аннотация: Гексагональная архитектура предложена Алистером Кокберном, которая решает проблемы, вызванные традиционной многоуровневой архитектурой. *
1. Преимущества
- Программное обеспечение, разработанное с использованием гексагональной архитектуры, не зависит от канала, поэтому оно может поддерживать несколько каналов.
- Простая замена входящих и исходящих точек интеграции
- Программное обеспечение для тестирования становится проще, поскольку точки интеграции можно легко моделировать.
2. Реализация Java
Согласно приведенному выше описанию, шестиугольная архитектура больше связана с портами и адаптерами. В Java интерфейс используется для определения порта, а класс реализации используется как адаптер. Давайте воспользуемся простым примером приложения Spring Boot, чтобы понять, как применять шестиугольную архитектуру.
Основная функция примера приложения — создание и просмотр информации о сотрудниках. Основная бизнес-логика реализована в «EmployeeService», а объект домена определяется как «Employee». Все они могут рассматриваться как внутренние модули.
**EmployeeService.java** ```java @Service public class EmployeeService < @Autowired private EmployeeRepositoryPort employeeRepository; public void create(String name, String role, long salary)< employeeRepository.create(name, role, salary); >public Employee view(Integer userId) < return employeeRepository.getEmployee(userId); >> ```
**Employee.java** ```java @Entity @Table(name = "employee") public class Employee < @Id @GeneratedValue @Column(name = "id") private Integer id; @Column(name = "name", nullable = false) private String name; @Column(name = "role", nullable = false) private String role; @Column(name = "salary", nullable = false) private long salary; // Setter, Getter методы >```
Теперь пример приложения может предоставлять услуги через REST или механизмы обмена сообщениями. Создайте класс EmployeeControllerAdapter, который реализует интерфейс EmployeeUIPort для предоставления служб REST.
**EmployeeControllerAdapter.java** ```java RestController @RequestMapping("/employees/") public class EmployeeControllerAdapter implements EmployeeUIPort < @Autowired private EmployeeService employeeService; @Override public void create(@RequestBody Employee request) < employeeService.create(request.getName(), request.getRole(), request.getSalary()); >@Override public Employee view(@PathVariable Integer id) < Employee employee = employeeService.view(id); return employee; >> ```
```java public interface EmployeeUIPort < @PostMapping("create") public void create(@RequestBody Employee request); @GetMapping("view/") public Employee view(@PathVariable Integer userId); > ```
В рамках бизнес-логики `EmployeeService` также должен вызывать внешнюю точку интеграции БД. Поэтому мы создали EmployeeRepositoryPort и EmployeeServiceAdapter, реализующие этот интерфейс.
**EmployeeServiceAdapter.java** ```java @Service public class EmployeeServiceAdapter implements EmployeeRepositoryPort < @PersistenceContext private EntityManager entityManager; @Transactional @Override public void create(String name, String role, long salary) < Employee employee = new Employee(); employee.setName(name); employee.setRole(role); employee.setSalary(salary); entityManager.persist(employee); >@Override public Employee getEmployee(Integer userId) < return entityManager.find(Employee.class, userId); >> ```
**EmployeeRepositoryPort.java** ```java public interface EmployeeRepositoryPort < void create(String name, String role, long salary); Employee getEmployee(Integer userId); >```
Итак, мы видим, как EmployeeService использует порт EmployeeUIPort для предоставления услуг, вызывает БД через EmployeeRepositoryPort и предоставляет службы REST API через EmployeeControllerAdapter и EmployeeServiceAdapter.
Подводя итог, шестиугольная архитектура — это метод проектирования, который разделяет приложение на внутреннюю и внешнюю части. Связь с внешними адаптерами через внутренние порты. Применяя этот метод, он может обслуживать несколько каналов и поддерживать несколько различных протоколов, сохраняя при этом основной код варианта использования неизменным. Мало того, он также может эффективно улучшить тестируемость приложений. Тем не менее, не рекомендуется полностью реализовывать гексагональную архитектуру во всем приложении, а выборочно использовать интерфейсы и адаптеры.
Редактор является программистом JAVA с 5-летним опытом работы. Что касается Java, у меня есть интеграция материалов, полный маршрут обучения программированию на Java, учебные материалы и инструменты, которые можно получить в моей группе 830783865 и раздать всем бесплатно. Я надеюсь, что вы своими силами сможете стать следующим отличным программистом.
Пример гексагональной архитектуры на Java
Как разработчикам нам часто приходится сталкиваться с легаси кодом, который тяжело поддерживать. Вы знаете как бывает сложно понять простую логику в большом запутанном спагетти-коде. Улучшение кода или разработка новой функциональности становятся ночным кошмаром для разработчика.
Одна из основных целей проектирования программного обеспечения — это удобство сопровождения. Код, который плохо поддерживать, становится сложными в управлении. Его не только трудно масштабировать, но становится проблемой привлечь новых разработчиков.
В мире ИТ все движется быстрыми темпами. Если вас попросят срочно реализовать новую функциональность или вы захотите перейти с реляционной базы данных на NoSQL, то какая будет ваша первая реакция?
Хорошее тестовое покрытие повышает уверенность разработчиков в том, что с новым релизом не будет проблем. Однако если ваша бизнес-логика переплетена с инфраструктурной логикой, то с ее тестированием могут быть проблемы.
Но хватит пустой болтовни, давайте посмотрим на гексагональную архитектуру. Использование этого шаблона поможет вам улучшить сопровождаемость, тестируемость и получить другие преимущества.
Введение в гексагональную архитектуру
Термин Hexagonal Architecture (гексагональная, шестиугольная архитектура) придумал в 2006 году Алистер Коберн (Alistair Cockburn). Этот архитектурный стиль также известен как Ports And Adapters Architecture (архитектура портов и адаптеров). Говоря простыми словами, компоненты вашего приложения взаимодействуют через множество конечных точек (портов). Для обработки запросов у вас должны быть адаптеры, соответствующие портам.
Здесь можно провести аналогию с USB-портами на компьютере. Вы можете ими воспользоваться, если у вас есть совместимый адаптер (зарядное устройство или флешка).
Эту архитектуру можно схематически представить в виде шестиугольника с бизнес-логикой в самом центре (в ядре), окруженную объектами с которыми она взаимодействует, и компонентами, которые ею управляют, предоставляя входные данные.
В реальной жизни с вашим приложением взаимодействуют и предоставляют входные данные пользователи, вызовы API, автоматизированные скрипты и модульное тестирование. Если ваша бизнес-логика смешана с логикой пользовательского интерфейса, то вы столкнетесь с многочисленными проблемами. Например, будет сложно переключить ввод данных с пользовательского интерфейса на модульные тесты.
Также приложение взаимодействует с внешними объектами, такими как базы данных, очереди сообщений, веб-серверы (через вызовы HTTP API) и т. д. При необходимости мигрировать базу данных или выгрузить данные в файл у вас должна быть возможность это сделать, не затрагивая бизнес-логику.
Как следует из названия “порты и адаптеры”, есть ”порты”, через которые происходит взаимодействие и “адаптеры” — компоненты, обрабатывающие пользовательский ввод и преобразующие его на “язык” домена. Адаптеры инкапсулируют логику взаимодействия с внешними системами, такими как базы данных, очереди сообщений и др., облегчают связь между бизнес-логикой и внешними объектами.
На диаграмме ниже показаны слои, на которые разделено приложение.
Гексагональная архитектура выделяет в приложении три слоя: домен (domain), приложение (application) и инфраструктура (framework):
- Домен. Слой содержит основную бизнес-логику. Он не должен знать детали реализации внешних слоев.
- Приложение. Слой действует как связующее звено между слоями домена и инфраструктуры.
- Инфраструктура. Реализация взаимодействия домена с внешним миром. Внутренние слои выглядят для него как черный ящик.
Левая сторона шестиугольника состоит из компонент, обеспечивающих ввод для домена (они “управляют” приложением), а правая — из компонент, которые управляются нашим приложением.
Пример
Давайте спроектируем приложение, которое будет хранить обзоры фильмов. У пользователя должна быть возможность отправить запрос с названием фильма и получить пять случайных отзывов.
Для простоты сделаем консольное приложение с хранением данных в оперативной памяти. Ответ пользователю будем выводить на консоль.
У нас есть пользователь (User), который отправляет запрос приложению. Таким образом, пользователь становится “управляющим” (driver). Приложение должно уметь получать данные из любого типа хранилища и выводить результаты на консоль или в файл. Управляемыми (driven) объектами будут “хранилище данных” ( IFetchMovieReviews ) и “принтер ответов” ( IPrintMovieReviews ).
На следующем рисунке показаны основные компоненты нашего приложения.
Слева находятся компоненты, которые обеспечивают ввод данных в приложение. Справа — компоненты, которые позволяют взаимодействовать с базой данных и консолью.
Давайте рассмотрим код приложения.
public interface IUserInput
public interface IFetchMovieReviews < public ListfetchMovieReviews(MovieSearchRequest movieSearchRequest); > public interface IPrintMovieReviews < public void writeMovieReviews(ListmovieReviewList); >
Адаптеры управляемого порта
Фильмы будем получить из репозитория фильмов (MovieReviewsRepo). Выводить обзоры фильмов на консоль будет класс ConsolePrinter . Давайте реализуем два вышеупомянутых интерфейса.
public class ConsolePrinter implements IPrintMovieReviews < @Override public void writeMovieReviews(ListmovieReviewList) < movieReviewList.forEach(movieReview ->< System.out.println(movieReview.toString()); >); > > public class MovieReviewsRepo implements IFetchMovieReviews < private Map movieReviewMap; public MovieReviewsRepo() < initialize(); >public List fetchMovieReviews(MovieSearchRequest movieSearchRequest) < return Optional.ofNullable(movieReviewMap.get(movieSearchRequest.getMovieName())) .orElse(new ArrayList<>()); > private void initialize() < this.movieReviewMap = new HashMap<>(); movieReviewMap.put("StarWars", Collections.singletonList(new MovieReview("1", 7.5, "Good"))); movieReviewMap.put("StarTreck", Arrays.asList(new MovieReview("1", 9.5, "Excellent"), new MovieReview("1", 8.5, "Good"))); > >
Домен
Основная задача нашего приложения — обрабатывать запросы пользователей. Необходимо получить фильмы, обработать их и передать результаты “принтеру”. На данный момент у нас есть только одна функциональность — поиск фильмов. Для обработки пользовательских запросов будем использовать стандартный интерфейс Consumer .
Давайте посмотрим на основной класс MovieApp .
public class MovieApp implements Consumer < private IFetchMovieReviews fetchMovieReviews; private IPrintMovieReviews printMovieReviews; private static Random rand = new Random(); public MovieApp(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) < this.fetchMovieReviews = fetchMovieReviews; this.printMovieReviews = printMovieReviews; >private List filterRandomReviews(List movieReviewList) < Listresult = new ArrayList(); // logic to return random reviews for (int index = 0; index < 5; ++index) < if (movieReviewList.size() < 1) break; int randomIndex = getRandomElement(movieReviewList.size()); MovieReview movieReview = movieReviewList.get(randomIndex); movieReviewList.remove(movieReview); result.add(movieReview); >return result; > private int getRandomElement(int size) < return rand.nextInt(size); >public void accept(MovieSearchRequest movieSearchRequest) < ListmovieReviewList = fetchMovieReviews.fetchMovieReviews(movieSearchRequest); List randomReviews = filterRandomReviews(new ArrayList<>(movieReviewList)); printMovieReviews.writeMovieReviews(randomReviews); > >
Теперь определим класс CommandMapperModel , который будет сопоставлять команды с обработчиками.
public class CommandMapperModel < private static final ClasssearchMovies = MovieSearchRequest.class; public static Model build(Consumer displayMovies) < Model model = Model.builder() .user(searchMovies) .system(displayMovies) .build(); return model; >>
Адаптеры управляющего порта
Пользователь будет взаимодействовать с нашей системой через интерфейс IUserInput . Реализация будет использовать ModelRunner и делегировать выполнение.
public class UserCommandBoundary implements IUserInput < private Model model; public UserCommandBoundary(IFetchMovieReviews fetchMovieReviews, IPrintMovieReviews printMovieReviews) < MovieApp movieApp = new MovieApp(fetchMovieReviews, printMovieReviews); model = CommandMapperModel.build(movieApp); >public void handleUserInput(Object userCommand) < new ModelRunner().run(model) .reactTo(userCommand); >>
Теперь посмотрим на пользователя, который использует вышеупомянутый интерфейс.
public class MovieUser < private IUserInput userInputDriverPort; public MovieUser(IUserInput userInputDriverPort) < this.userInputDriverPort = userInputDriverPort; >public void processInput(MovieSearchRequest movieSearchRequest) < userInputDriverPort.handleUserInput(movieSearchRequest); >>
Приложение
Далее, создадим консольное приложение. Управляемые адаптеры добавляем в качестве зависимостей. Пользователь будет создавать и отправлять запрос в приложение. Приложение будет получать данные, обрабатывать и выводить ответ на консоль.
Что можно улучшить, изменить
- В нашей реализации можно легко переключиться с одного хранилища данных на другое. Реализация хранилища может быть внедрена (inject) в код без изменения бизнес-логики. Например, можно перенести данные из памяти в базу данных, написав адаптер базы данных.
- Вместо вывода на консоль можно реализовать “принтер”, который будет записывать данные в файл. В таком многослойном приложении становится проще добавлять функциональность и исправлять ошибки.
- Для проверки бизнес-логики можно написать комплексные тесты. Адаптеры можно тестировать изолированно. Таким образом, можно увеличить общее тестовое покрытие.
Заключение
Можно отметить следующие преимущества гексагональной архитектуры:
- Сопровождаемость — слабосвязанные и независимые слои. Становится легко добавлять новые функции в один слой, не затрагивая других слоев.
- Тестируемость — модульные тесты пишутся просто и быстро выполняются. Можно написать тесты для каждого слоя с использованием объектов-заглушек, имитирующих зависимости. Например, мы можем убрать зависимость от базы данных, сделав хранилище данных в памяти.
- Адаптивность — основная бизнес-логика становится независимой от изменений во внешних объектах. Например, если потребуется мигрировать на другую базу данных то, нам не нужно вносить изменения в домен. Мы можем сделать соответствующий адаптер для базы данных.