电梯是上班族最常使用的工具之一。上班、下楼取快递、拿快递上楼、下楼取外卖、拿外卖上楼、下楼抽烟摸鱼、上楼、下班……每个人每天都要往返乘坐电梯很多次。
而电梯的设计也是五花八门。最常见的莫过于想上楼就按上想下楼就按下,进了电梯想去几楼就按几楼的那种。而有的电梯为分流考虑分高低层、奇偶层;有的小区电梯为安全考虑进去之后需要刷卡才能按楼层键,且只能按你注册的那个楼层;也有的电梯可以在外面就选择楼层等等。
而我最近的项目是给一个客户的系统做DDD改造,他们所在的大楼电梯比较新奇。你必须在电梯外面扫员工卡,然后在触摸屏上选择要去的楼层,扫卡处的显示屏会通知你将要乘坐的电梯编号。
比如你要去18层,刷卡之后点击18层,然后显示屏会显示C,提示C电梯将带你去18层。而其他人这时如果再选择其他楼层,很有可能为他服务的将是另一部电梯。如果你是一个程序员,显然就会意识到这时的调度算法肯定是和普通的电梯不一样的了。
这天我正在给团队介绍DDD中应用服务与领域服务的区别。可能是我表达能力有限,讲了半天大家还是一脸疑惑。已经到饭点儿了,只好先暂停,我和同事大饼准备下楼吃饭。
大饼刷了临时员工卡,按下了1层,屏幕上显示的是D。这时刚好电梯来了,大饼一边喊一边拉着我往里进:“DDD”。
DDD初学者最容易迷惑的概念就是应用服务和领域服务。因为一般来说代码只会有一个服务层,什么逻辑都往里面放。如果突然告诉他们还有应用服务和领域服务之分,自然是一头雾水。
但这部电梯的设计恰好完美解释了什么是应用服务什么是领域服务。
对于一部电梯来说其核心的业务逻辑应该是可以根据指令上行、下行(有可能还包括开门、关门),但至于指令是如何产生的其实并不关心。对于最普通的电梯系统来说,我们可以将它们都设计为核心领域的一部分。
随着对于系统安全性的重视,用磁卡代替直接按键来选择楼层的需求产生了,也就是说,生成指令的逻辑发生了变化。如果指令生成和上行下行的逻辑纠缠在一起,这样的改造就很难完成。
更好的设计是将产生指令的代码进行抽象,普通电梯产生指令的逻辑和基于磁卡产生指令的逻辑分别是这个抽象的两个实现。这样一来就做到了隔离变化。
另外可以接着思考,生成指令的逻辑是否属于电梯的领域逻辑。
对于一部电梯来说,上行下行这种行为几乎不会发生变化,不太可能出现需要横行的需求吧?而如何生成指令则不同了。根据不同的使用场景,比如普通的场景或安全性较高的场景,它们就会发生变化。变化的频率比上行下行要高,且变化的原因是“安全性”这种“非功能性需求”。假如所有逻辑都烤制在同一块电路板上的话,当出现类似的需求变化时,改变的成本是相当高的。
想到这里,我们应该就明白了,在设计电梯时,指令生成的这部分逻辑,应该放到应用服务层,而不是领域层。这是因为指令变化这种需求是因为“使用场景”(usecase)发生变化而产生的,不是领域逻辑发生了变化。
至此我们再来总结应用服务与领域服务的区别:
- 应用服务处理软件系统的应用场景或者叫用例;领域服务处理业务的核心逻辑,与是否有软件系统没有关系。
- 应用服务中处理更多的是系统横切面的非功能需求,如事务、安全等。
针对第一点想再多说一句的是,当下大多数领域驱动设计都是针对已有系统的DDD改造,领域专家(通常也是对已有系统最熟悉的人)与开发人员在建立统一语言的时候,由于有已有系统的先入为主,已经很难区分哪个是核心逻辑、哪个是软件场景。这一问题如何应对,我们以后再聊。
后记
本文在公司的博客大赛发表后,引起了一些讨论。其中同事祁兮和吴雪峰的回复我觉得非常有价值,于是就摘抄在此处:
祁兮:
文章中有一部分我并不认同:
-
指令生成的这部分逻辑不属于应用层。
应用服务处理软件系统的应用场景或者叫用例;领域服务处理业务的核心逻辑,与是否有软件系统没有关系。
普通电梯产生指令和基于磁卡产生指令,包含判断磁卡是否有效,是否有权限,可以选择哪些楼层,这些都是业务逻辑,与是否有软件系统没有关系,所以也应该属于领域层。
可以想象磁卡的权限、磁卡对应的楼层,这些都是领域层的模型。即使这些模型属于另一个上下文,也不代表他们属于应用层。 -
场景变化不只会影响usecase
指令生成的这部分逻辑,应该放到应用服务层,而不是领域层。
这是因为指令变化这种需求是因为“使用场景”(usecase)发生变化而产生的。所有的模型和抽象都是为了满足usecase,所以usecase的变化,不仅仅会引起应用服务的变化,也会导致领域层的变化。
新的需求「用磁卡代替直接按键」,的确并不会影响最高层的策略「电梯上行、下行、开门、关门等」,但领域层中不光包含最高层的策略,也有其他诸如「磁卡是否有效」的策略(业务逻辑)。
策略也是可以有分层的,参考《整洁架构之道》25章:
-
用磁卡代替直接按键不是非功能性需求,且变化的原因是“安全性”这种“非功能性需求”
随着对于系统安全性的重视,用磁卡代替直接按键来选择楼层的需求产生了。
这个「用磁卡代替直接按键」并不是非功能性需求,而是功能性需求,具体参考非功能性需求。
吴雪峰:
首先应用层是分层架构的概念,不是DDD的核心概念,而是DDD技术实现的一个参考架构。
指令生成到底应该是应用层,还是另外一个问题域,得看情况。
- 基于楼主描述的情况,指令生成应该是另外一个问题域,而不是应用层。
- 如果是接收一个按钮直接生成指令的电梯,那么“指令生成”可以是应用层。
怎么区分应用服务还是领域服务呢,比较难。引用Eric原书的段落:
It can be harder to distinguish application SERVICES from domain SERVICES. The application layer is responsible for ordering the notification. The domain layer is responsible for determining if a threshold was met—though this task probably does not call for a SERVICE, because it would fit the responsibility of an “account” object. That banking application could be responsible for funds transfers. If a SERVICE were devised to make appropriate debits and credits for a funds transfer, that capability would belong in the domain layer. Funds transfer has a meaning in the banking domain language, and it involves fundamental business logic. Technical SERVICES should lack any business meaning at all.
讲了发通知的例子,应用层发通知,领域层决定发不发通知,应用层处理的是技术问题,没有业务含义。那么楼主说的应用场景或用例,其实还是业务含义的,不纯粹是技术问题,所以这部分逻辑应该属于领域层,而不是应用层。
分层架构的应用层如果叫技术处理层可能更不容易混淆我们在需求分析时所说的“应用场景”的“应用”。