Architektura heksagonalna w Javie w 3 minuty

Chciałbym w tym krótkim artykule pokazać przykłady użycia architektury heksagonalnej (ang. Hexagonal Architecture). Zaznaczam jednak, że nie wyczerpię tu tematu, a raczej pokażę pewne podejście oraz implementację podstaw w Javie. Architektura heksagonalna (lub Porty i adaptery) została zapoczątkowana przez Alistaira Cockburn'a. Jej głównym celem jest unikanie niektórych znanych problemów konstrukcyjnych w projektowaniu oprogramowania, np. zanieczyszczania kodu UI logiką biznesową lub niepożądanych zależności między warstwami.
Architektura ta ma zatem na celu tworzenie luźno powiązanych komponentów, które można połączyć za pomocą „portów” i „adapterów” (patrz Ilustracja 1). W rezultacie sprawia to, że komponenty można wymieniać na dowolnym poziomie, a testowanie pojedynczych części jest łatwiejsze.
Ilustracja 1. Architektura heksagonalna
Definicja
Słowo „port” przywołuje na myśl porty w gadżetach elektronicznych. Dla każdego urządzenia zewnętrznego istnieje „adapter”, który konwertuje protokół wymagany przez to urządzenie i odwrotnie. Adaptery mogą się również kojarzyć z elementami scalającymi komponenty aplikacji ze światem zewnętrznym, dlatego też jednemu portowi może odpowiadać wiele adapterów. Warto również zwrócić uwagę na to, że wszystkie urządzenia zewnętrzne są identyczne z punktu widzenia aplikacji.
Nie należy jednak myśleć, że słowo „heksagonalny” lub „sześciokątny” ma tutaj znaczenie. Celem nie było zasugerowanie, że potrzebnych jest sześć portów lub adapterów. Chodzi raczej o to, aby pozostawić wystarczająco dużo miejsca do zobrazowania różnych interfejsów między aplikacją a światem zewnętrznym. Dlatego zostało wybrane słowo „heksagonalny”. Naturalnym odpowiednikiem portu w Javie jest interfejs, a adapter jest jedną implementacją tego interfejsu.
Przykład
Za przykład posłuży nam aplikacja do przechowywania książek. Dla uproszczenia wyszukujemy tylko zapisane książki. Książki można przechowywać w bazie danych i przeszukiwać za pomocą API (HTTP, przeglądarka i tak dalej). Poniższa ilustracja przedstawia implementację w Javie.
Ilustracja 2. Przykład architektury heksagonalnej w Javie
Najpierw definiujemy encję naszej aplikacji:
public class Book {
private String title;
private String isbn;
private String author;
// standard constructor and getters
}
Następnie definiujemy odpowiednik portu przychodzącego:
public interface ApiInterface {
Book get(String isbn);
}
Teraz definiujemy port wyjściowy:
public interface BookDaoInterface {
Book get(String isbn);
}
Następnie implementujemy wcześniej zdefiniowany port wyjściowy. Stąd odpowiednik adaptera:
public class BookDaoPostgres implements BookDaoInterface {
public Book get(String isbn) {
// TODO implement PostgreSQL logic here
return null;
}
}
Oto alternatywny adapter dla tego samego interfejsu:
public class BookDaoMock implements BookDaoInterface {
private HashMap<String, Book> books = new HashMap<String, Book>();
public BookDaoMock() {
books.put("mock", new Book("mock", "mock", "mock"));
}
public Book get(String isbn) {
return books.get(isbn);
}
}
Następnie definiujemy usługę domenową dla naszej aplikacji:
public class BookService {
private BookDaoInterface dao;
public BookService(BookDaoInterface bookDao) {
dao = bookDao;
}
public Book search(String isbn) {
return dao.get(isbn);
}
}
No koniec tworzymy adapter dla portu wejściowego:
public class HttpApi implements ApiInterface {
private BookService service;
public HttpApi(BookService service) {
this.service = service;
}
// TODO implement HTTP endpoint
public Book get(String isbn) {
return service.search(isbn);
}
}
Podsumowanie
Używając takiego podejścia, będziesz mieć większą „elastyczność” w zastępowaniu różnych zależności. W rezultacie zwiększysz ogólną „testowalność” aplikacji.
Oryginał tekst w języku angielskim przeczytasz tutaj.