Čeština

Zjistěte, jak hexagonální architektura, známá také jako porty a adaptéry, může zlepšit udržovatelnost, testovatelnost a flexibilitu vašich aplikací. Tato příručka poskytuje praktické příklady a akční poznatky pro vývojáře po celém světě.

Hexagonální architektura: Praktický průvodce porty a adaptéry

V neustále se vyvíjejícím prostředí vývoje softwaru je budování robustních, udržovatelných a testovatelných aplikací prvořadé. Hexagonální architektura, známá také jako porty a adaptéry, je architektonický vzor, ​​který řeší tyto obavy oddělením základní obchodní logiky aplikace od jejích externích závislostí. Tato příručka si klade za cíl poskytnout komplexní pochopení hexagonální architektury, jejích výhod a praktických strategií implementace pro vývojáře po celém světě.

Co je hexagonální architektura?

Hexagonální architektura, kterou vytvořil Alistair Cockburn, se točí kolem myšlenky izolace základní obchodní logiky aplikace od jejího vnějšího světa. Této izolace je dosaženo pomocí portů a adaptérů.

Představte si to takto: jádro aplikace sedí uprostřed, obklopené šestiúhelníkovou skořápkou. Porty jsou vstupní a výstupní body na této skořápce a adaptéry se zapojují do těchto portů a spojují jádro s vnějším světem.

Klíčové principy hexagonální architektury

Účinnost hexagonální architektury je založena na několika klíčových principech:

Výhody používání hexagonální architektury

Přijetí hexagonální architektury nabízí řadu výhod:

Implementace hexagonální architektury: Praktický příklad

Pojďme si ilustrovat implementaci hexagonální architektury na zjednodušeném příkladu systému registrace uživatelů. Pro jasnost použijeme hypotetický programovací jazyk (podobný Javě nebo C#).

1. Definujte jádro (aplikaci)

Jádro aplikace obsahuje obchodní logiku pro registraci nového uživatele.


// Core/UserService.java (or 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. Definujte porty

Definujeme porty, které jádro aplikace používá k interakci s vnějším světem.


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

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

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

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

3. Definujte adaptéry

Implementujeme adaptéry, které propojují jádro aplikace s konkrétními technologiemi.


// Adapters/DatabaseUserRepository.java (or 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 (or BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
    @Override
    public String hash(String password) {
        // Implementation using BCrypt library
        // ...
        return "hashedPassword"; //Placeholder
    }
}

//Adapters/SimpleUserValidator.java (or 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 (or 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 (or 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. Kompozice

Propojení všeho dohromady. Všimněte si, že tato kompozice (injekce závislostí) se obvykle děje ve vstupním bodě aplikace nebo v kontejneru pro injekci závislostí.


//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)

}

Vysvětlení:

Pokročilé úvahy a osvědčené postupy

Zatímco základní principy hexagonální architektury jsou přímočaré, je třeba mít na paměti některé pokročilé úvahy:

Příklady použití hexagonální architektury v reálném světě

Mnoho úspěšných společností a projektů přijalo hexagonální architekturu k budování robustních a udržovatelných systémů:

Výzvy a kompromisy

Zatímco hexagonální architektura nabízí značné výhody, je důležité si uvědomit související výzvy a kompromisy:

Je zásadní pečlivě vyhodnotit výhody a výzvy hexagonální architektury v kontextu vašich specifických požadavků projektu a schopností týmu. Není to stříbrná kulka a nemusí to být nejlepší volba pro každý projekt.

Závěr

Hexagonální architektura se svým důrazem na porty a adaptéry poskytuje výkonný přístup k budování udržovatelných, testovatelných a flexibilních aplikací. Oddělením základní obchodní logiky od externích závislostí vám umožňuje snadno se přizpůsobit měnícím se technologiím a požadavkům. I když je třeba zvážit výzvy a kompromisy, výhody hexagonální architektury často převažují nad náklady, zejména u složitých a dlouhodobých aplikací. Přijetím principů inverze závislostí a explicitních rozhraní můžete vytvořit systémy, které jsou odolnější, snáze pochopitelné a lépe vybavené pro splnění požadavků moderního softwarového prostředí.

Tato příručka poskytla komplexní přehled hexagonální architektury, od jejích základních principů až po praktické strategie implementace. Doporučujeme vám, abyste tyto koncepty dále prozkoumali a experimentovali s jejich aplikací ve vlastních projektech. Investice do učení a přijetí hexagonální architektury se nepochybně z dlouhodobého hlediska vyplatí a povede k softwaru vyšší kvality a spokojenějším vývojovým týmům.

Výběr správné architektury nakonec závisí na specifických potřebách vašeho projektu. Při rozhodování zvažte složitost, životnost a požadavky na udržovatelnost. Hexagonální architektura poskytuje pevný základ pro budování robustních a adaptabilních aplikací, ale je to jen jeden nástroj v sadě nástrojů softwarového architekta.