Українська

Дізнайтеся, як гексагональна архітектура, також відома як «Порти та адаптери», може покращити підтримку, тестування та гнучкість ваших застосунків. Цей посібник містить практичні приклади та корисні поради для розробників з усього світу.

Гексагональна архітектура: Практичний посібник з портів та адаптерів

У світі розробки програмного забезпечення, що постійно розвивається, створення надійних, зручних у супроводі та тестованих застосунків є першочерговим завданням. Гексагональна архітектура, також відома як «Порти та адаптери», — це архітектурний патерн, що вирішує ці проблеми шляхом відокремлення основної бізнес-логіки застосунку від його зовнішніх залежностей. Цей посібник має на меті надати всебічне розуміння гексагональної архітектури, її переваг та практичних стратегій реалізації для розробників у всьому світі.

Що таке гексагональна архітектура?

Гексагональна архітектура, термін якої ввів Алістер Коберн, обертається навколо ідеї ізоляції основної бізнес-логіки застосунку від зовнішнього світу. Ця ізоляція досягається за допомогою портів та адаптерів.

Уявіть це так: ядро застосунку знаходиться в центрі, оточене гексагональною оболонкою. Порти — це точки входу та виходу на цій оболонці, а адаптери підключаються до цих портів, з'єднуючи ядро із зовнішнім світом.

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

Ефективність гексагональної архітектури ґрунтується на кількох ключових принципах:

Переваги використання гексагональної архітектури

Застосування гексагональної архітектури пропонує численні переваги:

Реалізація гексагональної архітектури: Практичний приклад

Проілюструймо реалізацію гексагональної архітектури на спрощеному прикладі системи реєстрації користувачів. Для ясності ми будемо використовувати гіпотетичну мову програмування (схожу на 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) {
        // Валідація введених даних користувача
        ValidationResult validationResult = userValidator.validate(username, password, email);
        if (!validationResult.isValid()) {
            return Result.failure(validationResult.getErrorMessage());
        }

        // Перевірка, чи користувач вже існує
        if (userRepository.findByUsername(username).isPresent()) {
            return Result.failure("Користувач з таким іменем вже існує");
        }

        // Хешування пароля
        String hashedPassword = passwordHasher.hash(password);

        // Створення нового користувача
        User user = new User(username, hashedPassword, email);

        // Збереження користувача в репозиторії
        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) {
        // Реалізація з використанням JDBC, JPA або іншої технології доступу до бази даних
        // ...
        return Optional.empty(); // Заглушка
    }

    @Override
    public void save(User user) {
        // Реалізація з використанням JDBC, JPA або іншої технології доступу до бази даних
        // ...
    }
}

// Adapters/BCryptPasswordHasher.java (або BCryptPasswordHasher.cs)
public class BCryptPasswordHasher implements PasswordHasher {
    @Override
    public String hash(String password) {
        // Реалізація з використанням бібліотеки BCrypt
        // ...
        return "hashedPassword"; //Заглушка
    }
}

//Adapters/SimpleUserValidator.java (або SimpleUserValidator.cs)
public class SimpleUserValidator implements UserValidator {
  @Override
  public ValidationResult validate(String username, String password, String email){
    //Проста логіка валідації
     if (username == null || username.isEmpty()) {
            return new SimpleValidationResult(false, "Ім'я користувача не може бути порожнім");
        }
        if (password == null || password.length() < 8) {
            return new SimpleValidationResult(false, "Пароль має містити щонайменше 8 символів");
        }
        if (email == null || !email.contains("@")) {
            return new SimpleValidationResult(false, "Неправильний формат електронної пошти");
        }

        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)
//Рушійний адаптер - обробляє запити з вебу
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 "Реєстрація успішна!";
        } else {
            return "Реєстрація не вдалася: " + result.getFailure();
        }
    }
}


4. Композиція

З'єднуємо все разом. Зауважте, що ця композиція (впровадження залежностей) зазвичай відбувається в точці входу застосунку або в контейнері впровадження залежностей.


//Головний клас або конфігурація впровадження залежностей
public class Main {
    public static void main(String[] args) {
        // Створення екземплярів адаптерів
        DatabaseConnection databaseConnection = new DatabaseConnection("jdbc:mydb://localhost:5432/users", "user", "password");
        DatabaseUserRepository userRepository = new DatabaseUserRepository(databaseConnection);
        BCryptPasswordHasher passwordHasher = new BCryptPasswordHasher();
        SimpleUserValidator userValidator = new SimpleUserValidator();

        // Створення екземпляра ядра застосунку, впроваджуючи адаптери
        UserService userService = new UserService(userRepository, passwordHasher, userValidator);

        //Створення рушійного адаптера та підключення його до сервісу
        WebUserController userController = new WebUserController(userService);

        //Тепер ви можете обробляти запити на реєстрацію користувача через userController
        String result = userController.registerUser("john.doe", "P@sswOrd123", "john.doe@example.com");
        System.out.println(result);
    }
}



//DatabaseConnection - це простий клас лише для демонстраційних цілей
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;
    }

    // ... методи для підключення до бази даних (не реалізовані для стислості)
}

//Клас Result (подібний до Either у функціональному програмуванні)
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("Результат є невдалим");
        }
        return success;
    }

    public E getFailure() {
        if (isSuccess) {
            throw new IllegalStateException("Результат є успішним");
        }
        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;
    }

    // геттери та сеттери (пропущені для стислості)

}

Пояснення:

Додаткові аспекти та найкращі практики

Хоча основні принципи гексагональної архітектури є простими, слід пам'ятати про деякі додаткові аспекти:

Реальні приклади використання гексагональної архітектури

Багато успішних компаній та проєктів застосували гексагональну архітектуру для створення надійних та зручних у супроводі систем:

Виклики та компроміси

Хоча гексагональна архітектура пропонує значні переваги, важливо визнати пов'язані з нею виклики та компроміси:

Важливо ретельно оцінити переваги та виклики гексагональної архітектури в контексті вимог вашого конкретного проєкту та можливостей команди. Це не срібна куля, і вона може бути не найкращим вибором для кожного проєкту.

Висновок

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

Цей посібник надав всебічний огляд гексагональної архітектури, від її основних принципів до практичних стратегій реалізації. Ми заохочуємо вас досліджувати ці концепції далі та експериментувати з їх застосуванням у ваших власних проєктах. Інвестиції у вивчення та впровадження гексагональної архітектури, безсумнівно, окупляться в довгостроковій перспективі, що призведе до вищої якості програмного забезпечення та більш задоволених команд розробників.

Зрештою, вибір правильної архітектури залежить від конкретних потреб вашого проєкту. Враховуйте складність, довговічність та вимоги до супроводу при прийнятті рішення. Гексагональна архітектура надає міцну основу для створення надійних та адаптивних застосунків, але це лише один інструмент в арсеналі архітектора програмного забезпечення.