Ответ
Я разделяю приложение на слои по принципу единой ответственности и направлению зависимостей. Классическая многослойная архитектура (n-layer) включает:
- Presentation Layer (UI/API): Отвечает за взаимодействие с пользователем или внешними системами. Это могут быть REST-контроллеры, GraphQL резолверы, CLI-команды или компоненты фронтенда.
- Application Layer (Service): Содержит сценарии использования (use cases) приложения. Координирует работу доменного слоя для выполнения конкретных задач пользователя. Здесь живет бизнес-логика приложения (application business logic), но не бизнес-правила предметной области.
- Domain Layer (Business): Ядро системы. Содержит сущности (Entities), объекты-значения (Value Objects), доменные сервисы (Domain Services) и бизнес-правила (Business Rules). Этот слой не зависит от внешнего мира (баз данных, фреймворков, UI).
- 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), не затрагивая бизнес-логику.
- Читаемость: Четкие границы ответственности упрощают понимание кода новой командой.
Ответ 18+ 🔞
Слушай, вот объясняю, как я это вижу, чтобы не получилось говно в одном файле, где всё свалено в кучу. Представь, что твой код — это как квартира: есть прихожая, где грязные ботинки, есть кухня, где готовят, и есть спальня — святая святых, куда с грязными ногами не лезут. Вот так и с архитектурой.
Ёпта, слои — это чтобы не превратить проект в помойку. Классика жанра, которую все знают, но многие нихуя не соблюдают:
- Presentation Layer (Веб-морда/API): Это прихожая. Его задача — встретить гостя (пользователя или другой сервис), снять с него верхнюю одежду (распарсить JSON, валидировать входящие поля), и проводить в гостиную. Никакой бизнес-логики тут быть не должно! Только «здравствуйте-до свидания». REST-контроллеры, GraphQL-хуки — всё это здесь.
- Application Layer (Слой Приложения/Сервисов): Это кухня. Здесь готовят конкретное блюдо по заказу. «Сценарий использования» — это типа рецепта «Спагетти Карбонара». Шеф-повар (сервис) координирует: достать из холодильника (репозиторий) продукты, нарезать (обработать), сварить (применить бизнес-правила), подать. Здесь живёт логика приложения, но не глубинные законы вселенной твоего домена. Например, «перед регистрацией проверить, не занят ли email» — это логика приложения.
- Domain Layer (Домен/Ядро): Это спальня, святая святых, мозг проекта. Здесь живут сущности (User, Order, Invoice), объекты-значения (Email, Money, Address) и самые важные бизнес-правила. Этот слой — самодостаточный мудак. Он нихуя не знает про базы данных, HTTP-запросы или фреймворки. Он содержит только чистую бизнес-логику. Если из твоего домена убрать Spring и Hibernate, он должен спокойно компилироваться и его логику можно было бы протестировать голыми юнит-тестами.
- Infrastructure Layer (Инфраструктура): Это всё техническое говно вокруг: доступ к базе данных, отправка смс или email, работа с файловой системой, вызов внешних API. Этот слой реализует интерфейсы, которые ему продиктовали сверху (из домена или приложения).
Куда смотрят зависимости — это вообще ключ, ебать мои старые костыли! Зависимости идут внутрь, к ядру. Presentation зависит от Application. Application зависит от Domain. А Infrastructure, этот слуга, зависит от Domain и Application, потому что реализует их контракты (интерфейсы репозиториев, например). Это называется Dependency Inversion Principle, если по-умному.
Давай на живом примере, чтобы не было как у мартышлюшки в голове. Допустим, регистрация пользователя:
// Domain Layer (Спальня, мозг)
public class User {
private UserId id;
private Email email; // Это Value Object, а не строка, ёпта!
private PasswordHash passwordHash; // И это тоже объект
// Доменный метод. Создание пользователя — это его ответственность.
public static User register(Email email, PlainPassword password) {
// Доменная логика: пароль хешируется здесь или в сервисе, но правило "пароль должен быть хеширован" — доменное.
return new User(UserId.generate(), email, PasswordHash.create(password));
}
}
// Application Layer (Кухня)
public class RegisterUserUseCase {
private final UserRepository userRepository; // Зависим от интерфейса из домена!
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. Можем что-то еще сделать, например, событие кинуть.
}
}
// Presentation Layer (Прихожая, REST контроллер)
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<?> register(@RequestBody @Valid RegisterRequest request) {
// Задача контроллера: принять запрос, создать команду, вызвать сценарий.
var command = new RegisterUserCommand(request.getEmail(), request.getPassword());
registerUserUseCase.execute(command);
return ResponseEntity.status(201).build(); // "Всё ок, иди нахуй" со статусом 201.
}
}
// Infrastructure Layer (Подвал с техникой, реализация для JPA)
@Repository
public class JpaUserRepository implements UserRepository { // Реализуем доменный интерфейс!
@PersistenceContext
private EntityManager em;
@Override
public void save(User user) {
// Здесь уже знаем про JPA и EntityManager.
em.persist(user);
}
}
Ну и нахуя это всё? А вот нахуя:
- Тестируемость — овердохуища. Домен и Use Case можно тестировать в полной изоляции, подсовывая им заглушки (mocks) вместо репозиториев. Никаких поднятий контекста Spring, никаких тестовых баз данных. Быстро и надёжно.
- Гибкость — просто пиздец. Захотел сменить базу с PostgreSQL на MongoDB? Пожалуйста — пишешь новую реализацию
MongoUserRepositoryв инфраструктурном слое. Основная логика (домен и приложение) даже не чихнет. Захотел вместо REST сделать gRPC API? Пишешь новый контроллер в presentation-слое. Ядро остаётся нетронутым. - Читаемость и поддержка. Новый человек в команде открывает проект и сразу видит: а, тут чётко разделено, где что. Не надо рыться в трёхметровом
UserService, где перемешаны валидация запроса, SQL-запросы, отправка email и расчёт скидок. Всё лежит по полочкам. Доверия ебать ноль к монолитам в одном классе.
Вот так, без выёбов, но и без говнокода. Сначала кажется, что овер-инжениринг, но когда проект вырастает больше «Hello World», начинаешь понимать, что так и надо было делать с самого начала.