領(lǐng)域驅(qū)動設(shè)計(jì)DDD-綜述


引例

DDD

?? 子域與界限上下文

?? 經(jīng)典分層架構(gòu)

?? UI層與應(yīng)用層

?? 領(lǐng)域?qū)?/h2>

?? 基礎(chǔ)設(shè)施層

總結(jié)


引例

在電商系統(tǒng)中”修改商品數(shù)量“的業(yè)務(wù)需求:

在用戶未支付訂單(Order)之前,用戶可以修改Order中商品(Goods)的數(shù)量, 數(shù)量更新后需要更新相應(yīng)的訂單金額(totalPrice)哼鬓。


實(shí)現(xiàn)一: Service + 貧血模型

貧血類 Order, OrderItem, 充當(dāng)ORM持久化對象

public class Order {      // order 表
    private Long id;      // 訂單id
    private Long totalPrice;
    private Status status;
    // address, time etc
    // getter, setter
}

public class OrderItem {   // order_item 表
    private Long id;
    private Long orderId;     // 訂單id
    private Long goodsId;
    private Integer count;
    private Long price;
    // getter setter
}

OrderServiceImpl

    @Transactional
    public void changeGoodsCount(long id, long goodsId, int count) {
        Order order = dao.findOrderById(id);
        if (order.getStatus() == PAID) {
            throw new OrderCannotBeModifiedException(id);
        }
        List<OrderItem> orderItems = dao.findItemByOrderId(id);
        findAndSetCount(orderItems);
        order.setTotalPrice(calculateTotalPrice(orderItems));
        dao.saveOrUpdate(order);
        dao.saveOrUpdate(item);
    }

面向過程編程监右,業(yè)務(wù)邏輯Service層中實(shí)現(xiàn),隨著項(xiàng)目演化异希,這些業(yè)務(wù)邏輯會分散在不同的Service類中秸侣。


實(shí)現(xiàn)二: 基于事務(wù)腳本的實(shí)現(xiàn)

事務(wù)腳本: 通過過程的調(diào)用來組織業(yè)務(wù)邏輯,每個過程處理來自表現(xiàn)層的單個請求。

    @Transactional
    public void changeGoodsCount(long id, long goodsId, int count) {
        OrderStatus orderStatus = DAO.getOrderStatus(id);
        if (orderStatus == PAID) {
            throw new OrderCannotBeModifiedException(id);
        }
        DAO.updateGoodsCount(id, command.getGoodsId(), command.getCount());
        DAO.updateTotalPrice(id);
    }

實(shí)現(xiàn)三: 基于領(lǐng)域?qū)ο髮?shí)現(xiàn)

業(yè)務(wù)表達(dá)邏輯被內(nèi)聚到領(lǐng)域?qū)ο螅?code>Order)中味榛, Order不再是一個貧血對象椭坚,除了屬性還有行為。

public class Order {
    private List<OrderItem> items;

    public void changeGoodsCount(long goodsId, int count) {
        if (status == PAID) {
            throw new OrderCannotBeModifiedException(id);
        }
        OrderItem item = items.stream().filter(x -> x.getGoodsId() == goodsId).findFirst().get();
        item.setCount(count);
        this.totalPrice = items.stream().mapToLong(x -> x.getPrice() * x.getCount()).sum();
    }
}

OrderApplicationService提供接口

@Transactional
public void changeGoodsCount(long id, long goodsId, int count) {
    Order order = repository.findById(id);
    order.changeGoodsCount(goods, count);
    repository.save(order); 
}

Quote

I find this a curious term because there are few things that are less logical than business logic.

Matin Flowler @ Patterns of Enterprise Application Architecture


DDD簡述

2004年Eric Evans發(fā)表Domain-Driven Design: Tackling Complexity in the Heart of Software搏色,簡稱Evans DDD善茎。領(lǐng)域驅(qū)動設(shè)計(jì)分為兩個階段:

  • 以一種領(lǐng)域?qū)<摇⒃O(shè)計(jì)人員频轿、開發(fā)人員都能理解的通用語言作為相互交流的工具垂涯,在交流的過程中發(fā)現(xiàn)領(lǐng)域概念,然后將這些概念設(shè)計(jì)成一個領(lǐng)域模型航邢;
  • 由領(lǐng)域模型驅(qū)動軟件設(shè)計(jì)耕赘,用代碼來實(shí)現(xiàn)該領(lǐng)域模型;

它是一套完整而系統(tǒng)的設(shè)計(jì)方法膳殷,嘗試對業(yè)務(wù)復(fù)雜性和技術(shù)復(fù)雜性進(jìn)行分離或者至少降低耦合性操骡,達(dá)到軟件架構(gòu)和業(yè)務(wù)清晰的表達(dá)。


戰(zhàn)略: 子域和界限上下文劃分

領(lǐng)域: 一種邊界赚窃,范圍册招,可以理解為業(yè)務(wù)邊界;

Bounded Context: 界限上下文,定義了解決方案的邊界;

解題空間映射

戰(zhàn)術(shù): 經(jīng)典分層架構(gòu)

  • UI 層: controller, outer-api勒极;
  • 應(yīng)用層: 業(yè)務(wù)邏輯無關(guān)是掰,一個業(yè)務(wù)用例對應(yīng)ApplicationService上的一個方法;
  • 領(lǐng)域?qū)? 核心層, 表達(dá)業(yè)務(wù)辱匿,包含領(lǐng)域模型及領(lǐng)域服務(wù)键痛;
  • 基礎(chǔ)設(shè)施層: 為上層提供基礎(chǔ)服務(wù),如持久化服務(wù)等匾七。
DDD經(jīng)典4層架構(gòu)

用戶界面層(User Interface): 上下游適配器

適配不同的協(xié)議: REST,RPC, SOAP等絮短,負(fù)責(zé)向用戶展現(xiàn)信息以及執(zhí)行用戶命令。更細(xì)的方面來講就是:

  1. 請求應(yīng)用層以獲取用戶所需要展現(xiàn)的數(shù)據(jù)乐尊;
  2. 發(fā)送命令給應(yīng)用層要求其執(zhí)行某個用戶命令戚丸;
    @PostMapping("/{id}/changeGoods")
    public void changeGoodsCount(@PathVariable(name = "id") Long id, @RequestBody @Valid ChangeGoodsCountCommand command) {
        orderApplicationService.changeGoodsCount(id, command.getProdcutId(), command.getCount());
    }


應(yīng)用層 (ApplicationService): 領(lǐng)域模型的門面

作用: 對外為展現(xiàn)層提供各種應(yīng)用功能(包括查詢或命令)划址,對內(nèi)調(diào)用領(lǐng)域?qū)樱I(lǐng)域?qū)ο蠡蝾I(lǐng)域服務(wù))完成各種業(yè)務(wù)邏輯扔嵌。
原則:
(1). 一個業(yè)務(wù)用例對應(yīng)ApplicationService上的一個業(yè)務(wù)方法,比如修改產(chǎn)品個數(shù): OrderApplicationService.changeGoodsCount()夺颤;
(2). 與事務(wù)一一對應(yīng)痢缎;
(3). 不包含業(yè)務(wù)邏輯,所有業(yè)務(wù)內(nèi)聚在聚合根中世澜,只用對領(lǐng)域?qū)ο筮M(jìn)行調(diào)用独旷,無需知道領(lǐng)域模型內(nèi)部實(shí)現(xiàn);
(4). 作為領(lǐng)域模型的門面,封裝領(lǐng)域模型的對外提供的功能嵌洼,不應(yīng)處理UI交互或者通信協(xié)議之類的技術(shù)細(xì)節(jié)


領(lǐng)域服務(wù) (DomainService): 多領(lǐng)域模型協(xié)調(diào)者

領(lǐng)域中的一些操作比如涉及到多個領(lǐng)域模型對象不適合歸類到某個具體的領(lǐng)域?qū)ο笾邪钙#I(lǐng)域服務(wù)用來協(xié)調(diào)跨多個對象的操作,無狀態(tài)麻养。 比如在OrderPayByService中褐啡,代支付需收取1%手續(xù)費(fèi);

public void pay(Account account, Order order) {
    account.payby(order.getId(), order.getPrice()); // Account領(lǐng)域模型中實(shí)現(xiàn)1%業(yè)務(wù)邏輯
    order.paid();
    paymentGateway.pay(account.getId(), order.getPrice);
}

OrderApplicationService

@Transactional
public void orderPayBy(long orderId, long payByAccount) {
    Order order = DAO.findOrderById(id);
    Account account = DAO.findAccountById(payByAccount);
    orderPayByService.pay(account, order);
    DAO.saveOrUpdate(order, account); 
}

領(lǐng)域(Domain)層: 業(yè)務(wù)邏輯的載體

核心: 識別對象之間的內(nèi)在的關(guān)系鳖昌,構(gòu)建領(lǐng)域?qū)ο蟆?/p>

聚合(Aggreate): 定義了一組具有內(nèi)聚關(guān)系的相關(guān)對象的集合: (1)修改數(shù)據(jù)的單元; (2)業(yè)務(wù)邏輯的載體备畦。由聚合根,實(shí)體及值對象組成许昨;
聚合根(Aggreate Root): 是集合的根節(jié)點(diǎn)懂盐,可被外界獨(dú)立訪問,具有獨(dú)立的生命周期糕档。

訂單領(lǐng)域模型



實(shí)體(Entity) vs 值對象(Value Object)


聚合根 實(shí)體 值對象
有無標(biāo)識(id)
是否只讀
是否有生命周期
相等條件 ???? 對象標(biāo)識符 ???? ???? 對象標(biāo)識符 ???? ???? 對象屬性 ????
示例 Order OrderLineItem Goods, Address

聚合根的設(shè)計(jì)

  1. 聚合用來封裝不變性(Invariants) (業(yè)務(wù)規(guī)則)莉恼, 而不是簡單對象從屬關(guān)系
    帖子及回復(fù)關(guān)系? 公司與部門之間關(guān)系?
    public class Post extends AggregateRoot { 
        private string title; private List<Reply> replies; 
    }
  1. 聚合應(yīng)盡量小
  2. 聚合之間通過ID關(guān)聯(lián)翼岁,而不是對象
  3. 聚合內(nèi)強(qiáng)一致性类垫,聚合之間最終一致性

Order領(lǐng)域模型

public class Order { // Order聚合根
    private Long id;
    private Long totalPrice;
    private Status status;
    private List<OrderLineItem> items;  // 實(shí)體對象 1:N關(guān)系
    private Address address; // 值對象
}

public class OrderLineItem {  // 訂單商品對象: entity
    private Long id; private Long orderId; private Goods goods;  private Integer count;
}

public class Goods { // 商品對象: value object, 只讀
    private Long id; private String name; private String desc; private Long price;
}

public class Address {  // 送貨地址: value object, 只讀
    private String country; private String province; private String area; private String detail;
}


聚合根的創(chuàng)建 - Factory模式

  1. 直接在聚合根中實(shí)現(xiàn)Factory方法,常用于簡單的創(chuàng)建過程
  2. 獨(dú)立的Factory類琅坡,用于有一定復(fù)雜度的創(chuàng)建過程悉患,或者創(chuàng)建邏輯不適合放在聚合根上
public static Order create(Long id, List<OrderLineItem> items, Address address) {
    return new Order(id, items, address);
}
public class OrderFactory {
    private OrderIdGenerator idGenerator;

    public Order create(List<OrderLineItem> items, Address address) {
        long orderId = idGenerator.get();
        return Order.create(orderId, items, address);
    }
}

基礎(chǔ)設(shè)施層(Infrasture): 聚合根的家

倉儲(Repository)為聚合根提供查詢及持久化機(jī)制;類似JPA榆俺,可以看成聚合的容器售躁。

  • Repository與聚合根一一對應(yīng);
  • DAO直接與表數(shù)據(jù)進(jìn)行交互茴晋,比較薄陪捷。
@Repository
public class OrderRepository {

    public Order findById(Long id) {    }

    public Order save(Order order) {    }
}


模型到表映射

  • 實(shí)體,聚合根與表一一對應(yīng)
  • 值對象通常與聚合根一起诺擅,是表的一列

對于上述的Order聚合根來說市袖,可以映射到以下2張表中 (t_order_itemt_order):

列名 類型 含義 對象映射
item_id bigint 訂單項(xiàng)目id OrderLineItem.id
order_id bigint 訂單id, 外鍵 OrderLineItem.orderId; Order.id
goods varchar 商品信息, json格式 OrderLineItem.goods
count int 商品數(shù)量 OrderLineItem.count

t_order

列名 類型 含義 對象映射
order_id ???? bigint???? 訂單id Order.id
user_id bigint 用戶id Order.userId
total_price bigint 訂單總金額 Order.totalPrice
address varchar 地址,json格式 Order.address
status tinyint 訂單狀態(tài) Order.status

代碼結(jié)構(gòu)

    ├── OrderDddApplication.java
    ├── ddd
    │   ├── application
    │   │   └── OrderApplicationService.java
    │   ├── domain
    │   │   ├── Address.java
    │   │   ├── Goods.java
    │   │   ├── Order.java
    │   │   └── OrderLineItem.java
    │   ├── factory
    │   │   ├── OrderFactory.java
    │   │   └── RedisOrderIdGenerator.java
    │   ├── repository
    │   │   ├── OrderLineItemPoDao.java
    │   │   ├── OrderPoDao.java
    │   │   ├── OrderRepository.java
    │   │   └── po
    │   └── ui
    │       └── OrderController.java

Git: https://github.com/JiangWork/order-ddd-sample


DDD為什么有效

有效的邊界劃分,限定職責(zé)范圍

但劃分邊界是件有難度的事烁涌,隨著對業(yè)務(wù)深入的了解苍碟,劃分也會不同。

傳統(tǒng)以數(shù)據(jù)為中心與領(lǐng)域驅(qū)動設(shè)計(jì)復(fù)雜性對比

總結(jié)

DDD相關(guān)概念關(guān)系網(wǎng)

Reference

https://www.cnblogs.com/davenkin/p/ddd-coding-practices.html
http://www.reibang.com/p/6bce48596a69
https://www.cnblogs.com/netfocus/p/3307971.html
https://zhuanlan.zhihu.com/p/32459776
https://martinfowler.com/eaaCatalog/transactionScript.html
https://github.com/JiangWork/order-ddd-sample

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撮执,一起剝皮案震驚了整個濱河市微峰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抒钱,老刑警劉巖蜓肆,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颜凯,死亡現(xiàn)場離奇詭異,居然都是意外死亡仗扬,警方通過查閱死者的電腦和手機(jī)症概,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來早芭,“玉大人穴豫,你說我怎么就攤上這事”朴眩” “怎么了精肃?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長帜乞。 經(jīng)常有香客問我司抱,道長,這世上最難降的妖魔是什么黎烈? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任习柠,我火速辦了婚禮,結(jié)果婚禮上照棋,老公的妹妹穿的比我還像新娘资溃。我一直安慰自己,他們只是感情好烈炭,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布溶锭。 她就那樣靜靜地躺著,像睡著了一般符隙。 火紅的嫁衣襯著肌膚如雪趴捅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天霹疫,我揣著相機(jī)與錄音拱绑,去河邊找鬼。 笑死丽蝎,一個胖子當(dāng)著我的面吹牛猎拨,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屠阻,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼红省,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了栏笆?” 一聲冷哼從身側(cè)響起类腮,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤臊泰,失蹤者是張志新(化名)和其女友劉穎蛉加,沒想到半個月后蚜枢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡针饥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年厂抽,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片丁眼。...
    茶點(diǎn)故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡筷凤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苞七,到底是詐尸還是另有隱情藐守,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布蹂风,位于F島的核電站卢厂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏惠啄。R本人自食惡果不足惜慎恒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撵渡。 院中可真熱鬧融柬,春花似錦、人聲如沸趋距。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽节腐。三九已至靠欢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間铜跑,已是汗流浹背门怪。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锅纺,地道東北人掷空。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像囤锉,于是被迫代替她去往敵國和親坦弟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評論 2 345