DDD 领域驱动设计前夜:面向过程与面向对象思维


在大多数的情况下,我们都是从面向过程的语言(C语言)开始学起编程,然后是进入到面向对象的语言中,比如 Java、C#、Python 等。但在使用面向对象编程时,有可能依然保留着部分面向过程的思维,或者存在一些错误地面向对象思维。下面我将通过两个示例来对比面向过程与面向对象思维的不同,并在每个示例实现后,再举一个实际示例和错误示例来说明 两个问题:
  • 在面向对象编程中会存在一些过程化的脚本编码。
  • 对象建模中会存在一些对象建模错误问题的。


在描述面向过程与面向对象的区别时,有一个经典的例子叫做《把大象装进冰箱》:
  1. 人把冰箱门打开
  2. 人把大象装进去
  3. 人把冰箱门关上


然而又演变出另外一个版本《把大象走进冰箱》:
  1. 人把冰箱门打开
  2. 大象走进冰箱
  3. 人把冰箱门关上


这两个版本一个是把大象装进冰箱,另一个是大象自己走进冰箱。在使用面向过程和面向对象实现这两个用例时,你应该全部实现出来,也就是说应该使用面向过程分别实现这两个用例:装进冰箱和走进冰箱,使用面向对象也分别实现这两个用例:装进冰箱和走进冰箱。然后再去横向对比,使用面向过程实现的“把大象装进冰箱”与使用面向对象实现的“大象装进冰箱”。而不是使用面向过程实现装进冰箱,使用面向对象实现走进冰箱,然后交叉对比。如下表格:
1.png

把大象装进冰箱

首先来实现把大象装进冰箱的面向过程示例:
// 定义一个用于操作数组的 push 方法。
declare function push(array: any[], element: any)

class Elephant {
}

class Fridge {
public status: string
public elephants: Elephant[] = []
}

function open(fridge: Fridge) {
fridge.status = "opened"
}

function put(fridge: Fridge, elephant: Elephant) {
push(fridge.elephants, elephant)
}

function close(fridge: Fridge) {
fridge.status = "closed"
}

function main() {
const elephant = new Elephant()
const fridge = new Fridge()
open(fridge)
put(fridge, elephant)
close(fridge)


其中使用ElephantFridge对象来模拟结构体,然后使用打开(open)、放进(put)、关闭(close)这三个函数来分别完成对应的业务逻辑。

然后是把大象放进冰箱的面向对象的实现示例:
class Elephant {
}

class Fridge {
status: string
elephants: Elephant[] = []

open() {
    this.status = "opened"
}

put(elephant: Elephant) {
    this.elephants.push(elephant)
}

close() {
    this.status = "closed"
}
}

function main() {
const elephant = new Elephant() 
const fridge = new Fridge()
fridge.open()
fridge.put(elephant)
fridge.close()


就这两个示例好像是对比说明了面向过程与面向对象的一些差别。但这两种思维对日常工作能有什么影响和帮助呢?

比如现在要开发一个智能家居系统,其中有一个功能:“智能管家”可以将一些水果放置到冰箱里,并在操作完成后,智能管家通过调用后端服务接口来及时更新冰箱信息,然后房主就可以通过手机查看到冰箱内的物品。现在要将“智能家居系统”设计成一个后端服务,这样就可以为智能管家和手机终端提供服务。

使用面向过程的实现:
class FridgeService {

private readonly fridgeRepository: FridgeRepository

constructor(fridgeRepository: FridgeRepository) {
    this.fridgeRepository = fridgeRepository
}

// /v1/fruits/:fridgeId/open
public openFridge(fridgeId: string): Fridge {
    const fridge = this.fridgeRepository.findById(fridgeId)
    fridge.status = "opened"
    return this.fridgeRepository.save(fridge)
}

// /v1/fruits/:fridgeId/put
public putFruit(fridgeId: string, fruit: Fruit): Fridge {
    const fridge = this.fridgeRepository.findById(fridgeId)
    if (fridge.status !== "opened") {
        throw new Error("The fridge is not open")
    }
    fridge.fruits.push(fruit)
    return this.fridgeRepository.save(fridge)
}


这应该是大家最常见的编码方式,直接在 Service 里面编写业务逻辑,但这并既不是一个好地面向过程编码规则也不是一个好地面向对象的编码规则。首先你没有像面向过程那样将业务封装成一个个功能函数,也没有像面向对象那样将业务封装到实体对象的方法内,这只能算是脚本化开发

使用面向对象的实现:
class Fridge {
status: string
fruits: Fruit[] = []

open() {
     this.status == "opened"
}

isOpened() {
    return this.status === "opened"
}

put(fruit: Fruit) {
    if (!this.isOpened()) {
        throw new Error("The fridge is not open")
    }
    this.fruits.push(fruit)
}
}

class FridgeService {

// /v1/fruits/:fridgeId/open
public openFridge(fridgeId: string): Fridge {
    const fridge = this.fridgeRepository.findById(fridgeId)
    fridge.open() // Open fridge
    return this.fridgeRepository.save(fridge)
}

// /v1/fruits/:fridgeId/put
public putFruit(fridgeId: string, fruit: Fruit): Fridge {
    const fridge = this.fridgeRepository.findById(fridgeId)
    fridge.put(fruit) // Put fruit
    return this.fridgeRepository.save(fridge)
}


从最开始的将“大象装进冰箱”的两种实现和“智能家居系统”的两种实现,其实是三种编程方式:具有小函数思维的面向过程、脚本化的面向过程以及面向对象的编程方式。

通过这三种方式的对比,我们应该发现到脚本化开发是最简单的,学习成本最低并且最常见的。但是这种模式所开发的项目经过日积月累以后会变的难以维护。他几乎没有功能抽象性,只是对一个功能进行逻辑实现,好一些的脚本代码会对一个功能进行分解,但还没有达到像“大象装进冰箱”的面向过程示例那样,将一个业务功能细化的分解成:开门(open)、放进去(put)、关门(close)这些功能函数。一旦细化后那他就是具有小函数思维的面向过程。如果你现在是使用的像 Java、C# 这样的纯面向对象非多范式的编程语言,这两种思维你都不适合,你应该在面向对象的编程语言里使用面向对象的思维进行编码,而不是在面向对象里留恋面向过程。在培养面向对象思维时,就像《Java 编程思想》说的那样“如果说它有缺点,那就是掌握它需付出的代价。”
2.png

大象走进冰箱

使用面向过程的实现:
function into(elephant: Elephant, fridge: Fridge) {
push(fridge.elephants, elephant)
}

function main() {
const elephant = new Elephant()
const fridge = new Fridge()
open(fridge)
into(elephant, fridge) // 大象走进冰箱
close(fridge)


这个对比“把大象装进冰箱的面向过程的实现”来看,区别不大。只是 put(fridge,elephant) 方法改为了 into(elephant, fridge) 方法,但这形参位置一前一后的改变是编程语言与自然语言的顺序描述,或者是主动被动的区别,主动被动,在软件体系结构中主动被动是一个重要概念。

使用面向对象的实现:
class Elephant {
into(fridge: Fridge) {
    fridge.put(this)
}
}

function main() {
const elephant = new Elephant()
const fridge = new Fridge()
fridge.open()
elephant.into(fridge) // 大象自己走进冰箱。
fridge.close()


“大象走进冰箱”这样的用例在现实业务中还是有的,比如说:智能汽车自动入库,当告诉汽车需要驶入的车库后,智能汽车可以自动完成入库操作。

但是有一些在编写“大象走进冰箱”的代码实现上有一些错误。比如:
const fridge = new Fridge()
const elephant = new Elephant()
fridge.open()
elephant.into() // Error
fridge.close()

其中最大地问题在于 elephant.into() 这个方法调用。可以看到 into() 是个空参方法,这样与 fridge 对象就毫无关系,没有任何关系大象是走去哪呢?所以这个实现在严谨性和思维上有些失误。

总结

首先是通过“大象装进冰箱”的示例来说明面向过程与面向对象的区别,然后又通过“智能家居系统”的示例来说明在面向对象编程中可能存在着一些脚本化编码的问题。在“大象装进冰箱”和“智能家居系统”两个示例中逐步引导使用面向对象来思考问题。

其次又通过“大象走进冰箱”的示例来说明的被动与主动关系,并在最后通过错误调用(elephant.into())来引起大家对对象建模深层思考。

0 个评论

要回复文章请先登录注册