我自己的一生

是你的,是我的,到底是谁的?

导航

【译文】N-层应用程序模式

Posted on 2009-12-09 23:03  Abbott zhao  阅读(2212)  评论(6编辑  收藏  举报

 

N-层应用程序模式

作者:Daniel Simmons

译者:Abbott Zhao

 

clip_image002

在我的上篇文章中,我描述了你可以成功构建n层应用程序的基本职能,主要关注避免使用反模式。在对n层应用程序进行设计作决策之前,有一些问题需要我们考虑。在这篇文章中,我仔细检查了能够成功的n层模式,并仔细审视了实体框架特定的一些问题和关键API。我也提供了.NET框架4.0中即将带来的最顶尖的特征,使n层开发更加容易。

可变化集合

可变化集合模式后面的思想是创建一个可序列化的容器,能够保留与工作单位一起工作所需要的数据,理想上,自动在客户端上执行改变的跟踪。容器作为工作单元的一部分,用自定义格式黏合的一起,所以,这个方法也往往是充满容易被中间层和客户端使用特征。

DataSet是这个模式的一个通用范例,但其它范例也存在,比如,EntityBag,不久之前,我所写的一个作为实体框架技术研究中的例子。这两个例子都表现出这个模式的一些负面因素。首先,可变化集合模式为客户端造成了特别的约束,因为,金属丝的格式对可变化集合是易于使用在特别的情况下,但很难共同使用。实际上,客户端必须和中间层实现使用一样.NET的可变化集合。第二,金属丝的格式通常效率很低。在其它的事情中,因为集合被设计为可以处理任意架构模式,所以,跟踪实例的架构需要很大的成本。可变化集合(如DataSet)的实现的另外一个问题是,可变化集合并不仅仅是特定于这个模式,很容易于结束对两个层或者更多层的紧耦合,如果你有不同的变化频率,很容易于引起问题。最后,或许大部分关注的是滥用可变化集合是如何的容易。

  在一些方法中,在设计解决方案时,这个模式会自动淹没存在头脑最活跃的那些重要关注点。正因为它如此容易把数据放入到可变化集合中,发送它到中间层,然后持久化,所以,你就不能校验中间层中你要持久化的期望类型。想象一下,你有一个服务是增加费用报告给财务系统,结果修改了某个人的薪水。

可变化集合的使用最好要使用在,你完全对客户端部署能够控制的情况下,以致你可以解决耦合和技术需求问题。如果你想优化开发者效率,而不是运行效率,采用这个方式也是正确的。如果你采用这个模式,你确定要练习一些课程,校验中间层的任何变化,而不是盲目持久化到达过来的任何变化。

DTOs

可变化集合范围的两端是数据传输对象,或者称为DTO。这个模式的意图是,使用不同的类型隔离客户端和中间层,这些类型携带数据,在中间层、客户端数据和消息之间发送。

DTO方法需要花费更多的努力来实现,但,如果能恰当地实现,将会发挥更多的架构优势。因为无论你的客户端和中间层任何一个如何变化,你都可以在两层之间的传递保持稳定的数据格式,所以,可以使客户端和中间层的演变和改进完全隔离数据结构。自然,你有时需要在两端增加一些功能,而且你能够管理功能的首次展示与后期兼容问题,这个问题通过构建向前、向后兼容的版本化插件代码来映射对象和数据之间的转换。因为明确了两层之间转换的数据格式,所以,你所使用的方法,埋藏了客户端技术可能使用.NET之外的技术。如果需要,你要使用一个格式,能够有效地在金属丝两端之间发送,或者,你可以选择,例如,为安全的原因,仅交换实体数据的子集。

  实现DTO的负面效果是,需要付出更多的努力,在本质上同一个数据上,设计三个不同类型集合,且需要在这些类型之间作映射。那么,你可以考虑使用多种快捷方式,如把DTO作为客户端类型,这样就可以把三种类型变成了两种类型;使用LINQ to Objects,来减少必须在类型之间转换数据的编码;或者使用自动映射类库,未来可以减少代码量――通过模式之间的不同类型之间的相同属性名称进行数据拷贝。但是,在现实中,没有方法能够来减少到同其它选择有共同的劳动量――至少最初的实现是这样的。

只有你的解决方案非常大,而且需求相互协作比较成熟,需要长期维护等等的时候,这个模式才被考虑。一个需要更长生命周期的项目,DTO的付出是值得的。然而,在更多的项目中, 有模式能够使用更少的精力,完成你的目标。

简单实体

  像可变化集合模式,简单的实体模式可以在客户端上重用中间实体类型,但是,与可变化集合不同,多层之间的通讯的这些实体用复杂数据结构封装起来,简单实体努力要保持数据结构的复杂性为最小、直接传递实体实例给服务方法。简单的实体模式仅允许客户端修改实体实例的简单属性。如果需要更多复杂的操作,如,在实体之间改变关系,或者实现inserts, updates, deletes的联合实现,这些操作应该由服务方法的结构所扮演。

  简单实体方法的美妙之处是,不需要额外的类型,不需要付出努力,放置类型之间的映射。如果你能够控制客户端的部署,你可以重用相同的实体结构(相同程序集和代理),而且,即使你必须使用.NET之外的客户端技术,因为数据结构是简单地,因此容易共同操作。因为客户端需要最小的跟踪,所以,客户端的实现通常是直接的。当属性必须被修改,客户端在实体实例中,可以直接修改它们。当操作需要包括多个实体或者关系时,具体的服务方法来做这些事情。

  这个模式是基本劣势是,如果你需要完成复杂场景,而需要接触多个实体,通常需要服务端提供更多的方法。这个会导致客户端必须调用许多服务来完成一个场景,网络通信量的增加,或者用许多参数来使用特定目的的服务方法。

当你有相对简单的客户端,或者当场景中的操作比较同质化时,简单实体方法是特别有效率。例如,考虑包括创建新订单的大量的操作的电子商务系统的实现。你要设计你的应用程序交互模式,以便,像客户的数据信息修改被创建新订单的操作所隔离。然后,你需要服务方法通常会查询只读数据,在同时间内,修改一个实体,而不需要在关系方面改变,或者为新的订单插入一批相关实体。这样类型的场景,简单实体模式会工作的比较好。当解决方案开始变得很复杂时,当你的客户端变得更成熟,或者当网络性能需要你在协调格式上是决定性的,其它模式列合适。

自我跟踪实体

  设计的自我跟踪实体模式是构建在简单实体模式和完成对创建单个模式工作在许多场景里的不同关注点之间的良好平衡。这个观点是创建智能实体对象,保持它们变化及相关实体变化的跟踪。来减少客户端的约束,这些实体是纯原始的CLR对象(POCO),不依赖于任何特定的持久化技术――它们所表现的实体和一些信息被unchanged(未发生变化)、modified(已修改)、new(新建)和deletion(删除)标识所标识。

  由于实体是自我跟踪的,它们比可变化集合有更多易于使用的特征,而且,因为跟踪信息是构建在实体自己内部的,且是具体到它们的架构,所以,相比用可变化集合,金属丝格式会更有效。另外,它们是POCO,它们不会对客户端和内部操作提出要求。最后,因为验证逻辑被构建在实体的自身内部,你更容易保留特定的服务方法所使用操作执行的已作过的规定。相对比较可变化集合,自我跟踪实体有两个基本的优势。首先,如果客户端需要调用一个或多个服务方法来获取它需要数据,可变化集合可以允许多个可变化集合合并在一个方法中实现。自我跟踪实体完成这样的实现,要花费比可变化集合更多的精力。第二,实体的自身定义稍微有些复杂,因为它们直接包括跟踪信息,而不是把这些信息放在外边。这个信息经常保持为最小,所以,它通常对实体的灵活性和可维护性不造成太多的影响。

  自然,自我跟踪实体不像DTO那样,是完全的解耦合,有时,当带有DTO的金属丝格式的创建比自我跟踪实体会更有效率。不要阻止混合使用DTO和自我跟踪实体,事实上,跟踪信息的结构也尽可能地保持最小,如果发展需要,可以在后期把自我跟踪对象演变到DTO,也不会困难。

用实体框架实现N

  回顾一下需要n层应用程序的选择和决定,你可能会选择一个模式和客户端技术,知道这些技术是如何避免缺陷。现在你准备好开始实施了。那么,实体框架(EF)哪些地方完全满足这些需求吗?

EF提供了持久化需要关注点的基础。这个基础包括概念模型和数据之间的映射声明,从数据库结构的解耦出中间层;在应用合适的变化跟踪信息的刷新中,自动检测并发问题;在中间层透明变化跟踪。另外,EF是一个LINQ的提供者,意味着,相当容易创建精确的查询,帮助实体映射到DTO。

EF可能用于实现前面所描述四个模式中任何一个,但在第一个版本发布时,也有各种各样的限制(作为Visual Studio 2008 SP1/.NET 3.5 SP1的一部分),使简单模式之外的其它模式实现起来还是有些困难的。Visual Studio 2010/.NET 4中EF的发布的到来,大量特征的增加,使其它模式实现起来更容易。我们浏览这些发布的特征之前,让我们看一下现在实体框架在简单模式中是如何作的。

并发标识

  在考虑n层开发的任何方面之前,你首先需要作的是,创建模型,确保打上了并发标记。你可以在别处看到构建模型的基础,这些内容,在MSDN数据平台开发者中心的Entity Framework section上可以找到。

  这个讨论的重要一点是,要确保为每个实体打上指定的并发标识。最好的选择是,使用行记录的一个版本号,或者相同概念的等价物。无论数据库中行发生任何变化,行的版本自动会变化。如果你没有使用行版本,另外一个最好的选择是,使用象时间戳这样的东西,给数据库增加一个触发器,使数据库中行发生变化时,刷新时间戳。你也可以在客户端作这样的事情,但它易于发生敏感数据腐败的问题,因为多个客户端在不经意的情况下,会获取并发标识的新值。一旦你有合适的属性配置到数据库,打开模型的实体设计器,选择属性,并设置它的并发模式为Fixed(在属性面板中)。这个设置告诉EF使用这个属性检查并发,记住,你可以在现一个实体中,使用把多个属性的并发模式设置为Fixed,但通常不需要的。

序列化

  你处理先决条件之后,下一个主题是序列化。你需要一个方法,在层之间移动。如果你通过使用EF的默认实体代码生成器,你正在构建一个WCF服务,由于EF自动在类型上面生成了DataContract 属性和在属性上生成DataMember 。这也包括导航属性,意味着,如果你获取实体相关的地图到内存中,全部地图中自动被序列化。生成的代码即支持二进制序列化,也支持XML序列化,但,XML序列化仅应用到单个实体上,不应用到地图上。

  另外需要理解的重要概念是,在默认生成的实体支持序列化的同时,它的变化跟踪信息被存储在ObjectStateManager 中(ObjectContext的一部分),这个不支持序列化。在简单实体模式中,你通常会在中间层从数据库中获取未修改的实体,序列化它们到客户端,不需要变化跟踪信息。就象这样的代码:

public Customer GetCustomerByID(string id)

{

using (var ctx = new NorthwindEntities())

{

return ctx.Customers.Where(c => c.CustomerID == id).First();

}

}

  然而,当过一段时间,来执行刷新时,变化跟踪信息不知道在什么地方来管理了,引出下一个你需要理解的EF另外一个重要部分。

ObjectStateManager 来工作

  对于两层的操作化操作,ObjectStateManager会自动成为主要部分。你根本不必要考虑它。状态管理会保持它控制下存在的实体的跟踪;它的关键值;一个EnTityState值,可能是unchanged, modified, added, 或者deleted;已修改属性的列表;每个修改属性的原始值。当你从数据库中获取一个实体,增加到实体列表中,通过状态管理来跟踪,实体和状态管理一起工作来维护跟踪信息。如果你设置一个这实体上的属性,实体的状态会自动变化为Modified,属性被增加到已编辑属性的列表,原始值被保存。如果你增加,或者删除一个实体,相似的信息被跟踪。当你调用OjbectContext的SaveChanges,这个跟踪信息被用于计算相对数据库的刷新状态。如果刷新成功,删除的实体被从环境中移走,所有其它的实体转换到unchanged状态,以便处理可以重新开始。

  当你发送实体到另外一层,那么,这个自动的跟踪的处理就中断了。在中间层实现服务方法通过使用客户端的信息执行一个刷新,就为这个目的,你需要ObjectContext中存在的两个方法:Attach 和 ApplyPropertyChanges。

Attach方法告诉状态管理器,开始跟踪一个实体。通常,查询自动匹配实体,但,如果你有一个实体,是从另外一个途径(如从客户端序列来的)获取,然后,你需要调用Attach来开始一个跟踪过程。关于Attach有两个重要的事情要紧记。

   首先,成功调用Attach之结尾,实体总是被置为unchanged状态。如果你想最终使实体设为其它状态,如,modified或者deleted,你需要另外的步骤来转换实体到那个状态。实质上,Attach告诉EF,“相信我。最少过去的一些点上,这个实体如何和数据库对照的。”实体属性拥有的值在附着到它的时候,将会考虑那个属性的原始值。所以,如果你用查询获取实体,序列化它到客户端,然后再序列化到中间层,你可以使用Attach,而不是查询,附着到它里面。当你附着实体时,并发标识的值将会使用并发检查。

  关于Attach要知道的第三个事情是,如果你附着一个实体,这个实体是相关联实体地图中一部分,Attach方法将漫步地图,附着到它发现的每一个实体。这是因为一个混合状态存在一个地图中的原因造成的,它不允许部分附着,部分不附着。所以,如果EF附着一个实体在一个地图里,需要确保,地图中其它部分也会变成附着者。

ApplyPropertyChanges方法实现了另外一种在无连接的情况下的修改场景。顺便,我们看看另外一个实体的ObjectStateManager,这个实体与其它两个实体有相同的实体键作为它的参数,与这两个实体的每一个规则属性进行比较。当它发现一个属性不同,它设置实体的属性值的状态管理来匹配作为参数传递过来的实体的值。结果是,相似于,在它被跟踪时,好象你直接执行了实体状态管理器的变化。特别需要注意的是,这个方法仅操作于“规则”属性,不是导航属性,所以,仅能在一个单个实体上起作用,不能是全部的地图。它是特别为简单实体模式来设计的,这里,实体包含了所有需要的属性值的新拷贝――对于它的职责,没有额外的跟踪信息。

  为刷新一个实体,如果你使用Attach 和 ApplyPropertyChanges两个方法一起创建一个简单的服务方法,这个方法看起来象下面:

public void UpdateCustomer(Customer original, Customer modified)

{

using (var ctx = new NorthwindEntities())

{

ctx.Attach(original);

ctx.ApplyPropertyChanges(modified.EntityKey.EntitySetName, modified);

ctx.SaveChanges();

}

}

  在这些方法很容易作为服务来实现时,这个类型的服务契约增加一些复杂性给客户端,在修改它之前,不需要拷贝实体,许多时候,这个层次的复杂性比你想象的,或者客户端需要的更多。所以,使用ApplyPropertyChanges来替代,你能够附着修改实体或者使用ObjectStateManager的低层次API来告诉它,实体的那些属性是修改的。这个方法的优势是,从客户端到中间层(仅是一个实体拷贝)逐渐增多需要刷新到数据库成本的一些场景(如果客户端仅修改一些数据,因为没有方法告知那些属性是修改了,那些不是,所以每个属性都会被刷新)下减少必须传播数据。图1显示了类似的代码:

clip_image002[1] Figure 1 刷新的服务方法

public void UpdateCustomer(Customer modified)

{

using (var ctx = new NorthwindEntities())

{

ctx.Attach(modified);

var stateEntry = ctx.ObjectStateManager.GetObjectStateEntry(modified);

foreach (var propertyName in stateEntry.CurrentValues

.DataRecordInfo.FieldMetadata

.Select(fm => fm.FieldType.Name))

{

stateEntry.SetModifiedProperty(propertyName);

}

}

ctx.SaveChanges();

}

我们展开这个服务,包括可以直截了当地增加新客户和删除客户的方法,图2显示了这样代码的例子:

clip_image001 Figure 2 增加和删除服务方法

clip_image001[1]

public void AddCustomer(Customer customer)

{

using (var ctx = new NorthwindEntities())

{

ctx.AddObject("Customers", customer);

ctx.SaveChanges();

}

}

public void DeleteCustomer(Customer customer)

{

using (var ctx = new NorthwindEntities())

{

ctx.Attach(customer);

ctx.DeleteObject(customer);

ctx.SaveChanges();

}

}

  这个方法被扩展用作,改变实体之间的关系,或者执行其它操作。要记住的关键概念是,你首先需要状态管理器作一些事情,象它是否存在原来你查询数据库的状态,然后,改变你认为有效的实体状态,然后调用SaveChanges。

.NET 3.5 SP1里简单实体模式之外的其它模式

  如果你决定使用EF第一个发布版本,实现其它模式,我首先的建议是,好好读读下一节,.NET 4会作一些事情,使这些更加容易。如果你的项目需要一个.NET 4发布之前用到其它模式的其中一个,那么,这里有一些问题需要考虑。

可变化集合当然可以被实现。即使你选择从零开始构建一个可变化集合,那么,你想采用它的关键步骤是,在客户端创建一个带概念模型元数据(没有映射、没有存储模式,或者没有需要连接到数据库的真空连接)的ObjectContext,然后,使用它作为客户端边界的变化跟踪者。

DTO也有一种可能。事实上,实现DTO在EF的第一个版本里也没有太多的困难。任一情况下,你都必须写你自己代码,或者使用自动映射器在实体和DTO之间移动数据。一个被主伙是好的注意是,使用LINQ反射,直接众查询中拷贝数据到DTO。例如,如果你创建一个CustomerDTO类,有名称和电话属性,然后,我可以创建一个服务方法返回一批CustomerDTO,就象下面:

public List<CustomerDTO> GetCustomerDTOs()

{

using (var ctx = new NorthwindEntities())

{

var query = from c in ctx.Customers

select new CustomerDTO()

{

Name = c.ContactName,

Phone = c.Phone

};

return query.ToList();

}

}

  不幸地是,在第一个版本(SP1)中,两层之间的自身跟踪的实现是比较艰难的一个模式,主要有两个原因。第一,.NET 3.5 SP1中的EF不支持POCO,所有你所实现的任何一个实体都必须依赖.NET 3.5 SP1中的EF,所以,序列化格式将不适合这样的协作。你可以通过写客户代理来实现的处理,但,正确的实现还是比较棘手的。第二,自身跟踪的一个好特征是,你可以使用一个单一相关地图,实现混合操作――一些实体可能是modified,另一些可能是new,还有一些标记为Deletion――但是,在中间层实现一个方法来处理混合方式是比较困难的。如果你调用了Attach方法,它将遍历全部的地图,附着它可能找到每个内容。类似,如果你调用了AddObject方法,它也会遍历整个地图,找到所有相关的内容。这些操作任何一个发生之后,你都会碰到一些情况,这些情况下,你都不容易转换一些实体到它们最终已经定义的状态上,这是因为,状态管理器,只允许某些状态进行转换。例如,可以从unchanged状态转移到modified,但,不能够从unchanged转换成added状态。想使混合地图附着到环境中,你需要撕开地图成单个实体,分别增加或者附着每个,然后,再接合它们的关系。这个代码比较困难。

.NET 4中的API改进

  在即将到来的EF发布,是运行在Visual Studio 2010 and .NET 4上,他们在易于在n层模式上使用提供大量的改进――特别是自身跟踪实体。我将下面一些段落中论述一部分重要的特征。

POCO EF将来的持久化,完全可以不知道实体类。这意味着,你可以创建一个实体,不依赖EF,或者其它相关的持久化的DLL。为持久化使用的单个实体,也可以工作在Silverlight上面,或者早期的.NET版本。POCO也有益于从持久化关注点从隔离业务逻辑,且,创建非常清晰的类成为可能,共同使用序列化格式。

N-层支持API的改进  由于状态转换不再受约束,ObjectStateManager的使用将会更容易。首次增加或者附着的实体地图,然后,在这个地图上漫游,从而改变实体状态到正确的状态,成为了可能。你也能够为实体设置原始值,改变实体状态到任何值,并改变它们的一个关系的状态。

外键属性支持 EF的第一个版本,仅当完全从实体中分离的时候,才支持模型化关系,这意味着,改变关系是唯一的方法通过导航属性,或者RelationshipManager。在即将到来的发布版本中,你能够用一个实体暴露在外部键属性来构建一个模型,这个键可以被直接维护。

基于T4代码生成器 EF最后一个重要变化的是,T4模板引擎的使用,使能够更容易、完全可控的实体代码生成。这个很重要,因为它意味着,微软可以创建和发布模板,为多种多样的场景和使用模式生成代码,你也可以定制这些模板,甚至写你自己的代码。Visual studio 2010将发布其中一个模板,会产生实现自向跟踪实体模式实现的类,这里面不包括你需要自定义的代码。这个产生的类允许每个简单的客户和服务的创建。

更多学习 我希望这篇文章能够给予你更好纵览设计问题,包括创建n层应用程序,用实体框架实现这些设计的一些特定的提示。当然,有更多的内容可以学习,所以,我鼓励你去看看Application Architecture Guide from the patterns & practices group 和 Entity Framework FAQ.。

 

作者:

Danny Simmons 是微软实体框架团队的开发经理。你可以通过博客blogs.msdn.com/dsimmons获取更多的主题和他对实体框架的想法。