Ein Beispiel fĂŒr eine hexagonale Architektur in Java

Eine Übersetzung des Artikels wurde speziell fĂŒr Studenten des Java Developer- Kurses erstellt .





Als Entwickler mĂŒssen wir uns hĂ€ufig mit Legacy-Code befassen, der schwer zu warten ist. Sie wissen, wie schwierig es ist, einfache Logik in einem großen, verschlungenen Spaghetti-Code zu verstehen. Das Verbessern des Codes oder das Entwickeln neuer Funktionen wird fĂŒr den Entwickler zum Albtraum.

Eines der Hauptziele des Software-Designs ist die einfache Wartung. Schlecht gepflegter Code wird schwierig zu verwalten. Es ist nicht nur schwierig zu skalieren, sondern es wird auch zu einem Problem, neue Entwickler zu gewinnen.

In der IT-Welt geht es schnell voran. Was ist Ihre erste Reaktion, wenn Sie aufgefordert werden, dringend neue Funktionen zu implementieren, oder wenn Sie von einer relationalen Datenbank zu NoSQL wechseln möchten?



Eine gute Testabdeckung erhöht das Vertrauen der Entwickler, dass es mit der neuen Version keine Probleme geben wird. Wenn Ihre GeschÀftslogik jedoch mit der Infrastrukturlogik verflochten ist, kann es zu Problemen beim Testen kommen.


Warum ich?

Aber es gibt genug leere GesprÀche. Schauen wir uns die sechseckige Architektur an. Mithilfe dieser Vorlage können Sie die Wartbarkeit, Testbarkeit und andere Vorteile verbessern.

EinfĂŒhrung in die hexagonale Architektur


Der Begriff hexagonale Architektur (hexagonale, hexagonale Architektur) wurde 2006 von Alistair Cockburn geprĂ€gt. Dieser Baustil wird auch als Ports And Adapter Architecture bezeichnet . Mit einfachen Worten, die Komponenten Ihrer Anwendung interagieren ĂŒber viele Endpunkte (Ports). Um Anforderungen verarbeiten zu können, benötigen Sie Adapter, die den Ports entsprechen.

Hier können Sie eine Analogie zu den USB-AnschlĂŒssen des Computers ziehen. Sie können sie verwenden, wenn Sie einen kompatiblen Adapter (LadegerĂ€t oder Flash-Laufwerk) haben.



Diese Architektur kann schematisch als Sechseck mit GeschÀftslogik in der Mitte (im Kern) dargestellt werden, umgeben von den Objekten, mit denen sie interagiert, und den Komponenten, die sie steuern und Eingabedaten bereitstellen.

Im wirklichen Leben interagieren Benutzer, API-Aufrufe, automatisierte Skripte und Komponententests und geben Eingaben fĂŒr Ihre Anwendung. Wenn Ihre GeschĂ€ftslogik mit der BenutzeroberflĂ€chenlogik gemischt ist, treten viele Probleme auf. Beispielsweise wird es schwierig sein, die Dateneingabe von der BenutzeroberflĂ€che auf Komponententests umzustellen.

Die Anwendung interagiert auch mit externen Objekten wie Datenbanken, Nachrichtenwarteschlangen, Webservern (ĂŒber HTTP-API-Aufrufe) usw. Migrieren Sie bei Bedarf die Datenbank oder laden Sie Daten in eine Datei hoch. Dies sollte möglich sein, ohne das GeschĂ€ft zu beeintrĂ€chtigen. Logik.

Wie der Name schon sagt, gibt es " Ports und Adapter ", "Ports", ĂŒber die Interaktion stattfindet, und "Adapter" sind Komponenten, die Benutzereingaben verarbeiten und in die "Sprache" der DomĂ€ne konvertieren. Adapter kapseln die Logik der Interaktion mit externen Systemen wie Datenbanken, Nachrichtenwarteschlangen usw. und erleichtern die Kommunikation zwischen GeschĂ€ftslogik und externen Objekten.

Das folgende Diagramm zeigt die Ebenen, in die die Anwendung unterteilt ist.



Die hexagonale Architektur unterscheidet drei Ebenen in einer Anwendung: DomÀne, Anwendung und Infrastruktur:

  • Domain . Die Schicht enthĂ€lt die KerngeschĂ€ftslogik. Er muss die Details der Implementierung der Ă€ußeren Schichten nicht kennen.
  • Anwendung . Eine Schicht fungiert als BrĂŒcke zwischen den Schichten einer DomĂ€ne und der Infrastruktur.
  • Infrastruktur . Die Implementierung der Interaktion der DomĂ€ne mit der Außenwelt. Die inneren Schichten sehen fĂŒr ihn wie eine Black Box aus.

GemĂ€ĂŸ dieser Architektur interagieren zwei Arten von Teilnehmern mit der Anwendung: primĂ€r (Treiber) und sekundĂ€r (gesteuert). Hauptakteure senden Anfragen und verwalten die Anwendung (z. B. Benutzer oder automatisierte Tests). Die sekundĂ€ren stellen die Infrastruktur fĂŒr die Kommunikation mit der Außenwelt bereit (dies sind Datenbankadapter, TCP- oder HTTP-Clients).
Dies kann wie folgt dargestellt werden: Die



linke Seite des Sechsecks besteht aus Komponenten, die Eingaben fĂŒr die DomĂ€ne bereitstellen (sie „steuern“ die Anwendung), und die rechte Seite besteht aus Komponenten, die von unserer Anwendung gesteuert werden.

Beispiel


Lassen Sie uns eine Anwendung entwerfen, in der Filmkritiken gespeichert werden. Der Benutzer sollte in der Lage sein, eine Anfrage mit dem Namen des Films zu senden und fĂŒnf zufĂ€llige Bewertungen zu erhalten.

Der Einfachheit halber erstellen wir eine Konsolenanwendung mit Datenspeicherung im RAM. Die Antwort an den Benutzer wird auf der Konsole angezeigt.

Wir haben einen Benutzer (Benutzer), der eine Anfrage an die Anwendung sendet. So wird der Benutzer zum „Manager“ (Fahrer). Die Anwendung sollte in der Lage sein, Daten von jedem Speichertyp zu empfangen und die Ergebnisse an die Konsole oder in eine Datei auszugeben. Verwaltete (gesteuerte) Objekte sind das "Data Warehouse" ( IFetchMovieReviews) und der "Antwortdrucker" ( IPrintMovieReviews).

Die folgende Abbildung zeigt die Hauptkomponenten unserer Anwendung.



Auf der linken Seite befinden sich die Komponenten, die die Dateneingabe in die Anwendung ermöglichen. Auf der rechten Seite befinden sich die Komponenten, mit denen Sie mit der Datenbank und der Konsole interagieren können.

Schauen wir uns den Anwendungscode an.

Steueranschluss

public interface IUserInput {
    public void handleUserInput(Object userCommand);
}

Verwaltete Ports

public interface IFetchMovieReviews {
    public List<MovieReview> fetchMovieReviews(MovieSearchRequest movieSearchRequest);
}

public interface IPrintMovieReviews {
    public void writeMovieReviews(List<MovieReview> movieReviewList);
}

Verwaltete

Portadapter Filme werden aus dem Film-Repository (MovieReviewsRepo) abgerufen. Das Anzeigen von Filmkritiken auf der Konsole ist eine Klasse ConsolePrinter. Lassen Sie uns die beiden oben genannten Schnittstellen implementieren.

public class ConsolePrinter implements IPrintMovieReviews {
    @Override
    public void writeMovieReviews(List<MovieReview> movieReviewList) {
        movieReviewList.forEach(movieReview -> {
            System.out.println(movieReview.toString());
        });
    }
}

public class MovieReviewsRepo implements IFetchMovieReviews {
    private Map<String, List<MovieReview>> movieReviewMap;

    public MovieReviewsRepo() {
        initialize();
    }

    public List<MovieReview> 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")));
    }
}

Domain


Die Hauptaufgabe unserer Anwendung ist die Bearbeitung von Benutzeranfragen. Sie mĂŒssen die Filme erhalten, verarbeiten und die Ergebnisse auf den „Drucker“ ĂŒbertragen. Im Moment haben wir nur eine FunktionalitĂ€t - die Filmsuche. Zur Bearbeitung von Benutzeranfragen verwenden wir die Standardschnittstelle Consumer.

Schauen wir uns die Hauptklasse an MovieApp.

public class MovieApp implements Consumer<MovieSearchRequest> {
    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<MovieReview> filterRandomReviews(List<MovieReview> movieReviewList) {
        List<MovieReview> result = new ArrayList<MovieReview>();
        // 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) {
        List<MovieReview> movieReviewList = fetchMovieReviews.fetchMovieReviews(movieSearchRequest);
        List<MovieReview> randomReviews = filterRandomReviews(new ArrayList<>(movieReviewList));
        printMovieReviews.writeMovieReviews(randomReviews);
    }
}

Jetzt definieren wir eine Klasse CommandMapperModel, die Handlern Befehle zuordnet.

public class CommandMapperModel {
    private static final Class<MovieSearchRequest> searchMovies = MovieSearchRequest.class;

    public static Model build(Consumer<MovieSearchRequest> displayMovies) {
        Model model = Model.builder()
            .user(searchMovies)
            .system(displayMovies)
            .build();

        return model;
    }
}

Steueranschlussadapter


Der Benutzer wird ĂŒber die BenutzeroberflĂ€che mit unserem System interagieren IUserInput. Die Implementierung verwendet ModelRunnerund delegiert die AusfĂŒhrung.

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);
    }
}

Schauen wir uns nun den Benutzer an, der die obige OberflÀche verwendet.

public class MovieUser {
    private IUserInput userInputDriverPort;

    public MovieUser(IUserInput userInputDriverPort) {
        this.userInputDriverPort = userInputDriverPort;
    }

    public void processInput(MovieSearchRequest movieSearchRequest) {
        userInputDriverPort.handleUserInput(movieSearchRequest);
    }
}

Anwendung


Erstellen Sie als NĂ€chstes eine Konsolenanwendung. Verwaltete Adapter werden als AbhĂ€ngigkeiten hinzugefĂŒgt. Der Benutzer erstellt eine Anfrage und sendet sie an die Anwendung. Die Anwendung empfĂ€ngt Daten, verarbeitet sie und zeigt eine Antwort an die Konsole an.

public class Main {

    public static void main(String[] args) {
        IFetchMovieReviews fetchMovieReviews = new MovieReviewsRepo();
        IPrintMovieReviews printMovieReviews = new ConsolePrinter();
        IUserInput userCommandBoundary = new UserCommandBoundary(fetchMovieReviews, printMovieReviews);
        MovieUser movieUser = new MovieUser(userCommandBoundary);
        MovieSearchRequest starWarsRequest = new MovieSearchRequest("StarWars");
        MovieSearchRequest starTreckRequest = new MovieSearchRequest("StarTreck");

        System.out.println("Displaying reviews for movie " + starTreckRequest.getMovieName());
        movieUser.processInput(starTreckRequest);
        System.out.println("Displaying reviews for movie " + starWarsRequest.getMovieName());
        movieUser.processInput(starWarsRequest);
    }

}

Was verbessert werden kann, Àndert sich


  • In unserer Implementierung können Sie problemlos von einem Datenspeicher zu einem anderen wechseln. Die Speicherimplementierung kann in den Code eingefĂŒgt werden, ohne die GeschĂ€ftslogik zu Ă€ndern. Sie können beispielsweise Daten aus dem Speicher in eine Datenbank ĂŒbertragen, indem Sie einen Datenbankadapter schreiben.
  • Anstelle der Ausgabe an die Konsole können Sie einen „Drucker“ implementieren, der Daten in eine Datei schreibt. In einer solchen mehrschichtigen Anwendung wird es einfacher, Funktionen hinzuzufĂŒgen und Fehler zu beheben.
  • Um die GeschĂ€ftslogik zu testen, können Sie komplexe Tests schreiben. Adapter können isoliert getestet werden. Somit ist es möglich, die gesamte Testabdeckung zu erhöhen.

Fazit


Die folgenden Vorteile der hexagonalen Architektur können festgestellt werden:

  • Begleitung - lose gekoppelte und unabhĂ€ngige Schichten. Es wird einfach, einer Ebene neue Funktionen hinzuzufĂŒgen, ohne andere Ebenen zu beeinflussen.
  • Testbarkeit - Unit-Tests werden einfach und schnell geschrieben. Sie können Tests fĂŒr jede Ebene mit Stub-Objekten schreiben, die AbhĂ€ngigkeiten simulieren. Zum Beispiel können wir die AbhĂ€ngigkeit von der Datenbank entfernen, indem wir ein Data Warehouse im Speicher erstellen.
  • AnpassungsfĂ€higkeit - Die HauptgeschĂ€ftslogik wird unabhĂ€ngig von Änderungen an externen Objekten. Wenn Sie beispielsweise in eine andere Datenbank migrieren mĂŒssen, mĂŒssen wir keine Änderungen an der DomĂ€ne vornehmen. Wir können einen geeigneten Adapter fĂŒr die Datenbank erstellen.

Verweise



Das ist alles. Wir sehen uns auf dem Kurs !

All Articles