
//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
An entity holds a business rule of the shop and knows nothing about a database, a screen or the network, so it can be tested without them.Read more问题。商店对金额达到 200 的每笔订单减免 10%。如果这条规则写在网页里,又在数据库查询里写了一遍,两份副本很快就会不一致。要检查这条规则,就得启动网页或数据库。商店最重要的知识分散各处,难以测试。 思路。实体是一段数据,连同与它相关的业务规则:订单包含什么,总额如何计算,何时可以付款。它对数据库、界面或网络一无所知。整洁架构有一条主要规则,即依赖规则:外层代码可以了解内层代码,但内层代码从不了解外层代码。实体是最内层的代码,因此不依赖任何东西。 如何阅读示例。示例中有两个实体:订单行和订单本身。订单汇总各行,对 200 及以上的金额减免 10%,并计算应付金额。订单为空时,它拒绝付款。客户端代码构建含两个订单行的订单 1001,并打印其中的数字:总额 250,折扣 25,应付 225。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |