听说过「DCI」吗?

跨界架构师

共 6175字,需浏览 13分钟

 · 2023-01-03

这里是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?分为以下四步:
  1. 识别领域场景

  2. 罗列其中的业务行为

  3. 分析这些定位属于什么角色,定义角色接口

  4. 确定承担这些角色的数据对象,定义数据类以及数据类的本地方法


好了,今天就聊这些,希望对你有所启发。


推荐阅读:


原创不易,如果你觉得这篇文章还不错,就「 点赞 」或者「在看」一下吧,鼓励我的创作 :)


也可以分享我的公众号名片给有需要的朋友们。


如果你有关于软件架构、分布式系统、产品、运营的困惑

可以试试点击「阅读原文

浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报