領域驅動設計(DDD)前夜:面向過程與面向對象思惟

面向過程與面向對象思惟

在大多數的狀況下,咱們都是從面向過程的語言(C語言)開始學起編程,而後是進入到面向對象的語言中,好比 Java、C#、Python 等。但在使用面向對象編程時,有可能依然保留着部分面向過程的思惟,或者存在一些錯誤地面向對象思惟。git

下面我將經過兩個示例來對比面向過程與面向對象思惟的不一樣,並在每一個示例實現後,再舉一個實際示例和錯誤示例來講明兩個問題:typescript

  • 在面向對象編程中會存在一些過程化的腳本編碼。
  • 對象建模中會存在一些對象建模錯誤問題的。

在描述面向過程與面向對象的區別時,有一個經典的例子叫作《把大象裝進冰箱》:編程

  1. 人把冰箱門打開
  2. 人把大象裝進去
  3. 人把冰箱門關上

然而又演變出另一個版本《把大象走進冰箱》:後端

  1. 人把冰箱門打開
  2. 大象走進冰箱
  3. 人把冰箱門關上

這兩個版本一個是把大象裝進冰箱,另外一個是大象本身走進冰箱。在使用面向過程和麪向對象實現這兩個用例時,你應該所有實現出來,也就是說應該使用面向過程分別實現這兩個用例:裝進冰箱和走進冰箱,使用面向對象也分別實現這兩個用例:裝進冰箱和走進冰箱。而後再去橫向對比,使用面向過程實現的「把大象裝進冰箱」與使用面向對象實現的「大象裝進冰箱」。而不是使用面向過程實現裝進冰箱,使用面向對象實現走進冰箱,而後交叉對比。以下表格:數組

面向過程 面向對象
裝進冰箱 裝進冰箱
走進冰箱 走進冰箱

把大象裝進冰箱

首先來實現把大象裝進冰箱的面向過程示例:服務器

// 定義一個用於操做數組的 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()
}

就這兩個示例好像是對比說明了面向過程與面向對象的一些差異。但這兩種思惟對平常工做能有什麼影響和幫助呢?學習

好比如今要開發一個智能家居系統,其中有一個功能:「智能管家」能夠將一些水果放置到冰箱裏,並在操做完成後,智能管家經過調用後端服務接口來及時更新冰箱信息,而後房主就能夠經過手機查看到冰箱內的物品。如今要將「智能家居系統」設計成一個後端服務,這樣就能夠爲智能管家和手機終端提供服務。ui

使用面向過程的實現:

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 編程思想》說的那樣「若是說它有缺點,那就是掌握它需付出的代價。」

tij-1

大象走進冰箱

使用面向過程的實現:

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 對象就毫無關係,沒有任何關係大象是走去哪呢?因此這個實如今嚴謹性和思惟上有些失誤。

開源電商

Mallfoundry 是一個徹底開源的使用 Spring Boot 開發的多商戶電商平臺。它能夠嵌入到已有的 Java 程序中,或者做爲服務器、集羣、雲中的服務運行。

  • 領域模型採用領域驅動設計(DDD)、接口化以及面向對象設計。

項目地址:https://gitee.com/mallfoundry/mall

總結

首先是經過「大象裝進冰箱」的示例來講明面向過程與面向對象的區別,而後又經過「智能家居系統」的示例來講明在面向對象編程中可能存在着一些腳本化編碼的問題。在「大象裝進冰箱」和「智能家居系統」兩個示例中逐步引導使用面向對象來思考問題。

其次又經過「大象走進冰箱」的示例來講明的被動與主動關係,並在最後經過錯誤調用(elephant.into())來引發你們對對象建模深層思考。

相關文章
相關標籤/搜索