Какие знаешь сущности в Domain слое Clean Architecture?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Сущности (Entities) в Domain-слое Clean Architecture
В Domain-слое (слое бизнес-логики) Clean Architecture ключевой абстракцией являются сущности (Entities). Это ядро приложения, которое полностью независимо от фреймворков, UI, баз данных и внешних сервисов. Сущности инкапсулируют наиболее общие и высокоуровневые бизнес-правила.
Характеристики сущностей
- Бизнес-логика: Содержат критически важные для предметной области данные и методы, которые их обрабатывают. Это «сердце» приложения.
- Независимость: Не имеют зависимостей от других слоёв (Data, Presentation). Они не знают, как данные сохраняются или отображаются.
- Уникальность идентичности: Отличаются не своими атрибутами, а уникальным идентификатором (ID). Даже если все поля двух объектов совпадают, но ID разный — это разные сущности.
- Инварианты: Обеспечивают целостность своих данных через соблюдение инвариантов (неизменных условий) предметной области.
Типы сущностей в Domain-слое
Обычно в Domain-слое выделяют несколько типов объектов, но все они вращаются вокруг центральной концепции Entity.
-
Entity (Классическая сущность): Объект с уникальной идентичностью и жизненным циклом.
// Kotlin пример data class Product( val id: ProductId, // Value Object для типа ID val name: String, val price: Money, // Value Object val stockQuantity: Int ) { // Бизнес-логика внутри сущности fun reduceStock(quantity: Int) { require(quantity > 0) { "Quantity must be positive" } require(stockQuantity >= quantity) { "Insufficient stock" } stockQuantity -= quantity } fun isAvailable(): Boolean = stockQuantity > 0 } -
Value Object (Объект-значение): Не имеет идентификатора. Определяется исключительно своими атрибутами. Неизменяем (immutable). Используется для описания свойств.
// Kotlin пример data class Money( val amount: BigDecimal, val currency: Currency ) { operator fun plus(other: Money): Money { require(currency == other.currency) { "Currency mismatch" } return Money(amount + other.amount, currency) } } data class Address( val street: String, val city: String, val postalCode: String ) // Два адреса с одинаковыми полями считаются одинаковыми. -
Aggregate Root (Агрегат): Особый тип сущности, который инкапсулирует одну или несколько связанных сущностей и Value Objects в единую границу транзакционной согласованности. Внешние классы могут ссылаться только на корень агрегата.
// Kotlin пример class Order( val id: OrderId, private val items: MutableList<OrderItem> = mutableListOf(), var status: OrderStatus = OrderStatus.CREATED ) : AggregateRoot() { // AggregateRoot - маркерный класс/интерфейс // Доступ к внутренней коллекции только через методы агрегата fun addItem(productId: ProductId, price: Money, quantity: Int) { require(status == OrderStatus.CREATED) { "Cannot modify completed order" } items.add(OrderItem(productId, price, quantity)) // Может генерировать Domain Event registerEvent(OrderItemAddedEvent(this.id, productId)) } fun totalAmount(): Money { return items.map { it.price * it.quantity }.reduce { acc, money -> acc + money } } } // Внутренняя сущность, принадлежащая агрегату Order data class OrderItem( val productId: ProductId, val price: Money, val quantity: Int ) -
Domain Service (Сервис предметной области): Содержит бизнес-логику, которая не естественным образом принадлежит какой-либо одной сущности или Value Object. Часто координирует работу нескольких сущностей.
// Kotlin пример interface PaymentService { // Domain Service как интерфейс fun processPayment(order: Order, paymentDetails: PaymentDetails): PaymentResult } class OrderProcessingService( private val paymentService: PaymentService, private val inventoryService: InventoryService ) { fun completeOrder(order: Order, paymentDetails: PaymentDetails) { // Координация нескольких правил и внешних сервисов (определённых в Domain) val result = paymentService.processPayment(order, paymentDetails) if (result.isSuccess) { order.items.forEach { item -> inventoryService.reserve(item.productId, item.quantity) } order.status = OrderStatus.PAID } } } -
Domain Event (Событие предметной области): Объект, который представляет факт, произошедший в доменной области. Используется для ослабления связей между агрегатами.
// Kotlin пример sealed interface DomainEvent { val aggregateId: String val occurredOn: Instant } data class OrderCompletedEvent( override val aggregateId: String, val totalAmount: Money, override val occurredOn: Instant = Instant.now() ) : DomainEvent -
Repository Interface (Интерфейс репозитория): Определяется в Domain-слое, а реализуется в Data-слое. Позволяет Domain-слою декларативно работать с хранением данных, не зная деталей реализации.
// Kotlin пример. Интерфейс живет в Domain! interface ProductRepository { suspend fun findById(id: ProductId): Product? suspend fun save(product: Product) suspend fun findAllAvailable(): List<Product> }
Принципы проектирования
- Анемичная модель — антипаттерн: Сущности не должны быть просто наборами полей с геттерами/сеттерами (анемичная модель). Они должны содержать поведение (методы), соответствующее бизнес-правилам.
- Соответствие языку предметной области (Ubiquitous Language): Имена классов, методов и полей должны максимально точно соответствовать терминам, используемым экспертами в данной предметной области.
- Инкапсуляция: Состояние сущности должно изменяться только через её публичные методы, которые гарантируют соблюдение всех инвариантов.
Итог: Domain-слой в Clean Architecture — это не просто набор POJO-классов. Это богатая модель, построенная вокруг сущностей (Entities), Value Objects и агрегатов (Aggregates), которые вместе с Domain Services, Domain Events и интерфейсами репозиториев полностью и точно описывают бизнес-логику приложения, обеспечивая её максимальную независимость и тестируемость.