По какому принципу ты бы разделял уровни (layers) в приложении?

«По какому принципу ты бы разделял уровни (layers) в приложении?» — вопрос из категории Архитектура, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Я разделяю приложение на слои по принципу единой ответственности и направлению зависимостей. Классическая многослойная архитектура (n-layer) включает:

  1. Presentation Layer (UI/API): Отвечает за взаимодействие с пользователем или внешними системами. Это могут быть REST-контроллеры, GraphQL резолверы, CLI-команды или компоненты фронтенда.
  2. Application Layer (Service): Содержит сценарии использования (use cases) приложения. Координирует работу доменного слоя для выполнения конкретных задач пользователя. Здесь живет бизнес-логика приложения (application business logic), но не бизнес-правила предметной области.
  3. Domain Layer (Business): Ядро системы. Содержит сущности (Entities), объекты-значения (Value Objects), доменные сервисы (Domain Services) и бизнес-правила (Business Rules). Этот слой не зависит от внешнего мира (баз данных, фреймворков, UI).
  4. Infrastructure Layer: Реализует технические детали: доступ к данным (репозитории), отправку email, вызов внешних API, работу с файловой системой. Этот слой зависит от доменного слоя.

Принцип зависимостей: Зависимости направлены внутрь. Presentation → Application → Domain. Infrastructure реализует интерфейсы, определенные в Domain/Application.

Пример для функции регистрации пользователя:

// Domain Layer: Сущность и бизнес-правило
public class User {
    private UserId id;
    private Email email;
    private PasswordHash passwordHash;

    public static User register(Email email, PlainPassword password) {
        // Доменное правило: email должен быть уникален (проверяется на уровне приложения)
        // Доменная логика: хеширование пароля
        return new User(UserId.generate(), email, PasswordHash.create(password));
    }
}

// Application Layer: Сценарий использования
public class RegisterUserUseCase {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public void execute(RegisterUserCommand command) {
        // 1. Проверка бизнес-правила (уникальность email) - это логика приложения
        if (userRepository.existsByEmail(command.getEmail())) {
            throw new EmailAlreadyExistsException();
        }
        // 2. Создание доменного объекта
        User newUser = User.register(command.getEmail(), command.getPassword());
        // 3. Сохранение через инфраструктуру
        userRepository.save(newUser);
        // 4. Публикация доменного события (опционально)
        domainEventPublisher.publish(new UserRegisteredEvent(newUser.getId()));
    }
}

// Presentation Layer (REST Controller)
@RestController
public class UserController {
    @PostMapping("/users")
    public ResponseEntity<UserResponse> register(@RequestBody RegisterRequest request) {
        var command = new RegisterUserCommand(request.getEmail(), request.getPassword());
        registerUserUseCase.execute(command);
        return ResponseEntity.created(...).build();
    }
}

// Infrastructure Layer: Реализация репозитория для JPA
@Repository
public class JpaUserRepository implements UserRepository {
    @PersistenceContext
    private EntityManager em;

    @Override
    public void save(User user) {
        em.persist(user);
    }
}

Преимущества такого разделения:

  • Тестируемость: Домен и логику приложения можно тестировать изолированно, без базы данных или веб-сервера.
  • Гибкость: Можно легко заменить способ хранения данных (с SQL на NoSQL) или интерфейс (с REST на gRPC), не затрагивая бизнес-логику.
  • Читаемость: Четкие границы ответственности упрощают понимание кода новой командой.