【DDD读书笔记】Specification模式:将隐式概念显性化

目录

  • 业务需求
  • 实现
  • 优化
  • 宽限期
  • 将隐式概念显性化
  • Specification模式
  • 完整代码
      • Bill
      • Customer
      • Specification
      • 用例

业务需求

*
Customer
+HasBadReputation()
Bill
- loanDate // 借款日
- dueDate // 到期日

在一个借贷系统里面,客户(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方法,用来判断账单是否超期

*
Customer
+HasBadReputation()
Bill
- loanDate // 借款日
- dueDate // 到期日
+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
+HasBadReputation()
Bill
- loanDate // 借款日
- dueDate // 到期日
+IsOverDue()

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模式,它用来将隐式概念显性化

Specification模式

*
Realization
Realization
Customer
- gracePeriod // 宽限期
+HasBadReputation()
Bill
- loanDate // 借款日
- dueDate // 到期日
+GetDueDate()
«interface»
BillSpecification
+IsOverDue(Bill)
NormalOverDue
GracePeriod
- gracePeriod

BillSpecification定义了规则接口,它对Customer提供了IsOverDue方法,这个接口的有两种实现:

  • NormalOverDue: 只要当前日期超过应付款日期(dueDate),就认为超期
  • GracePeriod: 可以对客户设置宽限期,宽限期内不认为超期

完整代码

Bill

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

Customer

// 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)
}

Specification

// 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())
}

你可能感兴趣的:(领域驱动设计,领域启动设计,specification模式,读书笔记,golang)