这里是Z哥的个人公众号
每周五11:45 按时送达
当然了,也会时不时加个餐~
我的第「229」篇原创敬上
大家好呀,好久不见,我是Z哥。
在我最近消失的这段时间里,有些小伙伴微信问我去哪了,老读者应该知道哈,Z哥自从换了工作后的确太忙,打理公众号的时间都用来加班了。一晃眼,不知不觉都过去4个月了,太惭愧了。
不过最近收获还挺多的,我接下来慢慢花时间整理出来分享给大家。
前几周和团队里的「DDD」爱好者交流的时候,有人提到了一个概念叫「DCI」。我当时就想我只知道「DI」和「CI」,「DCI」又是什么鬼。
后来在网上搜了一下才发现在 2009 年这个概念就被提出来了,当时的文章是《The DCI Architecture: A New Vision of Object-Oriented Programming》。
DCI 中的 3 个字母分别代表:Data,Context,Interactive。在我看来,这 3 个概念共同配合表达出了某个“角色”做的事情:
「谁/什么东西(Data)」-「在什么场景下(Context)」-「做什么事(Interactive)」
比如,当你在家里打扫卫生的时候,你的角色其实是“清洁工”;当你在家里烧饭的时候,你的角色是“厨师”;当你在家里运动的时候,你的角色是“运动者”。你看,同样的一个人在不同场景下所产生的行为都与当时所处的角色有关。
DCI 的核心就是那个 Interactive,也是“角色”这个概念存在的地方。
我们来举个例子看看它的作用。
就拿前面提到的例子来看,如果我们只是识别出其中的实体是 Person,那么自然打扫卫生的方法 Clean(),以及烧饭的方法 Cook(),和运动的方法 Exercise()自然而然就落到了 Person 这个实体上。那么问题就来了,一个人在社会生活中需要做的事情有很多,如果按照这个思路,Person 这个实体将成为一个上帝类。
func (p Person) Clean() {
fmt.Println("Clean")
}
func (p Person) CookFood() {
fmt.Println("CookFood")
}
func (p Person) Sport() {
fmt.Println("Sport")
}
type Person struct {
Name string
Age int
}
同时,如果有某个领域服务或者实体上的方法以 Person 作为参数,那么其内部将可以任意调用 Clean()、Cook() 或者 Exercise()。
func MethodA(p Person){
p.Clean()
p.CookFood()
p.Sport()
}
这很明显与 OO 思想中的核心概念「高内聚低耦合」背道而驰,违反了「迪米特法则」。
而函数式编程的三层架构之所以流行了很多年,就是因为它的世界里主要关注的是颗粒度最小的「方法」应该放到三层中的哪一层,而对「方法」在某一层内放到哪个对象之中是没有明确规定的。因此在上面的例子中,如果我们将Clean()、Cook() 和 Exercise() 分别写在不同的 XXXService 中,并同时接收 Person 作为入参,那么可以轻松地消除“上帝类”,但与此同时 Person 也成为了一个只有属性的「贫血模型」。
因此 DCI 的提出就是通过一个新的视角来定义对象,它通过增加一层概念——「角色」,将传统 DDD 中对象上的非通用方法转移到了不同的角色对象中,避免了需要在一个对象上同时表达“是什么”和“能做什么”而可能出现的「上帝类」问题。同时,通过由多个角色组成的对象也避免了「贫血模型」的发生。
接下来看看如何使用 DCI 来重新设计上面的代码。
其实很简单,将 Person 设计成由 3 个角色 Cleaner、Cook、Sporter 组成,在每个角色中分别定义 Clean()、Cook() 和 Exercise()方法。
type Cleaner interface {
Clean()
}
type Cook interface {
CookFood()
}
type Sporter interface {
Sport()
}
type Person struct {
Name string
Age int
}
func (p Person) Clean() {
fmt.Println("Clean")
}
func (p Person) CookFood() {
fmt.Println("CookFood")
}
func (p Person) Sport() {
fmt.Println("Sport")
}
func DoSomeThing(cook Cook) {
fmt.Println("DoSomeThing")
cook.CookFood()
}
func main() {
var p Person
p.Clean()
p.CookFood()
p.Sport()
DoSomeThing(p)
}
如此一来,我们可以将一些使用 Person 作为入参的方法调整成相应的角色,以达到「迪米特法则」所提倡的效果。
我们再想深入一步,Cook() 和 Clean() 的实现中都需要“拿起东西”,这是一个和角色无关的行为,那么可以将它直接定义在Person中。
func (p Person) TakeUp(thing string) {
fmt.Println(fmt.Sprintf("%s TakeUp a %s", p.Name, thing))
}
func (p Person) Clean() {
p.TakeUp("扫帚")
fmt.Println("Clean")
}
func (p Person) CookFood() {
p.TakeUp("锅子")
fmt.Println("CookFood")
}
在 DCI 中,将角色上定义的方法称作「Role Method」,将对象(Data)上定义的方法称作「Local Method」。前者是填充业务逻辑的地方,而后者更像是对象(Data)自身天然具有的能力,与业务逻辑无关。
上面的这整套实现逻辑在 DCI 中被称作 Methodless Role,与之对应的还有 Methodful Role 的实现逻辑,在这里就不展开了。顾名思义就是在角色的定义上更丰富,将「Local Method」也定义出来。
另外,增加一层「角色」的概念后,我们可以发现,任何具有相同行为的对象都可以给他设置同一个角色。比如,机器人也可以打扫,那么这个 Cleaner 的角色也可以定义到 Robot 对象中,而不仅仅是 Person 对象。只不过,Robot.Clean() 的实现不是“拿起扫帚”,而是“制定一个行走路线”,然后它自己会把垃圾吸到自己身体里。
可能你会问,Context 呢?好像一直没提到它该怎么实现?以 Z 哥目前的理解来看,Context 所做的事情其实和传统 DDD 中的 Applicaion 层做的事情是重合的,只是代码结构的不同。因此,我认为这部分倒不是重点,你可以按照原先的 Application 层代码来写,相当于每一个 Application 层中的方法就是一个 Context。
本质上说,DCI 是一种 “角色接口” 设计思想,如果习惯 OO 编程的小伙伴应该是很熟悉这种写法的。
好了,我们总结一下。
这篇呢,Z哥和你分享了我对 DCI 的了解。
它通过引入「角色」的概念,将传统 DDD 建模时赋予「对象」的两个职责“是什么”和“能做什么”中的后者拆分到「角色」中去定义,避免上帝类问题。同时,因为角色最终还是会作用到「对象」上,所以也不会出现函数式编程中的贫血模型问题。
DCI 中,对定义在「角色」上的方法称为 Role Method,而直接定义在「对象」上的方法称作 Local Method。
对于「角色」在编码的实现,一般建议使用 interface 的方式来体现,因为“角色只定义行为”,具体行为要怎么做,由所在的对象来实现。如此符合「依赖倒置原则」的场景自然适合用 interface 来实现。
最后,Z 哥再分享一个实践 DCI 的思路给你。
首先是什么时候需要用 DCI?当你在实践 DDD 的过程中,觉得某个对象过大了,有点上帝类的味道,这时候就可以想一下是否可以通过 DCI 来重新设计一下。
如何落地DCI?分为以下四步:
识别领域场景
罗列其中的业务行为
分析这些定位属于什么角色,定义角色接口
确定承担这些角色的数据对象,定义数据类以及数据类的本地方法
好了,今天就聊这些,希望对你有所启发。
推荐阅读:
原创不易,如果你觉得这篇文章还不错,就「点赞」或者「在看」一下吧,鼓励我的创作 :)
也可以分享我的公众号名片给有需要的朋友们。
如果你有关于软件架构、分布式系统、产品、运营的困惑
可以试试点击「阅读原文」