Сущность

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

import (
    "errors"
    "fmt"
)

//entity: plain fields, no base type and no framework
type OrderLine struct {
    SKU   string
    Price int
    Count int
}

//go has no exceptions: a broken rule is a value
var errBadCount = errors.New("bad count")

//a line that breaks the rule is never built at all
func NewLine(sku string, price, n int) (OrderLine, error) {
    if n <= 0 {
        return OrderLine{}, errBadCount
    }
    return OrderLine{SKU: sku, Price: price, Count: n}, nil
}

//value receiver: counting changes nothing
func (l OrderLine) Cost() int {
    return l.Price * l.Count
}

//entity: lines keep their order, so a slice and not a map
type Order struct {
    ID    int
    lines []OrderLine
    paid  bool
}

//pointer receiver: the order itself changes here
func (o *Order) Add(line OrderLine) {
    o.lines = append(o.lines, line)
}

func (o Order) Total() int {
    sum := 0
    for _, line := range o.lines {
        sum += line.Cost()
    }
    return sum
}

//business rule: 10% off from 200
func (o Order) Discount() int {
    if o.Total() >= 200 {
        return o.Total() / 10
    }
    return 0
}

func (o Order) Payable() int {
    return o.Total() - o.Discount()
}

//business rule: an empty order is never paid
var errEmpty = errors.New("empty order")

func (o *Order) Pay() error {
    if len(o.lines) == 0 {
        return errEmpty
    }
    o.paid = true
    return nil
}

func (o Order) Paid() bool {
    return o.paid
}

func (o Order) Size() int {
    return len(o.lines)
}

//client code
//no database and no screen anywhere above
order := &Order{ID: 1001}
wanted := []OrderLine{
    {SKU: "pen", Price: 30, Count: 2},
    {SKU: "lamp", Price: 190, Count: 1},
}
for _, w := range wanted {
    line, err := NewLine(w.SKU, w.Price, w.Count)
    if err != nil {
        fmt.Println("refused:", err)
        continue
    }
    order.Add(line)
}

fmt.Printf("order %d\n", order.ID)
fmt.Printf("lines %d\n", order.Size())
fmt.Printf("total %d\n", order.Total())
fmt.Printf("discount %d\n", order.Discount())
fmt.Printf("payable %d\n", order.Payable())
if err := order.Pay(); err == nil {
    fmt.Printf("paid %v\n", order.Paid())
}

//a wrong count never becomes a line
if _, err := NewLine("pen", 30, 0); err != nil {
    fmt.Println("refused:", err)
}

//an empty order refuses to be paid
empty := &Order{ID: 1002}
fmt.Println("refused:", empty.Pay())
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid true
//         refused: bad count
//         refused: empty order


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

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

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

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

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

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