← Назад к вопросам

Для чего нужен Domain слой в Clean Architecture?

3.0 Senior🔥 191 комментариев
#Архитектура и паттерны#Многомодульность

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# 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, которое:

  1. Инкапсулирует чистую бизнес-логику без технических деталей
  2. Обеспечивает независимость от внешних изменений
  3. Повышает тестируемость и поддерживаемость кода
  4. Позволяет команде сосредоточиться на бизнес-ценности приложения
  5. Служит единым источником истины для бизнес-правил

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