
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
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。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |