
;; Entity folds into a plain value plus the functions that
;; hold its rules: no class, and no database or screen here
;; Entity
(defn order-line [sku price qty]
(when-not (pos? qty)
(throw (ex-info "bad count" {:sku sku :qty qty})))
{:sku sku :price price :qty qty})
(defn cost [line]
(* (:price line) (:qty line)))
;; Entity
(defn new-order [id]
{:id id :lines [] :paid false})
(defn add-line [order sku price qty]
(update order :lines conj (order-line sku price qty)))
(defn total [order]
(reduce + 0 (map cost (:lines order))))
;; Business rule: 10% off from 200
(defn discount [order]
(let [sum (total order)]
(if (>= sum 200)
(quot sum 10)
0)))
(defn payable [order]
(- (total order) (discount order)))
;; Business rule: an empty order is never paid
(defn pay [order]
(when (empty? (:lines order))
(throw (ex-info "empty order" {:id (:id order)})))
(assoc order :paid true))
(defn paid? [order]
(:paid order))
;; Client code
(def order
(-> (new-order 1001)
(add-line "pen" 30 2)
(add-line "lamp" 190 1)))
(println "order" (:id order))
(println "lines" (count (:lines order)))
(println "total" (total order))
(println "discount" (discount order))
(println "payable" (payable order))
;; Paying gives a new value; the old one stays as it was
(def paid-order (pay order))
(println "paid" (paid? paid-order))
(println "before paying" (paid? order))
;; The same rule on a small order: no discount at all
(def small
(-> (new-order 1002)
(add-line "pen" 30 2)))
(println "order" (:id small)
"total" (total small)
"discount" (discount small))
;; Nothing locks the map: (assoc order :paid true) would
;; pass. The rule holds because every layer calls pay,
;; so the rules live in one place and nowhere else
(defn attempt [f]
(try
(f)
(catch clojure.lang.ExceptionInfo error
(println "refused:" (ex-message error)
(ex-data error)))))
(attempt #(pay (new-order 1003)))
(attempt #(add-line (new-order 1003) "pen" 30 0))
;; printed: order 1001
;; lines 2
;; total 250
;; discount 25
;; payable 225
;; paid true
;; before paying false
;; order 1002 total 60 discount 0
;; refused: empty order {:id 1003}
;; refused: bad count {:sku pen, :qty 0}
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。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |