.NET领域驱动设计实战系列 专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
最后更新于:2022-04-02 00:14:04
# [.NET领域驱动设计实战系列]专题五:网上书店规约模式、工作单元模式的引入以及购物车的实现
## 一、前言
在前面2篇博文中,我分别介绍了规约模式和工作单元模式,有了前面2篇博文的铺垫之后,下面就具体看看如何把这两种模式引入到之前的网上书店案例里。
## 二、规约模式的引入
在[第三专题](http://www.cnblogs.com/zhili/p/SpecificationPattern.html)我们已经详细介绍了什么是规约模式,没看过的朋友首先去了解下。下面让我们一起看看如何在网上书店案例中引入规约模式。在网上书店案例中规约模式的实现兼容了2种模式的实现,兼容了传统和轻量的实现,包括传统模式的实现,主要是为了实现一些共有规约的重用,不然的话可能就要重复写这些表达式。下面让我们具体看看在该项目中的实现。
首先是ISpecification接口的定义以及其抽象类的实现
```
public interface ISpecification
{
bool IsSatisfiedBy(T candidate);
Expression> Expression { get; }
}
public abstract class Specification : ISpecification
{
public static Specification Eval(Expression> expression)
{
return new ExpressionSpecification(expression);
}
#region ISpecification Members
public bool IsSatisfiedBy(T candidate)
{
return this.Expression.Compile()(candidate);
}
public abstract Expression> Expression { get; }
#endregion
}
```
上面的实现稍微对传统规约模式进行了一点修改,添加 Expression属性来获得规约的表达式树。另外,在该案例中还定义了一个包装表达式树的规约类和没有任何条件的规约类AnySpecification。其具体实现如下:
```
public sealed class ExpressionSpecification : Specification
{
private readonly Expression> _expression;
public ExpressionSpecification(Expression> expression)
{
this._expression = expression;
}
public override Expression> Expression
{
get { return _expression; }
}
}
public sealed class AnySpecification : Specification
{
public override Expression> Expression
{
get { return o => true; }
}
}
```
接下来就是轻量规约模式的实现了,该实现涉及2个类,一个是包含扩展方法的类和一个实现统一表达式树参数的类。具体实现代码如下所示:
```
public static class SpecExprExtensions
{
public static Expression> Not(this Expression> one)
{
var candidateExpr = one.Parameters[0];
var body = Expression.Not(one.Body);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> And(this Expression> one,
Expression> another)
{
// 首先定义好一个ParameterExpression
var candidateExpr = Expression.Parameter(typeof(T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
// 将表达式树的参数统一替换成我们定义好的candidateExpr
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.And(left, right);
return Expression.Lambda>(body, candidateExpr);
}
public static Expression> Or(
this Expression> one, Expression> another)
{
var candidateExpr = Expression.Parameter(typeof(T), "candidate");
var parameterReplacer = new ParameterReplacer(candidateExpr);
var left = parameterReplacer.Replace(one.Body);
var right = parameterReplacer.Replace(another.Body);
var body = Expression.Or(left, right);
return Expression.Lambda>(body, candidateExpr);
}
}
public class ParameterReplacer : ExpressionVisitor
{
public ParameterReplacer(ParameterExpression paramExpr)
{
this.ParameterExpression = paramExpr;
}
public ParameterExpression ParameterExpression { get; private set; }
public Expression Replace(Expression expr)
{
return this.Visit(expr);
}
protected override Expression VisitParameter(ParameterExpression p)
{
return this.ParameterExpression;
}
}
```
这样,规约模式在案例中的实现就完成了,下面具体介绍下工作单元模式是如何在该项目中实现的。
## 三、工作单元模式的引入
工作单元模式,主要是为了保证数据的一致性,一些涉及到多个实体的操作我们希望它们一起被提交,从而保证数据的正确性和一致性。例如,在该项目,用户成功注册的同时需要为用户创建一个购物车对象,这里就涉及到2个实体,一个是用户实体,一个是购物车实体,所以此时必须保证这个操作必须作为一个操作被提交,这样就可以保证要么一起提交成功,要么都失败,不存在其中一个被提交成功的情况,否则就会出现数据不正确的情况,上一专题的转账业务也是这个道理,只是转账业务涉及的是2个相同的实体,都是账户实体。
从上面描述可以发现,**要保证数据的一致性,必须要有一个类统一管理提交操作,而不能由其仓储实现来提交数据改变**。根据上一专题我们可以知道,首先需要定义一个工作单元接口IUnitOfWork,工作单元接口的定义通常放在基础设施层,其定义代码如下所示:
在该项目中,对工作单元接口的方法进行了一个分离,把其方法分别定义在2个接口中,工作单元接口中仅仅定义了一个Commit方法,RegisterNew, RegisterModified和RegisterDelete方法定义在IRepositoryContext接口中。当然我觉得也可以把这4个操作都定义在IUnitOfWork接口中。这里只是遵循dax.net案例中设计来实现的。然后EntityFrameworkRepositoryContext来实现这4个操作。工作单元模式在项目中的实现代码如下所示:
```
// 仓储上下文接口
// 这里把传统的IUnitOfWork接口中方法分别在2个接口定义:一个是IUnitOfWork,另一个就是该接口
public interface IRepositoryContext : IUnitOfWork
{
// 用来标识仓储上下文
Guid Id { get; }
void RegisterNew(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
void RegisterModified(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
void RegisterDeleted(TAggregateRoot entity)
where TAggregateRoot : class, IAggregateRoot;
}
public interface IEntityFrameworkRepositoryContext : IRepositoryContext
{
#region Properties
OnlineStoreDbContext DbContex { get; }
#endregion
}
// IEntityFrameworkRepositoryContext接口的实现
public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext
{
// ThreadLocal代表线程本地存储,主要相当于一个静态变量
// 但静态变量在多线程访问时需要显式使用线程同步技术。
// 使用ThreadLocal变量,每个线程都会一个拷贝,从而避免了线程同步带来的性能开销
private readonly ThreadLocal _localCtx = new ThreadLocal(() => new OnlineStoreDbContext());
public OnlineStoreDbContext DbContex
{
get { return _localCtx.Value; }
}
private readonly Guid _id = Guid.NewGuid();
#region IRepositoryContext Members
public Guid Id
{
get { return _id; }
}
public void RegisterNew(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Set().Add(entity);
}
public void RegisterModified(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Entry(entity).State = EntityState.Modified;
}
public void RegisterDeleted(TAggregateRoot entity) where TAggregateRoot : class, Domain.IAggregateRoot
{
_localCtx.Value.Set().Remove(entity);
}
#endregion
#region IUnitOfWork Members
public void Commit()
{
var validationError = _localCtx.Value.GetValidationErrors();
_localCtx.Value.SaveChanges();
}
#endregion
}
```
到此,工作单元模式的引入也就完成了,接下面,让我们继续完成网上书店案例。
## 四、购物车的实现
在前一个版本中,只是实现了商品的展示和详细信息等功能。在网上商店中,都有购物车这个功能,作为一个完整的案例,该案例也不能少了这个功能。在实现购物车之前,我们首先理清下业务逻辑:访问者看到商品,然后点击商品下的加入购物车按钮,把商品加入购物车。
在上面的业务逻辑中,涉及了下面几个更细的业务逻辑:
* 如果用户没注册时,访客点击加入购物车按钮应跳转到注册界面,这样就涉及到用户注册功能的实现
* 用户注册成功后需要同时为用户创建一个购物车实例与该用户进行绑定,之后用户就可以把商品加入自己的购物车
* 加入购物车之后,用户可以查看购物车中的商品,同时也应该可以进行更新和移除操作。
通过上面的描述,大家应该自然明白了我们接下来需要哪些功能了吧,下面我们来理理:
1. 用户注册功能,涉及用户注册页面。自然就涉及用户注册服务和用户仓储的实现
2. 注册成功同时创建购物车实例。自然涉及创建购物车服务方法和购物车仓储的实现
3. 加入购物车成功后,可以查看购物车中的商品、更新和移除操作,自然涉及到购物车页面的实现。这里自然涉及到获得购物车和更新商品数量和删除购物项的服务方法。
理清了思路之后,接下来就应该去实现功能了,首先应该实现自然就是用户注册模块。实现功能模块都从底向上来实现,首先应该先定义用户聚合根,接着实现用户仓储和用户服务,最后实现控制器和视图。下面是用户注册涉及的主要类的实现:
```
// 用户聚合根
public class User : AggregateRoot
{
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public bool IsDisabled { get; set; }
public DateTime RegisteredDate { get; set; }
public DateTime? LastLogonDate { get; set; }
public string Contact { get; set; }
//用户的联系地址
public Address ContactAddress { get; set; }
//用户的发货地址
public Address DeliveryAddress { get; set; }
public IEnumerable Orders
{
get
{
IEnumerable result = null;
//DomainEvent.Publish(new GetUserOrdersEvent(this),
// (e, ret, exc) =>
// {
// result = e.Orders;
// });
return result;
}
}
public override string ToString()
{
return this.UserName;
}
#region Public Methods
public void Disable()
{
this.IsDisabled = true;
}
public void Enable()
{
this.IsDisabled = false;
}
// 为当前用户创建购物篮。
public ShoppingCart CreateShoppingCart()
{
var shoppingCart = new ShoppingCart { User = this };
return shoppingCart;
}
#endregion
}
public interface IUserRepository : IRepository
{
bool CheckPassword(string userName, string password);
}
public class UserRepository : EntityFrameworkRepository, IUserRepository
{
public UserRepository(IRepositoryContext context)
: base(context)
{
}
public bool CheckPassword(string userName, string password)
{
Expression> userNameExpression = u => u.UserName == userName;
Expression> passwordExpression = u => u.Password == password;
return Exists(new ExpressionSpecification(userNameExpression.And(passwordExpression)));
}
}
// 用户服务契约
[ServiceContract(Namespace = "")]
public interface IUserService
{
#region Methods
[OperationContract]
[FaultContract(typeof (FaultData))]
IList CreateUsers(List userDtos);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool ValidateUser(string userName, string password);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool DisableUser(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
bool EnableUser(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
void DeleteUsers(UserDto userDto);
[OperationContract]
[FaultContract(typeof(FaultData))]
IList UpdateUsers(List userDataObjects);
[OperationContract]
[FaultContract(typeof (FaultData))]
UserDto GetUserByKey(Guid id);
[OperationContract]
[FaultContract(typeof(FaultData))]
UserDto GetUserByEmail(string email);
[OperationContract]
[FaultContract(typeof(FaultData))]
UserDto GetUserByName(string userName);
#endregion
}
public class **UserServiceImp** :ApplicationService, IUserService
{
private readonly IUserRepository _userRepository;
private readonly IShoppingCartRepository _shoppingCartRepository;
public UserServiceImp(IRepositoryContext repositoryContext,
IUserRepository userRepository,
IShoppingCartRepository shoppingCartRepository)
: base(repositoryContext)
{
_userRepository = userRepository;
_shoppingCartRepository = shoppingCartRepository;
}
public IList CreateUsers(List userDtos)
{
if (userDtos == null)
throw new ArgumentNullException("userDtos");
return PerformCreateObjects
';
- , UserDto, User>(userDtos,
_userRepository,
dto =>
{
if (dto.RegisterDate == null)
dto.RegisterDate = DateTime.Now;
},
ar =>
{
var shoppingCart = ar.CreateShoppingCart();
_shoppingCartRepository.Add(shoppingCart);
});
}
public bool ValidateUser(string userName, string password)
{
if (string.IsNullOrEmpty(userName))
throw new ArgumentNullException("userName");
if (string.IsNullOrEmpty(password))
throw new ArgumentNullException("password");
return _userRepository.CheckPassword(userName, password);
}
public bool DisableUser(UserDto userDto)
{
if(userDto == null)
throw new ArgumentNullException("userDto");
User user;
if (!IsEmptyGuidString(userDto.Id))
user = _userRepository.GetByKey(new Guid(userDto.Id));
else if (!string.IsNullOrEmpty(userDto.UserName))
user = _userRepository.GetByExpression(u=>u.UserName == userDto.UserName);
else if (!string.IsNullOrEmpty(userDto.Email))
user = _userRepository.GetByExpression(u => u.Email == userDto.Email);
else
throw new ArgumentNullException("userDto", "Either ID, UserName or Email should be specified.");
user.Disable();
_userRepository.Update(user);
RepositorytContext.Commit();
return user.IsDisabled;
}
public bool EnableUser(UserDto userDto)
{
if (userDto == null)
throw new ArgumentNullException("userDto");
User user;
if (!IsEmptyGuidString(userDto.Id))
user = _userRepository.GetByKey(new Guid(userDto.Id));
else if (!string.IsNullOrEmpty(userDto.UserName))
user = _userRepository.GetByExpression(u => u.UserName == userDto.UserName);
else if (!string.IsNullOrEmpty(userDto.Email))
user = _userRepository.GetByExpression(u => u.Email == userDto.Email);
else
throw new ArgumentNullException("userDto", "Either ID, UserName or Email should be specified.");
user.Enable();
_userRepository.Update(user);
RepositorytContext.Commit();
return user.IsDisabled;
}
public IList