领域驱动设计
第一次看到这个概念,觉得和我们项目中的引擎的概念很像,于是就开始研究了。准备在营业收费系统中使用领域驱动设计的理念进行设计。
主要参考资料
l http://www.cnblogs.com/daxnet/archive/2010/11/02/1867392.html
一个非常不错的介绍领域模型的博文,文章和代码中也大量参考此超链的内容
l 《领域驱动设计c#2008实现》
网上只找到英文版,看了50来页不想看了,买了本中文版的
领域模型与一般3层架构的区别
传统的三层架构
领域驱动设计的分层
l 基础结构层:该层专为其它各层提供技术框架支持。注意,这部分内容不会涉及任何业务知识。众所周知的数据访问的内容,也被放在了该层当中,因为数据的读写是业务无关的。
l 领域层:包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系。这部分内容的具体表现形式就是领域模型(Domain Model)。领域驱动设计提倡富领域模型,即尽量将业务逻辑归属到领域对象上。
l 应用层:该层不包含任何领域逻辑,但它会对任务进行协调,并可以维护应用程序的状态,因此,它更注重流程性的东西。在某些领域驱动设计的实践中,也会将其称为“工作流层”。
l 表现层:这个好理解,跟三层里的表现层意思差不多,但请注意,“Web服务”虽然是服务,但它是表现层的东西
从上图还可以看到,表现层与应用层之间是通过数据传输对象(DTO)进行交互的,数据传输对象是没有行为的POCO对象,它的目的只是为了对领域对象进行数据封装,实现层与层之间的数据传递。为何不能直接将领域对象用于数据传递?因为领域对象更注重领域,而DTO更注重数据。不仅如此,由于“富领域模型”的特点,这样做会直接将领域对象的行为暴露给表现层。
对应的我们看我们的项目也应该有如下层:
这里的规则是项目名.层名,如果还需要细分则加入层功能名。
具体项目分层实现
CSMS2
算是我们营收系统的第二个版本吧,以前的叫CSMS,这次的改动比较多所以叫CSMS2。下面将从逻辑结构的最顶层,表现层开始说起。
CSMS2.Presentation
我们从顶层表现层开始说明,这里主要是界面的具体展现部分,以后的项目不会和原项目那样按照功能模块建立多个项目,然后通过一个Main项目进行统一调用。因为实践发现发布时大都还是全部发布,也不差这几M的大小。我们会通过文件夹的形式在项目中在进行划分。
表现层家会继承ViewModel基类,用来实现一些通用的方法。一些的增删改查界面将会继承EditViewModel类,已实现一些特定的方法,该类也会继承ViewModel基类。当需要实现特定的业务逻辑时,该类会通过DTO调用应用层或领域服务的方法。
这里我同意表现层直接调用领域服务,这样可以少写些代码。尽管我认为这样的代码不会很多,但应该还是有的。在调用之前会先对相应的DTO赋值,然后调用一个数据检验方法。关于数据检验将在DTO层说明。
CSMS2.Application.DTO
这层考虑良久最后还是放在应用层中,原先有考虑放在基础架构层,但后来想把数据检验也放在这一层实现,而数据检验有可能设计到和领域层的交互,所以最后就放在应用层了。之所以将检验放DTO中,而没有放在领域模型中主要是因为如果要放领域模型的话,我们就得使用配置文件将所有的检验逻辑写在配置文件中。最终配置文件将会非常庞大,对于崇尚配置文件就写个连接字符串的我来说非常难以忍受。另外查了下baidu也确实有在DTO实现业务逻辑的做法,所以就将数据检验放在DTO中了。
数据检验
主要是利用微软企业库中的数据检验部分,使用时需引用Microsoft.Practices.EnterpriseLibrary.Validation.dll、System.ComponentModel.DataAnnotations.dll这两个DLL。
这样我们就可在DTO中直接加入特性,用来做基本的校验了。比如如下代码:
[TypeConversionValidator(typeof(decimal), MessageTemplate = "预存金额必须为数字")]
public string YuCunJinE { get; set; }
表示预存款金额必须是数字类型的,对于错误提示,我们既可以使用Message,也可以使用资源文件。个人觉得使用Message就行了,如果碰到向浦东那样要双语的就用我们的GetString方法好了。其他的一些检验特性可看网站http://msdn.microsoft.com/en-us/library/ff664694%28v=PandP.50%29.aspx
另外我也下了个企业库的帮助文档,可以问我来要。
关于自定义的数据检验,我前段时间曾经写过一个检验类,目的是可以向其中添加自己的检验类,然后在使用枚举的方式调用就可。但是微软企业库实现的更为方便。它只要继承
Validator基类,并实现一些相应的代码就可以了。为此我专门加了个SelfValidation项目。用来专门实现自定义的数据检验。
CSMS2.Application.SelfValidation
用来专门实现自定义的数据检验,使用时主要是继承Validator基类,并且还要再实现个属性类。其中currentTarget、key到底是做什么的我也没仔细研究,因为现在项目里面只要用到个MessageTemplate。具体代码如下:
public class CIDValidator : Validator<string>
{
public CIDValidator(string messageTemplate, string tag)
: base(messageTemplate, tag)
{
}
protected override string DefaultMessageTemplate
{
get { throw new NotImplementedException(); }
}
protected override void DoValidate(string objectToValidate,object currentTarget,
string key,ValidationResults validationResults)
{
bool result = false;
//to do 检验用户编号
if (!result)
{
LogValidationResult(validationResults, this.MessageTemplate, currentTarget, key);
}
}
}
public class CIDValidatorAttribute : ValidatorAttribute
{
protected override Validator DoCreateValidator(Type targetType)
{
return new CIDValidator(string.Empty,string.Empty);
}
}
CSMS2.Model
模型层是领域驱动设计最重要的一层,所有的业务逻辑最终都将在这一层体现。它包括实体、值对象、领域服务,下面将领域驱动设计会用到的一些概念大致介绍下:
聚合
聚合这个术语用来定义对象所有权,以及对象和关系之间的边界。聚合通常定义为一组相关联对象,作为一个数据更改的单元处理。每个聚合只可能有一个根对象,这个对象是一个实体对象。例如我们的营业账、营业账子表。注意建立聚合是我们项目成败的关键。聚合等于一个缩小版的引擎。我们以前有表务引擎、账务引擎,这其实是一个更大的概念。现在的聚合粒度更小些。上文我提到过UML中的聚合和组合的概念。这里的聚合其实包括了这两层含义。拿用户和账户举例,在用户的聚合根上有一个账户,这时我们要查询某个用户的账户余额可以通过用户的聚合根,但如果我们要修改账户余额,则必须在账户的聚合根上进行操作。但向营业账和营业账子表,则我们只需要建立一个营业账的聚合跟。
仓储
领域模型的具体数据库实现。官方解释是对于每一个成为聚合的对象,为该对象创建一个仓储,赋予它特定类型驻内存对象集合的外观。我们建立了一个聚合根,并在领域服务中建立的相应的方法,而这些方法的底层数据库实现就是在仓储中。仓储包括增、删、改、主键查、按照特定条件查5个必要的方法。
void Add(TEntity entity);
TEntity GetByKey(Guid id);
IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec);
void Remove(TEntity entity);
void Update(TEntity entity);
实体
由标识定义的对象称为实体,标识我认为就是主键,在具体设计时我们引入了IAggregateRoot类,该类中一个ID的主键标识,其他类中继承该类,并做泛型约束(所谓的泛型约束是指子类必须实现接口的属性和方法代码用where T : IAggregateRoot表示)。
值对象
值对象没有标识。大家可以将姓名理解为一个值对象,里面包含姓、名两个子段。在我们的项目里面不一定会用到,没有发现特别大的必要性。
领域服务
这一层书面解释是负责和领域中的实体对象、值对象,以及为了处理该领域所必须的其他领域层对象交互。我做个补充说明,使用该层有个好处是该层的方法都是静态的,这样在应用层或表现层就不需要再次构造对象了。领域服务负责调用对应的仓储实现,也就是我们说的IOC。同时对于一些插入营业账,但是需要通过抄表ID勾连获得其他领域层数据的我们也可以在这里实现。
IOC
学名叫控制反转。很早就有了,在这里的作用主要是解耦,解决一个循环引用的问题。现在大家会发现仓储层引用领域层IRepository,同时引用了领域层的CSMSEntities(这个有争议在具体用EntityFramework实现领域驱动设计时最纠结的莫过于edmx文件到底应该放在哪一层。为此我曾经建立了4、5个项目。最终将edmx放在了领域层。参看过许多其他的大型项目,最符合领域驱动设计的应该是将edmx的实体部分放在领域层,将上下文部分放在基础架构层。这里我全部放在领域层是因为我们的项目对数据库字段的增改太频繁了),而领域服务又必须调用仓储层的方法。为此我们可以使用IOC,它只调用本层的仓储接口,这样领域层就没有调用仓储层了,接口的具体实现放在配置文件中。
RepositoryFactory.GetRepository().Resolve<IZW_YingYeZRepository>("ZW_YingYeZRepository");
上面代码又做了下封装,其实也是用微软企业库做的,主要就是一个读配置,然后反射的过程,但这里不知道怎么传一个CSMSEntities的参数过去,所以领域服务代码中的方法调用前必须加上下面这一段。
public static CSMSEntities ObjContext { get; set; }
public static void Add(ZW_YingYeZ yingYeZ)
{
_ZW_YingYeZRepository.ObjContext = ObjContext;
ZW_YingYeZService._ZW_YingYeZRepository.Add(yingYeZ);
}
为什么使用edmx建立领域模型
用edmx而没有用codefirst主要是因为我们的项目还有oracle项目。目前的ODAC112030版本应该还不支持codefirst(具体的还没有研究过)。
CSMS2.Infrastructure.Repositories
前面其实已经介绍过仓储的概念了,这一层就是领域模型的具体数据库实现。在领域层我们会为每一个聚合根建立一个文件夹里面有聚合根扩展、仓储接口、领域服务,同时在仓储层会有一个对应的仓储实现类。所有的仓储实现类继承一个EFRepositoryBase的基类,该类实现了对数据库的基本操作,一个的一个实体一个聚合根的只要继承该类就可以了。向其他的营业账的聚合根需要复写对应的Add、Update等方法。以下是仓储和领域模型的关系,还可对应的参看前面的IOC部分。
CSMS2.Infrastructure
基础架构层,主要会放入一些和业务逻辑无关的通用代码。如我们的Platform等。关于异常捕获的、以及异常处理的通用代码也会放在这里,会通过委托方法实现。具体实现和以前的一样,只是项目后期写的,大家都没调用。
总结
写的有些乱,想到哪里写到哪里。主要是对近期架构工作的一个梳理,同时方便其他同事了解我的设计。