如何一步一步用ddd设计一个电商网站数据库设计

上海普陀区招聘3年以上.Net/Android开发。邮箱:,待遇从优
本系列所有文章
  上篇中我们初步运用了领域事件,其中还有一些问题我们没有解决,所以实现是不健壮的,下面先来回顾一下。
  先贴一下上篇中的遗留的问题:
public Result Create(OrderRequest orderRequest)
if (!string.IsNullOrWhiteSpace(orderRequest.CouponId))
var couponResult = DomainRegistry.SellingPriceService().IsCouponCanUse(orderRequest.CouponId, orderRequest.OrderTime);
if (!couponResult.IsSuccess)
return Result.Fail(couponResult.Msg);
var orderId = DomainRegistry.OrderRepository().NextIdentity();
var order = Domain.Order.Aggregate.Order.Create(orderId, orderRequest.UserId, orderRequest.Receiver,
orderRequest.CountryId, orderRequest.CountryName, orderRequest.ProvinceId, orderRequest.ProvinceName,
orderRequest.CityId, orderRequest.CityName, orderRequest.DistrictId, orderRequest.DistrictName,
orderRequest.Address, orderRequest.Mobile, orderRequest.Phone, orderRequest.Email,
orderRequest.PaymentMethodId, orderRequest.PaymentMethodName, orderRequest.ExpressId,
orderRequest.ExpressName, orderRequest.Freight, orderRequest.CouponId, orderRequest.CouponName, orderRequest.CouponValue, orderRequest.OrderTime);
foreach (var orderItemRequest in orderRequest.OrderItems)
order.AddOrderItem(orderItemRequest.ProductId, orderItemRequest.Quantity, orderItemRequest.UnitPrice, orderItemRequest.JoinedMultiProductsPromotionId, orderItemRequest.ProductName);
DomainRegistry.OrderRepository().Save(order);
DomainEventBus.Instance().Publish(new OrderCreated(order.ID, order.UserId, order.Receiver));
return Result.Success();
  不知道大家有没有发现这里代码上的一个问题,就是DomainEventBus.Instance().Publish()方法在聚合的Save操作之后进行,其实本身不是很符合DDD的概念,任何的领域事件都是基于一个领域对象的,没有领域对象何来领域事件,所以领域事件一般都是由领域对象内部产生,故这里应该要把DomainEventBus.Instance().Publish()方法搬到Order.Create中调用。如果发现这个问题的童鞋,恭喜你对于领域事件的理解已经又深入了一个层次了。好了上篇中这么写其实是为了凸显出本地数据修改提交和领域事件的发布是涉及到数据一致性的问题的,其中的问题是:
  1.如果领域事件发布出现异常了怎么办?
  2.如果订阅者处理出现异常了怎么办?
  本篇我们就来一个一个解决问题。
三、本地的一致性
  在解决上面的2个问题之前,我们先需要考虑在修改多个聚合的场景下本地上下文内的一致性问题,这个职责在DDD中由工作单元(UnitOfWork)来负责,工作单元就是为了保证本地的事务一致性,在.Net里的实现一般就是对SqlTransaction的封装运用。关于工作单元的实现一般有2种方式:
  (1)完全依赖于SqlTransaction,在工作单元第一次运用的时候就开启数据库事务。
  (2)使用本地变量存储变动的聚合,然后在工作单元Commit()的时候开启数据库事务并写入。
  2个实现方案各有优缺点,需要在一致性和性能之间做出权衡。另外工作单元和领域事件发布的结合运用可以参考我之前写的2篇文章:和,注意的是我在这2篇中运用的是方式(2)的实现方式。秉着没有最好只有更好的精神,如何才能做到更好的一致性,这里需要引出几个架构层面的概念:ES、Saga、A+ES。这些内容有一篇蟋蟀兄的文章()讲的很好,推荐大家阅读一下,我就不展开讲这些内容了。里面每一种方案的运用都有成本,大家根据实际情况权衡再运用即可,切记:软件开发中没有银弹。
四、领域事件发布出现异常
  这个现象是否会出现需要根据领域事件发布的实现方式来决定,只要实现方式是&非本地&的方案,那么必然会出现一些异常的状况。假如领域事件是通过消息队列来实现,那么涉及到了网络传输必然会大大的增加出现异常的可能性。如何来解决此类问题,秉承着一图胜千言的思想我直接贴个思维导图,先看下一般的几种实现方案的特点,见图1:
                             【图1】
  根据这个图,我们发现鱼和熊掌不可兼得,每个方案都由各自的特点,我们应当根据不同的场景使用不同的实现方案去做,才是最好的选择,并且据我所知,目前支持事务的消息队列开源方案非常的少,所以我们需要通过一定的补偿机制来处理与消息队列通信出现问题的场景。另外在分布式系统中,服务端的接口设计尽量需要满足无状态和幂等性(不展开去讲了,大家自行百度或者google),这也是整个系统高可用的重要的一环。最后的最后,通过对账机制作为最后一道防线,确保重要的数据不产生差错。
  那么我们来看一下这2个实现方案对应我们的编码应该如何来做:
  1.通过消息机制的发布就是把我在Demo中运用DomainEventBus的内部实现由Dictionary替换为外部的消息队列即可,然后需要注册DistributeExceptionEvent来处理丢给消息队列进行分发时出现异常的问题,做补偿措施。
  2.通过DB的方案,大致的伪代码如下:
var unitOfWork = new UnitOfWork();
unitOfWork.RegisterSaved(order);
var domainEvents = GetEventsFromBus();
foreach(var domainEvent in domainEvents)
var body = Serialize(domainEvent);
unitOfWork.RegisterSaved(new Message{Body = body});
return mit();
  大家可以看到,这个方式首先带来的问题是让工作单元变得异常的臃肿,随之导致整个事务的总耗时增加。并且此时Message表中的现存数据可能还在同步进行消费/推送,那么产生资源竞争是必然会遇到的问题,导致的后果是整个工作单元的提交失败。
五、订阅者处理出现异常
  这个问题也是比较常见的,特别是处理业务复杂的接口和涉及过多RPC调用的接口出现的概率更大。所以每个应用每个接口都需要考虑好此类问题。一般的解决方案我也梳理了一个思维导图,如下图2:
                              【图2】
  其实很明显通过回滚的方式有很多局限性。所以说个人建议选择下面的方案,尽量做到内部消化,以提高接口对外的自治性。另外针对重试进行一些限制,一是为了减少一些无用功来占用系统资源,二是避免在系统本身达到瓶颈的情况下出现马太效应,让拥堵问题越发严重。
  本篇没有增加太多代码,只是在Mall.Infrastructure中增加了几个工作单元(方式(2))相关的类,其中只包含了一些核心逻辑代码,具体的实现希望大家能够自己动手。多谢各位看官。
本文完整的源码地址:。
作者:出处:
阅读(...) 评论()如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成
时间: 08:10:19
&&&& 阅读:44
&&&& 评论:
&&&& 收藏:0
标签:&&&&&&&&&&&&&&&&&&&&&&&&&&&阅读目录
  前面几篇已经实现了一个基本的购买+售价计算的过程,这次再让售价丰满一些,增加一个会员价的概念。会员价在现在的主流电商中,是一个不大常见的模式,其带来的问题是:
  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)方法返回的数据结构吗(传送门:):
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
: ((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概念的运用必然都不能丢。
本文的源码地址:。
作者:出处:
前言建模实现结语标签:&&&&&&&&&&&&&&&&&&&&&&&&&&&
&&国之画&&&& &&
版权所有 京ICP备号-2
迷上了代码!上海普陀区招聘3年以上.Net/Android开发。邮箱:,待遇从优
本系列所有文章
  之前的文章中已经涉及到了购买商品加入购物车,购物车内购物项的金额计算等功能。本篇准备把剩下的购物车的基本概念一次处理完。
  在动手之前我对之前的购买上下文内对象做了一次回顾。先梳理一下已经在上下文内出现的领域对象,如图1所示:
                          【图1】
  在梳理的过程中,我把原来Cart.AddCartItem(string productId, int quantity, decimal price)重构为了Cart.AddCartItem(Product product, int quantity),这样的好处的是2个:
  1.更清晰的表述出了在购物车中添加商品的意思。
  2.约束了外部只能通过Product对象来进行商品的添加,这样在Product构造函数中的约束在这里无需再次验证(如salename不能空等)。
  目前的购物车中在操作上的方法只有一个。参照目前主流电商平台的设计,我们需要增加:
  1.修改数量
  2.删除
  3.选择参与的促销(如果存在多个非单品级促销)
  4.收藏商品
  前面3个比较简单,都是购物车自身的概念,只有其中第四点超出了购物车自身的范畴,并且笔者认为收藏本就不是购物车特有的概念,而是在任何看得到商品的地方都可以做添加收藏的操作。那么自然引出了一个新的概念&&收藏夹。看下最新的UML图,如图2所示:
                          【图2】
  我想会有一部分同学在设计收藏夹(Favorites)的时候会以另外的方式来做,比如像下图3这样:
                          【图3】
  这里我认为这样考虑的原因可能是由于DBFirst的思想导致的,因为图2中的&收藏夹&仅仅是维护了一个&用户&与&收藏项&之间的关系,那么只要在&收藏项&上增加一个UserId就直接可以省去了这一层关系,并且数据结构更加简单。这时候我们就需要注意了,千万不能有DBFirst思想去影响领域的建模,这样的方式会把&添加购物项&这类的业务含义泄露到了Repository层或者Application层去实现,导致无法用通用语言进行完整的业务描述了。
  并且在这个场景下,我个人观点认为,收藏商品其实只是为商品的展示途径中增加了一种途径而已,所以它应该被设计为独立存在的,由它自身来管理这些&被收藏的商品&,它的存在与否都不影响其它领域对象。
  要实现这4个操作,那么需要在ICartService中增加下面4个接口:
Result ChangeQuantity(string userId, string id, int quantity);
Result DeleteCartItem(string userId, string id);
Result AddToFavorites(string userId, string productId);
Result ChangeMultiProductsPromotion(string userId, string productId, string selectedMultiProductsPromotionId);
  其中的部分实现如下:
public Result AddToFavorites(string userId, string productId)
var cart = _confirmUserCartExistedDomainService.GetUserCart(userId);
if (cart.IsEmpty())
return Result.Fail("当前购物车中并没有商品");
var cartItem = cart.GetCartItem(productId);
if (cartItem == null)
return Result.Fail("该购物项已不存在");
var favorites = DomainRegistry.FavoritesRepository().GetByUserId(userId) ?? new Favorites(userId, null);
favorites.AddFavoritesItem(cartItem);
DomainRegistry.FavoritesRepository().Save(favorites);
return Result.Success();
  其中关于Favorites的构造函数我是这么做的:
public Favorites(string userId, IEnumerable&FavoritesItem& favoritesItems)
if (string.IsNullOrWhiteSpace(userId))
throw new ArgumentNullException("userId");
this.UserId = userId;
this._favoritesItems = new List&FavoritesItem&();
if (favoritesItems != null && favoritesItems.Any())
foreach (var favoritesItem in favoritesItems)
AddFavoritesItem(favoritesItem);
  这样可以重用AddFavoritesItem中的一些守卫操作,保证在业务产生变动之后历史数据从DB取出来的时候经过一次最新的业务验证,确保数据在流转过程中的合法性。这个方式可以择机运用在任何聚合的构造函数中。
  本篇主要的观点还是在建模上的思维惯性,抛开DB,抛开DB,抛开DB,重要的事情说3遍。
本文的源码地址:。
作者:出处:
阅读(...) 评论()上海普陀区招聘3年以上.Net/Android开发。邮箱:,待遇从优
本系列所有文章
  之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍。现在来到了这最后一个环节,提交订单。单从业务上看,这个动作的背后包含了多个业务操作,根据用户填写的订单信息生成订单、扣除使用的余额和积分、使用选择的礼券等等。其中涉及到多个上下文的操作,包括新引入的订单上下文,那么如何同时与多个上下文进行数据的写入操作是本篇主要想讨论的问题。
二、解决数据一致性的方案
  分布式系统中的多个子系统之间的同时写入问题,也就是所谓的数据一致性问题。讲解决数据一致性方案的文章比较多,我就不赘述了,其中的根本是CAP理论,大家可自行百度/Google下。总结一下一般在分布式场景中无非就是两种方式来解决:2阶段提交的强一致性(选择CP)或者最终一致性(选择AP)。2阶段提交大家都懂,是性能杀手,阻塞式的操作会导致整个系统的瓶颈提早到来。最终一致性是非阻塞式的异步机制,通过消息体在多个系统内流转,并各自根据消息体来处理不同的业务,并且最终一致性有很多种形式来实现,这里暂不展开讨论。
三、回到DDD
  在DDD中实现最终一致性需要引入一个之前一直没提到的概念:领域事件。
  问1:什么是领域事件?
  答:领域事件是领域的一部分,表示领域中所发生的事情。
  问2:它存在的作用是?
  答:①作为实现最终一致性的载体
    ②解耦
    ③通过事件让不同的上下文分散处理下游业务,减少对数据的反向获取。处理单元更小化。  
    ④对开闭原则(OCP:Open-Closed Principle)最好体现。
  问3:那么我们如何运用到DDD中?
  答:①哪怕是同一个上下文中的不同聚合也需要通过领域事件来进行同步。
    ②把领域事件设计成聚合,但是其中的大部分代表事件发生与过去的部分属性应该为只读。设计为聚合拥有了唯一标识这样便于跟踪事件、持久化和跨限界上下文交互。
    ③使用发布 && 订阅的方式来处理事件,降低耦合。
    ④有时,有必要使用领域服务来注册事件订阅方。这样的动机可能和让应用服务来注册订阅方一样,但是此时我们可能有特定于领域的原因。
    ⑤领域事件的一个经验法则是这样的:领域事件中所包含的信息应该满足80%的消费方,虽然对于很多消费方来说,这些信息是多余的。
  根据上面的描述,设计了以下的几个对象进行实现领域事件的发布和订阅,如下图1:
                  【图1】
  DomainEventBus是一个单例。事件(继承自DomainEvent)的发布全部经由它来处理,分发失败的时候会抛出一个DistributeExceptionEvent的事件,由调用方决定后续的处理方式。另外事件订阅者(继承自DomainEventSubscriber)也通过DomainEventBus来注册订阅。类型依赖图如下图2:
                      【图2】
  为了能够比较直观的表达当前这个提交订单业务操作的处理流程,我粗略画了个时序图,如下图3。
                          【图3】
  这里的事件发布是订单上下文内的一个组件,是一个进程内操作。另外事件具体发布的目的地由不同的订阅者控制,暂时就列出了2个。
  好了根据上面的时序图描述,下面贴出其中的核心代码:
  1.事件订阅
var types = Assembly.Load("Mall.Domain.Order.DomainEventSubscribers").GetTypes().Where(ent =& !ent.IsGenericType && ent.GetInterface(typeof(IDomainEventSubscriber).FullName) != null).ToList();
foreach (var type in types)
var subscriberInstance = Activator.CreateInstance(AppDomain.CurrentDomain, type.Assembly.FullName, type.FullName).Unwrap();
var subscriber = (IDomainEventSubscriber)subscriberI
DomainEventBus.Instance().Subscribe(subscriber);
  2.和2个对订单创建事件的订阅者
public class OrderCreatedSubscriberPaymentContext : DomainEventSubscriber&OrderCreated&
public override void HandleEvent(OrderCreated domainEvent)
//TODO anything
throw new NotImplementedException();
public class OrderCreatedSubscriberSellingPriceContext : DomainEventSubscriber&OrderCreated&
public override void HandleEvent(OrderCreated domainEvent)
//TODO anything
throw new System.NotImplementedException();
  3.事件发布
public Result Create(OrderRequest orderRequest)
if (!string.IsNullOrWhiteSpace(orderRequest.CouponId))
var couponResult = DomainRegistry.SellingPriceService().IsCouponCanUse(orderRequest.CouponId, orderRequest.OrderTime);
if (!couponResult.IsSuccess)
return Result.Fail(couponResult.Msg);
var orderId = DomainRegistry.OrderRepository().NextIdentity();
var order = Domain.Order.Aggregate.Order.Create(orderId, orderRequest.UserId, orderRequest.Receiver,
orderRequest.CountryId, orderRequest.CountryName, orderRequest.ProvinceId, orderRequest.ProvinceName,
orderRequest.CityId, orderRequest.CityName, orderRequest.DistrictId, orderRequest.DistrictName,
orderRequest.Address, orderRequest.Mobile, orderRequest.Phone, orderRequest.Email,
orderRequest.PaymentMethodId, orderRequest.PaymentMethodName, orderRequest.ExpressId,
orderRequest.ExpressName, orderRequest.Freight, orderRequest.CouponId, orderRequest.CouponName, orderRequest.CouponValue, orderRequest.OrderTime);
foreach (var orderItemRequest in orderRequest.OrderItems)
order.AddOrderItem(orderItemRequest.ProductId, orderItemRequest.Quantity, orderItemRequest.UnitPrice, orderItemRequest.JoinedMultiProductsPromotionId, orderItemRequest.ProductName);
DomainRegistry.OrderRepository().Save(order);
DomainEventBus.Instance().Publish(new OrderCreated(order.ID, order.UserId, order.Receiver));
return Result.Success();
  注意其中标红的部分,暂时没有考虑出现异常的情况。另外这里的OrderCreated事件只是象征性的写一下,实际的事件需要哪些属性,只要贯彻好二八原则,设计一个满足80%场景下的直接可用,剩下的20%可以增加一些查询来满足实际业务需要。&
  如果说领域对象、应用层、仓储层等这些概念还和传统的三层架构傻傻分不清楚的话。那么领域事件应该是整个DDD中最容易理解的一部分概念,因为这一部分是独立于传统的三层架构之外的完全不同的部分,也是整个DDD设计中低耦合的关键。本篇先进行了一个对领域事件最简单的实现,主要阐述了领域事件在整个项目设计过程中的作用和运用的方式。这是一个基础,在这个基础之上已经有很多成熟的解决方案可以让我们的系统做的更好。下篇会主要讲关于异常的处理(上文中标红的那部分),数据一致性的保证等更好的提高系统可用性的部分。谢谢各位看官。
本文完整的源码地址:。
作者:出处:
阅读(...) 评论()上海普陀区招聘3年以上.Net/Android开发。邮箱:,待遇从优
本系列所有文章
  实际编码已经写了2篇了,在这过程中非常感谢有听到观点不同的声音,借着这个契机,今天这篇就把大家提出的建议一个个的过一遍,重新整理,重新出发,为了让接下去的DDD之路走的更好。
二、单元测试
  蟋蟀兄在我的第三篇文章下面指出:
  这点其实是我偷懒了,单元测试其实不单单在DDD中是一个很重要的一环,在我们崇尚敏捷,快速迭代的大背景下,有良好的单元测试模块可以保证快速迭代下的项目质量。有甚至可以使用测试先行的TDD模式。
  单元测试的好处我就不多说了,那么现在开始在项目中增加单元测试。单元测试有多种命名方式,我个人的方式是给每一个对象单独建立一个测试类,然后里面每个单元测试方法的命名规则为"方法名_条件_预期的结果"这样子。那么根据我们之前的Cart和CartItem的建模,编写的单元测试如下:
[TestClass]
public class CartTest
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_CartIdDefault_ThrowArgumentException()
var cart = new Cart(default(Guid), Guid.NewGuid(), DateTime.Now);
Assert.AreNotEqual(null, cart);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_UserIdDefault_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), default(Guid), DateTime.Now);
Assert.AreNotEqual(null, cart);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_LastChangeTimeDefault_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), default(DateTime));
Assert.AreNotEqual(null, cart);
[TestMethod]
public void AddCartItem_NotExisted_TotalItemCountIsIncreased()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new CartItem(new Guid("1-"), 1, 100));
Assert.AreEqual(1, cart.TotalItemCount());
cart.AddCartItem(new CartItem(new Guid("2-"), 1, 100));
Assert.AreEqual(2, cart.TotalItemCount());
[TestMethod]
public void AddCartItem_Existed_TotalItemCountIsNotIncreasedTotalItemNumIsIncreased()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new CartItem(new Guid("1-"), 1, 100));
Assert.AreEqual(1, cart.TotalItemCount());
Assert.AreEqual(1, cart.TotalItemNum());
cart.AddCartItem(new CartItem(new Guid("1-"), 1, 100));
Assert.AreEqual(1, cart.TotalItemCount());
Assert.AreEqual(2, cart.TotalItemNum());
[TestClass]
public class CartItemTest
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyQuantity_LessZero_ThrowArgumentException()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyQuantity(-1);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyQuantity_EqualsZero_ThrowArgumentException()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyQuantity(0);
[TestMethod]
public void ModifyQuantity_MoreZero_Success()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyQuantity(10);
Assert.AreEqual(10, cartItem.Quantity);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyPrice_LessZero_ThrowArgumentException()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyPrice(-1);
[TestMethod]
public void ModifyQuantity_EqualsZero_Success()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyQuantity(0);
Assert.AreEqual(0, cartItem.Price);
[TestMethod]
public void ModifyQuantity_MoreZero_Success()
var cartItem = new CartItem(new Guid("1-"), 1, 100);
cartItem.ModifyQuantity(10);
Assert.AreEqual(10, cartItem.Price);
三、纠正错误,重新出发
  在写CartItemTest的时候发现了一个问题。领域对象的设计中有一个要点,就是实体必须需要通过其所属的聚合根才能访问,这样才能体现出聚合的的整体性,并且减少外界对聚合内部过多的了解。而目前对于CartItem的运用却有些背道而驰的意思,由外部对象进行实例化,必然增加了外部调用方对整个购物项构造过程的了解。有一位园友有提到这点。
  &我思考了下,觉得这位园友的建议是对的。他建议的改法恰恰能够满足这个要求,隐藏了构造CartItem实体的细节。
  好了那先把CartItem的构造函数访问类型设置为internal吧,这样也只能在CartItem所在的Domain项目中进行实例化了,然后再修改Cart.AddCartItem方法的参数。变为如下:
public void AddCartItem(Guid productId, int quantity, decimal price)
var cartItem = new CartItem(productId, quantity, price);
var existedCartItem = this._cartItems.FirstOrDefault(ent =& ent.ProductId == cartItem.ProductId);
if (existedCartItem == null)
this._cartItems.Add(cartItem);
existedCartItem.ModifyPrice(cartItem.Price); //有可能价格更新了,每次都更新一下。
existedCartItem.ModifyQuantity(existedCartItem.Quantity + cartItem.Quantity);
单元测试也做出相应的更改:
[TestClass]
public class CartTest
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_CartIdDefault_ThrowArgumentException()
var cart = new Cart(default(Guid), Guid.NewGuid(), DateTime.Now);
Assert.AreNotEqual(null, cart);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_UserIdDefault_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), default(Guid), DateTime.Now);
Assert.AreNotEqual(null, cart);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void Constructor_LastChangeTimeDefault_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), default(DateTime));
Assert.AreNotEqual(null, cart);
[TestMethod]
public void AddCartItem_NotExisted_TotalItemCountIsIncreased()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
Assert.AreEqual(1, cart.TotalItemCount());
cart.AddCartItem(new Guid("2-"), 1, 100);
Assert.AreEqual(2, cart.TotalItemCount());
[TestMethod]
public void AddCartItem_Existed_TotalItemCountIsNotIncreasedTotalItemNumIsIncreased()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
Assert.AreEqual(1, cart.TotalItemCount());
Assert.AreEqual(1, cart.TotalItemNum());
cart.AddCartItem(new Guid("1-"), 1, 100);
Assert.AreEqual(1, cart.TotalItemCount());
Assert.AreEqual(2, cart.TotalItemNum());
[TestClass]
public class CartItemTest
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyQuantity_LessZero_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(1, cartItem.Quantity);
cartItem.ModifyQuantity(-1);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyQuantity_EqualsZero_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(1, cartItem.Quantity);
cartItem.ModifyQuantity(0);
[TestMethod]
public void ModifyQuantity_MoreZero_Success()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(1, cartItem.Quantity);
cartItem.ModifyQuantity(10);
Assert.AreEqual(10, cartItem.Quantity);
[TestMethod]
[ExpectedException(typeof(ArgumentException))]
public void ModifyPrice_LessZero_ThrowArgumentException()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(100, cartItem.Price);
cartItem.ModifyPrice(-1);
[TestMethod]
public void ModifyPrice_EqualsZero_Success()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(100, cartItem.Price);
cartItem.ModifyPrice(0);
Assert.AreEqual(0, cartItem.Price);
[TestMethod]
public void ModifyPrice_MoreZero_Success()
var cart = new Cart(Guid.NewGuid(), Guid.NewGuid(), DateTime.Now);
cart.AddCartItem(new Guid("1-"), 1, 100);
var cartItem = cart.GetCartItem(new Guid("1-"));
Assert.AreNotEqual(null, cartItem);
Assert.AreEqual(100, cartItem.Price);
cartItem.ModifyPrice(10);
Assert.AreEqual(10, cartItem.Price);
  这样一来,被和2位园友所指出的奇怪的&UserBuyProductDomainService&也自然消失了。应用层代码变成:
public Result Buy(Guid userId, Guid productId, int quantity)
var product = DomainRegistry.ProductService().GetProduct(productId);
if (product == null)
return Result.Fail("对不起,未能获取产品信息请重试~");
var cart = _getUserCartDomainService.GetUserCart(userId);
cart.AddCartItem(productId, quantity, product.SalePrice);
DomainRegistry.CartRepository().Save(cart);
return Result.Success();
  DDD的道路是坎坷的,我希望通过在园子里发布的文章能够结交到志同道合的DDD之友,欢迎大家不吝啬自己的见解,多多留言,也让想学习或者正在学习DDD的园友少走一些弯路。
本文的源码地址:。
作者:出处:
阅读(...) 评论()

我要回帖

更多关于 一步一步网站建设教程 的文章

 

随机推荐