听说过「DCI」吗?

这里是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. 确定承担这些角色的数据对象,定义数据类以及数据类的本地方法

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

推荐阅读:

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

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

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

可以试试点击「阅读原文

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值