Для чего нужен Domain слой в Clean Architecture?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Domain слой в Clean Architecture: суть и назначение
📌 Основное предназначение
Domain слой (слой предметной области) в Clean Architecture — это центральный, самый внутренний и наиболее важный слой приложения. Он содержит бизнес-логику приложения и полностью независим от фреймворков, библиотек и внешних зависимостей. Его основная цель — инкапсулировать правила и логику предметной области, делая их устойчивыми к изменениям во внешних слоях.
// Пример структуры Domain слоя
package com.example.app.domain
// Entity (Сущность)
data class User(
val id: String,
val name: String,
val email: String,
val isActive: Boolean
) {
fun canPerformAction(): Boolean {
// Бизнес-правило внутри Entity
return isActive && email.isNotEmpty()
}
}
// Repository interface (Контракт)
interface UserRepository {
suspend fun getUserById(id: String): User
suspend fun saveUser(user: User): Boolean
}
// Use Case (Интерактор)
class GetUserUseCase(
private val userRepository: UserRepository
) {
suspend operator fun invoke(userId: String): User {
// Бизнес-логика в UseCase
val user = userRepository.getUserById(userId)
require(user.isActive) { "Пользователь неактивен" }
return user
}
}
🎯 Ключевые компоненты Domain слоя
1. Entities (Сущности)
Объекты, содержащие бизнес-правила и данные предметной области. Они являются фундаментом всего приложения.
// Entity с бизнес-логикой
class Order(
val id: String,
val items: List<OrderItem>,
val status: OrderStatus,
val totalAmount: Double
) {
fun calculateTax(): Double {
// Бизнес-правило расчета налога
return totalAmount * 0.20
}
fun canBeCancelled(): Boolean {
// Бизнес-правило для отмены заказа
return status == OrderStatus.PENDING || status == OrderStatus.PROCESSING
}
}
2. Use Cases (Сценарии использования)
Инкапсулируют конкретные бизнес-правила и операции. Каждый UseCase представляет собой одну бизнес-операцию.
class ProcessPaymentUseCase(
private val paymentRepository: PaymentRepository,
private val orderRepository: OrderRepository
) {
suspend fun execute(orderId: String, paymentDetails: PaymentDetails): PaymentResult {
val order = orderRepository.getOrder(orderId)
// Валидация бизнес-правил
require(order.canBePaid()) { "Заказ не может быть оплачен" }
require(paymentDetails.isValid()) { "Неверные данные платежа" }
// Выполнение бизнес-логики
val payment = Payment.create(order, paymentDetails)
val result = paymentRepository.processPayment(payment)
if (result.isSuccessful) {
order.markAsPaid()
orderRepository.updateOrder(order)
}
return result
}
}
3. Repository Interfaces (Интерфейсы репозиториев)
Определяют контракты для доступа к данным, но без реализации. Реализация находится во внешних слоях.
// Интерфейс в Domain слое
interface ProductRepository {
suspend fun getFeaturedProducts(): List<Product>
suspend fun searchProducts(query: String, filters: ProductFilters): List<Product>
suspend fun updateProductStock(productId: String, quantity: Int)
}
// Value Object в Domain слое
data class ProductFilters(
val minPrice: Double? = null,
val maxPrice: Double? = null,
val category: ProductCategory? = null,
val inStockOnly: Boolean = false
) {
fun isValid(): Boolean {
return minPrice == null || maxPrice == null || minPrice <= maxPrice
}
}
🔥 Преимущества Domain слоя
Независимость от инфраструктуры
- Domain слой не знает о базе данных, сетевых библиотеках или UI-фреймворках
- Можно менять реализации во внешних слоях без изменения бизнес-логики
// Domain слой работает только с интерфейсами
interface AnalyticsTracker {
fun trackEvent(eventName: String, properties: Map<String, Any>)
}
// UseCase не зависит от конкретной реализации
class TrackUserRegistrationUseCase(
private val analyticsTracker: AnalyticsTracker
) {
fun execute(user: User) {
analyticsTracker.trackEvent("user_registered", mapOf(
"user_id" to user.id,
"registration_date" to LocalDate.now()
))
}
}
Тестируемость
- Бизнес-логику можно тестировать изолированно, без зависимостей
- Легко создавать юнит-тесты для UseCases и Entities
// Юнит-тест для UseCase
@Test
fun `process payment should fail for cancelled order`() = runTest {
// Arrange
val mockOrderRepo = mockk<OrderRepository>()
val mockPaymentRepo = mockk<PaymentRepository>()
val useCase = ProcessPaymentUseCase(mockOrderRepo, mockPaymentRepo)
val cancelledOrder = Order(id = "1", status = OrderStatus.CANCELLED)
coEvery { mockOrderRepo.getOrder(any()) } returns cancelledOrder
// Act & Assert
assertFailsWith<IllegalArgumentException> {
useCase.execute("1", PaymentDetails())
}
}
Поддержка DDD (Domain-Driven Design)
- Позволяет использовать такие паттерны DDD как Aggregate Roots, Value Objects, Domain Events
- Улучшает коммуникацию между разработчиками и бизнес-экспертами
// Domain Event в Domain слое
sealed class OrderEvent {
data class OrderCreated(val orderId: String, val createdAt: Instant) : OrderEvent()
data class OrderStatusChanged(val orderId: String, val oldStatus: OrderStatus, val newStatus: OrderStatus) : OrderEvent()
data class OrderCancelled(val orderId: String, val reason: String) : OrderEvent()
}
// Aggregate Root
class OrderAggregate(
private val order: Order,
private val events: MutableList<OrderEvent> = mutableListOf()
) {
fun cancelOrder(reason: String) {
require(order.canBeCancelled()) { "Order cannot be cancelled" }
order.cancel()
events.add(OrderEvent.OrderCancelled(order.id, reason))
}
fun getDomainEvents(): List<OrderEvent> = events.toList()
}
🏗️ Роль в архитектуре приложения
Зависимости и направление потоков
- Domain слой НЕ зависит ни от каких других слоев
- Все внешние слои (Data, Presentation) зависят от Domain слоя
- Это обеспечивается через Dependency Inversion Principle
┌─────────────────────────────────┐
│ Presentation Layer │
│ (UI, ViewModels, Controllers) │
└───────────────┬─────────────────┘
│ зависит от
▼
┌─────────────────────────────────┐
│ Domain Layer │ ← Самый стабильный слой
│ (Entities, UseCases, Repos) │
└───────────────┬─────────────────┘
│ зависит от (интерфейсы)
▼
┌─────────────────────────────────┐
│ Data Layer │
│ (Repository Implementations) │
└─────────────────────────────────┘
Изоляция изменений
- При изменении API, базы данных или UI фреймворка, Domain слой остается неизменным
- Новые требования к бизнес-логике реализуются только в Domain слое
📊 Практические сценарии использования
Сценарий 1: Миграция базы данных
// Domain слой не меняется при смене Room на Realm
interface UserPreferencesRepository {
suspend fun getThemePreference(): Theme
suspend fun saveThemePreference(theme: Theme)
suspend fun getNotificationSettings(): NotificationSettings
}
// UseCase остается неизменным
class GetUserSettingsUseCase(
private val preferencesRepo: UserPreferencesRepository
) {
suspend fun execute(): UserSettings {
val theme = preferencesRepo.getThemePreference()
val notifications = preferencesRepo.getNotificationSettings()
return UserSettings(theme, notifications)
}
}
Сценарий 2: Добавление новой бизнес-функциональности
// Расширение Domain слоя новой бизнес-логикой
class ApplyDiscountUseCase(
private val cartRepository: CartRepository,
private val discountValidator: DiscountValidator
) {
suspend fun execute(cartId: String, discountCode: String): Cart {
val cart = cartRepository.getCart(cartId)
val discount = discountValidator.validateDiscount(discountCode, cart)
// Сложная бизнес-логика
if (discount.isEligible) {
cart.applyDiscount(discount)
cartRepository.updateCart(cart)
cart.addEvent(CartEvent.DiscountApplied(discountCode))
}
return cart
}
}
🚀 Заключение
Domain слой — это сердце Clean Architecture, которое:
- Инкапсулирует чистую бизнес-логику без технических деталей
- Обеспечивает независимость от внешних изменений
- Повышает тестируемость и поддерживаемость кода
- Позволяет команде сосредоточиться на бизнес-ценности приложения
- Служит единым источником истины для бизнес-правил
Правильно спроектированный Domain слой — это инвестиция в долгосрочную жизнеспособность проекта, которая окупается снижением стоимости изменений и увеличением скорости разработки при эволюции приложения.