Polski

Dowiedz się, jak architektura heksagonalna, znana również jako porty i adaptery, może poprawić łatwość konserwacji, testowalność i elastyczność Twoich aplikacji. Ten przewodnik zawiera praktyczne przykłady i przydatne informacje dla programistów na całym świecie.

Architektura Heksagonalna: Praktyczny przewodnik po portach i adapterach

W stale zmieniającym się krajobrazie rozwoju oprogramowania, budowanie solidnych, łatwych w utrzymaniu i testowaniu aplikacji jest najważniejsze. Architektura heksagonalna, znana również jako Porty i Adaptery, to wzorzec architektoniczny, który odpowiada na te obawy poprzez oddzielenie podstawowej logiki biznesowej aplikacji od jej zewnętrznych zależności. Niniejszy przewodnik ma na celu zapewnienie kompleksowego zrozumienia architektury heksagonalnej, jej zalet i praktycznych strategii implementacji dla programistów na całym świecie.

Co to jest Architektura Heksagonalna?

Architektura heksagonalna, której autorem jest Alistair Cockburn, koncentruje się wokół idei izolowania podstawowej logiki biznesowej aplikacji od jej świata zewnętrznego. Izolacja ta jest osiągana poprzez użycie portów i adapterów.

Pomyśl o tym w ten sposób: rdzeń aplikacji znajduje się w centrum, otoczony heksagonalną powłoką. Porty są punktami wejścia i wyjścia na tej powłoce, a adaptery podłączają się do tych portów, łącząc rdzeń ze światem zewnętrznym.

Kluczowe zasady architektury heksagonalnej

Kilka kluczowych zasad leży u podstaw skuteczności architektury heksagonalnej:

Korzyści z używania architektury heksagonalnej

Przyjęcie architektury heksagonalnej oferuje liczne zalety:

Implementacja architektury heksagonalnej: praktyczny przykład

Zilustrujmy implementację architektury heksagonalnej uproszczonym przykładem systemu rejestracji użytkowników. Dla jasności użyjemy hipotetycznego języka programowania (podobnego do Java lub C#).

1. Zdefiniuj rdzeń (aplikację)

Rdzeń aplikacji zawiera logikę biznesową rejestracji nowego użytkownika.


// Core/UserService.java (lub 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) {
        // Validate user input
        ValidationResult validationResult = userValidator.validate(username, password, email);
        if (!validationResult.isValid()) {
            return Result.failure(validationResult.getErrorMessage());
        }

        // Check if user already exists
        if (userRepository.findByUsername(username).isPresent()) {
            return Result.failure("Username already exists");
        }

        // Hash the password
        String hashedPassword = passwordHasher.hash(password);

        // Create a new user
        User user = new User(username, hashedPassword, email);

        // Save the user to the repository
        userRepository.save(user);

        return Result.success(user);
    }
}

2. Zdefiniuj porty

Definiujemy porty, których rdzeń aplikacji używa do interakcji ze światem zewnętrznym.


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

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

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

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

3. Zdefiniuj adaptery

Implementujemy adaptery, które łączą rdzeń aplikacji z określonymi technologiami.


// Adapters/DatabaseUserRepository.java (lub 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) {
        // Implementation using JDBC, JPA, or another database access technology
        // ...
        return Optional.empty(); // Placeholder
    }

    @Override
    public void save(User user) {
        // Implementation using JDBC, JPA, or another database access technology
        // ...
    }
}

// Adapters/BCryptPasswordHasher.java (lub BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
    @Override
    public String hash(String password) {
        // Implementation using BCrypt library
        // ...
        return "hashedPassword"; //Placeholder
    }
}

//Adapters/SimpleUserValidator.java (lub SimpleUserValidator.cs)
public class SimpleUserValidator implements UserValidator {
  @Override
  public ValidationResult validate(String username, String password, String email){
    //Simple Validation logic
     if (username == null || username.isEmpty()) {
            return new SimpleValidationResult(false, "Username cannot be empty");
        }
        if (password == null || password.length() < 8) {
            return new SimpleValidationResult(false, "Password must be at least 8 characters long");
        }
        if (email == null || !email.contains("@")) {
            return new SimpleValidationResult(false, "Invalid email format");
        }

        return new SimpleValidationResult(true, null);
  }
}

//Adapters/SimpleValidationResult.java (lub 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;
  }
}



//Adapters/WebUserController.java (lub WebUserController.cs)
//Driving Adapter - handles requests from the 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 "Registration successful!";
        } else {
            return "Registration failed: " + result.getFailure();
        }
    }
}


4. Kompozycja

Łączenie wszystkiego razem. Należy pamiętać, że ta kompozycja (wstrzykiwanie zależności) zwykle odbywa się w punkcie wejścia aplikacji lub w kontenerze wstrzykiwania zależności.


//Main class or dependency injection configuration
public class Main {
    public static void main(String[] args) {
        // Create instances of the adapters
        DatabaseConnection databaseConnection = new DatabaseConnection("jdbc:mydb://localhost:5432/users", "user", "password");
        DatabaseUserRepository userRepository = new DatabaseUserRepository(databaseConnection);
        BCryptPasswordHasher passwordHasher = new BCryptPasswordHasher();
        SimpleUserValidator userValidator = new SimpleUserValidator();

        // Create an instance of the core application, injecting the adapters
        UserService userService = new UserService(userRepository, passwordHasher, userValidator);

        //Create a driving adapter and connect it to the service
        WebUserController userController = new WebUserController(userService);

        //Now you can handle user registration requests through the userController
        String result = userController.registerUser("john.doe", "P@sswOrd123", "john.doe@example.com");
        System.out.println(result);
    }
}



//DatabaseConnection is a simple class for demonstration purposes only
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;
    }

    // ... methods to connect to the database (not implemented for brevity)
}

//Result class (similar to Either in functional programming)
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("Result is a failure");
        }
        return success;
    }

    public E getFailure() {
        if (isSuccess) {
            throw new IllegalStateException("Result is a success");
        }
        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;
    }

    // getters and setters (omitted for brevity)

}

Wyjaśnienie:

Zaawansowane zagadnienia i najlepsze praktyki

Chociaż podstawowe zasady architektury heksagonalnej są proste, należy pamiętać o kilku zaawansowanych kwestiach:

Przykłady użycia architektury heksagonalnej w świecie rzeczywistym

Wiele odnoszących sukcesy firm i projektów przyjęło architekturę heksagonalną, aby budować solidne i łatwe w utrzymaniu systemy:

Wyzwania i kompromisy

Chociaż architektura heksagonalna oferuje znaczące korzyści, ważne jest, aby uznać związane z nią wyzwania i kompromisy:

Konieczne jest dokładne przeanalizowanie korzyści i wyzwań architektury heksagonalnej w kontekście konkretnych wymagań projektu i możliwości zespołu. Nie jest to panaceum i może nie być najlepszym wyborem dla każdego projektu.

Wniosek

Architektura heksagonalna, z naciskiem na porty i adaptery, zapewnia potężne podejście do budowania łatwych w utrzymaniu, testowaniu i elastycznych aplikacji. Oddzielając podstawową logikę biznesową od zewnętrznych zależności, umożliwia dostosowanie się do zmieniających się technologii i wymagań z łatwością. Chociaż należy wziąć pod uwagę wyzwania i kompromisy, korzyści płynące z architektury heksagonalnej często przewyższają koszty, szczególnie w przypadku złożonych i długowiecznych aplikacji. Przyjmując zasady odwrócenia zależności i jawnych interfejsów, możesz tworzyć systemy, które są bardziej odporne, łatwiejsze do zrozumienia i lepiej przygotowane do sprostania wymaganiom współczesnego krajobrazu oprogramowania.

Ten przewodnik zawiera kompleksowy przegląd architektury heksagonalnej, od jej podstawowych zasad po praktyczne strategie implementacji. Zachęcamy do dalszego zgłębiania tych koncepcji i eksperymentowania z ich stosowaniem we własnych projektach. Inwestycja w naukę i przyjęcie architektury heksagonalnej bez wątpienia opłaci się w dłuższej perspektywie, prowadząc do oprogramowania wyższej jakości i bardziej zadowolonych zespołów programistycznych.

Ostatecznie wybór odpowiedniej architektury zależy od konkretnych potrzeb Twojego projektu. Rozważ wymagania dotyczące złożoności, trwałości i łatwości konserwacji podczas podejmowania decyzji. Architektura heksagonalna stanowi solidny fundament do budowania solidnych i adaptowalnych aplikacji, ale jest to tylko jedno narzędzie w zestawie narzędzi architekta oprogramowania.

Architektura Heksagonalna: Praktyczny przewodnik po portach i adapterach | MLOG