从此
📄文章 #️⃣专题 🌐酷站 👨‍💻技术 📺 📱

🏠 » 📄文章 » 内容

 欢迎来访!

DCI 架构 - OOP 新视角(Data, context and interaction) 增强MVC模式 | 类似组合式游戏系统ECS

🕗2024-08-15👁️14

DCI 的概念在 2009 年就被提出来了,当年的文章名是《The DCI Architecture: A New Vision of Object-Oriented Programming》。

 

DCI 中的 3 个字母分别代表:Data,Context,Interactive(interaction)。在我看来,这 3 个概念共同配合表达出了某个“角色”做的事情:

「谁/什么东西(Data)」-「在什么场景下(Context)」-「做什么事(Interactive)」

比如,当你在家里打扫卫生的时候,你的角色其实是“清洁工”;当你在家里烧饭的时候,你的角色是“厨师”;当你在家里运动的时候,你的角色是“运动者”。你看,同样的一个人在不同场景下所产生的行为都与当时所处的角色有关。

DCI 的核心就是那个 Interactive(interactions),也是“角色”这个概念存在的地方。

 

我们来举个例子看看它的作用。

就拿前面提到的例子来看,如果我们只是识别出其中的实体是 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. 确定承担这些角色的数据对象,定义数据类以及数据类的本地方法

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