一、序言
领域驱动设计是一种解决业务复杂性的设计思想,不是一种标准规则的解决方法。在本文中的实战示例可能会与常见的DDD规则方法不太一样,是简单、入门级别,新手可以快速实践版的DDD。如果不熟悉DDD设计思想可看下
Ebiubiu:领域驱动设计(DDD)-基础思想zhuanlan.zhihu.com二、设计阶段
领域建模设计阶段常见的方法有 四色建模法、EventSourcing等,本文DDD的设计方法是为了新手可以实战落地,又能表达出DDD思想的简单易懂设计方法(非新手阶段建议了解下四色建模、EventSoucing等领域建模思想)
理解业务
领域驱动设计是对业务模型在系统设计中的一种表现形式,在进行DDD实战前一定要熟悉业务,不熟悉业务无法把业务模型翻译成域模型。理解业务-站在业务方和产品角度,梳理系统业务的所有细节,明白每一个业务细节点。
下面举一个简单交易系统DDD(交易系统太宽广,本文示例是仅包含创建订单这一个阶段的行为,不包含订单状态变更、订单管理这些操作)落地示例,在电商或外卖行业交易系统创建订单都会有收货人、下单人、商品、订单金额等业务属性和创建订单行为。
业务抽象
在梳理业务过程中会把业务每一个具体的点都给罗列出来,是一盘零碎的业务点。通过对业务的理解进行抽象,把相关性的业务点进行分组聚合。业务抽象过程中涉及边界划分问题,比如收货人和下单人信息是一起抽象成用户信息,还是分别抽象成用户信息和地址信息?在订单业务中,一个用户可以下多笔订单,该用户下的所有订单都是同一个用户信息,但用户的每笔订单的收货信息可以不同(可以给自己买东西、给亲朋好友买东西),在订单业务中用户信息和地址信息是需要抽象成两个独立的业务模块。
模型翻译
在经过业务抽象之后,整体业务模型已经清晰明了,在域模型设计上,只需要把业务模型经过简单翻译映射成域模型即可。
子域划分
业务模型翻译成域模型后,当一个域模型比较复杂的时候需要把一个域模型进行子域划分
三、实现阶段
搭建项目结构
DDD项目与传统三层项目结构比较类似,DDD中API包是接口定义负责对外打包给外部(SOA和Http)调用使用,Service包是API包接口的实现,不做具体业务逻辑处理,只做数据的转换,把Domain层的域模型转换成对外使用的字段。Domain层是所有的具体业务逻辑处理层。
传统三层架构
原生领域驱动架构
模块和包
一个简单的DDD项目会包含API、Service、Domain三个模块
在Domain模块中,一个基本的域模型会包含 Entity、Value Object、Service、Factory、Repository这几个包(Entity、Service)。metadata包是域模型元数据包,metadata包下的接口是一个DDD设计的标识,这个包通常会抽象成一个独立的模块供其他项目依赖使用。
域模型搭建
订单域中肯定会有地址、用户、店铺、订单商品等信息,统一直接在订单Model中肯定会很臃肿并且不利于维护,应把订单域拆分成一个个子域。在拆分成子域选模型(Entity、ValueObject、Service)的时候会存在模棱两可的情况,这个订单地址信息子域应该是Entity、还ValueObject,如果不太确定就以最简单化原则,Entity复杂度大于ValueObject,订单地址域就可以以ValueObject模型存在。
metadata包类
/**
* 域模型工厂
* @param
*/
public interface DomainFactory {
}
/**
* 实体
* @param
*/
public interface Entity extends Serializable {
default boolean sameIdentityAs(T other) {
return true;
}
}
/**
* 持久化
* @param
*/
public interface Repository {
}
/**
*服务
* @param
*/
public interface Service {
}
/**
* 值对象
* @param
*/
public interface ValueObject extends Serializable {
default boolean sameValueAs(T other) {
return true;
}
}
entity包类
/**
* 订单实体
*/
@Data
public class OrderE implements Entity {
/**
* 用户信息
*/
private OrderUserV orderUserV = new OrderUserV();
/**
* 购物车信息
*/
private OrderCartV orderCartV = new OrderCartV();
/**
* 地址相关信息
*/
private OrderAddressV orderAddressV = new OrderAddressV();
/**
* 店铺相关操作
*/
private OrderShopV orderShopV = new OrderShopV();
/**
* 订单基础信息
*/
private OrderBasicInfoV orderBasicInfoV = new OrderBasicInfoV();
/**
* 订单金额
*/
private OrderMoneyV orderMoneyV = new OrderMoneyV();
/**
* 持久化操作
*/
private OrderR orderR;
/**
* 创建订单
*
* @return
*/
public OrderE createOrder(OrderE orderE) throws Exception {
return orderR.createOrder(orderE);
}
/**
* 从主库查询
*
* @param orderE
* @return
* @throws Exception
*/
public OrderE queryOrderFromDBMater(OrderE orderE) throws Exception {
return orderR.queryOrderFromDBMaster(orderE);
}
/**
* 从Eos等三方查询
*
* @param orderNumber
* @return
* @throws Exception
*/
public void queryOrderFromEos(String orderNumber) throws Exception {
orderR.queryOrderFromEos(orderNumber);
}
/**
* 订单有效
*
* @param orderNumber
* @throws Exception
*/
public void enableOrder(String orderNumber) throws Exception {
orderR.enableOrder(orderNumber);
}
}
repository包类
/**
* 订单数据源 操作层 所有的与外部的交互都走这一层
*/
public interface OrderR extends Repository {
/**
* 创建订单
* @param orderE
* @return
* @throws Exception
*/
OrderE createOrder(OrderE orderE) throws Exception;
/**
* 从DB主库查询信息
* @param orderE
* @return
* @throws Exception
*/
OrderE queryOrderFromDBMaster(OrderE orderE) throws Exception;
/**
* 从EOS查询订单详情
* @param orderNumber
* @return
* @throws Exception
*/
void queryOrderFromEos(String orderNumber) throws Exception;
/**
* 开启订单
* @param orderNumber
* @throws Exception
*/
void enableOrder(String orderNumber) throws Exception;
}
@Service
public class OrderRImpl implements OrderR {
private static final Log LOG = LogFactory.getLog(OrderRImpl.class);
@Override
public OrderE createOrder(OrderE orderE) throws Exception {
return null;
}
@Override
public OrderE queryOrderFromDBMaster(OrderE orderE) throws Exception {
return null;
}
@Override
public void queryOrderFromEos(String orderNumber) throws Exception {
}
@Override
public void enableOrder(String orderNumber) throws Exception {
}
}
value_object包类
/**
* 订单地址值对象
*/
@Data
public class OrderAddressV implements ValueObject {
/**
* 地址ID
*/
private String addressId;
/**
* 订单地址
*/
private String address;
/**
* 收货人
*/
private String name;
/**
* 收货人手机号
*/
private String phone;
/**
* 经度
*/
private BigDecimal longitude;
/**
* 纬度
*/
private BigDecimal latitude;
/**
* 地址类型
*/
private Integer addressType;
/**
* 三方地址Id
*/
private String thirdAddressId;
/**
* longitude、latitude转HashString
*
* @param longitude
* @param latitude
* @return
*/
public String getGeoHash(BigDecimal longitude, BigDecimal latitude, int length) {
String geoHash = GeoHash.encodeHash(latitude.doubleValue(), longitude.doubleValue(), length);
return geoHash;
}
/**
* longitude、latitude转HashString 默认12位
* @param longitude
* @param latitude
* @return
*/
public String getGeoHash(BigDecimal longitude, BigDecimal latitude) {
return getGeoHash(longitude, latitude, 12);
}
/**
* geoHash 转经纬度
*
* @param geoHash
* @return
*/
public BigDecimal[] getLatLog(String geoHash) {
LatLong latLong = GeoHash.decodeHash(geoHash);
BigDecimal[] bigDecimals = new BigDecimal[2];
bigDecimals[0] = BigDecimal.valueOf(latLong.getLat());
bigDecimals[1] = BigDecimal.valueOf(latLong.getLon());
return bigDecimals;
}
}
@Data
public class OrderBasicInfoV implements ValueObject {
/**
* 订单Id
*/
private String orderId;
/**
* 订单创建时间
*/
private LocalDateTime createAt;
/**
* 订单状态
*/
private Integer orderStatus;
}
@Data
public class OrderCartV implements ValueObject {
/**
* 购物车Id
*/
private String cartId;
/**
* 购物车创建时间
*/
private LocalDateTime createTime;
/**
* 购物车总价
*/
private BigDecimal total;
/**
* 购物车原价
*/
private BigDecimal originalTotal;
/**
* 最低多少元起送
*/
private BigDecimal minDeliverAmount;
/**
* 配送费
*/
private BigDecimal deliveryFee;
/**
* 商品总数量
*/
private Integer totalQuantity;
/**
* 商品List
*/
private List
一些释义
Model生命周期管理方式
域模型中Model在系统环境中的生命周期管理有两种方式,一种是使用容器管理生命周期在Spring中Model类上打@Service、@Component等注解 ;另一种是通过手动管理生命周期可以与普通DTO、POJO一样通过new对象 的形式使用(本文中就是与普通的DTO一样通过new对象形式使用的),我比较推荐new对象的形式使用。
new对象形式的Model如何动态注入Repository数据源(是一个接口),数据源操作负责实现这个接口,与数据源交互(DB、HTTP......)。如何在一个普通new对象中注入Spring容器管理的对象?通过控制反转在使用时再值设置进去,通过Factory实现控制反转,这点是避免通过容器托管Model的关键所在。@辉子盒马领域驱动设计实践 这篇博文详细的阐述了领域模型下的依赖注入。
其他
@Data注解是Lombok注解在类中会自动生成Get/Set等方法
metadata包接口是域模型搭建的一种标识,通常会抽出来作为公共模块。
最后
面对复杂性设计,领域驱动设计六边形(Hexagonal)、洋葱(Onion)、简洁(Clean)架构都是把问题抽象、内聚进行分层设计解决,术不同、道相同。
本文代码实战中属于DDD原生模式,核心是在领域驱动设计思想上,以领域为核心通过网关层来代理不同的数据源来驱动系统设计。
四、参考引用
【1】领域驱动设计(DDD)-基础思想
【2】域模型之一, 域模型的价值
【3】如何进行域划分设计
【4】盒马领域驱动设计实践