[翻译]在 Go 应用中使用简明架构(3)

原文在此,续……

——–翻译分隔线——–

在 Go 应用中使用简明架构(3)

用例层

现在来看看用例层代码,同样,它刚刚好能放在一个文件中:

package usecases

import (
	"domain"
	"fmt"
)

type UserRepository interface {
	Store(user User)
	FindById(id int) User
}

type User struct {
	Id       int
	IsAdmin  bool
	Customer domain.Customer
}

type Item struct {
	Id    int
	Name  string
	Value float64
}

type Logger interface {
	Log(message string) error
}

type OrderInteractor struct {
	UserRepository  UserRepository
	OrderRepository domain.OrderRepository
	ItemRepository  domain.ItemRepository
	Logger          Logger
}

func (interactor *OrderInteractor) Items(userId, orderId int) ([]Item, error) {
	var items []Item
	user := interactor.UserRepository.FindById(userId)
	order := interactor.OrderRepository.FindById(orderId)
	if user.Customer.Id != order.Customer.Id {
		message := "User #%i (customer #%i) "
		message += "is not allowed to see items "
		message += "in order #%i (of customer #%i)"
		err := fmt.Errorf(message,
			user.Id,
			user.Customer.Id,
			order.Id,
			order.Customer.Id)
		interactor.Logger.Log(err.Error())
		items = make([]Item, 0)
		return items, err
	}
	items = make([]Item, len(order.Items))
	for i, item := range order.Items {
		items[i] = Item{item.Id, item.Name, item.Value}
	}
	return items, nil
}

func (interactor *OrderInteractor) Add(userId, orderId, itemId int) error {
	var message string
	user := interactor.UserRepository.FindById(userId)
	order := interactor.OrderRepository.FindById(orderId)
	if user.Customer.Id != order.Customer.Id {
		message = "User #%i (customer #%i) "
		message += "is not allowed to add items "
		message += "to order #%i (of customer #%i)"
		err := fmt.Errorf(message,
			user.Id,
			user.Customer.Id,
			order.Id,
			order.Customer.Id)
		interactor.Logger.Log(err.Error())
		return err
	}
	item := interactor.ItemRepository.FindById(itemId)
	if domainErr := order.Add(item); domainErr != nil {
		message = "Could not add item #%i "
		message += "to order #%i (of customer #%i) "
		message += "as user #%i because a business "
		message += "rule was violated: '%s'"
		err := fmt.Errorf(message,
			item.Id,
			order.Id,
			order.Customer.Id,
			user.Id,
			domainErr.Error())
		interactor.Logger.Log(err.Error())
		return err
	}
	interactor.OrderRepository.Store(order)
	interactor.Logger.Log(fmt.Sprintf(
		"User added item '%s' (#%i) to order #%i",
		item.Name, item.Id, order.Id))
	return nil
}

type AdminOrderInteractor struct {
	OrderInteractor
}

func (interactor *AdminOrderInteractor) Add(userId, orderId, itemId int) error {
	var message string
	user := interactor.UserRepository.FindById(userId)
	order := interactor.OrderRepository.FindById(orderId)
	if !user.IsAdmin {
		message = "User #%i (customer #%i) "
		message += "is not allowed to add items "
		message += "to order #%i (of customer #%i), "
		message += "because he is not an administrator"
		err := fmt.Errorf(message,
			user.Id,
			user.Customer.Id,
			order.Id,
			order.Customer.Id)
		interactor.Logger.Log(err.Error())
		return err
	}
	item := interactor.ItemRepository.FindById(itemId)
	if domainErr := order.Add(item); domainErr != nil {
		message = "Could not add item #%i "
		message += "to order #%i (of customer #%i) "
		message += "as user #%i because a business "
		message += "rule was violated: '%s'"
		err := fmt.Errorf(message,
			item.Id,
			order.Id,
			order.Customer.Id,
			user.Id,
			domainErr.Error())
		interactor.Logger.Log(err.Error())
		return err
	}
	interactor.OrderRepository.Store(order)
	interactor.Logger.Log(fmt.Sprintf(
		"Admin added item '%s' (#%i) to order #%i",
		item.Name, item.Id, order.Id))
	return nil
}

商城的用例层主要是由一个 User 实体和两个用例组成。由于用户需要一套保存和读取的持久化机制,所以这个实体跟领域层中的实体一样有存储区。

意料之中,用例就是函数,例如 OrderInteractor 结构上的方法。这并不是必须的,完全可以是没有任何绑定的函数。不过,接下来就会看到,将其附加在结构体上使得在确定的依赖关系中,更容易做到功能注入。

上面的代码就是软件禅道“将什么放到哪里”这个话题的主要示例。首先,外部层次的所有东西都需要注入到 OrderInteractor 中去 AdminOrderInteractor,而这个结构仅仅对用例层和内部东西命名。再一次强调,这全部都是因为依赖规则。这个包被设计为不依赖任何外部的领域或用例;例如,这样就可以使用模拟存储区来测试,或可以平滑的替换日志的实现部分,也就是说,不用修改任何上层代码。

Bob Martin 这样描述用例“…安排来自实体的数据流走向,并且让这些实体使用全面的商业规则来达到用例的目标。”

例如这里你看到的 OrderInteractor 的 Add 方法。这个方法控制获取所需要的对象,并让用合理的方式让它们工作,以便完整满足用例的需要。它管理了这个特别的用例可能出现的错误情况,并且确保规则生效;同时记录是哪个规则。由于 $250 的限制规则是在所有用例上生效的商业规则,所以是在领域层处理的。检查哪个用户可以向订单添加商品,从另一方面来说是用例特有的,它所有的实体 User 不应当在领域层考虑。因此在用例层处理,并且依赖于是普通用户还是管理员用户添加商品而有着不同的处理方式。

也来谈谈日志是如果在这个层次处理的吧。在软件应用中,各种日志出现在各个层次中。虽然所有的日志实体最终可能都是硬盘上的一个文本文件,不过再次强调的是将技术从概念细节中分离非常重要。我们的用例层不知道文本文件和硬盘。概念上来说,这个层次只是说:“关于应用的用例,有些有趣的事情发生了,我希望这个事件被记录下来”,而“记录”并不是说“写到什么地方”,只是说“记录”而已——不要产生任何多余的想法是相当明智的。

因此,仅仅提供一个接口来满足用例的需求,并注入实际的实现——在未来任何时候,不论应用变得多么复杂,通过这个办法都可以随时将日志消息记录到数据库来代替普通文件,只要确保接口的调用者与实现分离,就无需修改内部层次的任何一行代码。

最好的办法就是在这里设置两个不同的 OrderInteractor。当想要管理员的操作记录到一个文件,普通用户的记录到另一个文件,这就会变得非常简单。只需要创建两个不同的日志实例,都实现 usecases.Logger 接口,并且将它们分别注入到对应的 OrderInteractor 中去。

在用例代码中另一个重要的细节就是 Item 结构。我们在领域层不是已经有一个了吗?为什么不在 Items() 方法里直接返回这个?是因为不在外部层次上暴露用例层的实体是非常明智的。实体所具有的不仅仅是数据,还有行为。这些行为只应当由用例触发。只要不将实体暴露给外部层次,就能确保这些行为只会被用例触发。外部层次仅仅需要的是结构中的数据来完成其工作,因此,这就是我们应当提供给它们的全部东西。

跟领域层一样,这些代码帮助理解简明架构对于一个特定的软件是如何工作的:了解实现了哪些商业领域以及哪些规则生效,只需要查看领域层的代码,了解用户和商业之间的全部内部信息,只需要查看用例代码。就可以了解到这个应用允许客户自己添加商品到订单,并且列出订单的所有商品,以及管理员可以为用户向订单中添加商品。只要打印出来,你就有了一个关于用例的随时更新的、可靠并准确的格式化文档。

——–翻译分隔线——–

未完待续……

Join the Conversation

1 Comment

Leave a comment

Your email address will not be published. Required fields are marked *