领域驱动设计DDD-综述


引例

DDD

   子域与界限上下文

   经典分层架构

   UI层与应用层

   领域层

   基础设施层

总结


引例

在电商系统中”修改商品数量“的业务需求:

在用户未支付订单(Order)之前,用户可以修改Order中商品(Goods)的数量, 数量更新后需要更新相应的订单金额(totalPrice)。


实现一: Service + 贫血模型

贫血类 Order, OrderItem, 充当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);
    }

面向过程编程,业务逻辑Service层中实现,随着项目演化,这些业务逻辑会分散在不同的Service类中。


实现二: 基于事务脚本的实现

事务脚本: 通过过程的调用来组织业务逻辑,每个过程处理来自表现层的单个请求。

    @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);
    }

实现三: 基于领域对象实现

业务表达逻辑被内聚到领域对象(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发表Domain-Driven Design: Tackling Complexity in the Heart of Software,简称Evans DDD。领域驱动设计分为两个阶段:

  • 以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;
  • 由领域模型驱动软件设计,用代码来实现该领域模型;

它是一套完整而系统的设计方法,尝试对业务复杂性和技术复杂性进行分离或者至少降低耦合性,达到软件架构和业务清晰的表达。


战略: 子域和界限上下文划分

领域: 一种边界,范围,可以理解为业务边界;

Bounded Context: 界限上下文,定义了解决方案的边界;

解题空间映射

战术: 经典分层架构

  • UI 层: controller, outer-api;
  • 应用层: 业务逻辑无关,一个业务用例对应ApplicationService上的一个方法;
  • 领域层: 核心层, 表达业务,包含领域模型及领域服务;
  • 基础设施层: 为上层提供基础服务,如持久化服务等。
DDD经典4层架构

用户界面层(User Interface): 上下游适配器

适配不同的协议: REST,RPC, SOAP等,负责向用户展现信息以及执行用户命令。更细的方面来讲就是:

  1. 请求应用层以获取用户所需要展现的数据;
  2. 发送命令给应用层要求其执行某个用户命令;
    @PostMapping("/{id}/changeGoods")
    public void changeGoodsCount(@PathVariable(name = "id") Long id, @RequestBody @Valid ChangeGoodsCountCommand command) {
        orderApplicationService.changeGoodsCount(id, command.getProdcutId(), command.getCount());
    }


应用层 (ApplicationService): 领域模型的门面

作用: 对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑。
原则:
(1). 一个业务用例对应ApplicationService上的一个业务方法,比如修改产品个数: OrderApplicationService.changeGoodsCount()
(2). 与事务一一对应;
(3). 不包含业务逻辑,所有业务内聚在聚合根中,只用对领域对象进行调用,无需知道领域模型内部实现;
(4). 作为领域模型的门面,封装领域模型的对外提供的功能,不应处理UI交互或者通信协议之类的技术细节


领域服务 (DomainService): 多领域模型协调者

领域中的一些操作比如涉及到多个领域模型对象不适合归类到某个具体的领域对象中,领域服务用来协调跨多个对象的操作,无状态。 比如在OrderPayByService中,代支付需收取1%手续费;

public void pay(Account account, Order order) {
    account.payby(order.getId(), order.getPrice()); // Account领域模型中实现1%业务逻辑
    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); 
}

领域(Domain)层: 业务逻辑的载体

核心: 识别对象之间的内在的关系,构建领域对象。

聚合(Aggreate): 定义了一组具有内聚关系的相关对象的集合: (1)修改数据的单元; (2)业务逻辑的载体。由聚合根,实体及值对象组成;
聚合根(Aggreate Root): 是集合的根节点,可被外界独立访问,具有独立的生命周期。

订单领域模型



实体(Entity) vs 值对象(Value Object)


聚合根 实体 值对象
有无标识(id)
是否只读
是否有生命周期
相等条件      对象标识符           对象标识符           对象属性     
示例 Order OrderLineItem Goods, Address

聚合根的设计

  1. 聚合用来封装不变性(Invariants) (业务规则), 而不是简单对象从属关系
    帖子及回复关系? 公司与部门之间关系?
    public class Post extends AggregateRoot { 
        private string title; private List<Reply> replies; 
    }
  1. 聚合应尽量小
  2. 聚合之间通过ID关联,而不是对象
  3. 聚合内强一致性,聚合之间最终一致性

Order领域模型

public class Order { // Order聚合根
    private Long id;
    private Long totalPrice;
    private Status status;
    private List<OrderLineItem> items;  // 实体对象 1: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;
}


聚合根的创建 - Factory模式

  1. 直接在聚合根中实现Factory方法,常用于简单的创建过程
  2. 独立的Factory类,用于有一定复杂度的创建过程,或者创建逻辑不适合放在聚合根上
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);
    }
}

基础设施层(Infrasture): 聚合根的家

仓储(Repository)为聚合根提供查询及持久化机制;类似JPA,可以看成聚合的容器。

  • Repository与聚合根一一对应;
  • DAO直接与表数据进行交互,比较薄。
@Repository
public class OrderRepository {

    public Order findById(Long id) {    }

    public Order save(Order order) {    }
}


模型到表映射

  • 实体,聚合根与表一一对应
  • 值对象通常与聚合根一起,是表的一列

对于上述的Order聚合根来说,可以映射到以下2张表中 (t_order_itemt_order):

列名 类型 含义 对象映射
item_id bigint 订单项目id OrderLineItem.id
order_id bigint 订单id, 外键 OrderLineItem.orderId; Order.id
goods varchar 商品信息, json格式 OrderLineItem.goods
count int 商品数量 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 订单状态 Order.status

代码结构

    ├── 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为什么有效

有效的边界划分,限定职责范围

但划分边界是件有难度的事,随着对业务深入的了解,划分也会不同。

传统以数据为中心与领域驱动设计复杂性对比

总结

DDD相关概念关系网

Reference

https://www.cnblogs.com/davenkin/p/ddd-coding-practices.html
https://www.jianshu.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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343