以前七七八八看了些DDD相关概念,充血模型、领域事件、领域服务、应用服务等,大体能理解但从未实践。最近在用ABP作个电商模块,尝试用DDD方式来实现购物车功能,感受还行,下面作个记录。前端
下面这些内容只是我的理解,未必正确。git
购物车模块涉及到两个实体ShoppingCartEntity(购物车)、ShoppingCartItemEntity(购物车明细),按我以前的作法会直接定义成POCO,伪代码以下:数据库
public class ShoppingCartEntity { public long Id { get; set; } //关联的顾客id public long CustomerId{ get; set; } public CustomerEntity Customer { get; set; } //购物车明细集合 public List<ShoppingCartItemEntity> Items { get; set; } //,...略 }
购物车明细定义就省了,意思就是属性所有get; set; ,它只是用来给EF作映射,作数据库操做,作数据传递用。session
但这样的领域实体类没法真是表达业务规则,添加商品到购物车、从购物车移除商品等购物车相关操做,咱们可能会放到应用层(或者老式的业务逻辑层BLL),这样没法体现购物车实体的功能,代码复用性也很低。app
咱们思考下:函数
这样一来,购物车相关的操做都封装进购物车实体,未来应用层的代码就会变得不多,代码复用性、可扩展性也高。本属于购物车的功能就定义在购物车实体上也更直观。ui
先说一点,咱们定义一个方法、一个属性、一个类、一个软件时,必定要考虑这些功能可能在任什么时候候、任何地方、被任何一个SB(包括我本身)调用,他们极可能不按你的预期来。spa
购物车必须是属于某个顾客的,也就是必须有个关联的CustomerId,这是咱们的业务规则,也是约束,但按咱们上面的定义为get; set; 别人可能给他赋值个0或负数,这就让购物车实体处于不正确的状态,因此应该把CustomerId设置为{ get; private set; },同理在定义购物车明细时关联的ProductId(商品Id)也应该是只读的,由于购物车明细必须与某个商品关联才是正常的。设计
咱们能够在构造函数中定义参数来初始化这些只读属性。对象
如此这般,当建立一个购物车实体后,这个对象不管被谁访问,CRUD工程师们没法像之前同样破坏它的状态。
至于到底哪些属性该是只读的,哪些是public的应该根据场景,每一个属性认真思考再决定。
有时候你发现仅仅是经过构造函数才能初始化一个对象,感受很不方便,由于对象可能须要先new出来,而后在各个步骤对它进行赋值,最后才能造成一个咱们满意的对象(有严格约束,且符合业务规则),我的以为这个时候应该为它建立一个对应的Builder对象,把那些临时的状态属性设置到Builder上,最后Builder.Build();生成一个符合业务规则的对象。这种状况不只仅适用用域领域实体,整个软件设计中都适用。
在目前的购物车功能好像体现不了这个。
这个很重要,以前一直晓得领域实体属性有些应该是只读的,但考虑用ef没法给只读属性赋值,因此后来放弃了,也不晓得从啥时候开始,咱们定义的领域实体的私有构造函数和属性EF是能够直接访问的,这就给咱们定义符合业务规则的实体创造了机会。
上面的领域实体若是关键属性为只读的了,我们作dto到实体的映射呢?印象里AutoMapper是能够经过构造函数作映射的,恰好咱们上面说了咱们的实体是有对应的构造函数的。这个规则有待证明。
想象下,将商品加入购物车这个功能,按我原来的作法会在应用层查询出购物车,好比这个对象叫shoppingCart,那么我会直接
在应用层中: shoppingCart.Items.Add(item); //计算明细对应的金额(明细数量*关联商品的单价) //其它处理
仔细考虑下,将商品加入购物车这个方法不是应该定义在购物车实体上吗?若是这样,商品进入购物车,后续要重新计算金额、积分之类的逻辑也都会写在购物车实体内部,而不是放在应用层。这样,应用层未来只须要shoppingCart.AddItem(item);是否是更符合业务场景?
以订单支付这个方法为例,订单支付 要修改支付状态为已支付、改变支付金额、将物流状态改成待发货等等,支付状态、物流状态、支付金额 这些属性都是订单实体类的,购物车实体中的方法也只是修改本身实体的状态属性。
领域实体里只是根据业务定义相关方法,这些操做都是跟这个领域实体相关的,状态属性的改变。依赖注入、访问数据库或其它服务能够在应用层或领域服务去作。
咱们能够在购物车中定义这样的事件:当商品明细加入购物车后触发、当移除购物车明细时触发、当购物车明细数量变动时触发.....等等。这样咱们的购物车模块能够作得很干净,未来别人使用这个模块时能够订阅这些事件来扩展购物车模块。
这个事件的功能是abp自带的事件总线,能够去参考官方文档。
而且这个事件仍是事务性的,意思说若是未来别人扩展咱们的模块,在它们的事件处理代码中若操做数据库,和咱们处理购物车逻辑是在一个数据库事务中,他们能够抛出异常来阻止咱们的正常提交。
DDD的说法是当一个功能没法只归结到一个领域实体上时能够考虑领域服务,协调多个实体或其它领域服务时也行。
目前在购物车模块中没有使用领域服务,仍是以订单支付为例
上面说了,订单实体自己定义了个“支付”的方法,它内部改变订单本身的状态(修改订单状态、修改支付状态、修改物流状态),然而订单支付还涉及到其它处理,好比:要先判断顾客会员等级、余额状况、是否是黑名单 等等,这里就涉及到多个实体和服务了,因此在订单领域服务中有个支付方法,它会作各类业务判断处理后再调用订单实体.支付();
领域服务也属于领域层,也能够触发相关事件,以这种方式来预留扩展点。abp也提供了这个功能。
我比较倾向用事件,上面说支付订单前要作各类业务判断,好比会员等级决定折扣、余额检查等,用领域服务很直观,可是不够灵活,好比未来又变了,要在支付前作更多判断呢?此时若是在支付前触发一个事件,那么未来有新的需求就能够加新的事件处理器,不符合业务规则的状况,在事件处理逻辑中抛异常就能够了。
不建议,当前登录用户严格来讲是应用程序状态,而领域服务是细小的领域逻辑,它与应用程序状态无关。
领域层整好了,这个代码会变得不多,
它访问数据库获得领域实体,也能够依赖注入领域服务。按业务流程逐个调用领域实体和领域服务的相关方法,一般感受对应用户的一个操做,好比点个按钮提交
它访问当前用户
它作权限判断等。
它作基本数据校验
它作dto到实体的映射
开始事务、调用领域服务、实体后提交事务
顾客点击“加入购物车”,前端上传商品(或skuId)
应用层作权限判断、基本数据验证、而后查询当前用户关联的购物车
调用购物车.AddItem(item);
购物车领域实体检测这个商品是否已存在购物车了,若在则累加数量,并触发购物车明细数量改变的事件;若不存在则添加商品到购物车并触发 购物车明细增长成功的事件
事件处理程序预留给模块使用方进行扩展的
若是业务流程复杂,在应用层可能还有好几个步骤要作,但如何完成一般是交给领域服务和实体
应用层最后保存数据到数据库(事务)