在一个借贷系统里面,客户(Customer
)每借一笔钱,就会产生个账单(Bill
),账单有借款日(loanData
)和到期日(dueDate
),如果这笔借款在到期日后还没有付清,那么就会被标记为超期账单(over due)
对Customer
来说,如果他有超过1/3
的订单都处于超期状态,那么这个客户会被标记为信誉不佳(HasBadReputation
)
func (c *Customer) HasBadReputation() bool {
overDueCount := 0
for _, bill := range c.bills {
if time.Now().After(bill.GetDueDate()) {
overDueCount++
}
}
return overDueCount*3 > len(c.bills)
}
Customer.HasBadReputation
遍历自己名下的账单,统计超期账单,如果超过1/3
的账单超期,则返回true
。但这个实现有一个问题是,Customer
代替Bill
完成了是否超期
的运算,也就是for
循环中的if
语句
我们首先进行一个简单优化,也就是提取IsOverDue
方法,用来判断账单是否超期
func (bill *Bill) IsOverDue() bool {
return time.Now().After(bill.dueDate)
}
func (c *Customer) HasBadReputation() bool {
overDueCount := 0
for _, bill := range c.bills {
if bill.IsOverDue() {
overDueCount++
}
}
return overDueCount*3 > len(c.bills)
}
根据客户资质的不同,为每个客户设置一个宽限期,在dueDate
之后一定时间内(如30天),账单不会被视为超期
Customer
新增了一个属性:宽限期(gracePeriod
)
type Customer struct {
gracePeriod time.Duration
bills []*Bill
}
判断账单是否超期时也要考虑宽限期:
func (c *Customer) HasBadReputation() bool {
overDueCount := 0
for _, bill := range c.bills {
if bill.IsOverDue(c.gracePeriod) {
overDueCount++
}
}
return overDueCount*3 > len(c.bills)
}
func (bill *Bill) IsOverDue(gracePeriod time.Duration) bool {
return time.Now().After(bill.dueDate.Add(gracePeriod))
}
请大家考虑一个问题:以上处理逻辑是不是足够清晰了?在我们的示例内没有问题,但请想象一下,如果这些代码出现在一个大型系统内
会怎么样?在一个大型系统内,如果仅仅使用一个方法(IsOverDue
)来表达规则,那么这个规则将会很快被淹没在复杂的对象中,特别是在新增了宽限期
这个规则后, 而如果这个规则还是频繁变化
的,那就更加需要进行一层抽象与建模。
为此,领域驱动设计提出了Specification模式
,它用来将隐式概念显性化
。
BillSpecification
定义了规则接口,它对Customer
提供了IsOverDue
方法,这个接口的有两种实现:
NormalOverDue
: 只要当前日期超过应付款日期(dueDate
),就认为超期GracePeriod
: 可以对客户设置宽限期,宽限期内不认为超期// Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
// specification
package specification
import "time"
type Bill struct {
loanDate time.Time
dueDate time.Time
}
func NewBill(dueDate time.Time) *Bill {
return &Bill{loanDate: time.Now(), dueDate: dueDate}
}
func (bill *Bill) IsOverDue(gracePeriod time.Duration) bool {
return time.Now().After(bill.dueDate.Add(gracePeriod))
}
func (bill *Bill) GetDueDate() time.Time {
return bill.dueDate
}
// Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
// specification
package specification
import "time"
type Customer struct {
bills []*Bill
gracePeriod time.Duration
specification BillSpecification
}
func NewCustomer() *Customer {
return &Customer{specification: &NormalOverDue{}}
}
func (c *Customer) AddBill(bill *Bill) {
c.bills = append(c.bills, bill)
}
func (c *Customer) WithGracePeriod(gracePeriod time.Duration) {
c.gracePeriod = gracePeriod
c.specification = &GracePeriod{gracePeriod: gracePeriod}
}
func (c *Customer) HasBadReputation() bool {
overDueCount := 0
for _, bill := range c.bills {
if c.specification.IsOverDue(bill) {
overDueCount++
}
}
return overDueCount*3 > len(c.bills)
}
// Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
// specification
package specification
import "time"
type BillSpecification interface {
IsOverDue(bill *Bill) bool
}
type NormalOverDue struct {
}
func (n *NormalOverDue) IsOverDue(bill *Bill) bool {
return time.Now().After(bill.GetDueDate())
}
type GracePeriod struct {
gracePeriod time.Duration
}
func (g *GracePeriod) IsOverDue(bill *Bill) bool {
return time.Now().After(bill.dueDate.Add(g.gracePeriod))
}
// Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
// specification
package specification
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
const oneDay = time.Hour * 24
func Test_customer_not_in_bad_reputation_with_grace_period(t *testing.T) {
bills := []*Bill{
NewBill(time.Now().Add(-oneDay * 2)),
NewBill(time.Now().Add(-oneDay)),
NewBill(time.Now().Add(oneDay)),
NewBill(time.Now().Add(oneDay * 2)),
NewBill(time.Now().Add(oneDay * 3)),
}
customer := NewCustomer()
for _, bill := range bills {
customer.AddBill(bill)
}
// 默认超期策略
assert.True(t, customer.HasBadReputation())
// 修改为宽限期策略
customer.WithGracePeriod(oneDay)
assert.False(t, customer.HasBadReputation())
}