領域驅動設計(DDD):面向對象思想

架構師(JiaGouX)

面向對象

面向對象是一種對世界理解和抽象的方法。那麼對象是什麼呢?

對象是對世界的理解和抽象,世界又代稱爲萬物。理解世界是比較複雜的,但是世界又是由事物組成的。

正是這樣的一種關係,認識事物是極其重要的。那什麼是事物呢?

事物:由事和物兩個方面組成。事即事情,物即物體,那什麼是事情?什麼是物體呢?

由於對象是對事物的理解和抽象,所以對象就是對一個事物的屬性和行爲的理解和抽象。正是這樣的一種關係,面向對象就是對一個事物的屬性和行爲的理解和抽象的方法。

理解對象以及抽象 “對象” 就是在理解和抽象事物的屬性和行爲。

屬性和操作

面向對象的核心是對象,對象是由屬性和方法組合而成的。在使用面向對象進行分析、設計、編碼的時候,你首先應該想到的是屬性和方法組合形成的對象。在需要組合的時候就不應該出現只包含屬性的對象或者只包含方法的對象。

事物由事情和物體組成。事情是行爲,物體是屬性。

對象建模

在數據庫系統中,它們關心的是事物中的物體,所以在抽象事物時它們只抽象了事物中的屬性。在應用系統中,它們關心的是表達事物的三種方式(屬性和方法的組合、只包含屬性、只包含方法),所以在抽象事物時需要思考你需要那種方式。

只要需要抽象事物(事情和物體)中的屬性,也就是物體的這部分,那有可能是需要持久化的。只要需要持久化,通常是保存到關係型數據庫中,在關係型數據庫中的表(Table)基本上是與面向對象中的對象(Object)的屬性是一一對應的。

由於數據庫中的表只抽象了事物中的屬性,所以它有可能是不完整的。就抽象事物的屬性來說依然有兩種:只抽象事物的屬性、抽象事物的屬性和方法的組合。

正是數據庫中表的這種抽象形成了數據模型,它對比對象模型是不完整,所以在面向對象分析(OOA)時一定要採用對象(事物)抽象而不是數據(屬性、物體)抽象。

舉個例子:

簡單金融賬戶(Account)

屬性有:賬號(id)、餘額(balance)、狀態(status)

操作有:開戶(open)、註銷(close)、存錢(credit)、取錢(debit)。

數據模型的只需要設計字段(fields)和關聯關係,所以下面的 SQL 基本已完成。

create table account
(
    id      integer,
    balance integer,
    status  integer
);

如果把上述 SQL 轉換成 Java 的對象的話,得到將是一個用面向對象設計的數據模型,而不是完整的對象模型。這種模型在 Java 開發中非常普遍,這是數據模型思維所導致的結果。

@Getter
@Setter
public class Account {
    private int id;
    private int balance;
    private AccountStatus status;
}

如果使用對象模型的思維來設計模型,從接口上來看,他應該是這樣的:

public interface Account {

    int getId();

    int getBalance();

    AccountStatus getStatus();

    void open();

    void close();

    void credit(int amount);

    void debit(int amount);
}

如果 Account 接口符合金融賬戶的設計,那麼 Account 最簡單地實現應該如下:

@Getter
public class Account {
    private int id;
    private int balance;
    private AccountStatus status;

    public void open() {
        this.status = AccountStatus.OPENED;
    }

    public void close() {
        this.status = AccountStatus.CLOSED;
    }

    public void credit(int amount) {
        this.balance += amount;
    }

    public void debit(int amount) {
        this.balance -= amount;
    }
}

這是從兩個建模的角度來對比對象模型和數據模型的不同,下面我們還要從完整地執行流程來對比。

Account Credit

首先是使用數據模型所設計的時序圖,因爲數據模型下的 Account 不包含業務邏輯,所有的業務邏輯都在 AccountService 中,所以通常稱爲業務邏輯服務(層)或者事務腳本。如圖下:

使用 Java 代碼的實現:

public class AccountService {

    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    public Account creditAccount(int accountId, int amount) {
        var account = this.accountRepository.findById(accountId)
                .orElseThrow(() -> new AccountException("The Account was not found"));
        if (AccountStatus.OPENED != account.getStatus()) {
            throw new AccountException("The Account is not open");
        }
        account.setBalance(account.getBalance() + amount);
        return this.accountRepository.save(account);
    }
}
複製代碼

現在我們要使用對象模型的思維進行設計時序圖

使用 Java 代碼的實現:

public class AccountService {

    private final AccountRepository accountRepository;

    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }

    public Account creditAccount(int accountId, int amount) {
        var account = this.accountRepository.findById(accountId)
                .orElseThrow(() -> new AccountException("The Account was not found"));
        account.debit(amount);
        return this.accountRepository.save(account);
    }
}

在 AccountService 的 creditAccount 方法中已經沒有了業務代碼,更多地是協調調用執行流程。對於這種只用來實現執行流程,不在包含業務邏輯的服務對象,將它們稱爲應用服務 (Application Service)。

舉個家政服務公司與 AccountService 相似的例子:

比如你想請一位保潔阿姨給家裏做一做清潔工作,首先是你打電話給家政服務公司說你要給家裏做一做清潔工作,然後家政公司安排一位保潔阿姨去你家幫忙完成清潔工作,在這個過程中家政公司主要做了接待、協調、安排、最後可能包含一些保潔阿姨的績效等一系列工作。上面的 AccountService 也一樣是在做這樣的一件事情。所以在對象模型中,AccountService 只需要做像家政公司這樣協調工作,具體地工作由保潔阿姨來完成,這裏的保潔阿姨就相當於 Account 對象。

從兩處對比來看,採用數據模型建模配合業務邏輯服務的方式更像是過程化編程,只是在使用面嚮對象語言來編寫過程化代碼。而採用對象模型配合應用服務的方式纔是符合面向對象編程。

組合與聚合

在多數的業務開發中,普遍提到的是關聯關係(一對一、一對多、多對多)和繼承泛化,很少去關注組合與聚合,但是組合與聚合在面向對象中是相當重要的。

組合與聚合是在探討整體與部分的關係,這種整體與部分的關係是一種比關聯關係更強的關係。比如:汽車與輪胎,汽車是一個整體,輪胎是汽車的一部分。如果汽車沒有輪胎,那麼就無法構成汽車的完整性,所以在討論整體與部分的關係時,要特別注意整體對部分的依賴性而不是部分對整體的依賴。

首先通過一個人進食過程的用例來考慮整體與部分的依賴關係,然後在例子中說明組合與聚合區別和聯繫。

這個進食過程需要多個人體器官協作配合。首先是通過一種方式將食物送進口腔,由牙齒的咀嚼和舌頭的攪拌,再由喉嚨吞嚥,從食道進入胃中,在通過胃裏進行初步消化,將飲食變成食糜,然後傳入小腸後,在脾的運化作用下,精微物質被吸收。

注意:這個從嘴到胃的執行過程並不是一個 Input/Output 方式,而是一個 Stream 方式,後面還有連接。從這個角度來考慮嘴只是 Stream 的入口,但是這個用例主要是想說明整體與部分的聯繫,所以把這種連接的每一個部分修改成 Input/Output 調用方式。

爲這次進食過程來建模吧!首先確定關鍵的對象模型有:人 (Person)、嘴 (Mouth)、食道 (Esophagus)、胃 (Stomach)、腸道 (Intestine)。代碼如下:

// 嘴
public class Mouth {
    public Object chew(Object food) {
        return food;
    }
}

// 食道
public class Esophagus {
    public Object transfer(Object paste) {
        return paste;
    }
}

// 胃
public class Stomach {
    public Object fill(Object paste) {
        return paste;
    }
}

// 腸道
public class Intestine {
    public void absorb(Object chyme) {
        // absorbing...
    }
}

public class Person {
    private final Mouth mouth;
    private final Esophagus esophagus;
    private final Stomach stomach;
    private final Intestine intestine;

    public Person() {
        this.mouth = new Mouth();
        this.esophagus = new Esophagus();
        this.stomach = new Stomach();
        this.intestine = new Intestine();
    }

    public void eat(Object food) { // 進食。
        var paste = this.mouth.chew(food); // 咀嚼形成漿糊。
        paste = this.esophagus.transfer(paste); // 通過食道傳送食物。
        var chyme = this.stomach.fill(paste); // 填充到胃裏形成食糜。
        this.intestine.absorb(chyme); // 在腸道里吸收營養。
        // 便祕中...
    }
}

public class PersonTests {
    public static void main(String[] args) {
        new Person().eat("chips");
    } 
}

在整個進食流程中,是由人 (Person) 做的喫 (eat) 這個動作開始,然後由人體內部的多個參與的部分對象協調完成的,這就是整體與部分的關係。Person 是個整體,Mouth, Esophagus, Stomach, Intestine 是整體內的部分。然後在考慮一個事情,這些部分對象是不是依附在整體對象上,比如:嘴是不是獨立於人體不能存活,伴隨着人的存在而存在,消亡而消亡。這種部分對象的創建、存在和消亡都是和整體對象一起的就稱爲組合。而聚合就不像組合的整體與部分的關係那麼強,比如:汽車與輪胎是一個整體與部分的關係,汽車沒有輪胎肯定跑不了。但是汽車可以更換輪胎,這種可以更換的關係就沒有組合關係那麼強。除了更換還有缺少的,比如:螃蟹有八條腿,總的來說螃蟹沒有腿肯定是無法行走的,但是缺少一個兩個還是能行走的,可能行走有一些困難。這樣的可以在初始化之後能夠更換的或者不需要強制完整的整體與部分的關係稱之爲聚合。

隨着時間的向前和空間的擴大,組合和聚合還是會存在轉換的情況,比如未來人可以換嘴、進食流程不需要嘴的參與,再比如說一次性轎車,出廠後就不能維修更換等等。所以在討論組合與聚合的關係時,要在一定的限界下來討論。

開源電商

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

項目地址:gitee.com/mallfoundry…

總結

本文轉自網友: 發佈的領域驅動設計系列文章,點擊閱讀原文可以訪問作者主頁

如喜歡本文,請點擊右上角,把文章分享到朋友圈
如有想了解學習的技術點,請留言給若飛安排分享

來源:https://juejin.cn/user/4441682708804142

架構師

我們都是架構師!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/cOR1oec1-UE7IhPaXYayiw