Сущность

Паттерны проектирования / Паттерны чистой архитектуры
image

;; 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}


Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.
Read more

Задача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно.

Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят.

Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же.

Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры.

Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно.