
领域服务可能是最容易被误解的 DDD 模式,各种 Web 框架都对此感到困惑。在许多框架中,服务承担着多种角色。它负责管理业务逻辑、创建 UI 组件(如表单字段)、处理会话和 HTTP 请求,有时甚至充当包罗万象的“实用程序”类或包含可能属于最简单的值对象的代码。其实这些都不属于领域服务。在本文中,我将努力让大家更清楚地了解其目的和正确用法。
领域服务的一个关键规则是它们不能维持任何状态。此外,领域服务不得拥有任何具有状态的字段。虽然这条规则似乎很明显,但值得强调的是,它并不总是被遵循。根据开发人员的背景,他们可能有使用为每个请求运行独立进程的语言进行 Web 开发的经验。在这种情况下,如果服务包含状态,可能就不是什么问题了。然而,在使用 Go 时,通常对整个应用程序使用域服务的单个实例。因此,当多个客户端访问内存中的相同值时,必须考虑后果。
type Account struct {
ID uint
Person Person
Wallets []Wallet
}type Money struct {
Amount int
Currency Currency
}type DefaultExchangeRateService struct {
repository *ExchangeRateRepository
useForceRefresh bool
}
type CasinoService struct {
bonusRepository BonusRepository
bonusFactory BonusFactory
accountService AccountService
}如上例所示,实体和值对象都保留状态。实体可以在运行时修改其状态,而值对象始终保持相同的状态。当我们需要值对象的新实例时,我们会创建一个新的实例。相比之下,领域服务不包含任何有状态对象。它仅包含其他无状态结构,例如存储库、其他服务、 工厂和配置值。虽然它可以启动状态的创建或持久化,但它本身并不保留该状态。
type TransactionService struct {
bonusRepository BonusRepository
result Money // field that contains state
}
func (s *TransactionService) Deposit(account Account, money Money) error {
bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
if err != nil {
return err
}
//
// some code
//
s.result = s.result.Add(money) // changing state of service
return nil
}在上面的例子中,TransactionService以值对象的形式维护一个有状态字段Money。每当我们打算进行新的存款时,我们都会执行应用逻辑Bonuses,然后将其添加到最终结果中,最终结果就是服务内部的一个字段。这种方法是错误的,因为它会导致每次有人存款时总额都会被修改。这不是理想的行为;相反,我们应该保持每个的汇总Account。为了实现这一点,我们应该将计算结果作为方法的结果返回,如下例所示。
type TransactionService struct {
bonusRepository BonusRepository
}
func (s *TransactionService) Deposit(current Money, account Account, money Money) (Money, error) {
bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
if err != nil {
return Money{}, err
}
//
// some code
//
return current.Add(money), nil // returning new value that represents new state
}newTransactionService总是生成最新的计算,而不是将其存储在内部。不同的用户无法在内存中共享同一个对象,并且域服务可以再次充当单个实例。在这种方法中,此服务的客户端现在负责维护新结果并在每次存款时更新它。
领域服务代表特定于 问题领域的行为。它为无法整齐地封装在单个实体或值对象中的复杂业务不变量提供解决方案。有时,特定行为可能涉及与多个实体或值对象的交互,这使得确定哪个实体应该拥有该行为变得具有挑战性。在这种情况下,领域服务可以提供帮助。
必须澄清的是,域服务不负责处理会话或请求,不了解 UI 组件,不执行数据库迁移,也不验证用户输入。它的唯一作用是管理域内的业务逻辑。
type ExchangeRateService interface {
IsConversionPossible(from Currency, to Currency) bool
Convert(to Currency, from Money) (Money, error)
}
type DefaultExchangeRateService struct {
repository *ExchangeRateRepository
}
func NewExchangeRateService(repository *ExchangeRateRepository) ExchangeRateService {
return &DefaultExchangeRateService{
repository: repository,
}
}
func (s *DefaultExchangeRateService) IsConversionPossible(from Currency, to Currency) bool {
var result bool
//
// some code
//
return result
}
func (s *DefaultExchangeRateService) Convert(to Currency, from Money) (Money, error) {
var result Money
//
// some code
//
return result, nil
}在上面的例子中,我们以ExchangeRateService为例。每当我需要提供一个无状态结构并将其注入另一个对象时,我都会定义一个接口。这种做法有助于单元测试。ExchangeRateService负责管理与货币兑换相关的整个业务逻辑。它包括ExchangeRateRepository来检索所有汇率,从而允许它对任何金额进行转换。
type TransactionService struct {
bonusRepository BonusRepository
accountService AccountService
//
// some other fields
//
}
func (s *TransactionService) Deposit(account Account, money Money) error {
bonuses, err := s.bonusRepository.FindAllEligibleFor(account, money)
if err != nil {
return err
}
//
// some code
//
for _, bonus := range bonuses {
err = bonus.Apply(&account)
if err != nil {
return err
}
}
//
// some code
//
err = s.accountService.Update(account)
if err != nil {
return err
}
return nil
}如上所述,域服务封装了过于复杂而无法局限于单个实体或值对象中的业务不变量。在上面的示例中,管理 每当 进行新存款时TransactionService应用的复杂逻辑。与其强迫或实体相互依赖,或者更糟的是,为实体方法提供预期的存储库或服务,更合适的方法是创建域服务。此服务可以封装整个业务逻辑,以根据需要应用于任何实体 。BonusesAccountAccountBonusBonusesAccount
在某些情况下,我们的有界上下文 依赖于其他上下文。一个常见的例子是微服务集群,其中一个微服务通过 REST API 访问另一个微服务。通常,从外部 API 获取的数据对于主要有界上下文的运行至关重要。因此,在我们的领域层中,我们应该能够访问该数据。必须将领域层与技术复杂性分开。这意味着将与外部 API 或数据库的集成直接纳入我们的业务逻辑被视为代码异味。
这就是领域服务发挥作用的地方。在领域层,我总是为服务提供一个接口作为外部集成的契约。然后我们可以在整个业务逻辑中注入该接口,而实际的实现则位于基础架构层。
type AccountService interface {
Update(account Account) error
}type AccountAPIService struct {
client *http.Client
}
func NewAccountService(client *http.Client) domain.AccountService {
return &AccountAPIService{
client: client,
}
}
func (s AccountAPIService) Update(account domain.Account) error {
var request *http.Request
//
// some code
//
response, err := s.client.Do(request)
if err != nil {
return err
}
//
// some code
//
return nil
}在上面的示例中,我AccountService在领域层中定义了接口。它充当其他领域服务可以利用的契约。但是,实际的实现是通过提供的AccountAPIService。 AccountAPIService负责将 HTTP 请求发送到外部 CRM 系统 或我们专为处理而设计的内部微服务Accounts。这种方法允许灵活性,因为我们可以创建替代的实现AccountService。例如,我们可以开发一个与Accounts文件中的测试一起工作的实现,适用于独立的测试环境。
到目前为止,我们已经清楚何时以及为何应该提供领域服务。但是,在某些情况下,我们无法立即确定服务是否也应被视为领域服务或属于不同的层。基础设施服务通常是最容易识别的。它们总是包含技术细节、数据库集成或与外部 API 的交互。通常,它们充当来自其他层的接口的具体实现。
展示服务也很容易识别。它们始终涉及与 UI 组件或用户输入验证相关的逻辑,表单服务 就是一个典型的例子。
区分应用程序和域服务时会出现挑战。我个人发现区分这两种类型最具挑战性。根据我的经验,我主要使用应用程序服务来提供管理会话或处理请求的一般逻辑。它们也适用于管理授权和访问权限。
type AccountSessionService struct {
accountService AccountService
}
func (s *AccountSessionService) GetAccount(session *sessions.Session) (*Account, error) {
value, ok := session.Values["accountID"]
if !ok {
return nil, errors.New("there is no account in session")
}
id, ok := value.(string)
if !ok {
return nil, errors.New("invalid value for account ID in session")
}
account, err := s.accountService.ByID(id)
if err != nil {
return nil, err
}
return account, nil
}在许多情况下,我使用应用服务作为域服务的包装结构。每当我需要在会话中缓存某些内容并利用域服务作为数据检索的后备时,我都会采用这种方法。您可以在上面的示例中观察到这种方法。在此示例中,AccountSessionService用作应用服务,包含域层的功能AccountService。它的职责是从会话存储中检索值,然后利用它来Account从底层服务中检索详细信息。
领域服务是一种无状态结构,它封装了来自实际业务领域的行为。它与各种对象(例如实体和值对象)交互,以处理复杂的行为,尤其是那些在其他对象中没有明确归属的行为。需要注意的是,领域服务与其他层的服务只有名称相同,因为其目的和职责完全不同。
领域服务仅与业务逻辑相关,应该与技术细节、会话管理、处理请求或任何其他特定于应用程序的问题保持分离。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系外文翻译,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。