求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
     
   
分享到
Microsoft NLayerApp案例理论与实践
 

作者:陈晴阳 ,发布于2013-2-22,来源:博客园

 

目录:

Microsoft NLayerApp案例理论与实践 - 项目简介与环境搭建

Microsoft NLayerApp案例理论与实践 - 多层架构与应用系统设计原则

Microsoft NLayerApp案例理论与实践 - DDD、分布式DDD及其分层

Microsoft NLayerApp案例理论与实践 - 基础结构层(Cross-Cutting部分)

Microsoft NLayerApp案例理论与实践 - 基础结构层(数据访问部分)

Microsoft NLayerApp案例理论与实践 - 领域模型层

Microsoft NLayerApp案例理论与实践 - 应用层

Microsoft NLayerApp案例理论与实践 - 分布式服务

Microsoft NLayerApp案例理论与实践 - 项目简介与环境搭建

项目简介

Microsoft – Spain团队有一个很不错的“面向领域多层分布式项目”案例:Microsoft – Domain Oriented N-Layered .NET 4.0 App Sample(在本系列文章中,我使用NLayerApp作为该项目的名称进行介绍),在codeplex上的地址是:http://microsoftnlayerapp.codeplex.com/。它是学习领域驱动设计(DDD)的一个非常不错的案例项目。该项目采用的是经典的DDD架构,而不是CQRS架构,但我觉得整个案例做的非常不错,基本上包含了基于DDD的架构实践的各个方面。因此,应不少社区朋友的要求,我打算花一部分精力来写一个介绍该项目理论与实践的系列文章。这部分系列文章将分为两个部分:

1.原理部分:这部分介绍Microsoft NLayerApp的一些理论依据,包括架构设计原则、分层架构、DDD、Distributed DDD、面向对象分析与设计等。事实上,microsoftnlayerapp.codeplex.com站点上已经有一些文档对这部分内容作了介绍,因此,原理部分的内容我将基本上是对这些英文文档进行翻译整理,然后再添加一些自己的注释,这样做的好处是,能够就整个企业级项目的开发与设计为读者提供一套相对系统全面的学习材料。NLayerApp的官方站点本身也在做西班牙语到英语的翻译工作,所以这部分英文文档也并不全面,我会在新英文版文档发布后,在此相应地添加所缺失的部分

2.实践部分:这部分将对整个NLayerApp Solution的结构、各个逻辑层、各种用到的技术进行剖析和介绍。与原理部分不同,此部分内容更关注技术的具体实现细节,而不是去讨论什么是面向对象,什么是分层架构等基础性问题

注意:Microsoft – Spain团队一直以“Domain Oriented”一词来形容这个项目,而不是用“Domain Driven Design”,原因是,Domain Driven Design包含的内容,不仅仅是某一种架构技术,它还包含软件项目的开发方式、开发团队的协作管理、用于领域专家和软件人员之间的“通用语言”的创建等内容。然而,在整个NLayerApp项目中,并没有用到DDD的所有这些内容,项目的范围仅限于逻辑/技术层面的架构设计。

NLayerApp项目环境搭建

在开始这个系列文章之前,先让我们把NLayerApp的项目环境搭建好。在搭建环境之前,请检查你的电脑是否满足下面的先决条件:

  • Visual Studio 2010 RTM 和 .NET 4.0 RTM
  • Expression Blend 4(好像微软公共官方网站没有直接的下载地址,只有MSDN订阅用户才能下载到完整版本。不过使用Expression Blend SDK for Silverlight 4应该也是可以的。下载地址是:http://www.microsoft.com/downloads/en/details.aspx?FamilyID=d197f51a-de07-4edf-9cba-1f1b4a22110d)
  • SQL Server 2008 R2 Express/Standard/Enterprise(本系列文章使用的是Express版本)
  • Unity Application Block 2.0 – 5/5/2010发布(下载地址:http://www.microsoft.com/downloads/en/details.aspx?FamilyID=2D24F179-E0A6-49D7-89C4-5B67D939F91B&displaylang=en)
  • Pex & Moles 0.94.51023.0, Visual Studio 2010 Power Tools, 10/29/2010发布(地址:http://research.microsoft.com/en-us/projects/pex/downloads.aspx#PexMSDN)
  • WPF Toolkit:http://wpf.codeplex.com/releases/view/40535
  • Silverlight 4 Tools for Visual Studio 2010: http://www.microsoft.com/downloads/en/details.aspx?displaylang=en&FamilyID=b3deb194-ca86-4fb6-a716-b67c2604a139
  • Silverlight 4.0 Toolkit (4/15/2010): http://silverlight.codeplex.com/releases/view/43528
  • Windows Server AppFabric(可选,本系列文章用不到):http://www.microsoft.com/downloads/en/details.aspx?FamilyID=467e5aa5-c25b-4c80-a6d2-9f8fb0f337d2
  • Windows Azure SDK & Azure Tools for VS2010, Nov. 2010(可选,本系列文章用不到):http://www.microsoft.com/downloads/en/details.aspx?FamilyID=7a1089b6-4050-4307-86c4-9dadaa5ed018

请按下面的步骤安装和配置NLayerApp:

1.完成上述开发包的安装和配置(最后两项可以不安装,本系列文章没有用Windows Server AppFabric和Azure的功能)

2.下载NLayerApp v1.0的压缩包,地址是:http://microsoftnlayerapp.codeplex.com/releases/view/56660,选择V1.0 - N-Layer DDD Sample App NET4.0,本系列文章将使用这个版本进行介绍

3.解压缩下载完的zip包,包含三个文件夹:CORE、CORE-APPFABRIC和CORE-AZURE。本系列文章没使用AppFabric和Azure,所以,直接进入CORE目录

4.暂时直接无视Tests,所以,双击打开NLayerAppWithoutTesting.sln解决方案

5.这个解决方案没有将Infrastructure.Data.MainModule.Mock项目添加进来,这会导致Infrastructure.CrossCutting.IoC项目无法编译通过。在Visual Studio中,将解决方案展开到1.5.1 Data节点,在该节点上右键单击,选择Add | Existing Project,然后在CORE的Infrastructure.Data.MainModule.Mock目录下选择Infrastructure.Data.MainModule.Mock.csproj项目文件,并单击Open按钮

6.在Server Explorer中,右键单击Data Connections节点,选择Create New SQL Server Database选项

7.在打开的Create New SQL Server Database对话框中,填入你的Server地址,然后输入数据库名称,再单击OK按钮。本案例使用SQL Express(with Windows Authentication),使用默认的数据库名称NLayerApp

你完全可以选择自己定义的SQL Server和数据库名称,如果你是使用自己定义的SQL Server和数据库的话,请同时修改2 – Database节点下NLayerAppDatabase项目的属性:右键单击NLayerAppDatabase项目,选择Properties,在Property页的Deploy选项卡中修改相关参数:

8.右键单击NLayerAppDatabase项目,然后单击Deploy,这将创建数据库Schema

9.编译整个解决方案

10.在1.2 – Distributed Services节点下,找到DistributedServices.Deployment项目,右键单击项目下的MainModule.svc文件,选择View in Browser,以启动WCF服务

11.启动用户界面。NLayerApp v1.0提供以下几种用户界面:基于RIA的Silverlight 4.0 Client,基于Web的ASP.NET MVC Client,基于Windows的WPF Client

基于RIA的Silverlight 4.0 Client

右键单击Silverlight.Client.Web项目下的Silverlight.Client.Web.html文件,选择View in Browser,则启动基于RIA的Silverlight 4.0 Client

基于Web的ASP.NET MVC Client

将MVC.Client项目设置为启动项目并直接运行,可以启动基于Web的ASP.NET MVC Client

可能是我浏览器版本低的缘故,得到的ASP.NET MVC页面布局有点乱

基于Windows的WPF Client

将WPF.Client项目设置为启动项目并直接运行,可以启动基于Windows的WPF Client

本文介绍了NLayerApp项目的基本情况和环境搭建。从下一讲开始,我们将进入架构设计的理论学习部分,包括:分层架构、SOLID与设计原则、面向领域驱动设计(DDD)的架构趋势以及分布式DDD(Distributed DDD,DDDD)。这部分内容将主要来自于NLayerApp官网(microsoftnlayerapp.codeplex.com)提供的英文文档,daxnet将在此做翻译、整理与注解。

Microsoft NLayerApp案例理论与实践 - 多层架构与应用系统设计原则

在对NLayerApp实际项目进行讨论之前,让我们首先学习一下(或者应该说重温一下)分层/多层架构与应用系统设计原则。很多朋友会认为这些都是老掉牙的内容,只要是软件从业人员,都会对这些内容非常熟悉。然而,果真如此吗?我在这里整理这部分内容,一方面是为介绍NLayerApp打下基础,而另一方面,则是希望借此机会将这些理论性的东西做个归纳,也希望读者朋友能够认真阅读,毕竟温故知新嘛。

需要说明的是,从本章节开始,大多数理论性的东西都源自Microsoft Spain团队针对NLayerApp所编写的《Architecture Guide Book》,事实上这本Guideline的英文版至今也还没有完成,我会从中抽出部分章节做些翻译和归纳,有兴趣的朋友请直接上microsoftnalyerapp.codeplex.com站点上下载英文版阅读。

Layers与Tiers

对Layers与Tiers这两个单词进行区分是非常重要的。从中文翻译看,两者都是“层”的意思,因此我们往往会将这两个概念弄混。Layer一词更多的是表示对系统组件或功能的逻辑区分,它并没有包含将组件分布到不同的区域、不同的服务器上的意思。而Tier则是表示系统组件和功能在服务器、网络环境以及远程位置的物理部署。尽管这两个概念同时使用者非常相近的一些术语,比如“展示”、“服务”、“业务”和“数据”等,但我们必须了解它们之间的差别。下面这幅图表明了多层(N-Layer)逻辑架构与三层(3-Tier)物理结构之间的差异:

需要注意的是,对于具有一定复杂度的应用程序而言,采用多层(N-Layer)逻辑架构的实现方式是非常必要的,这会降低系统的复杂度,并在设计、开发、测试、部署及维护等各个环节为应用系统带来高可用性、高延展性等正面效应。然而,并非所有的应用程序必须以三层(3-Tier)/多层(N-Tier)物理结构进行部署,我们可以将多个逻辑层部署在同一台机器上,也可以根据需求,将这些逻辑层部署在网络中的不同机器上。

逻辑分层(Layer)的设计

在讨论DDD的分层之前,先让我们看看传统的分层方式。就像上文所述,我们应该根据项目的实际需求,将组件/功能模块合理地划分到“逻辑层”中。同一层中的组件,应该是高内聚的,并具有相同的抽象层次。层与层之间应该低耦合。对于以分层设计的应用程序而言,最关键的问题就是如何处理层与层之间的依赖关系。考察传统的多层架构应用,处于某层的组件,只能对同层或下层的其它组件进行访问,这样做可以有效地降低层与层之间的依赖关系。通常会有两种分层设计:严格分层与灵活分层

1.“严格分层”迫使组件只能访问同层的其它组件,或者只能访问直接下层的其它组件,于是,第N层的组件只能访问第N或N-1层的组件,而第N-1层的组件只能访问第N-1或N-2层的组件,以此类推

2.“灵活分层”允许组件访问同层的其它组件,以及所有下层的其它组件,于是,第N层的组件可以访问第N或N-1、N-2…层的组件

使用“灵活分层”的架构可以提高系统性能,因为这样的结构无需引入过多的请求/反馈的传递操作,因为一个层可以直接访问位于其下的任何层;而“严格分层”却降低了层与层之间的耦合性,对低层的修改不会对整个系统造成广泛的影响。根据Eric Evans在其《领域驱动设计-软件核心复杂性应对之道》一书中的描述,DDD的分层选用的是“灵活分层”模式。

让我们再把讨论的粒度细化,来看看层中的组件与组件之间的关系。事实上,在很多复杂的应用中,位于同一层的组件与组件虽然具有相同的抽象层次,它们也不一定是高内聚的。因此,我们可以引入“模块(Module)”的概念,将同一层中高内聚的组件放在同一个模块中,于是,每个层又会由一个或多个高内聚的子系统(模块)所组成,如下UML组件图所示:

使用分层架构,有如下几点好处:

  • 提高系统的可测试性
  • 对解决方案的维护和管理变得更加简单。层内高内聚、层间低耦合的结构,使得系统实现与分层组织方式变得非常灵活方便
  • 其它外部应用程序能够非常方便地使用不同的层所提供的特定功能
  • 当系统以层的方式进行组织时,分布式开发也变得非常简单易行
  • 在某些情况下,分层系统的物理部署方式能够给系统带来延展性,当然,应该有效地评估具体的实践方式,因为这种做法有可能损伤系统性能

应用系统基本设计原则 - SOLID

应用系统的设计应该遵循一些基本的设计原则,这能帮助你有效地创建一个低成本、高可用、高可扩展的应用程序。在这里,我们引入一个SOLID设计原则,SOLID由如下几点构成:

  • Single Responsibility Principle(单一职责原则)
  • Open Close Principle(开-闭原则)
  • Liskov Substitution Principle(里氏替换原则)
  • Interface Segregation Principle(接口分离原则)
  • Dependency Inversion Principle(依赖反转原则)

下面简要介绍一下这几个“原则”。

  • 单一职责原则:每个类应该只有一个独一无二的职责,或者说每个类只能有一个主要功能,由此派生出一个结论:每个类应该尽可能少地依赖于其它类
  • 开-闭原则:每个类,应该对“扩展”进行开放,而对“修改”进行封闭,也就是支持扩展,而不是支持修改:类中的方法可以通过继承关系进行扩展,而不会改变类本身的代码
  • 里氏替换原则:子类可以被基类型(基类或者接口)替换。应用程序依赖抽象运行,其行为不会因为具体实现的改变而更改,应用程序应该依赖于抽象(基类或者接口),而不是具体实现。接下来将要讨论到的“依赖注入(Dependency Injection)”就与这条原则有关
  • 接口分离原则:接口的职责也应该是单一的,接口中应该包含哪些方法,需要进行严格的评估,如果其中某些方法的职责与接口的本身定义不相符合,则应该将其分离到其它接口中。类需要根据其调用者所需要的不同接口类型,来暴露不同的接口
  • 依赖反转原则:抽象不能依赖于具体,而具体则应该依赖于抽象。类之间的直接依赖应该用抽象来取代,这样做的一个优点是,我们可以实现自上而下的设计方式:在下层的具体实现还没有确定的情况下,只要能够在抽象层面将接口确定,就能够完成上层的设计与开发,这同样给可测试性带来便捷

除了以上所述的SOLID原则之外,还有以下几个关键的设计原则可供参考:

  • 组件设计应该是高内聚的:相信大家都很熟悉这点了,就不多说了。例如:不要将数据访问逻辑写进领域模型的业务逻辑中,这与上述“单一职责原则”是密切相关的
  • 将Cross-Cutting的代码从特定于应用程序的逻辑中分离开来:Cross-Cutting的代码是一些面向横面的代码,比如安全、操作管理、日志以及测量/计量系统等。将这些代码与应用系统业务逻辑混在一起会增加系统的复杂性,给将来的扩展和维护造成很大的麻烦。这与“面向方面编程(Aspect-Oriented Programming,AOP)”有关
  • 关注点分离(Separation of Concerns,SoC):将应用系统分成多个子部分(子系统),各个部分之间的功能尽量不要重复,其目的就是为了减少交互点,以实现高内聚和低耦合
  • Don’t Repeat Yourself(DRY):一个特定的功能只能在某个特定的组件中实现一次,同样的功能不要在多个组件中重复多次
  • 避免YAGNI(You Ain’t Gonna Need It)效应:只考虑和设计必须的功能,避免过度设计

好了,本讲就介绍到这里,估计对大多数接触过架构的软件朋友来说,本讲的部分内容都是废话。下一讲开始,我会花部分笔墨在DDD/DDDD的分层介绍上,虽然有可能还是废话,但这对我们理解NLayerApp的解决方案组织结构会有相当的帮助。

Microsoft NLayerApp案例理论与实践 - DDD、分布式DDD及其分层

这段时间一直在忙工作,已经有一个月没更新博客了。从现在开始,我将继续讨论Microsoft NLayerApp案例,希望各位爱好Microsoft NLayerApp案例、架构设计以及DDD的朋友们能够继续关注。

从架构上看,Microsoft NLayerApp对“复杂的业务系统应用程序”这样一种应用程序的架构设计提供了一系列的设计准则。所谓“复杂的业务系统应用程序”是指这样一类业务系统应用程序,这类应用程序具有相对较长的生命周期,在其生命周期中,将发生一些可预期的“革命性变更”(比如,所使用的技术/框架的版本升级甚至替换),因此后期维护会变得非常重要。于是,针对这种类型应用程序的设计,我们应该做到,当“革命性变更”来临时,将这种变更对应用程序其他部分的影响减少到最小程度,例如,我们要确保基于基础结构层的设施变更不会影响到其上层的各个部分。更确切地说,应用程序的领域模型部分应该只关注领域本身,变更应用程序的其它部分,不会影响到领域模型。在“复杂的业务系统应用程序”中,业务规则的行为方式(也就是“领域逻辑”)将会是经常变化的,因此,使其具有很好的可修改性和可测试性将会非常重要。要达到这样的效果,就需要实现领域模型部分与系统其它部分的解耦。作为领域驱动设计(DDD)的一部分的面向领域的多层分布式架构,关注的就是这样的问题。

还是那句话,DDD不仅仅只是架构+模式。DDD是开发应用程序的一种方式,是团队在项目中工作的一种方式。根据DDD,项目团队需要以一种特殊的方式进行合作,应该能够直接与领域专家(通常就是客户)进行沟通。整个团队需要使用“通用语言”这一能够被所有人接受的语言,等等。然而,本案例没有包含这些内容,因为这是一种“过程”,一种ALM。所以,如果你真的希望100%实践DDD,你需要阅读Eric Evans写的《领域驱动设计-软件核心复杂性应对之道》一书,或者其它的一些讨论DDD过程的书籍,这些书籍会对DDD过程、项目和团队做比较详细的介绍,而不仅仅是谈论架构和模式。架构和模式仅仅是DDD中很小的一部分,而我们在这里展示的就恰好是这一部分。总之,我们必须强调,DDD不仅仅是架构+模式。

在社区中,不少朋友觉得DDD风格的架构模式(经典的也好,CQRS也好)在实际项目的应用与推广中存在一定的问题,比如从老系统向新系统的过渡过程中,DDD风格架构很难找到切入点,再比如,基于CQRS架构的应用系统难度较大,复杂度高,普通的开发人员很难在短期内掌握相关知识,一旦出现团队人员流动,新加入的团队成员将在短期内无法胜任开发职位,对项目本身造成影响。不少朋友都在关注领域驱动设计以及CQRS架构,或许也从我的博客系列文章中受到了启发,于是希望能够在实际工作和项目中能够应用DDD的理念和技术进行开发,但却在应用的过程中遇到了重重阻碍,最后不得不放弃。对于我来说,我专注于.NET技术以及DDD,我只不过是一直在探索某一种类型的应用程序的架构方式,并试图将这种设计思想和架构风格展示给大家。注意这里“某一种”的措辞,DDD风格架构并不适用于所有的实际项目和应用系统,就软件团队本身而言,推行DDD的开发过程也非一朝一夕之事。架构师们需要对项目实际情况进行分析,这包括应用系统的架构本身,以及团队建设的各方面因素(比如,团队是否能够首先引入Agile开发过程,进而去适应DDD的开发模式,等等)。DDD过程以及DDD风格的架构模式只不过是摆在您面前的又一个选项。接下来的介绍或许能够帮助您更准确地做出抉择。

不选用面向领域的多层分布式架构(DDD风格架构)的理由

如果应用程序相对简单,而且在其生命周期的整个过程中,基础结构所使用的技术和框架以及业务逻辑层等各方面都不会有太大的变更,那么你就不需要选用基于DDD的多层分布式架构。你可以选择一些RAD(Rapid Application Development)的技术,比如WCF RIA Services或者Visual Studio Lightswitch等,它能使得开发简单的应用程序变得非常高效。这些简单的应用程序关注的是Time to Market(TTM),而对于合理的结构、分层解耦等概念却并不是很关注。通常,我们把这样的应用程序成为“数据驱动的应用程序”。

选用面向领域的多层分布式架构(DDD风格架构)的理由

如果你希望你的应用程序在较长的一段时间内都能够适应业务逻辑的变化,那么,强烈建议你选用面向领域的多层分布式架构。在这种情况下,领域模型将降低由业务逻辑变化而引起的高额代价,组件之间、层与层之间低耦合的结构,使得在每次出现业务逻辑变更的时候,你都能够将领域模型隔离出来进行调整和测试,而不需要更改应用程序的其它部分,这样有效地降低了需求变更带来的开发风险,并节省了项目开支。

分布式DDD(Distributed DDD, DDDD)

这个概念是Microsoft NLayerApp在其Guide Book中提及的,就是在DDD风格架构的基础上,将分布式的特性包含进来。在Eric Evans的《领域驱动设计-软件核心复杂性应对之道》一书中,他并没有提及太多的有关分布式技术的内容(比如Web Service技术等),主要也是因为针对DDD的讨论本身也是立足于Domain的。然而,在实际的应用程序实现和部署过程中,分布式技术是必不可少的。事实上,Microsoft NLayerApp是面向分布式DDD的,在实现“分布式”的过程中,采用了微软特有的技术,比如WCF等。DDDD也使得应用程序能够更好地适应分布式场景,甚至可以使应用程序更方便地部署到云计算的环境中。

面向领域的多层架构

早在《EntityFramework之领域驱动设计实践 (二):分层架构》一文中,我就对基于DDD风格的分层架构做了介绍。现在回顾一下,DDD风格架构主要分为四层:表现层、应用层、领域层和基础结构层:

现在让我们来对比一下Microsoft NLayerApp的架构分层方式。在Microsoft NLayerApp的Guide Book中提供了下面的分层架构图,其分层方式在大体上与上文所述基本相同,同时,下图还对各个层的内部做了细化描述,便于读者能够更清楚地了解到每个层所包含的组件及其之间的协作方式。

了解整个项目的整体架构对于理解整个系统的运作方式有着很大的帮助。下面,我将对上述架构中的每个层进行介绍,让我们看看这些层都包含了哪些组件,以及这些组件是如何协作的。

  • 表现层(Presentation):该层的主要职责是通过用户界面向用户展示必要的数据信息,同时接收用户的反馈。该层中的组件主要实现了与图形界面、用户操作捕获、数据转发等用户界面功能。建议根据项目的实际情况,选用相关的模式(比如MVC、MVP或者MVVM等)将这些组件细分到更小的层中。
  • 分布式服务层(Distributed Service Layer):当应用程序以服务提供商(Service Provider)的方式向其它远程应用程序提供业务功能时,或者应用程序的客户端本身是被部署在另一个远程位置时,其业务逻辑就必须通过分布式服务层向外界发布。分布式服务层(通常被实现为Web Service)可以根据可配置的通信通道与数据消息格式,为应用程序提供远程访问的功能。需要注意的是,分布式服务层中不应该包含任何业务逻辑的实现。
  • 应用层(Application Layer):应用层用于协调领域模型与其它应用组件的工作,以完成一个特定的、明确的系统任务。这种协调可以包括:事务调度、UoW(Unit Of Work,PoEAA)的执行,以及调用一些系统必须的处理任务等。应用层同时还可以包括应用程序的优化、数据的转发和格式转换等工作,当然,我们将这些工作统称为“任务调度”,至于每个任务的核心部分,应用层都会将其转发到下层去处理。应用层通常会被看做是一种“业务层外观(Business Facade)”,但它却不仅仅是转发领域模型层的处理请求/反馈那么简单。它通常可以包含下面这些内容:
    • 通过仓储契约(Repository Contract)来访问持久层机制,以读取或保存领域对象。注意这里访问的是仓储契约,而并非仓储的具体实现。仓储的具体实现是基础结构层的内容
    • 对来自于不同领域对象的数据进行组织和整理,以便能够让分布式服务层更有效地传递这些数据。通常,我们会将数据整理在数据传输对象(Data Transfer Object,PoEAA)中,例如WCF的Data Contracts
    • 管理和维护应用程序的状态(而不是领域模型中领域对象的状态)
    • 协调领域对象之间、领域模型与基础结构层组件之间的协作关系。比如在银行转账系统中,资金从一个账户转移到另一个账户,首先需要通过仓储读取“账户”领域对象,然后在领域对象上进行转账操作(可以是“账户”本身的行为,也可以是,按Evans的举例,使用领域服务(Domain Service))。或许在完成转账后,无论成功与否,都需要向外发送电子邮件,这就需要基础结构层的电子邮件组件协作完成
    • 应用服务(Application Services):首先需要注意,DDD中提到的服务与平时所说的Web Service等并不是一个概念,它可以存在于应用层、领域模型层甚至基础结构层。DDD中Service所表述的概念,其实是“无法归结到任何一个对象”的一系列操作的集合,因此,Service通常是在协调不同对象之间的工作。应用服务也是如此,它会对其它下层组件(比如领域模型层与基础结构层)进行协调
    • 业务工作流(Business Workflow):业务工作流并非必须的,对于某些由特定步骤组成的业务过程,引入业务工作流会使问题变得简单
  • 领域模型层(Domain Model Layer):该层的主要职责是展现业务/领域逻辑、业务处理状态,以及实现业务规则,它同时也包含了领域对象的状态信息。这一层是整个应用程序的核心部分,它可以包含下面这些概念和内容:
    • 实体(Entities)
    • 值对象(Value Objects)
    • 领域服务(Domain Services)
    • 仓储契约/接口(Repository Contracts/Interfaces)
    • 聚合及其工厂(Aggregates and Factories)
    • 聚合根(Aggregate Roots)
    • 规约对象(Specifications)
  • 基础结构层(数据持久化部分)(Data Persistence Infrastructure Layer):该层为应用程序的数据存取提供服务,它可以是应用程序本身的持久化机制,也可以是外部系统提供的数据访问的Web Service等。根据分层架构的设计原则,该层应该以“低耦合”的方式向上层提供数据持久化服务。因此,该层可以包含如下这些内容:
    • 仓储的具体实现:从概念上看,“仓储”意味着对一组相同类型对象的集中管理,就好像是存取同一类型对象的仓库。然而在实践中,仓储主要用来在特定的持久化机制/技术上执行对象的读取和保存操作。这些持久化机制/技术可以是Entity Framework、NHibernate或者是针对某一数据库引擎的ADO.NET组件。为了简单起见,我们将数据访问操作集中到仓储中,并针对不同的持久化机制/技术开发一个仓储的具体实现,这将会对应用程序的维护和部署带来便捷。在设计仓储时,通常的做法是,首先对领域模型划分聚合并区分聚合根,然后针对每一个聚合设计一个仓储,仓储通过聚合根对聚合进行管理。在领域模型层中,各组件是通过仓储契约(接口)来实现对仓储的访问的,这样做就使得领域模型层无需了解任何仓储的具体实现和持久化细节(Persistence Ignorance),读者可以参考我前面写的《EntityFramework之领域驱动设计实践(八)》一文。此外,我们通常所讲的“数据访问对象(Data Access Object)”并不是仓储,首先,仓储通过聚合根,负责整个聚合的读取和存储,它是一个领域概念,而数据访问对象则是对单个对象(更确切地说应该是单个数据结构)直接进行数据库操作;其次,操作方式也不同,仓储会在提交前先对内存中的对象进行标记,最后的一次提交过程(Unit Of Work,PoEAA)则是在上层组件(比如在应用层)中完成的
    • 层超类型(Layer Supertype,PoEAA):通常,在实现某层的特定功能时,我们会将一系列对象的公共逻辑提取出来,然后将这些逻辑置于一个抽象类型中,同时使得其它类型都继承于该抽象类型以避免逻辑重复。这样的抽象类型被称为层超类型。大多数数据访问任务可以使用层超类型以简化开发,减少代码维护成本。例如,在实现面向ADO.NET的数据库访问组件时,我们可以在层超类型中使用DbConnection、DbCommand等对象实现公用逻辑,然后在子类中继承这些逻辑并提供具体的SqlConnection、SqlCommand或者OleDbConnection、OleDbCommand实例
    • 数据模型(Data Model):如果使用ORM来实现仓储,那么通常情况下ORM都会使用一个数据模型(比如Entity Framework)来实现需要的功能,这样的数据模型有点像实体模型,但它与数据传输对象一样,跟领域模型层的实体模型是完全不同的。数据模型甚至是一种可视化的图形描述,由专门的可视化设计工具负责维护
    • 远程/外部服务代理:当采用外部系统来实现数据持久化机制时,远程/外部服务代理负责连接外部系统并转发数据操作请求及响应信息
  • 基础结构层(Cross-Cutting):该层提供了能被其它各层访问的通用技术框架,比如异常捕获与处理、日志、认证、授权、验证、跟踪、监视、缓存等等。这些操作通常会横向散布在应用程序的各个层面,我们平时讨论的面向方面编程(AOP)关注的就是如何在不影响对象本身处理逻辑的基础上来实现这些横切的却又必不可少的功能点。在实践中,通过使用一些流行的Interception框架(例如Microsoft Unity、Castle DynamicProxy等)可以帮助我们方便地实现AOP

总结

本文以文字描述为主,结合Microsoft NLayerApp项目,更详细地对DDD及其分层架构做了介绍,文中也引入了不少来自于Martin Fowler《企业应用架构模式(PoEAA)》一书中所介绍的概念与模式名称,帮助读者朋友更好地理解DDD分层架构中各层的主要职责。与《Microsoft NLayerApp案例理论与实践 - 多层架构与应用系统设计原则》一文一起,通过这两篇文章的学习,我们已经对应用程序的设计与架构,以及DDD风格架构及其分层有了一定的了解。从下章节开始,我们会把本文所描述的分层架构与Microsoft NLayerApp项目结合起来,进一步学习Microsoft NLayerApp项目的具体实现。

Microsoft NLayerApp案例理论与实践 - 基础结构层(Cross-Cutting部分)

从这篇文章开始,我将逐步介绍NLayerApp的基础结构层、领域层、应用层以及分布式服务层。本文着重介绍基础结构层,根据上文对NLayerApp的架构分析,它将包含两大部分的内容:处理数据访问的基础结构层组件和Cross-Cutting的基础结构层组件。处理数据访问的基础结构层组件主要包含了仓储的具体实现、Unit Of Work(PoEAA,Martin Fowler)的实现、NLayerApp的实体模型定义,以及为单体测试做准备的Service Stubs(PoEAA,Martin Fowler);Cross-Cutting的基础结构层组件则主要包含了IoC(Inversion of Control)容器以及跟踪应用程序执行过程的Trace工具。虽然这些都是基础结构层的组件,但也包含了很多技术细节甚至是设计要点,就让我们一起对这些内容做一个详细的解读。

NLayerApp中IoC容器的实现

在应用程序设计的过程中,我们会基于这样一个设计准则,就是类型之间的关联应该依赖于接口或者抽象,而非具体的实现。这样就使得我们能够在保证整个程序结构不变的情况下,很方便地替换组件的具体实现方式,这不仅使得Service Stub模式的应用成为可能,从而提高了系统的可测试性,而且解耦了组件之间的依赖关系,降低了应用程序的维护成本。IoC容器是这样一种对象,它在应用程序的执行环境中维护着接口与其实现之间的映射关系,以及各个实现对象之间的依赖关系,以便当客户程序向IoC容器提出请求时,能够返回与所请求的接口或抽象类型所对应的具体实现,客户程序不需要去关心返回的具体实现究竟是什么,以及如何去初始化这个具体实现。本文不会对IoC作过多的介绍,有兴趣的朋友可以阅读《Inversion of Control Containers and the Dependency Injection pattern》这篇文章。

NLayerApp中IoC容器的实现依赖于Microsoft Patterns&Practices Unity,其实大多数应用程序甚至是开发框架都会依赖于第三方的类库来实现IoC容器,因为IoC本身涉及的内容就比较多,很好地解决类型之间复杂的依赖关系也不是一件很容易的事情。Unity并非IoC的唯一选择,除了Unity之外,Spring.NET、Castle Windsor、Ninject、StructureMap等都可以成为IoC容器不错的选择。NLayerApp中与IoC容器实现有关的类及其之间的关系如下图所示:

在上图中,IContainer接口定义了IoC容器相关的方法,它是与具体的实现技术无关的接口(接口的层次结构树、其中定义的方法的参数以及返回值等都不会依赖于任何第三方的组件),因此,理论上我们可以通过继承IContainer接口然后用我们自己的技术方式来实现IoC容器。NLayerApp是使用Unity作为IoC容器的,因此,上图的IoCUnityContainer类实现了IContainer接口,然后在IoCFactory的单件实例中通过new关键字创建了IoCUnityContainer的实例:

#region Constructor
/// <summary>
/// Only for singleton pattern, remove before field init IL anotation
/// </summary>
static IoCFactory() { }
IoCFactory()
{
    _CurrentContainer = new IoCUnityContainer();
}
#endregion 

当然,对于NLayerApp这一特定的应用程序案例而言,这样做是没什么问题的,但如果我们目前设计的是一个开发框架的话,直接使用new关键字来创建IoCUnityContainer的实例,就会使得IoCFactory强行依赖于IoCUnityContainer类型,于是也就违背了“关联应该依赖于接口或者抽象,而非具体实现”的设计准则。在最新版的Apworks框架的代码中,开发人员可以通过应用程序的配置信息来选择合适的IoC容器,比如你可以在应用程序启动的时候就决定是使用Unity还是Castle Windsor,这就使得框架本身具有更好的扩展性。刚才我们也讨论过,如果要使NLayerApp能够使用我们自定义的IoC容器,就要继承IContainer接口,那么现在我们还需要修改IoCFactory的私有构造函数,以使用我们自己的IoC容器来初始化_CurrentContainer私有成员。

Unity容器有一个非常实用的特点,就是“根容器”与“子容器”的概念,在“根容器”上通过调用CreateChildContainer方法即可创建与之关联的子容器。根容器和子容器都可以接受抽象类型的注册。每当客户程序向子容器请求类型(Resolve Type)时,Unity首先检查子容器中是否有所请求的类型,如果有,则直接返回该类型的具体实现,如果没有,则会将该请求转发给其父容器。利用Unity的这种特性,我们可以将针对不同部署环境的IoC容器进行统一管理,比如将各种部署环境中相同的类型映射注册在根容器中,然后为每个部署环境创建一个子容器,将与部署环境相关的特定类型映射注册在各自的子容器中。下图展示了NLayerApp中Unity IoC容器的基本情况:

通过阅读IoCUnityContainer的源代码我们可以了解到,在IoCUnityContainer的构造函数中,创建了rootContainer,并在rootContainer上使用CreateChildContainer创建了用于真实运行环境的realAppContainer以及用于单体测试的fakeAppContainer,之后就是使用下面的私有方法逐个初始化这些容器:

/// <summary>
/// Configure root container.Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureRootContainer(IUnityContainer container)
{
    // Omitted... Please refer to the source code for details.
}

/// <summary>
/// Configure real container. Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureRealContainer(IUnityContainer container)
{
    container.RegisterType<IMainModuleUnitOfWork, MainModuleUnitOfWork>(new PerExecutionContextLifetimeManager(),
        new InjectionConstructor());
}

/// <summary>
/// Configure fake container.Register types and life time managers for unity builder process
/// </summary>
/// <param name="container">Container to configure</param>
void ConfigureFakeContainer(IUnityContainer container)
{
    //Note: Generic register type method cannot be used here, 
    //MainModuleFakeContext cannot have implicit conversion to IMainModuleContext

    container.RegisterType(typeof(IMainModuleUnitOfWork), 
        typeof(FakeMainModuleUnitOfWork), new PerExecutionContextLifetimeManager());
} 

在ConfigureRootContainer方法中,对所有环境(真实运行环境以及单体测试环境)需要用到的类型进行了注册,然后,就IMainModuleUnitOfWork而言,由于真实运行环境和单体测试环境所使用的Unit Of Work具体实现不同:真实运行环境使用的是MainModuleUnitOfWork实现,而测试环境则是使用的FakeMainModuleUnitOfWork,于是,也就在ConfigureRealContainer和ConfigureFakeContainer方法中分别作了注册。

最后,每当IContainer.Resolve方法被调用时,系统会通过读取配置文件来决定目前应该使用哪个容器来解析类型,因此,我们只需要在配置文件中正确设置容器的名称,即可在NLayerApp中使用指定的Unity IoC容器。下面这段配置信息来自于DistributedServices.Deployment项目,从中我们可以看到,NLayerApp的Distributed Services使用的是realAppContainer:

<appSettings>
  <!--RealAppContext - Real Container-->
  <!--FakeAppContext - Fake Container-->
  <!--<add key="defaultIoCContainer" value="FakeAppContext" />-->
  <add key="defaultIoCContainer" value="RealAppContext" />
</appSettings> 

NLayerApp中用于跟踪程序执行过程的Trace工具

NLayerApp中Trace工具的实现非常简单,在Infrastructure.CrossCutting项目中定义了ITraceManager,然后在Infrastructure.CrossCutting.NetFramework项目中定义了ITraceManager的具体实现。TraceManager使用了System.Diagnostics命名空间下与Trace相关的类型实现其功能,应用程序则通过IoCFactory来获得ITraceManager的具体实现。

在上面讨论的ConfigureRootContainer方法中,NLayerApp对ITraceManager类型进行了注册:

//Register crosscuting mappings
container.RegisterType<ITraceManager, TraceManager>(new TransientLifetimeManager());

因此,在整个应用程序中,就可以使用下面的方式来获取ITraceManager的具体实现,以便完成Trace功能:

ITraceManager traceManager = IoCFactory.Instance.CurrentContainer.Resolve<ITraceManager>();
traceManager.TraceError(/* error message*/);

总结

本文对NLayerApp的基础结构层(Cross-Cutting部分)进行了研究与探讨,与这部分相关的项目有:Infrastructure.CrossCutting、Infrastructure.CrossCutting.IoC以及Infrastructure.CrossCutting.NetFramework。下一讲我们将继续研究NLayerApp的基础结构层(数据访问部分)。

Microsoft NLayerApp案例理论与实践 - 基础结构层(数据访问部分)

上篇文章讲解了NLayerApp案例的基础结构层(Cross-Cutting部分),现在,让我们继续解读NLayerApp的基础结构层(数据访问部分)。NLayerApp的基础结构层(数据访问部分)包含如下内容:Unit Of Work(PoEAA)、仓储的具体实现、NLayerApp的数据模型以及与测试相关的类。下面,我们将对前三个部分进行讨论,与测试相关的内容,我打算最后单独一章进行介绍。

Unit Of Work(PoEAA)

Unit Of Work(UoW)模式在企业应用架构中被广泛使用,它能够将Domain Model中对象状态的变化收集起来,并在适当的时候在同一数据库连接和事务处理上下文中一次性将对象的变更提交到数据中。在没有引入UoW之前,你可以在每次增加、删除对象或者更改对象状态后,直接调用数据库以保存对象的变化,但这样做会导致应用程序对数据库这一外部技术架构的频繁访问,严重影响了系统性能。这就好像我们打开Notepad进行文字编辑一样,我们完全可以每输入一个字符,就按下Ctrl+S保存一次,但这样做非常耗时(也没必要),我们通常的做法可能是,每完成一个段落的编辑(输入字符、删除字符或者更改字符等)再保存一次,那么Notepad就会在我们编辑段落的时候跟踪段落及其中字符的变化情况,最后一次性将这些变更写到硬盘上。从UoW的模式描述上看,它有点像数据库事务(Transaction),因为它们都具有“提交”和“回滚”的操作。但从语义上讲,它并不能等同于数据库事务。我觉得应该这样理解:我们可以将UoW看成是一个事务对象,但它不是数据库事务,它的事务性体现在能够在一个原子操作中将对象一次性提交给持久化机制,或者如果在提交过程中出现问题,它还能将对象返回到提交前的状态。不仅如此,UoW还具有跟踪领域对象变化的功能,它能够跟踪某一个业务步骤范围内领域对象的变化情况,正如上面的例子中,每个段落的编辑就可以看成是一个业务步骤,那么在这个业务步骤中(编辑段落的过程中),UoW会对领域对象进行跟踪,而在业务步骤完成之时(完成段落编辑之时),UoW就会对跟踪到的变更做一次性提交。

从上面的分析让我们大致了解到,UoW与仓储一样,本身应该是属于Domain Model的,它的设计应该是技术无关的(也就是常说的POCO或者IPOCO),因为它跟踪的是Domain Model中领域对象的变化情况;当然,一个更好的设计应该是使用Separated Interface(PoEAA)模式,将UoW接口与仓储的接口一起设计在Domain Model中。从UoW的实现上来看,NLayerApp采用了Entity Framework的一些特性,并基于Entity Framework的模型,利用T4自动化产生代码。目前我们不要去关心在NLayerApp中是如何使用T4产生这些代码的,我们需要关心的是为什么需要产生这些代码。有关Visual Studio中的模型项目、Domain Specific Language(DSL)以及T4代码自动化生成,我们在此将不作讨论。有兴趣的朋友可以参考我前面的文章《在Visual Studio 2010中使用Modeling Project定制DSL以及自动化代码生成》。以下是NLayerApp中与UoW相关的类关系图:

在了解NLayerApp的UoW执行机制之前,首先让我们了解一下NLayerApp中与UoW相关的三个接口。

  • IObjectWithChangeTracker接口
    该接口下只定义了一个ObjectChangeTracker的属性,在NLayerApp中,所有的实体都要实现IObjectWithChangeTracker接口,以向外界(主要是UoW和仓储)提供ObjectChangeTracker实例。ObjectChangeTracker的主要功能就是记录当前实体中的状态变化。比如,实体的当前状态、变更前所有属性的原始数据、向集合属性添加的所有对象、从集合属性中删除的所有对象等等。当仓储通过Unit Of Work来注册已变更的实体时,Unit Of Work会使用ObjectChangeTracker所提供的信息来向Entity Framework进行变更注册。
  • INotifyPropertyChanged接口
    NLayerApp的实体不仅实现了IObjectWithChangeTracker接口,同时还实现了INotifyPropertyChanged接口。实现这个接口的主要目的就是为了在实体的某个属性发生变化时,能及时地将这种变化记录在ObjectChangeTracker中。因此,只要客户程序通过实体的属性来改变实体的状态时,实体本身就会将状态变化记录到ObjectChangeTracker中。
  • IRepository接口
    IRepository接口是定义在Domain Model层的接口,之所以在此提及,是因为对象的持久化过程是通过仓储完成的,而持久化又离不开UoW。在NLayerApp中,IRepository接口有一个IUnitOfWork的属性,因此所有的仓储都必须实现这个属性,以便Repository能够在UoW中记录对象的变更信息。从NLayerApp的源代码可以看到,其实仓储本身并不负责将实体保存到数据库的这一具体任务,它只是通过IObjectWithChangeTracker接口,将需要保存的对象设置为相应的状态,并向UoW注册对象变更;剩下的与数据库打交道的任务,则是由UoW完成的

通过这些信息我们可以了解到,NLayerApp中的实体都是各自管理自己的变更记录,称之为“自跟踪实体”(Self-Tracking Entities,STE)。其实从DDD的角度来看,STE并不是一个很好的设计,因为它给Domain Model带来了太多技术关注点。例如在实现STE的时候,当你向Customer添加一个Order时,你需要首先判断Customer的ObjectChangeTracker中是否已经将该Order标记为“删除”状态了,如果是这样的话,那么你需要将这个Order从ObjectChangeTracker的“删除”列表中移去。类似这样的业务逻辑本不应该放在Domain Model中。此外,NLayerApp为了迎合Entity Framework的需求,所实现的STE也并非纯粹的与技术无关的。UoW的实现也是如此,比如在上面的类图中,我们可以很明显地看到,MainModuleUnitOfWork是ObjectContext的子类。

现在我们将思路串联起来,以修改Customer为例,从整个架构服务端的最上层(Distributed Service层)开始,看看Unit Of Work与仓储是如何协作的。

1、DistributedServices.MainModule项目:MainModuleService类通过使用位于应用层的CustomerManagementService实现Customer信息的变更:

public void ChangeCustomer(Customer customer)
{
    try
    {
        //Resolve root dependency and perform operation
        ICustomerManagementService customerService = IoCFactory
            .Instance
            .CurrentContainer.Resolve<ICustomerManagementService>();
        customerService.ChangeCustomer(customer);
    }
    catch (ArgumentNullException ex)
    {
        // ......
    }
 
}

上述代码通过IoCFactory从IoC容器中获得ICustomerManagementService的具体实现,有关NLayerApp中IoC容器的实现,请参考前一篇文章。

2、Application.MainModule项目:CustomerManagementService类实现了ICustomerManagementService接口,同时实现了ChangeCustomer方法。在该方法中,首先通过CustomerRepository的UnitOfWork属性获得UoW,然后调用仓储的Modify方法以将要更改的Customer实体注册到UoW中,同时改变了Customer实体的状态。最后,使用UoW的CommitAndRefreshChanges方法将变更的实体对象提交到数据库:

public void ChangeCustomer(Customer customer)
{
    if (customer == (Customer)null)
        throw new ArgumentNullException("customer");
    IUnitOfWork unitOfWork = _customerRepository.UnitOfWork as IUnitOfWork;
    _customerRepository.Modify(customer);
    unitOfWork.CommitAndRefreshChanges();
} 

值得一提的是,在CustomerManagementService中,CustomerRepository以构造器注入的方式获得实例化的:

/// <summary>
/// Create new instance 
/// </summary>
/// <param name="customerRepository">Customer repository dependency, 
/// intented to be resolved with dependency injection</param>
/// <param name="countryRepository">Country repository dependency, 
/// intended to be resolved with dependency injection</param>
public CustomerManagementService(ICustomerRepository customerRepository, 
    ICountryRepository countryRepository)
{
    if (customerRepository == (ICustomerRepository)null)
        throw new ArgumentNullException("customerRepository");

    if (countryRepository == (ICountryRepository)null)
        throw new ArgumentNullException("countryRepository");

    _customerRepository = customerRepository;
    _countryRepository = countryRepository;
} 

3、Infrastructure.Data.Core项目:Repository类的Modify方法首先将当前状态不是Deleted的实体设置为“Modified”,同时在UoW中,通过RegisterChanges调用以向UoW注册该实体:

public virtual void Modify(TEntity item)
{
    //check arguments
    if (item == (TEntity)null)
        throw new ArgumentNullException("item", Resources.Messages.exception_ItemArgumentIsNull);

    //Set modifed state if change tracker is enabled and state is not deleted
    if (item.ChangeTracker != null
        &&
        ((item.ChangeTracker.State & ObjectState.Deleted) != ObjectState.Deleted)
       )
    {
        item.MarkAsModified();
    }
    //apply changes for item object
    _CurrentUoW.RegisterChanges(item);
    _TraceManager.TraceInfo(
                   string.Format(CultureInfo.InvariantCulture,
                                Resources.Messages.trace_AppliedChangedItemRepository,
                                typeof(TEntity).Name));
} 

4、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的RegisterChanges方法简单地利用Entity Framework所提供的机制,向Entity Framework注册对象状态变更。这是Entity Framework技术实现的细节内容,我们在此也不去深入分析其中的实现方式了:

 public void RegisterChanges<TEntity>(TEntity item)

    where TEntity : class, IObjectWithChangeTracker
{
    this.CreateObjectSet<TEntity>().ApplyChanges(item);
 }

5、Infrastructure.Data.MainModule项目:MainModuleUnitOfWork类的CommitAndRefreshChanges方法通过Entity Framework将变更提交到数据库,同时将实体对象的状态设置为“未更改”:

public void CommitAndRefreshChanges()
{
    try
    {
        //Default option is DetectChangesBeforeSave
        base.SaveChanges();
        //accept all changes in STE entities attached in context
        IEnumerable<IObjectWithChangeTracker> steEntities = (from entry in
            this.ObjectStateManager
            .GetObjectStateEntries(~EntityState.Detached) where
                entry.Entity != null &&
                (entry.Entity as IObjectWithChangeTracker != null)
        select
            entry.Entity as IObjectWithChangeTracker);
        steEntities.ToList().ForEach(ste => ste.MarkAsUnchanged());
    }
    catch (OptimisticConcurrencyException ex)
    {
        //......
    }
} 

整个执行过程我们可以使用下面的序列图来表示:

NLayerApp中的Unit Of Work我们先介绍到这里,有疑问的朋友可以以评论的方式交流。

仓储的具体实现

NLayerApp中的仓储实现也是基础结构层(数据访问部分)的一个重要组件,这一点与DDD的经典架构风格是相符的。因为从理论上讲,仓储的具体实现需要依赖于外部系统,而这部分内容是不能暴露给Domain Model层的,也就是我们平时所说的,需要做到Persistence Ignorance。NLayerApp首先为所有实体(确切地说应该是聚合根)设计了一个通用的泛型仓储,你可以在Infrastructure.Data.Core项目中找到这个泛型仓储的源代码,它实现了一个仓储应具有的所有基本功能,比如添加、删除、修改实体对象以及基于规约的一些查询操作等;然后,针对某些聚合根,NLayerApp会根据项目的实际需求,在仓储中实现一些特定的操作。比如:CustomerRepository继承于Repository这个通用仓储,同时实现了ICustomerRepository接口,以向外界提供通过规约(Specification)来查找Customer信息的功能。这样的设计在一定程度上做到了关注点分离,比如当我们对实体进行通用的仓储操作时,我们只需要获得IRepository接口的具体实现即可,而无需使用ICustomerRepository来获得与Customer有关的仓储实现。有关ICustomerRepository与关注点分离的相关内容,我将在下一讲(领域模型层)进行讲解。

以下是NLayerApp中仓储的类关系图,在此贴出以供读者参考。

NLayerApp的仓储实现也使用了不少与Entity Framework相关的技术细节,比如ObjectSet等,这些都是具体技术实现上的内容,在此就不多作介绍了。有兴趣的读者请参考与Entity Framework技术相关的资料文档。

NLayerApp的数据模型

NLayerApp使用Entity Framework的ADO.NET Entity Data Model设计器来设计数据模型,这使得我们能够对整个Domain Model的对象结构有一个很直观的认识。该数据模型位于Infrastructure.Data.MainModule项目下,直接双击MainModuleDataModel.edmx就可以在设计器中打开,对象结构及其之间的关系就能很清楚地展现在你面前。你会发现,其实在这个数据模型的后台代码文件中,除了一些注释以外,并没有任何实质性内容,这是因为NLayerApp仅仅是利用这个设计器来设计数据模型,而真正的Domain Model的代码则会在Domain Model层中,根据该数据模型,利用T4进行自动化生成,详情请见Domain.MainModule.Entities项目。这也使得我们会去思考这样一个纠结的问题:Entity Framework为我们提供的,到底是一个面向数据库设计的数据模型,还是面向领域驱动的领域模型?或许在实际应用中,我们更多地是将其放在ORM的位置上,于是Entity Data Model就变成了位于Domain Model实体对象与数据库之间的行数据入口(Row Data Gateway,PoEAA)。之前我对于基于Entity Framework的领域驱动设计实践也写过一些文章,读者朋友可以参考《领域驱动设计系列文章汇总》。

总结

本文对NLayerApp的基础结构层(数据访问部分),尤其是Unit Of Work的实现进行了分析与介绍;下一讲开始,我们将一起学习NLayerApp的Domain Model部分。

Microsoft NLayerApp案例理论与实践 - 领域模型层

本文将重点介绍Microsoft NLayerApp的领域模型层,这涉及到Domain.Core、Domain.Core.Entities、Domain.MainModule以及Domain.MainModule.Entities四个项目。Domain.Core项目包含了基本接口的定义以及规约模式(Specification Pattern)的实现;Domain.Core.Entities则包含了支持Entity Framework的STE(Self-Tracking Entity)的实现代码,在上文《Microsoft NLayerApp案例理论与实践 - 基础结构层(数据访问部分)》我对STE做了一些介绍,但它的实现与Entity Framework(EF)结合的比较紧密,EF超出了本系列文章的讨论范围,因此,我们也不会针对STE的具体实现方式做太多讨论;Domain.MainModule根据项目需求,针对不同的实体定义了仓储接口,同时实现了项目所需的规约类型。领域服务也是该项目的重要部分;Domain.MainModule.Entities项目中包含了NLayerApp领域模型的核心代码。本文将从仓储接口、规约、领域服务、领域模型这四个方面对NLayerApp的Domain Model层做一个简单的介绍。

仓储接口

根据我们在《Microsoft NLayerApp案例理论与实践–DDD、分布式DDD及其分层》一文中的讨论,仓储的具体实现是放在基础结构层的,而仓储的接口则是放在领域模型层的。Domain.Core项目的IRepository接口就是仓储接口,所有的仓储类都需要实现该接口中定义的属性与方法。在Domain.Core项目下还有一个继承IRepository接口的IExtendedRepository接口,它包含了一些额外的方法来扩展IRepository的功能。事实上在整个NLayerApp中并没有真正用到IExtendedRepository接口,因此我们也不在此做过多讨论。下图是NLayerApp中与仓储的接口和实现相关的类关系图,为了方便浏览和描述,该图中仅包含了Customer仓储的定义与实现部分:

首先,ICustomerRepository接口继承于IRepository接口,以扩展IRepository来定义特定于Customer实体的仓储。因此,所有实现ICustomerRepository接口的类,不仅具备仓储的基本功能,而且还具有特定于Customer实体的仓储操作。其次,Repository类实现了IRepository接口,并作为所有仓储实现的基类,实现了IRepository接口中定义的方法,它在仓储部分的角色就是一个层超类型(Layer Supertype)。最后,CustomerRepository类继承于Repository类,同时实现了ICustomerRepository接口,由于Repository类中已经实现了IRepository中定义的所有方法,因此CustomerRepository类就无需去实现这些方法,只需要把关注点放在ICustomerRepository的实现上即可。以下是位于基础结构层的CustomerRepository代码,供读者朋友参考:

 public class CustomerRepository
    :Repository<Customer>,ICustomerRepository
{
    #region Constructor
    /// <summary>
    /// Default constructor
    /// </summary>
    /// <param name="traceManager">Trace manager dependency</param>
    /// <param name="unitOfWork">Specific unitOfWork for this repository</param>
    public CustomerRepository(IMainModuleUnitOfWork unitOfWork, ITraceManager traceManager) 
        : base(unitOfWork, traceManager) { }
    #endregion
 
    #region ICustomerRepository implementation
    /// <summary>
    /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
    /// </summary>
    /// <param name="specification">
    /// <see cref="Microsoft.Samples.NLayerApp.Domain.MainModule.Customers.ICustomerRepository"/>
    /// </param>
    /// <returns>Customer that match <paramref name="specification"/></returns>
    public Customer FindCustomer(ISpecification<Customer> specification)
    {
        //validate specification
        if (specification == (ISpecification<Customer>)null)
            throw new ArgumentNullException("specification");

        IMainModuleUnitOfWork activeContext = this.UnitOfWork as IMainModuleUnitOfWork;
        if (activeContext != null)
        {
            //perform operation in this repository
            return activeContext.Customers
                                .Include(c => c.CustomerPicture)
                                .Where(specification.SatisfiedBy())
                                .SingleOrDefault();
        }
        else
            throw new InvalidOperationException(string.Format(
                CultureInfo.InvariantCulture,
                Messages.exception_InvalidStoreContext,
                this.GetType().Name));
    }
    #endregion
}

正如上图所述,ICustomerRepository接口扩展了IRepository接口以提供与Customer有关的仓储操作。对于应用程序开发框架来说,这样的设计有助于提高系统的扩展性。比如之前有网友针对Apworks框架提问,觉得Apworks的仓储接口只提供了一些很基本的操作,但他希望能够在仓储上增加一些诸如分页查询对象的操作,之前他的设计是,另外定义一个接口(IFooRepository),其中添加一些分页查询操作,然后让仓储实例同时实现IRepository和IFooRepository。如下:

这样做看上去FooRepository是一个完整的仓储实现,但IFooRepository与IRepository之间没有任何联系,IFooRepository本身并没有体现“仓储”的语义,但它原本就是一种仓储。从实践上看,我们需要在IoC容器中分别为IRepository和IFooRepository注册相同的类型:FooRepository,以便在程序中能够正确地解析IRepository和IFooRepository的具体实现,从而通过IRepository或者IFooRepository分别获得不同的仓储操作。当然,对于我们目前的情形,FooRepository同时实现IRepository和IFooRepository接口,那么C#是可以通过as关键字将该实例在IRepository和IFooRepository的实例间进行转换的,比如:

IContainer container = IoCFactory.Instance.CurrentContainer;
using (IRepositoryContext ctx = container.Resolve<IRepositoryContext>())
{
    IRepository<Foo> repository = ctx.GetRepository<Foo>();
    // do sth. with repository ...
    IFooRepository<Foo> fooRepository = repository as IFooRepository<Foo>();
    if (fooRepository != null) // this is required...
    {
        // do sth. with fooRepository
    }
} 

但是在应用程序开发的过程中,我们无法去约束开发人员一定要让FooRepository去实现IFooRepository接口,这就造成了上面的类型转换不成功,因此,判断fooRepository实例是否为空就显得非常重要。

这样的设计还有另外一个缺陷,就是由于IFooRepository没有体现“仓储”的语义,这就导致它无法应用到基于仓储的类型约束上。例如,假设根据需求我们需要用到一个接口IMyInterface,它的定义如下:

interface IMyInterface<T, S>
    where T : IRepository<S>
    where S : class
{    }

那么很明显我们就无法去定义一个类,在这个类中通过泛型参数T来使用IFooRepository接口:

// error:
class MyClass : IMyInterface<IFooRepository<MyEntity>, MyEntity>
{    }

相比之下,NLayerApp用了一个从语义上来讲更为合理的设计(如下图),它充分体现了“IFooRepository是一种仓储”的概念,总之,两种不同的设计的主要区别在各自所表达的面向对象语义上。

规约(Specification)

在Domain.Core项目下,NLayerApp定义了应用程序领域模型层所需要用到的规约框架,主要是通过LINQ Expression来实现的。在ISpecification接口中定义了SatisfiedBy方法,该方法返回一个LINQ Expression,用来执行判断领域对象是否能够满足当前规约条件的逻辑。NLayerApp的规约结构如下图所示:

有关规约模式,请参见:《Specifications》、《Specification Pattern》;有关规约模式、应用场景以及支持LINQ Expression的.NET规约实现,请参见:《EntityFramework之领域驱动设计实践(十):规约(Specification)模式》。本文就不再重复这些内容了。

值得一提的是,NLayerApp的规约实现,在Specification抽象类中重载了一些逻辑运算符,这使得在实际应用中使用规约变得非常方便。

领域服务(Domain Services)

在DDD中,“服务”的概念得到了扩展,它表示在任何层中,包含了这样一种操作的类型,这种操作从逻辑上无法归结到任何对象上。因此“服务”并不仅仅是应用层或者基础结构层的专利,领域模型中也存在服务。在我的《EntityFramework之领域驱动设计实践【扩展阅读】:服务(Services)》一文中,对领域服务做了简单的介绍,供读者朋友参考。就NLayerApp而言,它实现了一个Bank Transfer的服务,首先定义了IBankTransferDomainService的接口,然后由BankTransferDomainService实现该接口。服务执行的参与者就是两个BankAccount实体,参数就是需要转账的金额。在Application层,BankingManagementService的PerformTransfer方法就使用了该服务来实现银行账户转账。

领域模型(Domain Model)

之前我也提到过,NLayerApp的领域模型是根据Entity Framework的Data Model,通过T4自动生成的,代码中除了包含了Data Model本身所定义的对象属性及对象间的关系外,还包含了基于Entity Framework实现STE的代码。从严格上讲,这并不是一个纯净的领域模型,其中STE的实现牵涉到了很多技术(而非领域)实现细节;此外,所有的领域对象都被DataContract修饰,也就意味着它们将同时以DTO的身份穿梭在网络中。NLayerApp的官方资料中对这种实现有过说明,解释过这种做法并不是很好的DDD实践,但它能够适用于NLayerApp。另外,NLayerApp采用C#的partial关键字向领域对象中添加了业务方法,Domain.MainModule.Entities项目下Partial子目录中包含了这些代码,比如在Order实体上实现了GetNumberOfItems操作。这一点与我以前在《EntityFramework之领域驱动设计实践 (一):从DataTable到EntityObject》一文中讨论的思路是相同的。在此,我们也不对NLayerApp的领域逻辑实现过程做太多介绍,有问题的朋友可以通过留言进行讨论。

总结

本文对NLayerApp的领域模型层做了简单的介绍,尤其对仓储接口的设计做了详细讨论。下篇文章我将介绍NLayerApp的应用层。

Microsoft NLayerApp案例理论与实践 - 应用层

NLayerApp中,在领域模型层之上是应用层与分布式服务(Distributed Services)部分。应用层主要负责接收来自客户端的请求数据,然后协调领域模型层与基础结构层组件完成语义上相对独立的任务;而分布式服务部分则为应用层与客户端之间提供通讯的接口和技术架构,严格地说它已经不具备任何任务处理的责任了,在整个应用程序中是一个可有可无的角色:对于ASP.NET Web应用程序而言,它只需要引用应用层组件的接口,然后通过IoC获得应用层组件实体即可,无需分布式服务的支持。当然,如果还需要考虑与其它系统的集成的话,那么实现一个分布式服务还是很有必要的。今天我们先讨论NLayerApp中的应用层。NLayerApp在应用层中将服务(Application Service)分为三种:Banking Management、Customers Management以及Sales Management。这可以从Application.MainModule.csproj项目中看出。在每种应用服务中,首先为该种服务定义了接口,比如IBankingManagementService等,然后使用相应的类实现了这些接口。从结构上看,还是比较简单的,本文也不再对其中每个应用层服务的具体实现作过多介绍,但有几个方面我还是打算再进一步讨论一下。

构造器注入(Constructor Injection)

应用服务的实现,使用了构造器注入以获得所需对象的实例。例如CustomerManagementService类的构造函数接收两个参数:ICustomerRepository的实例,以及ICountryRepository的实例。当分布式服务组件使用IoCFactory.Instance.CurrentContainer.Resolve方法来获得ICustomerManagementService的具体实现时,IoC容器会根据配置信息来自动解析ICustomerRepository和ICountryRepository的依赖,从而在创建ICustomerManagementService对象的时候,将解析出来的repository实体传给CustomerManagementService的构造函数。我们可以从Infrastructure.CrossCutting.IoC.csproj项目的IoCUnityContainer类的ConfigureRootContainer中找到这种依赖关系的设置代码。有关NLayerApp中IoC容器的实现请参考《Microsoft NLayerApp案例理论与实践 - 基础结构层(Cross-Cutting部分)》。

//Register Repositories mappings
// ...
container.RegisterType<ICustomerRepository, CustomerRepository>(new TransientLifetimeManager());
container.RegisterType<ICountryRepository, CountryRepository>(new TransientLifetimeManager());
 
//Register application services mappings
// ...
container.RegisterType<ICustomerManagementService, CustomerManagementService>(new TransientLifetimeManager());

回过来再看CustomerManagementService类,它的构造函数需要ICustomerRepository和ICountryRepository两个参数,这是因为CustomerManagementService类本身在实现上需要用到这些仓储对象。事实上,ICustomerManagementService接口的实现并不规定实现类必须接收这两个参数。例如,假设我们因为测试的需要,设计了一个MockCustomerManagementService,它也实现了ICustomerManagementService接口,但由于是做测试,我们在这个Mock类中使用Dictionary、List等数据结构来模拟repository的功能,于是在MockCustomerManagementService中,也就无需ICustomerRepository和ICountryRepository的实例了。比如我们的MockCustomerManagementService可以实现如下:

public class MockCustomerManagementService 
    : ICustomerManagementService
{
    private readonly List<Customer> customerRepository =
        new List<Customer> customerRepository;
    public MockCustomerManagementService() { }
    public void AddCustomer(Customer customer)
    {
        if (!customerRepository.Contains(customer))
            customerRepository.Add(customer);
    }
    // other method implementations...
}

然后,在IoCUnityContainer中,将注册ICustomerManagementService的代码改为如下即可:

container.RegisterType<ICustomerManagementService, MockCustomerManagementService>(new TransientLifetimeManager());

数据传输对象(DTO)

在NLayerApp中,使用领域实体(Domain Entities)作为数据传输对象(DTO),同时也实现了一些用于特定用途的DTO,比如DistributedServices.MainModule.csproj项目里的PagedCriteria。在应用服务上将领域实体作为数据传输对象来处理,也就决定了在其更高层:分布式服务中,也必须使用领域实体作为DTO。原因很简单:分布式服务并没有将DTO转换为领域实体的职责,这是应用层的任务。另一方面,原本WCF会在客户端产生Contracts的代理类型的时候,会屏蔽掉领域实体作为DTO所带来的弊端,但貌似NLayerApp的客户端程序是直接引用的领域实体来进行数据交换的,从DDD的角度讲,这种设计是有问题的。当然也应该具体情况具体分析。NLayerApp中,大多数View Model都能够与领域实体的结构相对应,并且直接将领域实体用作DTO在一定程度上降低了开发复杂度,提高了生产率。NLayerApp在其官方的资料中也提到过这个问题:

The latter case is when we use DTOs (Data Transfer Objects) for remote communications between Tiers, where the domain model's internal entities would not flow to the presentation layer or any other point beyond the internal layers of the Service. DTO objects would be those provided to the presentation layer in a remote location.

If the implementation of the entities is strongly linked to a specific technology, it is contrary to the DDD Architecture recommendations because we are contaminating the entire architecture with a specific technology. However, we have the option of sending domain entities that are POCO (Plain Old CLR Objects), that is, serialized classes that are 100% custom code and do not depend on any data access technology. In this case, the approach can be good and very productive, because we could have tools that generate code for these entity classes for is.

Thus, this approach (Serialization of Domain entities themselves) has the disadvantage of leaving the service consumer directly linked to the domain entities, which could have a different life cycle than the presentation layer data model and even different changing rates. Therefore, this approach is suitable only when we maintain direct control over the whole application (including the client that consumes the web-service), like a typical N-Tier application. On the other hand, when implementing SOA services for unknown consumers it is usually a better option to use DTOs, as explained below.

NLayerApp使用的是“序列化的领域实体”(Serialized Domain Entities)这种方式。现在我们来了解一下几个有关DTO的设计要点。

1.DTO的设计需要面向客户端(包括客户端应用程序、与外部系统集成的Web Services等),客户端的View Model需要什么样的数据,就设计什么样的DTO。应用层负责收发DTO数据,并根据DTO数据访问领域模型中的实体,根据实体组装DTO。ORM解决的是Domain Model与关系型数据库之间的阻抗失衡,而DTO解决的是View Model与Domain Model之间的阻抗失衡

2.DTO应该是POCO,它不能依赖于任何技术框架

3.对于中小型系统,可以考虑使用类似NLayerApp的Serialized Domain Entities方式,这可以提高开发效率;但如果是大型系统,还是建议使用DTO,有朋友会觉得每次根据View Model去设计DTO很耗时,但我觉得如果应用程序规模较大的时候,还是做足功夫比较好,磨刀不误砍柴工,这样在今后做系统集成的时候也会方便一些。可以考虑使用DSL与自动化代码生成技术来解决DTO的设计问题

4.WCF产生的代理类Data Contracts就是一种DTO,如果专用微软的技术,那么也就与上述第二点不矛盾,Serialized Domain Entities可以以Data Contracts的形式出现在客户端程序中,一定程度上屏蔽了直接将Serialized Domain Entities用作DTO的负面影响

应用层服务对任务的协调职能

很多朋友无法理解应用层存在的意义,总觉得按照传统的三层架构就是数据访问层(DAL)、业务逻辑层(BLL)和表现层(Presentation)。NLayerApp的系统架构为我们展现了应用层的任务协调职能及其存在的必要性。例如BankingManagementService的PerformTransfer方法中,包含了位于基础结构层的分布式事务处理和位于Domain Model层的repository与UoW的操作。而整个PerformTransfer方法则将这些操作整合起来,以完成一个特定的应用任务:完成转账的功能。通常情况下,应用层的代码中会包含对其下各层组件的访问,因此,DDD的分层并不是严格型的(上层仅能依赖于其直接下层)。当然,如果你的应用程序并不存在需要多层协调才能完成特定任务的情况的话,应用层也可以省略。

OK,今天就先讨论到这里,下一讲我将简要介绍一下NLayerApp中的分布式服务(Distributed Services)部分。

Microsoft NLayerApp案例理论与实践 - 分布式服务

Microsoft NLayerApp采用基于WCF的分布式服务组件为外界(各种类型的GUI)提供了访问接口,客户端程序只需要添加Service引用即可使用NLayerApp应用程序所提供的功能。在NLayerApp中,分布式服务部分的设计与结构还是比较简单的,主要包括DistributedServices.Core、DistributedServices.MainModule以及DistributedServices.Deployment三个项目。

DistributedServices.Core

该项目为所有位于分布式服务层的组件提供公共的类型定义与功能实现,比如在这个项目中定义了与异常处理相关的Fault Contract类型与特性(Attribute)定义。

DistributedServices.MainModule

该项目根据NLayerApp应用程序本身的需求,设计了所需的DTO、服务契约(Service Contract)、操作契约(Operation Contract),并根据模块划分,用C#的partial class特性分别实现了银行管理、客户管理和销售管理三个部分的操作。IMainModuleService接口中定义了NLayerApp的分布式服务所能提供的所有操作接口,而MainModuleService部分类则实现了该接口。根据模块划分的不同,MainModuleService类的实现部分被分配到三个不同的文件中:MainModuleService.BankingManagement.cs、MainModuleService.CustomersManagement.cs和MainModuleService.SalesManagement.cs。

打开IMainModuleService.cs文件,我们可以看到,对于所有的方法,无论是方法的参数还是返回值,都是以原始数据类型(Primitive Data Types)或者DTO的形式实现数据传输的。NLayerApp将Domain Entities同时作为DTO来处理,有关DTO以及Domain Entities as DTOs的详细内容,请参考上篇《Microsoft NLayerApp案例理论与实践 - 应用层》一文,这里就不再多说了。

在IMainModuleService接口的实现类MainModuleService类中,各方法都通过IoC容器(前面也详细讲过IoC容器,NLayerApp事实上使用的是Microsoft Unity)获得应用层组件的实例,从而执行相应的操作。通过上文我们可以得知,NLayerApp在应用层中也是使用IoC容器来获得仓储、领域服务(Domain Service)的具体实现的,由此可见,NLayerApp在层与层之间就是使用的IoC容器实现分层解耦。以下是MainModuleService类中GetBankAccounts方法的实现代码,从中我们可以了解到分布式服务中IoC容器的使用方式。

public List<BankAccount> GetBankAccounts(BankAccountInformation bankAccountInformation)
{
    //Resolve root dependency and perform operation
    IBankingManagementService bankingManagement = IoCFactory
        .Instance
        .CurrentContainer
        .Resolve<IBankingManagementService>();

    List<BankAccount> bankAccounts = null;

    //perform work!
    bankAccounts = bankingManagement.FindBankAccounts (
        bankAccountInformation.BankAccountNumber, 
        bankAccountInformation.CustomerName);
    return bankAccounts;
} 

DistributedServices.Deployment

该项目其实就是一个WCF Web Application,它是分布式服务的宿主项目,它可以以Web应用程序的方式部署到ASP.NET Web Server(比如IIS)上。该项目下的MainModule.svc文件定义了所使用的WCF Service(也就是DistributedServices.MainModule项目中的MainModuleService类);而web.config文件则对如下信息进行了配置:

  • 用于Entity Framework的连接字符串
  • 所使用的IoC容器的名称
  • 用于诊断和跟踪程序的配置信息
  • Web Application的配置信息
  • WCF Service的配置信息

在部署NLayerApp的时候,需要将DistributedService.Deployment项目部署到ASP.NET Web Server(比如IIS)上,并启动Web服务器,之后,客户端程序即可通过WCF的客户端配置以及代理类来访问NLayerApp的应用程序了。

分布式服务程序的调试

我们可以用soapUI工具来进行分布式服务的调试。soapUI是一款先进的开源的针对Web Service的调试与测试工具,你可以点击此处查看该工具的官网首页,并从中获得下载链接。现在,让我们开始使用soapUI来进行分布式服务程序的调试(由于本人的系统是英文版,为了避免翻译的不准确性以致误导读者,因此请读者朋友们自行参照自己的中文版系统进行演练)。

  • 成功编译NLayerApp
  • 在DistributedServices.Deployment项目下,找到MainModule.svc文件,右键单击并选择View in Browser,这将启动ASP.NET Development Server,并在IE浏览器中展示如下页面:

  • 启动soapUI,在Navigator Panel中,右键单击Projects节点,选择New soapUI Project,此时弹出New soapUI Project对话框,在Initial WSDL/WADL文本框中输入http://localhost:88/MainModule.svc?wsdl,此时Project Name文本框会自动以“MainModule”填充,暂时别管其它的选项,直接单击OK按钮

  • 在Navigator Panel中展开MainModule节点,我们可以看到,它包含两个Endpoint:WS2007ForIntranetClients和BasicBindingForSliverlightClients,这与DistributedServices.Deployment项目的web.config中的配置是相符的

  • 展开BasicBindingForSliverlightClients节点,我们可以看到由IMainModuleService接口所发布的所有方法,展开GetCustomerByCode操作,并双击Request 1,在打开的Request 1对话框中,左边部分列出了调用该操作的SOAP Envelope

  • 在<mic:customerCode>节点上输入A0001,我们获得如下Request XML:
<soapenv:Envelope xmlns:soapenv=
<a href="http://schemas.xmlsoap.org/soap/envelope/">http://schemas.xmlsoap.org/soap/envelope/</a> <br>
xmlns:mic="Microsoft.Samples.NLayerApp.DistributedServices.MainModuleService">
   <soapenv:Header/>
   <soapenv:Body>
      <mic:GetCustomerByCode>
         <!--Optional:-->
         <mic:customerCode>A0001</mic:customerCode>
      </mic:GetCustomerByCode>
   </soapenv:Body>
</soapenv:Envelope> 
  • 单击Request 1对话框左上角的绿色箭头,将直接调用GetCustomerByCode方法,并获得返回结果

  • 要调试分布式服务,首先设置好断点,然后在Visual Studio中选择Debug –> Attach to Process菜单,在弹出的Attach to Process对话框中,选择ASP.NET Development Server – Port 88,然后单击Attach按钮,这将使Visual Studio进入调试模式

  • 再次单击Request 1对话框中的绿色箭头以调用分布式服务,此时程序的执行将会停在断点处,供开发人员调试

总结

本文主要对NLayerApp的分布式服务所涉及的各个项目做了简单介绍,同时还给出了一个实践案例,对分布式服务的测试与调试进行了详细演示。分布式服务部分是客户端程序与NLayerApp应用程序进行交互的接口部分,不包含任何业务逻辑与任务协调操作,它只是一种通讯手段的技术实现。

NLayerApp的介绍也差不多快结束了,本系列文章将不再继续对其GUI部分做详细叙述了,因为GUI部分的开发与特定技术的结合非常紧密,比如WPF、Sliverlight以及ASP.NET MVC,有关这些内容,读者可以参考相关资料一起阅读学习,本系列文章就不再继续对WPF、Sliverlight以及ASP.NET MVC这些技术本身做进一步介绍了。


 
分享到
 
 


专家视角看IT与架构
软件架构设计
面向服务体系架构和业务组件
人人网移动开发架构
架构腐化之谜
谈平台即服务PaaS


面向应用的架构设计实践
单元测试+重构+设计模式
软件架构师—高级实践
软件架构设计方法、案例与实践
嵌入式软件架构设计—高级实践
SOA体系结构实践


锐安科技 软件架构设计方法
成都 嵌入式软件架构设计
上海汽车 嵌入式软件架构设计
北京 软件架构设计
上海 软件架构设计案例与实践
北京 架构设计方法案例与实践
深圳 架构设计方法案例与实践
嵌入式软件架构设计—高级实践
更多...