
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. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |