Deutsch

Lernen Sie, wie Hexagonale Architektur (Ports und Adapter) Ihre Apps flexibler, testbarer und wartbarer macht. Ein praktischer Leitfaden mit umsetzbaren Beispielen für Entwickler.

Hexagonale Architektur: Ein praktischer Leitfaden zu Ports und Adaptern

In der sich ständig weiterentwickelnden Softwareentwicklung ist der Bau robuster, wartbarer und testbarer Anwendungen von größter Bedeutung. Die Hexagonale Architektur, auch bekannt als Ports und Adapter, ist ein Architekturmuster, das diesen Anliegen begegnet, indem es die Kernlogik einer Anwendung von ihren externen Abhängigkeiten entkoppelt. Dieser Leitfaden zielt darauf ab, ein umfassendes Verständnis der Hexagonalen Architektur, ihrer Vorteile und praktischer Implementierungsstrategien für Entwickler weltweit zu vermitteln.

Was ist Hexagonale Architektur?

Die Hexagonale Architektur, geprägt von Alistair Cockburn, dreht sich um die Idee, die Kernlogik der Anwendung von ihrer Außenwelt zu isolieren. Diese Isolation wird durch die Verwendung von Ports und Adaptern erreicht.

Stellen Sie es sich so vor: Die Kernanwendung sitzt im Zentrum, umgeben von einer hexagonalen Hülle. Die Ports sind die Ein- und Ausgangspunkte auf dieser Hülle, und die Adapter stecken in diesen Ports und verbinden den Kern mit der Außenwelt.

Grundprinzipien der Hexagonalen Architektur

Mehrere Schlüsselprinzipien untermauern die Effektivität der Hexagonalen Architektur:

Vorteile der Hexagonalen Architektur

Die Einführung der Hexagonalen Architektur bietet zahlreiche Vorteile:

Implementierung der Hexagonalen Architektur: Ein praktisches Beispiel

Lassen Sie uns die Implementierung der Hexagonalen Architektur anhand eines vereinfachten Beispiels eines Benutzerregistrierungssystems veranschaulichen. Zur Klarheit verwenden wir eine hypothetische Programmiersprache (ähnlich Java oder C#).

1. Den Kern (Anwendung) definieren

Die Kernanwendung enthält die Geschäftslogik zur Registrierung eines neuen Benutzers.


// Kern/UserService.java (oder UserService.cs)
public class UserService {
    private final UserRepository userRepository;
    private final PasswordHasher passwordHasher;
    private final UserValidator userValidator;

    public UserService(UserRepository userRepository, PasswordHasher passwordHasher, UserValidator userValidator) {
        this.userRepository = userRepository;
        this.passwordHasher = passwordHasher;
        this.userValidator = userValidator;
    }

    public Result<User, String> registerUser(String username, String password, String email) {
        // Benutzereingabe validieren
        ValidationResult validationResult = userValidator.validate(username, password, email);
        if (!validationResult.isValid()) {
            return Result.failure(validationResult.getErrorMessage());
        }

        // Prüfen, ob Benutzer bereits existiert
        if (userRepository.findByUsername(username).isPresent()) {
            return Result.failure("Benutzername existiert bereits");
        }

        // Passwort hashen
        String hashedPassword = passwordHasher.hash(password);

        // Neuen Benutzer erstellen
        User user = new User(username, hashedPassword, email);

        // Benutzer im Repository speichern
        userRepository.save(user);

        return Result.success(user);
    }
}

2. Die Ports definieren

Wir definieren die Ports, die die Kernanwendung zur Interaktion mit der Außenwelt verwendet.


// Ports/UserRepository.java (oder UserRepository.cs)
public interface UserRepository {
    Optional<User> findByUsername(String username);
    void save(User user);
}

// Ports/PasswordHasher.java (oder PasswordHasher.cs)
public interface PasswordHasher {
    String hash(String password);
}

//Ports/UserValidator.java (oder UserValidator.cs)
public interface UserValidator{
  ValidationResult validate(String username, String password, String email);
}

//Ports/ValidationResult.java (oder ValidationResult.cs)
public interface ValidationResult{
  boolean isValid();
  String getErrorMessage();
}

3. Die Adapter definieren

Wir implementieren die Adapter, die die Kernanwendung mit spezifischen Technologien verbinden.


// Adapter/DatabaseUserRepository.java (oder DatabaseUserRepository.cs)
public class DatabaseUserRepository implements UserRepository {
    private final DatabaseConnection databaseConnection;

    public DatabaseUserRepository(DatabaseConnection databaseConnection) {
        this.databaseConnection = databaseConnection;
    }

    @Override
    public Optional<User> findByUsername(String username) {
        // Implementierung mit JDBC, JPA oder einer anderen Datenbankzugriffstechnologie
        // ...
        return Optional.empty(); // Platzhalter
    }

    @Override
    public void save(User user) {
        // Implementierung mit JDBC, JPA oder einer anderen Datenbankzugriffstechnologie
        // ...
    }
}

// Adapter/BCryptPasswordHasher.java (oder BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
    @Override
    public String hash(String password) {
        // Implementierung mit BCrypt-Bibliothek
        // ...
        return "hashedPassword"; //Platzhalter
    }
}

//Adapter/SimpleUserValidator.java (oder SimpleUserValidator.cs)
public class SimpleUserValidator implements UserValidator {
  @Override
  public ValidationResult validate(String username, String password, String email){
    //Einfache Validierungslogik
     if (username == null || username.isEmpty()) {
            return new SimpleValidationResult(false, "Benutzername darf nicht leer sein");
        }
        if (password == null || password.length() < 8) {
            return new SimpleValidationResult(false, "Passwort muss mindestens 8 Zeichen lang sein");
        }
        if (email == null || !email.contains("@")) {
            return new SimpleValidationResult(false, "Ungültiges E-Mail-Format");
        }

        return new SimpleValidationResult(true, null);
  }
}

//Adapter/SimpleValidationResult.java (oder SimpleValidationResult.cs)
public class SimpleValidationResult implements ValidationResult {
  private final boolean valid;
  private final String errorMessage;

    public SimpleValidationResult(boolean valid, String errorMessage) {
        this.valid = valid;
        this.errorMessage = errorMessage;
    }
  @Override
  public boolean isValid(){
    return valid;
  }

  @Override
  public String getErrorMessage(){
    return errorMessage;
  }
}



//Adapter/WebUserController.java (oder WebUserController.cs)
//Treibender Adapter - verarbeitet Anfragen aus dem Web
public class WebUserController {
    private final UserService userService;

    public WebUserController(UserService userService) {
        this.userService = userService;
    }

    public String registerUser(String username, String password, String email) {
        Result<User, String> result = userService.registerUser(username, password, email);
        if (result.isSuccess()) {
            return "Registrierung erfolgreich!";
        } else {
            return "Registrierung fehlgeschlagen: " + result.getFailure();
        }
    }
}


4. Komposition

Alles miteinander verbinden. Beachten Sie, dass diese Komposition (Dependency Injection) typischerweise am Einstiegspunkt der Anwendung oder innerhalb eines Dependency-Injection-Containers stattfindet.


//Hauptklasse oder Konfiguration der Abhängigkeitsinjektion
public class Main {
    public static void main(String[] args) {
        // Instanzen der Adapter erstellen
        DatabaseConnection databaseConnection = new DatabaseConnection("jdbc:mydb://localhost:5432/users", "user", "password");
        DatabaseUserRepository userRepository = new DatabaseUserRepository(databaseConnection);
        BCryptPasswordHasher passwordHasher = new BCryptPasswordHasher();
        SimpleUserValidator userValidator = new SimpleUserValidator();

        // Eine Instanz der Kernanwendung erstellen und die Adapter injizieren
        UserService userService = new UserService(userRepository, passwordHasher, userValidator);

        //Einen treibenden Adapter erstellen und ihn mit dem Dienst verbinden
        WebUserController userController = new WebUserController(userService);

        //Jetzt können Sie Benutzerregistrierungsanfragen über den userController verarbeiten
        String result = userController.registerUser("john.doe", "P@sswOrd123", "john.doe@example.com");
        System.out.println(result);
    }
}



//DatabaseConnection ist nur eine einfache Klasse zu Demonstrationszwecken
class DatabaseConnection {
    private String url;
    private String username;
    private String password;

    public DatabaseConnection(String url, String username, String password) {
        this.url = url;
        this.username = username;
        this.password = password;
    }

    // ... Methoden zur Verbindung mit der Datenbank (aus Platzgründen nicht implementiert)
}

//Result-Klasse (ähnlich Either in der funktionalen Programmierung)
class Result<T, E> {
    private final T success;
    private final E failure;
    private final boolean isSuccess;

    private Result(T success, E failure, boolean isSuccess) {
        this.success = success;
        this.failure = failure;
        this.isSuccess = isSuccess;
    }

    public static <T, E> Result<T, E> success(T value) {
        return new Result<>(value, null, true);
    }

    public static <T, E> Result<T, E> failure(E error) {
        return new Result<>(null, error, false);
    }

    public boolean isSuccess() {
        return isSuccess;
    }

    public T getSuccess() {
        if (!isSuccess) {
            throw new IllegalStateException("Ergebnis ist ein Fehler");
        }
        return success;
    }

    public E getFailure() {
        if (isSuccess) {
            throw new IllegalStateException("Ergebnis ist ein Erfolg");
        }
        return failure;
    }
}

class User {
    private String username;
    private String password;
    private String email;

    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }

    // Getter und Setter (aus Platzgründen weggelassen)

}

Erklärung:

Fortgeschrittene Überlegungen und Best Practices

Obwohl die Grundprinzipien der Hexagonalen Architektur unkompliziert sind, gibt es einige fortgeschrittene Überlegungen zu beachten:

Praxisbeispiele der Hexagonalen Architektur im Einsatz

Viele erfolgreiche Unternehmen und Projekte haben die Hexagonale Architektur übernommen, um robuste und wartbare Systeme zu bauen:

Herausforderungen und Kompromisse

Obwohl die Hexagonale Architektur erhebliche Vorteile bietet, ist es wichtig, die damit verbundenen Herausforderungen und Kompromisse anzuerkennen:

Es ist entscheidend, die Vorteile und Herausforderungen der Hexagonalen Architektur im Kontext Ihrer spezifischen Projektanforderungen und Teamfähigkeiten sorgfältig zu bewerten. Es ist keine Wunderwaffe und möglicherweise nicht die beste Wahl für jedes Projekt.

Fazit

Die Hexagonale Architektur, mit ihrem Schwerpunkt auf Ports und Adaptern, bietet einen leistungsstarken Ansatz zum Bau wartbarer, testbarer und flexibler Anwendungen. Durch die Entkopplung der Kerngeschäftslogik von externen Abhängigkeiten ermöglicht sie es Ihnen, sich mühelos an sich ändernde Technologien und Anforderungen anzupassen. Obwohl es Herausforderungen und Kompromisse zu berücksichtigen gilt, überwiegen die Vorteile der Hexagonalen Architektur oft die Kosten, insbesondere bei komplexen und langlebigen Anwendungen. Durch die Übernahme der Prinzipien der Abhängigkeitsinversion und expliziten Schnittstellen können Sie Systeme schaffen, die widerstandsfähiger, leichter zu verstehen und besser gerüstet sind, um den Anforderungen der modernen Softwarelandschaft gerecht zu werden.

Dieser Leitfaden hat einen umfassenden Überblick über die Hexagonale Architektur gegeben, von ihren Grundprinzipien bis zu praktischen Implementierungsstrategien. Wir ermutigen Sie, diese Konzepte weiter zu erforschen und sie in Ihren eigenen Projekten anzuwenden. Die Investition in das Erlernen und die Übernahme der Hexagonalen Architektur wird sich zweifellos langfristig auszahlen und zu qualitativ hochwertigerer Software und zufriedeneren Entwicklungsteams führen.

Letztendlich hängt die Wahl der richtigen Architektur von den spezifischen Anforderungen Ihres Projekts ab. Berücksichtigen Sie Komplexität, Langlebigkeit und Wartbarkeitsanforderungen bei Ihrer Entscheidung. Die Hexagonale Architektur bietet eine solide Grundlage für den Bau robuster und anpassungsfähiger Anwendungen, ist aber nur ein Werkzeug im Werkzeugkasten des Softwarearchitekten.