如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
本系列所有文章
如何一步一步用DDD设计一个电商网站(一)—— 先理解核心概念
如何一步一步用DDD设计一个电商网站(四)—— 把商品卖给用户
如何一步一步用DDD设计一个电商网站(五)—— 停下脚步,重新出发
如何一步一步用DDD设计一个电商网站(六)—— 给购物车加点料,集成售价上下文
如何一步一步用DDD设计一个电商网站(七)—— 实现售价上下文
如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
如何一步一步用DDD设计一个电商网站(十)—— 一个完整的购物车
如何一步一步用DDD设计一个电商网站(十一)—— 最后的准备
如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单
如何一步一步用DDD设计一个电商网站(十三)—— 领域事件扩展
阅读目录
一、前言
前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念。会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是:
1.加大了运营的复杂度,会员价如何与促销结合,比如应在折前运用还是折后运用等。
2.如果是折前那么需要考虑满减类型促销的金额满足点门槛反而相对来说是提高了。
3.如果是折后那么享受了多重优惠,成本控制的时候需要考虑进去。
在我们这个练手的Demo中暂时决定让会员价在折后运用,并且仅在不满足满减促销的情况下才有效。
二、建模
那么开始先来建模,这次的会员价相对比较简单,一般就是一个打折率的问题。只要建立几个关系即可满足需求,如下:
会员与等级的关系(值对象):我认为等级的升级降级应该在“用户上下文”中处理,那么在这里的售价上下文中仅是对数据做的一个冗余,与“用户上下文”是一个最终一致性的关系。当然也可以不做这个冗余,从远程服务去获取,这可以根据实际情况来权衡。我认为用户等级的变化是一个非高频数据,所以在这里做冗余可以减少RPC次数。
等级与折扣的关系(值对象):这个数据应该是一旦确定就不大会变化了,并且会用于对外公示,毋庸置疑建立为值对象。如下图1所示:
【图1】
三、运用
先把上面定义的2个值对象数据来源确认一下,暂定把会员与等级的关系(UserRoleRelation)从用户上下文获取,因为我们还没开始引入最终一致性的概念;等级与折扣的关系(RoleDiscountRelation)存在本地上下文。那么这里第一次出现了在售价上下文中需要访问外部资源,我们也需要给其建立一个防腐层来处理这个RPC交互。既然如此和购买上下文一起,把防腐层放入到每个上下文的虚拟文件夹中,如下图2所示:
【图2】
下面的代码定义了这2个数据获取的接口:
public interface IUserService { UserRoleRelation GetUserRoleRelation(string userId); }
public interface IRoleDiscountRelationRepository// : IRepository<RoleDiscountRelation> { RoleDiscountRelation Get(string roleId); }
可以看到IRoleDiscountRelationRepository中有一行注释的代码,是因为这里需要把一个值对象独立的持久化到资源库中,在我们之前的设计中仅支持聚合根的持久化,所以此处先临时以手动定义的方式通过本篇的代码编写,会在下篇专门讲述如何处理这种情况。
然后由于计算会员价需要根据用户来计算,故要在CartRequest中增加UserId的参数,让购买上下文传递该数据才能保证这里的业务需要。
public class CartRequest { public string CartId { get; set; } public string UserId { get; set; } public CartItemRequest[] CartItems { get; set; } }
会员价的计算是等级与折扣(值对象)的功能,可以在这个值对象中创建一个方法,目前来说里面的实现就是对传入的价格进行折扣金额的计算然后就返回。如下代码:
public decimal CalculateDiscountedPrice(decimal price) { return price * Convert.ToDecimal(this.DiscountRate); }
然后我们开始把它和之前的促销业务结合起来。还记得我们之前的CalculateSalePriceService.Calculate(CartRequest cart)方法返回的数据结构吗(传送门:http://www.cnblogs.com/Zachary-Fan/p/DDD_7.html):
return new CalculatedCartDTO { CalculatedCartItems = boughtProducts.Where(ent => fullGroupDtos.SelectMany(e => e.CalculatedCartItems).All(e => e.ProductId != ent.ProductId)) .Select(ent => ent.ToDTO()).ToArray(), CalculatedFullGroups = fullGroupDtos.ToArray(), CartId = cart.CartId };
我们只要把给CalculatedCartItems赋值的数据再进行计算会员价就好了,因为这些就是未参与满减促销的购物项。但是这里为了让BoughtProduct支持我们业务操作并且假设界面上需要展示会员价和促销价分别优惠了多少金额,故在BoughtProduct值对象中增加了一个ReducePriceByMemberPrice,用于存储由会员价所减免的金额。随后BoughtProduct中增加相应的设置会员价减免金额的方法,如下:
public BoughtProduct ChangeReducePriceByMemberPrice(decimal reducePriceByMemberPrice) { if (reducePriceByMemberPrice < 0) throw new ArgumentException("reducePriceByMemberPrice不能小于0"); var selectedMultiProdcutsPromotionId = this.InMultiProductPromotionRule == null ? null : ((PromotionRule)this.InMultiProductPromotionRule).PromotoinId; return new BoughtProduct(this.ProductId, this.Quantity, this.UnitPrice, this.ReducePrice, reducePriceByMemberPrice, this._promotionRules, selectedMultiProdcutsPromotionId); }
最后CalculateService调整为下图3这样:
【图3】
四、结语
可能写到中途有些枯燥,但是我想我的主题是运用DDD从0开始实现一个电商网站的过程,DDD中业务是核心,所以业务的细枝末节和DDD概念的运用必然都不能丢。
本文的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo8。
作者:Zachary
出处:https://zacharyfan.com/archives/159.html
▶关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描右侧的二维码~。
定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。
如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。
如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。