Русский

Узнайте, как гексагональная архитектура, также известная как порты и адаптеры, может улучшить удобство сопровождения, тестируемость и гибкость ваших приложений.

Гексагональная архитектура: практическое руководство по портам и адаптерам

В постоянно развивающемся ландшафте разработки программного обеспечения первостепенное значение имеет создание надежных, удобных в сопровождении и тестируемых приложений. Гексагональная архитектура, также известная как порты и адаптеры, представляет собой архитектурный шаблон, который решает эти проблемы путем отделения основной бизнес-логики приложения от его внешних зависимостей. Это руководство призвано обеспечить всестороннее понимание гексагональной архитектуры, ее преимуществ и практических стратегий реализации для разработчиков во всем мире.

Что такое гексагональная архитектура?

Гексагональная архитектура, введенная Алистером Кокберном, вращается вокруг идеи изоляции основной бизнес-логики приложения от внешнего мира. Эта изоляция достигается за счет использования портов и адаптеров.

Представьте себе это так: основное приложение находится в центре, окруженное гексагональной оболочкой. Порты — это точки входа и выхода на этой оболочке, а адаптеры подключаются к этим портам, соединяя ядро с внешним миром.

Ключевые принципы гексагональной архитектуры

Несколько ключевых принципов лежат в основе эффективности гексагональной архитектуры:

Преимущества использования гексагональной архитектуры

Принятие гексагональной архитектуры дает множество преимуществ:

Реализация гексагональной архитектуры: практический пример

Давайте проиллюстрируем реализацию гексагональной архитектуры на упрощенном примере системы регистрации пользователей. Для ясности мы будем использовать гипотетический язык программирования (аналогичный Java или C#).

1. Определение ядра (приложения)

Основное приложение содержит бизнес-логику для регистрации нового пользователя.


// Core/UserService.java (или 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. Определение портов

Мы определяем порты, которые основное приложение использует для взаимодействия с внешним миром.


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

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

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

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

3. Определение адаптеров

Мы реализуем адаптеры, которые подключают основное приложение к конкретным технологиям.


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

//Adapters/SimpleUserValidator.java (или 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 (или 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 (или 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. Композиция

Соединение всего вместе. Обратите внимание, что эта композиция (внедрение зависимостей) обычно происходит в точке входа приложения или внутри контейнера внедрения зависимостей.


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

}

Пояснение:

Расширенные соображения и лучшие практики

Хотя основные принципы гексагональной архитектуры просты, следует помнить о некоторых расширенных соображениях:

Реальные примеры использования гексагональной архитектуры

Многие успешные компании и проекты приняли гексагональную архитектуру для создания надежных и удобных в сопровождении систем:

Проблемы и компромиссы

Хотя гексагональная архитектура предлагает значительные преимущества, важно учитывать связанные с этим проблемы и компромиссы:

Крайне важно тщательно оценить преимущества и проблемы гексагональной архитектуры в контексте конкретных требований вашего проекта и возможностей команды. Это не серебряная пуля, и она может быть не лучшим выбором для каждого проекта.

Заключение

Гексагональная архитектура с акцентом на портах и адаптерах обеспечивает мощный подход к созданию удобных в сопровождении, тестируемых и гибких приложений. Отделяя основную бизнес-логику от внешних зависимостей, она позволяет вам с легкостью адаптироваться к меняющимся технологиям и требованиям. Хотя есть проблемы и компромиссы, которые следует учитывать, преимущества гексагональной архитектуры часто перевешивают затраты, особенно для сложных и долгоживущих приложений. Приняв принципы инверсии зависимостей и явных интерфейсов, вы можете создавать системы, которые более устойчивы, проще для понимания и лучше подготовлены к удовлетворению потребностей современного ландшафта программного обеспечения.

Это руководство предоставило всесторонний обзор гексагональной архитектуры, от ее основных принципов до практических стратегий реализации. Мы рекомендуем вам изучить эти концепции дальше и поэкспериментировать с их применением в своих собственных проектах. Инвестиции в изучение и внедрение гексагональной архитектуры, несомненно, окупятся в долгосрочной перспективе, что приведет к более качественному программному обеспечению и более довольным командам разработчиков.

В конечном счете, выбор правильной архитектуры зависит от конкретных потребностей вашего проекта. Учитывайте сложность, долговечность и требования к удобству сопровождения при принятии решения. Гексагональная архитектура обеспечивает прочную основу для создания надежных и адаптируемых приложений, но это всего лишь один инструмент в арсенале архитектора программного обеспечения.