
//entity
class OrderLine(
val sku: String,
val price: Int,
val count: Int
) {
init {
//an invariant guarded where the data is born
require(count > 0) { "bad count" }
}
val cost: Int
get() = price * count
}
//entity
class Order(val id: Int) {
private val lines = mutableListOf<OrderLine>()
//only the entity itself may flip the flag
var paid: Boolean = false
private set
val size: Int
get() = lines.size
val total: Int
get() = lines.sumOf { it.cost }
//business rule: 10% off from 200
val discount: Int
get() = if (total >= 200) total / 10 else 0
val payable: Int
get() = total - discount
//business rule: a paid order is closed for changes
fun add(sku: String, price: Int, count: Int) {
check(!paid) { "order already paid" }
lines.add(OrderLine(sku, price, count))
}
//business rule: an empty order is never paid
fun pay() {
check(lines.isNotEmpty()) { "empty order" }
paid = true
}
}
//client code
//no database and no screen anywhere above
val order = Order(1001)
order.add("pen", 30, 2)
order.add("lamp", 190, 1)
println("order ${order.id}")
println("lines ${order.size}")
println("total ${order.total}")
println("discount ${order.discount}")
println("payable ${order.payable}")
order.pay()
println("paid ${order.paid}")
//the entity guards its own state, not a screen
try {
order.add("pen", 30, 1)
} catch (error: IllegalStateException) {
println("refused: ${error.message}")
}
//a smaller order stays under the discount rule
val small = Order(1002)
small.add("pen", 30, 1)
println("order ${small.id}")
println("total ${small.total}")
println("discount ${small.discount}")
//the rules refuse bad data, and nobody else has to
try {
small.add("chair", 30, 0)
} catch (error: IllegalArgumentException) {
println("refused: ${error.message}")
}
try {
Order(1003).pay()
} catch (error: IllegalStateException) {
println("refused: ${error.message}")
}
//printed: order 1001
// lines 2
// total 250
// discount 25
// payable 225
// paid true
// refused: order already paid
// order 1002
// total 30
// discount 0
// refused: bad count
// refused: empty order
Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.Read moreЗадача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно. Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят. Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |