EntityFramework之领域驱动设计实践(八)(转)
2011-10-31 10:37
435 查看
http://www.cnblogs.com/daxnet/archive/2010/07/07/1772780.html仓储的实现:基本篇我们先从技术角度考虑仓储的问题。实体框架(EntityFramework)中,操作数据库是非常简单的:在ObjectContext中使用LINQtoEntities即可完成操作。开发人员也不需要为事务管理而操心,一切都由EF包办。与原本的ADO.NET以及LINQtoSQL相比,EF更为简单,LINQtoEntities的引入使得软件开发变得更为“领域化”。
下面的代码测试了持久化一个Customer实体,并从持久化机制中查询这个Customer实体的正确性。从代码中可以看到,我们用了一种很自然的表达方式,表述了“我希望查询一个名字为Sunny的客户”这样一种业务逻辑。
隐藏行号复制代码?FindCustomerTest
如果你需要实现的系统并不复杂,那么按上面的方式添加、查询实体也不会有太大问题,你可以在ObjectContext中随心所欲地使用LINQtoEntities来方便地得到你需要的东西,更让人兴奋的是,.NET4.0允许支持并行计算的PLINQ,如果你的计算机具有多核处理器,你将非常方便地获得效率上的提升。然而,当你的架构需要考虑下面几个方面时,单纯的LINQtoEntities方式就无法满足需求了:
领域模型与技术架构分离。这是DDD的一贯宗旨,也就是说,领域模型中是不能混入任何技术架构实现的,业务和技术必须严格分离。考察以上实现,领域模型紧密依赖于实体框架,而目前实体框架并非是完全领域驱动的,它更偏向于一种技术架构。比如上面的Customer实体,在实体框架驱动的设计中,它已经被EF“牵着鼻子走”了
规约(Specification)模式的引入。以上实现中,虽然LINQ使得业务逻辑的表述方式更为“领域化”,可以看成是一种DomainSpecificLanguage(MicrosoftDynamicsAX早已引入了类似的语言集成的语法),但这种做法会使得模型对领域概念的描述变得难以更改。比如:可以用“fromemployeeinemployeeswhereemployee.Age>=60&&employee.Gender.Equals(Gender.Male)selectemployee”来表述“找出所有男性退休职工”的概念,但这种逻辑是写死在领域模型中的,倘若某天男性退休的年龄从60岁调至55岁,那么上面的查询就不正确了,此时不得不对领域模型作修改。更可怕的是,LINQtoEntities仍然没有避免“SQLeverywhere”的难处,领域模型中将到处充斥这这种LINQ查询,弊端也不多说了。解决方法就是引入规约模式
仓储实现的可扩展性。比如如果经过系统分析,发现今后可能需要用到其它的持久化解决方案,那么你就不能直接使用实体框架
于是,也就回到了上篇博客中我描述的问题:仓储不是DataObject,也不仅仅是进行数据库CRUD操作的DataManager,它承担了解耦领域模型和技术架构的重要职责。为了完美地解决上面提到的问题,我们仍然采用领域驱动设计中仓储的设计模式,而将实体框架作为仓储的具体实现部分。在详细介绍仓储的设计与实现之前,让我们回顾一下上文最后部分我提到的那个仓储的接口:
隐藏行号复制代码?IRepository
在本文的案例中,仓储是这样实现的:
将上述仓储接口定义在实体、值对象和服务所在的领域层。有朋友问过我,既然仓储需要与外部存储机制打交道,那么它必定需要知道技术架构方面的细节,而将其定义在领域层,就会使得领域层依赖于具体的技术实现方式,这样就会使领域层变得“不纯净”了。其实不然!请注意,我们这里仅仅只是将仓储的接口定义在了领域层,而不是仓储的具体实现(ConcreteRepository)。更通俗地说,接口作为系统架构的基础元素,决定了整个系统的架构模式,而基于接口的具体实现只不过是一种可替换的组件,它不能成为系统架构中的一部分。由于领域层需要用到仓储,我便将仓储的接口定义在了领域层。当然,从.NET的实现技术考虑,你可以新建一个ClassLibrary,并将上述接口定义在这个ClassLibrary中,然后在领域层和仓储的具体实现中分别引用这个ClassLibrary
新建一个ClassLibrary(在本文的案例中,命名为EasyCommerce.Infrastructure.Repositories),添加对领域层assembly的引用,并实现上述接口。由于我们采用实体框架作为仓储的具体实现,因此,将这个仓储命名为EdmRepository(EntityDataModelRepository)。EdmRepository有着类似如下的实现:[align=center]隐藏行号复制代码?EdmRepository实现[/align]
从上面的代码可以看到,EdmRepository将实体框架抽象到ObjectContext这一层,这也使我们没法通过LINQtoEntities来查询模型中的对象。幸运的是,ObjectContext为我们提供了一系列函数,用以实现实体的CRUD。为了使用这些函数,我们需要知道与实体相关的EntitySetName,为此,我定义了一个AggregateRootAttribute,并将其应用在聚合根上,以便在对实体进行操作的时候,能够正确地获得EntitySetName。类似的代码如下:
[align=center]隐藏行号复制代码?CustomerPartialClass[/align]
回头来看EdmRepository的构造函数,在构造函数中,我们使用.NET的反射机制获得了定义在聚合根类型上的EntitySetName
使用IoC/DI(控制反转/依赖注入)框架,将仓储的实现(EdmRepository)注射到领域模型中。至此,领域模型一直保持着对仓储接口的引用,而对仓储的具体实现方式一无所知。由于IoC/DI的引入,我们得到了一个纯净的领域模型。在这里我也想提出一个衡量系统架构优劣度的重要指标,就是领域模型的纯净度。常见的IoC/DI框架有Spring.NET和CastleWindsorMicroKernel。在本文的案例中,我采用了CastleWindsor。以下是针对CastleWindsor的配置文件片段:[align=center]隐藏行号复制代码?CustomerPartialClass[/align]
通过这个配置片段我们还可以看到,在框架创建针对“客户”实体的仓储实例时,我们案例中的领域模型容器(EntitiesContainer)也以构造器注入的方式,被注射到了EdmRepository的构造函数中。接下来我们做一个单体测试:
考察上面的代码,仓储的使用者(Client,可以是领域模型中的任何对象)对仓储的具体实现一无所知
总结
总之,仓储的实现可以用下图表述:
回头来看本文刚开始的三个问题:依赖注入可以解决问题1和3,而仓储接口的引入,也使得规约模式的应用成为可能。.NET中有一个泛型委托,称为Func<T,bool>,它可以作为LINQ的where子句参数,实现类似规约的功能。有关规约模式,我将在其它的文章中讨论。
从本文还可以了解到,依赖注入是维持领域模型纯净度的一大利器;另一大利器是领域事件,我将在后续的文章中详述。对于本文开始的第三个问题,也就是仓储实现的可扩展性,将在下篇文章中进行讨论,包括的内容有:事务处理和可扩展的仓储框架的实现。
下面的代码测试了持久化一个Customer实体,并从持久化机制中查询这个Customer实体的正确性。从代码中可以看到,我们用了一种很自然的表达方式,表述了“我希望查询一个名字为Sunny的客户”这样一种业务逻辑。
隐藏行号复制代码?FindCustomerTest
[TestMethod]
publicvoidFindCustomerTest()
{
Customercustomer=Customer.CreateCustomer("daxnet","12345",
newName{FirstName="Sunny",LastName="Chen"},
newAddress(),newAddress(),DateTime.Now.AddYears(-29));
using(EntitiesContainerec=newEntitiesContainer())
{
ec.Customers.AddObject(customer);
ec.SaveChanges();
}
using(EntitiesContainerec=newEntitiesContainer())
{
varquery=fromcustinec.Customers
wherecust.Name.FirstName.Equals("Sunny")
selectcust;
Assert.AreNotEqual(0,query.Count());
}
}
如果你需要实现的系统并不复杂,那么按上面的方式添加、查询实体也不会有太大问题,你可以在ObjectContext中随心所欲地使用LINQtoEntities来方便地得到你需要的东西,更让人兴奋的是,.NET4.0允许支持并行计算的PLINQ,如果你的计算机具有多核处理器,你将非常方便地获得效率上的提升。然而,当你的架构需要考虑下面几个方面时,单纯的LINQtoEntities方式就无法满足需求了:
领域模型与技术架构分离。这是DDD的一贯宗旨,也就是说,领域模型中是不能混入任何技术架构实现的,业务和技术必须严格分离。考察以上实现,领域模型紧密依赖于实体框架,而目前实体框架并非是完全领域驱动的,它更偏向于一种技术架构。比如上面的Customer实体,在实体框架驱动的设计中,它已经被EF“牵着鼻子走”了
规约(Specification)模式的引入。以上实现中,虽然LINQ使得业务逻辑的表述方式更为“领域化”,可以看成是一种DomainSpecificLanguage(MicrosoftDynamicsAX早已引入了类似的语言集成的语法),但这种做法会使得模型对领域概念的描述变得难以更改。比如:可以用“fromemployeeinemployeeswhereemployee.Age>=60&&employee.Gender.Equals(Gender.Male)selectemployee”来表述“找出所有男性退休职工”的概念,但这种逻辑是写死在领域模型中的,倘若某天男性退休的年龄从60岁调至55岁,那么上面的查询就不正确了,此时不得不对领域模型作修改。更可怕的是,LINQtoEntities仍然没有避免“SQLeverywhere”的难处,领域模型中将到处充斥这这种LINQ查询,弊端也不多说了。解决方法就是引入规约模式
仓储实现的可扩展性。比如如果经过系统分析,发现今后可能需要用到其它的持久化解决方案,那么你就不能直接使用实体框架
于是,也就回到了上篇博客中我描述的问题:仓储不是DataObject,也不仅仅是进行数据库CRUD操作的DataManager,它承担了解耦领域模型和技术架构的重要职责。为了完美地解决上面提到的问题,我们仍然采用领域驱动设计中仓储的设计模式,而将实体框架作为仓储的具体实现部分。在详细介绍仓储的设计与实现之前,让我们回顾一下上文最后部分我提到的那个仓储的接口:
隐藏行号复制代码?IRepository
publicinterfaceIRepository<TEntity>
whereTEntity:EntityObject,IAggregateRoot
{
voidAdd(TEntityentity);
TEntityGetByKey(intid);
IEnumerable<TEntity>FindBySpecification(Func<TEntity,bool>spec);
voidRemove(TEntityentity);
voidUpdate(TEntityentity);
}
在本文的案例中,仓储是这样实现的:
将上述仓储接口定义在实体、值对象和服务所在的领域层。有朋友问过我,既然仓储需要与外部存储机制打交道,那么它必定需要知道技术架构方面的细节,而将其定义在领域层,就会使得领域层依赖于具体的技术实现方式,这样就会使领域层变得“不纯净”了。其实不然!请注意,我们这里仅仅只是将仓储的接口定义在了领域层,而不是仓储的具体实现(ConcreteRepository)。更通俗地说,接口作为系统架构的基础元素,决定了整个系统的架构模式,而基于接口的具体实现只不过是一种可替换的组件,它不能成为系统架构中的一部分。由于领域层需要用到仓储,我便将仓储的接口定义在了领域层。当然,从.NET的实现技术考虑,你可以新建一个ClassLibrary,并将上述接口定义在这个ClassLibrary中,然后在领域层和仓储的具体实现中分别引用这个ClassLibrary
新建一个ClassLibrary(在本文的案例中,命名为EasyCommerce.Infrastructure.Repositories),添加对领域层assembly的引用,并实现上述接口。由于我们采用实体框架作为仓储的具体实现,因此,将这个仓储命名为EdmRepository(EntityDataModelRepository)。EdmRepository有着类似如下的实现:[align=center]隐藏行号复制代码?EdmRepository实现[/align]
internalclassEdmRepository<TEntity>:IRepository<TEntity>
whereTEntity:EntityObject,IAggregateRoot
{
#regionPrivateFields
privatereadonlyObjectContextobjContext;
privatereadonlystringentitySetName;
#endregion
#regionConstructors
///<summary>
///
///</summary>
///<paramname="objContext"></param>
publicEdmRepository(ObjectContextobjContext)
{
this.objContext=objContext;
if(!typeof(TEntity).IsDefined(typeof(AggregateRootAttribute),true))
thrownewException();
AggregateRootAttributeaggregateRootAttribute=(AggregateRootAttribute)typeof(TEntity)
.GetCustomAttributes(typeof(AggregateRootAttribute),true)[0];
this.entitySetName=aggregateRootAttribute.EntitySetName;
}
#endregion
#regionIRepository<TEntity>Members
publicvoidAdd(TEntityentity)
{
this.objContext.AddObject(EntitySetName,entity);
}
publicTEntityGetByKey(intid)
{
stringeSql=string.Format("SELECTVALUEentFROM{0}ASentWHEREent.Id=@id",EntitySetName);
varobjectQuery=objContext.CreateQuery<TEntity>(eSql,
newObjectParameter("id",id));
if(objectQuery.Count()>0)
returnobjectQuery.First();
thrownewException("Notfound");
}
publicvoidRemove(TEntityentity)
{
this.objContext.DeleteObject(entity);
}
publicvoidUpdate(TEntityentity)
{
//TODO
}
publicIEnumerable<TEntity>FindBySpecification(Func<TEntity,bool>spec)
{
thrownewNotImplementedException();
}
#endregion
#regionProtectedProperties
protectedstringEntitySetName
{
get{returnthis.entitySetName;}
}
protectedObjectContextObjContext
{
get{returnthis.objContext;}
}
#endregion
}
从上面的代码可以看到,EdmRepository将实体框架抽象到ObjectContext这一层,这也使我们没法通过LINQtoEntities来查询模型中的对象。幸运的是,ObjectContext为我们提供了一系列函数,用以实现实体的CRUD。为了使用这些函数,我们需要知道与实体相关的EntitySetName,为此,我定义了一个AggregateRootAttribute,并将其应用在聚合根上,以便在对实体进行操作的时候,能够正确地获得EntitySetName。类似的代码如下:
[align=center]隐藏行号复制代码?CustomerPartialClass[/align]
[AggregateRoot("Customers")]
partialclassCustomer:IAggregateRoot
{
}
回头来看EdmRepository的构造函数,在构造函数中,我们使用.NET的反射机制获得了定义在聚合根类型上的EntitySetName
使用IoC/DI(控制反转/依赖注入)框架,将仓储的实现(EdmRepository)注射到领域模型中。至此,领域模型一直保持着对仓储接口的引用,而对仓储的具体实现方式一无所知。由于IoC/DI的引入,我们得到了一个纯净的领域模型。在这里我也想提出一个衡量系统架构优劣度的重要指标,就是领域模型的纯净度。常见的IoC/DI框架有Spring.NET和CastleWindsorMicroKernel。在本文的案例中,我采用了CastleWindsor。以下是针对CastleWindsor的配置文件片段:[align=center]隐藏行号复制代码?CustomerPartialClass[/align]
<castle>
<components>
<!--ObjectContextforEntityDataModel-->
<componentid="ObjectContext"
service="System.Data.Objects.ObjectContext,System.Data.Entity,Version=4.0.0.0,Culture=neutral,
type="EasyCommerce.Domain.Model.EntitiesContainer,EasyCommerce.Domain"/>
<componentid="CustomerRepository"
service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer,EasyCommerce.Doma
type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCommerce.Doma
<objContext>${ObjectContext}</objContext>
</component>
</components>
</castle>
通过这个配置片段我们还可以看到,在框架创建针对“客户”实体的仓储实例时,我们案例中的领域模型容器(EntitiesContainer)也以构造器注入的方式,被注射到了EdmRepository的构造函数中。接下来我们做一个单体测试:
考察上面的代码,仓储的使用者(Client,可以是领域模型中的任何对象)对仓储的具体实现一无所知
总结
总之,仓储的实现可以用下图表述:
回头来看本文刚开始的三个问题:依赖注入可以解决问题1和3,而仓储接口的引入,也使得规约模式的应用成为可能。.NET中有一个泛型委托,称为Func<T,bool>,它可以作为LINQ的where子句参数,实现类似规约的功能。有关规约模式,我将在其它的文章中讨论。
从本文还可以了解到,依赖注入是维持领域模型纯净度的一大利器;另一大利器是领域事件,我将在后续的文章中详述。对于本文开始的第三个问题,也就是仓储实现的可扩展性,将在下篇文章中进行讨论,包括的内容有:事务处理和可扩展的仓储框架的实现。
相关文章推荐
- EntityFramework之领域驱动设计实践(三)(转)
- [转]EntityFramework之领域驱动设计实践
- 【转】EntityFramework之领域驱动设计实践(七)
- EntityFramework之领域驱动设计实践(六)
- 【转】EntityFramework之领域驱动设计实践(八)
- EntityFramework之领域驱动设计实践 - 前言
- EntityFramework之领域驱动设计实践(七)
- 【转】EntityFramework之领域驱动设计实践(九)
- EntityFramework之领域驱动设计实践 (一)
- EntityFramework之领域驱动设计实践(八)
- EntityFramework之领域驱动设计实践(四)(转)
- 【转】EntityFramework之领域驱动设计实践(十)
- EntityFramework之领域驱动设计实践(二)
- EntityFramework之领域驱动设计实践(九)
- EntityFramework之领域驱动设计实践(五)(转)
- 【转】EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)
- EntityFramework之领域驱动设计实践(三)
- EntityFramework之领域驱动设计实践【后续篇】:基于EF 4.3.1 Code First的领域驱动设计实践案例
- EntityFramework之领域驱动设计实践(十)
- EntityFramework之领域驱动设计实践(六)(转)