Microsoft – Spain团队有一个很不错的“面向领域多层分布式项目”案例:Microsoft – Domain Oriented N-Layered .NET 4.0 App Sample(在本系列文章中,我使用NLayerApp作为该项目的名称进行介绍),在codeplex上的地址是:http://microsoftnlayerapp.codeplex.com/。它是学习领域驱动设计(DDD)的一个非常不错的案例项目。
该文章翻译自项目的用户手册~
多层应用的关键在于对依赖的管理。传统的多层架构,层内的组件只能和同级或者低级层的组件交互。这有利于
减少不同层内组件的依赖。通常有两种多层架构的设计方法:严格和灵活的。
“严格的层设计”限定层内的组件只能和同一层、或者下一层的组件通信。在上图中,如果我们用这种设计,第N层
只能和第N-1层交互,N-1层只能和N-2层交互,等等。
“灵活的层设计”允许层内的组件和任何低级别层交互。这种设计中,第N层可以和N-1,N-2层交互。
这种设计由于不需要对其他层进行重复的调用,从而可以提高性能。然而,这种设计不提供层之间的同层隔离级别,
使得它难以在不影响多个高级层的时候替换一个低级的层。
在大型复杂的解决方案里,需要引入多个软件组件,在同一级别的层(抽象)里有许多组件是很常见。这样,不是
内聚的。在本例中,每一层必须被分隔成两个或者更多的内聚子系统叫做模块,垂直分布在每个同级层。模块的概念
会在这章中的后面作为建议架构的一部分介绍。
下面的UML图展示了层的组成,相应地在多个子系统内的情况。
测试的注意事项
在正确实现测试后,多层应用极大的提高了能力。
- 由于层之间是通过定义明确的接口进行交互这一事实,很容易为各层添加替代的实现(例如 Mock and Stubs)。
这使得对一个层的单元测试可以在其依赖层没完成的情况下进行,或者是要更快的执行一个大型的单元测试,但是
访问依赖层时很大程序的减慢了执行速度。而且,用mocks and stub隔离层组件限制了测试成功或失败的原因。
因此我们可以在不考虑外部因素的情况下真正的测试内部逻辑。这是真正的单元测试。不同于其他,我们将进行
集成测试。如果我们用基类("分层的超类型“模式)和基接口(”抽象接口“模式),由于它们进一步限制了层间的
依赖,这种能力会增强,由于接口提供了更先进的解耦技术,所以使用接口非常重要,将在后面介绍。
- 因为高层的组件只能和底层的交互,在单独的组件上进行测试是很容易的。这有助于分离单独的组件进行正确的
测试,可以更容易的更改低级层的组件而对应用影响很小(只要满足了接口要求)。
使用层的好处
- 功能容易确定位置,解决方案也就容易维护。层内高内聚,层间松耦合使得维护/组合层更容易。
- 其他的解决方案可以重用由不同层暴露的功能。
- 当项目按逻辑分层时,分布式的部署更容易实现。
- 把层分布到不同的物理层可以提高可伸缩性;然后这一步应该进行仔细的评估,因为可能对性能带来负面影响。
参考
Buschmann, Frank; Meunier, Regine; Rohnert, Hans; Sommerland, Peter; and Stal,
Michael. Pattern-Oriented Software Architecture, Volume 1: A System of Patterns.
Wiley & Sons, 1996.
Fowler, Martin. Patterns of Application Architecture. Addison-Wesley, 2003.
Gamma, Eric; Helm, Richard; Johnson, Ralph; and Vlissides, John. Design
Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley, 1995.
1.3.- 应该遵从对基本设计原则
设计系统时,一些基本对设计原则可以帮你构建一个经过实践验证的架构。下面的重要原则有助于减少维护费用,
最大限度地提高可用性并且提高可扩展性。
1.3.1.- "SOLID‟ 设计原则
SOLID是从下面对短语/原则的英文首字母:
我们总结了以下的设计原则:
单一职责原则:每个类都应该有一个唯一的职责或主要特征。就一个类而言,应该仅有一个引起它变化的原因。
这个原则的一个结果就是,通常类应该尽量不依赖其他对类。
开闭原则:一个类必须对扩展开放,同时拒绝修改。即,不需要修改类的代码就可以扩展类的行为。
里氏代换原则:子类必须可以被它对基类替换。这源于基于抽象的程序行为不应该随具体的实现而改变的事实。
程序应该使用抽象,而不是具体的实现。我们将在后面看到这条原则与依赖注入和替换实现同一接口的类紧密相关。
接口分离原则:类的接口实现不应该强制实现无用的方法。这意味着接口应该具体取决于使用它们的类,应该使用
小的接口而不是一个大的接口。针对不同的消费类,类应暴露不同的接口以提供不同的接口要求。
依赖倒置原则:抽象不应该依赖于细节,细节应该依赖于抽象。类之间的直接依赖应该用抽象替换允许自顶向下的
设计,而无需首先设计较低层。
1.3.2.- 其他重要的设计原则
组件的设计必须高内聚:不要在组件里增加不相关的功能。例如,避免把属于领域模型的业务逻辑放到数据访问层。
功能内聚后,我们可以创建有多个组件的程序集,置于应用中相应的层中。因此这条原则与多层模式和单一职责原则
密切相关。
把跨领域的代码从应用的逻辑抽象出来:跨领域的代码指的是水平方面的代码,例如安全性,操作管理,日志,规范
等等。在程序中使用具体的实现,可能导致在未来难以扩展和维护。面向切面编程(AOP)的原则和这部分有联系。
关注点分离:把应用程序划分成不同部分并且最大限度地减少这些部分之间的重叠功能。关键点是最小化的交互的地方
以实现高内聚低耦合。然后,错误的分离功能点可能导致高度的耦合和系统特性的复杂性。
不要重复自己:想做的必须在系统的某一个部分。例如在一个应用设计中,一个特定的功能应该只在一个组件里实现;
这个功能不能在别的组件里实现。
最小化自顶向下设计(前期设计):只设计需要的而不要过度设计,考虑敏捷设计原则。
1.4.- DDD架构的趋势方向(领域驱动设计) Orientation to DDD architecture trends
这篇架构框架的目的是提供了统一的基础和一套具体应用类型“复杂业务应用”的用户手册。这种类型的应用的特点是
有一个相对长的生命周期并且可以承受相当数量的变化。在这些应用中持续的维护是非常重要的,包括更新/替代新的技术
和框架,例如换新版本的O/RM。目标是所有这些变化的对程序的影响最小。改变基础设施层的技术不应影响高级别的层。
具体而言,“领域模型“层应该受到最小程度的影响。
复杂应用中,业务规则的行为(领域逻辑)经常变化,所以保留对领域层修改,进行简单且独立的方式测试是非常重要的。
实现这个重要目标需要领域模型(逻辑和业务规则)和系统其他层(表现层,基础设施层,数据持久化层等)之间的最小耦合。
应用架构的趋势是朝向实现层之间的耦合,尤其对于领域模型层来说。作为领域驱动设计的一部分,面向领域的多层架构
专注于这一目标。
重要:
领域驱动设计不仅仅是一种建议的架构;也是一种处理项目的方法,一种工作方法,基于领域专家(业务专家)的知识
识别通用语言的重要性,建立正确的模型等等。然而,这些方面不在本手册里;我们的范围仅限于逻辑和技术架构内的典型的
架构模式和建议的.NET实现。请参阅面向领域设计相关的书例如(“Domain-Driven Design‟, by Eric Evans)和其他更详细的
信息关于如何应用面向领域设计到你的项目的生命周期里
您不应该采用面向领域的N-层架构的原因
如果应用相对简单,在应用的生命周期里不会有基础设施技术的改变,尤其是业务逻辑很少会变动,那么你的解决方案就
可以不按照该手册里介绍的方法架构。相反,你应该考虑快速应用程序开发(RAD)。快速的实现在构建简单的组建和层的解耦
不重要的适合非常有效,强调的是生产力和上市时间。通常情况下,这些种应用程序是数据驱动的应用程序,而不是领域驱动
设计。
应该采用面向领域的N-层架构的原因
当业务行为会随时间变化时,应该考虑使用面向领域的N-层架构。在这些情况下,领域模型对每次改变需要的工作会减少,
应用使用更耦合的方式会有更低的总拥有成本(Total Cost of Ownership)。简言之,在软件的单独区域封装业务行为
会显着降低修改应用的时间。这是因为修改只在一个地方进行,并能很方便地进行隔离测试。能隔离领域模型的代码很大的
减少了在应用其他地方进行修改(有可能带来新的问题)的情形。如果你想减少并提高稳定周期和解决方案的调试,这是非常重要的。
领域模型有效的情形
业务规则的一些可操作说明可以用来确定是否实现领域模型。例如,在业务系统中,一个规则指明一个客户不能有超过2000
元的拖欠款,这个规则应该属于领域模型。实现这样的规则通常涉及一个或多个实体,必须在不同用例的背景下进行评估。
从而,一个领域模型会有很多的这样的业务规则,包括一些可以取代其他规则的规则。例如,关于上述规则,如果该用户是个
特殊的账户,拖欠款可以更高,等等。
简言之,应用的业务规则和用例越重要,越应该适合领域模型的架构而不是简单的
在一个面向数据的应用中定义的实体关系。
最后,为了将通过将内存中的对象集(实体对象/图)保存到一个关系型数据库,我们可以使用ORM类型来进行数据持久化,
例如Entity Framework 或者 NHibernate。然而,重要的是将这些具体的数据持久化技术(基础设施技术)和应用的业务行为
很好地分离开。这需要一个应用松耦合方式的N层架构,我们将在后面看到。
1.5.- 分布式领域驱动设计(DDDD)
四个D?是的,很显然, DDDD在领域驱动设计上考虑到分布式系统的演变/拓展。 Eric Evans,在他关于领域驱动设计的书里
几乎没有提到分布式技术的相关主题,而书中由于主要关注的是领域。然而,在许多情况下。我们需要分布式系统和远程服务。
实际上,由于我们从一开始就考虑到分布式服务,本文里的N层架构是基于分布式领域驱动设计的,同时我们用建议微软的技术
进行实现。
最终,分布式领域驱动设计使我们更接近分布式、高扩展性、甚至接近云计算的情景。我们会在最后一章进行阐述。
2.- 面向领域的N层架构设计
如上述,我们要弄清楚所讲的是面向领域架构,而不是所有关于领域驱动设计的东西。如果要讨论面向领域架构,除了架构
还应该讨论设计过程,开发团队的工作方式,通用语言等等。将会在此简要地讨论这几个方面。本手册的目的是专注于领域
驱动设计的架构以及如果用微软的技术实现。因为有很多不错的书的讨论领域驱动设计,所以我们不打算在这里详细说明和解释。
本节提出了我们建议的面向领域的N层架构和层的总的定义。
2.1.- 表现层,应用层,领域层,基础结构层
在最高和最抽象的级别,系统的逻辑架构视图可以看做一组在内部有关联的几个层,类似于以下的图(按照领域驱动设计的样式)
在面向领域架构中,关键是要清楚界定和分离领域模型层和其余的层。这是领域驱动设计的先决条件。“一切都必须围绕领域,
领域模型层必须和基础结构技术分离“。
因此,复杂应用应该分层。应该在各个层内进行设计。设计必须内聚且和系统中的其他层明确界定边界,应用标准的架构模式
使得依赖大多基于抽象而不应是层直接依赖别的层。领域模型的所有相关代码都应集中在一层,和其他层的代码分离出来。领域
不能有取得、保存领域模型、管理应用程序的任务等等的代码。领域必须专注于表达领域模型(数据和逻辑)。这使得领域模型
可以逐渐变得丰富和清晰来表现重要的业务知识,并在应用程序中实现的业务需求。
把领域层从其余各层分离开,可以让每层的设计更加干净。分离的层是更容易维护,因为经常在不同的时候进行不同需求的修改。
例如,基础结构层在技术升级时进行改造。另一方面,领域层只会在业务逻辑变化的时候改变。
此外,层的分离有助于分布式系统的部署,它允许不同的层部署在不同的服务器上,以便最大限度地减少通信和提高性能(引用
M. Fowler)。但是这种层的分布式部署取决于特定的应用需求(安全性,可伸缩性等等)
层之间组件的松耦合是必不可少的。应用的每层都有一系列的组件构成。这些组件应该内聚,但是层之间应该松耦合来支持单元
测试,模拟和重用来减少维护的影响。主要层的设计实现松耦合想在后面详细介绍。
2.2.- 面向领域N层架构
这种架构的目的是按照领域驱动设计的模式,简单清晰的构建多层的复杂业务应用程序。当用N层模式时,在应用中不同的内部层
和子层可以是不同的。
当然,这个特殊的多层架构是可以根据每个项目或偏好来定制的。我们只是提出一个建议的架构基础,可以根据需要和需求进行
调整和修改。
特别的,建议的“面向领域N层”应用的层图如下:
- 表现层
o 可视化组件子层(视图)
o 用户界面逻辑子层(控制器及类似)
- 分布式服务层(Web services)
o 瘦Web services门面模式
- 应用层 Application layer
o 应用服务(协调任务和用例)
o 适配器(格式转化等)
o 工作流子层(可选)
o 应用层基类(层超类型模式)
- 领域模型层
o 领域实体和聚合
o 工厂
o 领域服务
o 查询规范(可选)
o 仓储接口/契约
o 领域基类(层超类型模式)
- 数据持久化层
o 仓储实现
o 基类(层超类型模式)
o 数据逻辑模型和映射
o O/RM 技术基础设施
o 外部服务的服务代理
- 架构的横切组件
o 安全性,运营管理,监控,邮件系统等方面。
这里简单地说明这些层,会有一整章来分别介绍每一层。在这之前,有趣的是,从高层次的角度来看,层之间的交互的
很像我们这么划分的原因。
领域驱动设计的主要前提和来源是Eric Evans的“领域驱动设计- 应对软件的复杂性“,书里描述和解释了建议的N层架构
高层次的图:
值得注意的是,在某些情况下是直接访问其他层的。就是说,没有任何理由层直接的关系必须是单向的,虽然这取决于每个应用
的情况。为了说明这种情况,下面我们展示了Eric Evans之前的一个图。我们修改了这个图,增加了一些细节,于低级别的子层
和元素有关。
首先,我们看到基础结构层,该层为很多不同的环境(服务器和客户端环境)提供功能。基础结构层包含和技术/基础设施相关的
所有东西。还有一些基本概念,其中包含如数据持久化(仓储等等),也有例如安全性,日志,监控等等的横切主题。甚至还包含
具体的图像方面的库。由于软件环境的巨大差异和数据访问的重要性,在本文的架构中,我们会把数据持久化层和基础结构的其他
部分(通常是横切基础结构层)分开,这样这部分可以被任何层已横切的方式使用。
另外一个我们遇到的情况是,不只是通过一条单一路径访问一些层。特别地,必要的时候我们可以直接访问应用,领域,横切层。
例如,我们可以从表现层直接访问应用层,领域层或者横切基础结构层。然而,访问数据持久化层和层内的仓储对象,通常建议通过
应用层的协调对象(服务)来进行,因为应用层的对象是协调大部分基础设施对象的模块。
值得一提的是,这些层的实现和使用应该领灵活。将来也许图中会有更多的组合箭头。所以,不用在所有的应用有使用一样的方法。
此章的后面我们会简单介绍每一层和子层。也会提出了一些关于如何定义和实现这些层的总的概念。(例如层间的松耦合,不同物理
层的部署等等)
接下来,在下面的章节我们会详细解释每个高级别的层。
表现层
这层的作用是给用户展示信息和解释行为。 表现层的组件实现用户与应用交互的功能。
一般建议用MVC,MVP或者MVVM这样的模式来分隔这些组件为子层。
o 可视组件子层(视图): 里面的组件提供终端用户使用应用的能力。里面的组件用可视化控件显示数据
以及从用户取得输入数据。
o 控制器 : 从图形界面分离出组件有助于用户交互。这使得控件、页面不包含处理流程和状态管理逻辑的代码,
可以让我们脱离界面来重用代码的逻辑和模式。对表现层的逻辑进行单元测试页很有用。控制器通常基于MVC模式
及其衍生品。
分发服务层(Web服务)
当应用作为服务提供者为远程应用或者当表现层位于远程端时(富客户端,RIA,OBA应用等等),业务逻辑
通常通过分发服务层发布。这层提供了提供了一种基于通信信道和数据信息的远程访问。需要注意的是,本层
应该越薄越好而且不应该包含业务逻辑。
应用层
这层是建议的面向领域架构中的一部分。这层调用领域层和基础结构层(数据持久化等等)来完成应用的用例。
实际上, 应用层中不应有领域规则或者业务逻辑;应该执行程序的调用,而这是不用解释给领域专家或者用户
的。在应用层中,我们实现协调应用的“通道”,例如事务,执行单位操作,调用应用程序的任务。应用层中可以
实现其他的功能,比如应用优化,数据转换等等,都可以称为应用程序的协调。最终的操作,将会被委派到低层
对象。应用层中不能有表示内部业务逻辑的状态,但可以有展示给用户的表示执行程序任务的状态。
由于像为领域模型的一个门面,应用层有点类似于“业务外观”模式。但是,应用层不仅是简单的调用领域。包括
在这一层的功能有:
- 协调数据持久层的仓储对象的调用
- 分组/合并更高层需要的实体数据,实现减少远程调用的次数来增加效率。传送的数据为数据传输对象(DTO),
将实体和数据传输对象进行相互转换的叫做DTO适配器。
- 响应用户界面的操作的操作,执行领域的运算,调用相应的数据访问运算。
- 维护应用相关的状态(不是领域对象的内部状态)。
- 协调领域和基础结构层的操作。例如,执行一个银行转账需要从仓储获取数据,然后用领域对象的转账业务逻辑
,最后可能给相关人发送邮件。
- 应用服务:注意这个服务不是Web服务。首先,服务的概念在很多层里都有:应用层,领域层甚至基础设施层。
服务的概念是一组类,操作若干低层的类。因此,服务一般用来协调底层的对象。
应用服务,就是一般来协调其他底层的服务(领域服务或横切基础结构层服务)。例如,应用层可以调用领域层
来执行在内存中创建一个订单对象。当领域层执行这样的业务操作(多数改变的是内存中的对象),应用层会调用
基础设施层的仓储来执行数据源的操作。
- 业务工作流(可选):一些业务流程包括几个步骤,这应该按照具体的规则来实现,而且通常比较耗时。这种
业务流程应该由业务流程管理工具的工作流实现。
应用层也可以通过Web服务层作为一个门面发布,这样就可以被远程调用。
领域层
领域层负责展示业务/领域概念,业务流程的状态和领域规则的实现。应该包含展示业务流程的状态。
领域层是软件的心脏。
为此,领域曾的组件应该实现系统的领域核心功能,封装所有相关的业务逻辑(领域驱动设计术语里的领域逻辑)。
基本上,是一些用方法实现领域逻辑的类。按照面向领域的N层架构模式,领域层应该对数据持久化细节透明。
通常我们可以在领域层里定义下面的元素:
领域实体:领域对象包含数据和逻辑,用来在层间传输实体数据。领域驱动设计的一个基本特征是,领域实体包含领域
逻辑。例如在银行账户实体中,存款的操作应该在账户实体内部进行。也可以包括数据验证,属性计算,和其他实体的关系
等等。最后,这些类表达了现实世界中的实体。另一方面,应用内部的实体是在内存中的有着数据和逻辑的对象。如果只用
实体来做数据传输,没有相关的逻辑,我们会陷入最初由Martin Fowler描述的贫血领域模型的反模式,另外,推荐使用POCO
(Plain Old CLR Objects)实体,一种不基于任何数据访问技术或框架的类。该设计(持久化透明)的最终目标是领域类不能有任何直接数据
访问技术的引用。
由于领域类必须独立于任何基础结构技术,领域类必须放在领域层内。所有的情况下,实体是贯穿架构中最多层的对象。
关于领域驱动设计的定义,并且根据 Eric Evans的“一个由标示符定义的对象叫做实体”。实体是领域模型的基本概念,
必须仔细辨认和设计。在一些应用中的标识在别的应用可能不是。例如,地址在一些系统可能不是标识,但在其他的系统,
例如电力公司,客户的地址很重要,应该作为一个实体。
聚合:聚合是有清晰边界的实体和值类型对象的组合。将会在领域模型层的章节具体解释聚合。
工厂:当创建一个聚合很复杂时,用工厂来创建聚合就很有用。将会在领域模型层的章节具体解释工厂。
领域服务:在领域层,服务是一些组织执行领域逻辑的类。这些类一般不应该有领域相关的状态(无状态类)。
这些类用来协调领域实体的操作。典型的领域服务同时关联几个实体。但也可以有负责只和一个根实体交互的服务。
关于仓储,一般由应用层调用,尤其当执行事务或者使用工作单元模式(UoW)的时候。但有时需要根据领域逻辑
来从仓储获取数据,这种情况(一般是查询),可以在领域服务中使用仓储。
仓储契约:显而易见的是,仓储不在领域中实现,而是基础结构层的一部分。然而,接口(契约)必须属于领域。
契约表明了仓储应该提供什么来满足领域,而不管仓储内部是如何实现的。这些接口(契约)不应该知道使用的技术。
另一方面,实现这些接口的类会用某种技术来实现。因此重要的是仓储接口(契约)必须在领域层定义。这是按照
Martin Fowler 的分离接口模式,推荐的面向领域架构的模式。从逻辑上讲,为了能够遵守这条规则,领域实体和
值类型需要是POCO类型;即负责维护实体和数据的对象必须对数据访问技术透明。必须考虑到领域实体最终是仓储
传递是参数“类型”。
数据持久化基础结构层
这层提供持久化和访问数据的功能。数据可以是自己的系统或者外部系统的。因此,数据持久化层给高级的层
公开数据访问。这种公开应该是用过松耦合的方式。
- 仓储的实现:仓储,通用的术语是”在一个组内表示某一特定类型的所有的对象“(Eric Evans的定义)。
实践的方面,一个仓储通常是一个用某种技术完成持久化和数据访问操作的类。通过这样做,我们把数据访问功能放在
一个地方,这样可以更方便和直接的维护和配置应用。通常,我们为每个根实体创建一个仓储。根实体有时候只有一个
实体,有时候可以是一个复杂的聚合,包括很多实体,值类型。
应该通过在领域层的接口访问仓储,这样可以在领域层来进行分离仓储的单元测试,或者用另一种技术实现仓储
而不影响领域层。
仓储的关键是使得开发人员可以集中注意力在领域逻辑上,通过仓储契约来隐藏数据访问的实现方式。这个概念
叫做持久化透明,这意味着领域模型完全不知道数据的存储和查询方式。
最后,需要区分数据访问对象和仓储。主要的区别是数据访问对象在存储上直接进行持久化和数据访问操作。
然而,仓储先在内存中标记/保存对象,以及要进行的操作,但是会在稍后才进行真正的执行。这就是在应用层
这些持久化/数据访问操作会在一个事件中一次完成。通常基于工作单元模式,这会在下面的章节详细介绍。工作单元
模式可以提升应用的性能,也可以减少不一致的可能性;在高扩展系统里,减少由于事务引起的数据库锁数目。
- 基本组件:大部分的数据访问任务需要共通的逻辑,可以抽出并在一个单独的可重用的组件里。这有助于简化
数据访问组件,尤其是减少需要维护的代码量。这些组件可以有基类或者工具类的方式实现,可以在不用的项目重用。
这个概念是一个非常有名的由 Martin Fowler定义的分层超类模式,主要说的是“如果把类似的类中的通用行为抽象到
基类里,会减少很多重复的代码”。使用这个模式纯粹是为了方便但不要分散关于领域的注意。
“分层超类模式”可以在任何类型的层中使用。
- 数据模型/数据映射:这是有映射领域实体模型到数据库表的ORM。按照选择的ORM工具,映射可以是基础代码或者
可视化模式。
- 代理服务:有时候业务组件需要使用外部/远程服务提供的功能。在这些场景,需要实现一个管理通信并且映射数据
的组件。代理服务隔离特定的接口,这样可以模拟外部的服务来进行单元测试,甚至用另一个服务替换而系统的核心部分
不受影响。
横切基础结构层
这层给其他各层提供了通用的技术能力。最后,这层用它的功能“堆积木”。应用中有很多任务是要在不同层里实施的,
可以被各层使用的横切面。最常见的横切面有:安全性(身份验证,授权和验证),运营管理(策略,日志,跟踪,监测,
等等)。这些方面会在下面的章节详细介绍。
- 横切基础结构服务:服务的概念也是关于该层的。这些服务负责组织和协调基础结构动作,例如发送邮件,监控安全事件,
运营管理,日志,等等。这样,这些服务负责组织所有的技术方面的事宜。
- 横切基础结构对象:根据横切基础结构对象的类型,来实现需要的特定对象,不管是安全事件、跟踪、监控等等
需要用指定的API。这些横切基础结构层覆盖很多方面,很多和服务质量(QoS)有关,实际上和具体的技术相关。更多
的详情将在一章中讨论横切面。
服务是一个在不同层通用的概念
由于服务中在DDD架构的不同层都出现,我们在下面的表中总结了DDD中服务的概念:
表2:面向领域架构的服务
我们已经看过了所有的层,它们都可以有服务。由于服务在不同地方都有它的意义,我们可以比较方便的来看
服务在DDD中的总体方面。
首先,需要注意的是服务并不是为了进行远程调用的Web服务。Web服务可以位于分发服务层,可能给远程调用
或者应用、领域层使用较低层的服务。
DDD服务的概念,最干净实用的设计中,包括了层内不属于同一对象的操作(例如对多个实体的操作)。这种情况
下我们可以把这些操作组织为服务。
这些操作自然是由对多个对象的活动组成。由于编程模型是面向对象的,我们也应该把操作组织成对象。这些对象
叫做服务。
这么做的动机是,如果把这些操作作为原来对象的一部分,会歪曲真实对象的定义。例如,实体在逻辑上应该是和
内部的机制例如验证该实体等相关,但不应该把实体本身看做一个整体。例如,一个发动机实体执行有关发动机的
行为,而不应该和发动机是怎么制造的相关。同样地,实体类的逻辑不应该管理它的持久化和存储。
此外,服务是一个或一组作为接口提供的操作。服务不能封装状态(必须是无状态的)。这并不意味着实现的类必须
静态的;一般情况下回是一个实例类。服务是无状态的意味着服务端程序可以使用任何该服务的实例,而不用管每个
对象的状态。更重要的是,服务的执行可以使用甚至更改全局信息。但是服务本身没有状态来控制它自己的行为,
不像实体。服务这个词在服务模式里描述的提供了:服务可以提供给调用它的客户端的是,强调每一层与其它对象的关系。
服务一般由操作命名,而不是对象名。因此,服务和用例关联,而不是对象,即使有特定操作的抽象定义(例如,
“转账服务”和动作“从一个银行账户转钱到另一个”相关)。
为了解释这一点,怎样在不同的层中区分服务,下面举了一个简单的银行情景:
应用层:应用服务”银行服务‟
-接收并转化输入数据(例如把DTO转化为实体)。
-提供领域层转账的数据,以便在领域层处理业务逻辑。
-调用基础结构层的持久化对象(仓储库)来保存领域层的变化。
-决定是否要用横切层的服务发送通知。
-最后,实现了所有“协调技术的管道”,使领域层只负责清楚的表现逻辑。
领域层:领域服务”银行转账‟(动词转账)
-调用例如银行账户的实体的方法。
-提供了业务操作结构的确认。
横切层:横切服务"发送通知"
-协调邮箱发送或者其他类型的通知,调用基础结构的组件。
到目前为止,按照本章中所有的解释,你可以推断出按照商业应用开发中什么是第一条准则:
表3:DI设计原则
原则 #:D1. 复杂应用的内部架构应该设计成为基于多层应用的架构并且面向领域。
o 规则
- 通常,这条规则可以应用到有很多领域逻辑和长生命周期的复杂商业应用。
何时使用面向领域的多层架构
- 应该被使用到复杂的,有很多变化的业务逻辑的,有着相对长生命周期需要后期维护的商业应用。
何时不应使用面向领域的多层架构
- 在小型的完成后几乎不会有改变的应用。这类的应用有一个相对短的生命周期,开发速度优先。
这类应用推荐使用快速开发技术。然后,实现更复杂耦合的组件时会有缺点,这将导致在应用有相对
较差的质量。因此,技术的升级和未来的维护费用会随应用是否有大的改变而定。
使用多层架构的优点
- 在一个组织中,不同的应用使用结构化,同质活类似的开发流程。
- 简单的应用维护,由于不同类型的任务总是位在架构的同一地方。
- 改变应用的物理部署时更容易。
使用多层架构的缺点
- 在非常小的应用中,增加了过多的复杂性。这种情况下可能是过度设计。但这种情况下是不太
可能在有一定复杂性的商业应用出现。
参考
Eric Evans: Book “Domain-Driven Design: Tackling Complexity in the Heart of
Software”
Martin Fowler: Definition of „Domain Model Pattern‟ and book “Patterns of
Enterprise Application Architecture”
Jimmy Nilson: Book “Applying Domain-Driven-Design and Patterns with examples in
C# and .NET”
SoC - Separation of Concerns principle:
http://en.wikipedia.org/wiki/Separation_of_concerns
EDA - Event-Driven Architecture: SOA Through the Looking Glass – “The
Architecture Journal”
EDA - Using Events in Highly Distributed Architectures – “The Architecture Journal”
尽管这些层最初是为了覆盖多层应用架构的大部分,对于特定的应用,基础架构对引进新的层和进行
定制式开放的。
同样地,完全实现建议的层不是强制的。例如,在某些情况Web服务层可能不需要实现,因为不需要
远程访问,或者你可能不想实现某种模式,等等。
2.3.- 组件之间解耦
需要注意的是应用的组件不应该只在层间定义;我们还应该特别注意组件之间怎样交互,那就是,它们
怎样被使用,尤其是一些对象被另外的对象实例化。
通常,应该在所有属于不同层的对象之间解耦,,由于在应用中有一些层我们想以解耦的方式进行集成。
这是基础结构层的大多数情况,例如数据持久化层,可能和特定的ORM方案结合,或者是一个特定的外部后端。
简言之,为了在层中实现解耦,不应该直接实例化层中的对象(例如,不直接实例化仓储对象或其他和特定
技术相关的基础结构层的对象)。
这点本质上是关于任何类型对象的解耦,不管他们是不是领域层里的对象,或者表现层里的组件可以模拟
Web服务的功能,或者在持久化层里能够模仿外部Web服务等等。在这所有的情况下,应该用解耦的方法操作
为了用最小的影响可以用模拟的实现替换真实的实现。在所有这些例子中,结构式非常重要的方法。
最后,我们在应用中实现“使用最先进技术”的内部设计:“应用的体系结构都采用解耦的方式构建,让我们
可以在任何地方和事件增加功能。不只是在层之间使用解耦。”
只在层之间进行解耦可能不是最好的方法。例如在领域内部增加不同对象的集合(例如,一个客户端包括
垂直模块)解释了上述。
在该架构指南的示例应用中,我们选择为应用中的层的大部分对象解耦。所以这种方法是完全可用的。
解耦的技术基于依赖倒置原则,这阐明一种特殊的解耦方式,即将传统的面向对象的依赖关系反转。目标是
让层独立于具体实现的其它层,因此和实现的技术无关。
依赖倒置原则的如下所述:
A. 高层不应依赖于低层。二者都应该依赖于抽象(接口)。
B. 抽象不应该依赖于细节,细节应该依赖于抽象(接口)。
这条原则的目的是为了高层的组件于低层的组件解耦,以便重用高层的组件而使用不用的低层组件。例如,
重用领域层而使用不同的基础结构层,但是实现在领域层定义的同样的接口。
契约/接口定义了低层组件的行为。这些接口应该在高层的程序集中。
当低层组件实现接口,意味着低层组件依赖于高层的组件。因此,传统的依赖关系被反转,这就是为什么
这叫做“依赖倒置”。
有几种技术和模式来实现依赖倒置,例如Plugin, Service Locator,依赖注入和控制反转(IoC)。
我们建议的实现组件解耦技术如下:
- 控制反转(IoC)
- 依赖注入(DI)
- 分布式服务接口(提供给远程访问的层)
正确使用这些技术,可以得到下面的好处:
- 可以替换当前的层/模块而不影响应用。例如,数据库访问模块可以被替换成访问外部系统或其他系统,
只要实现了相同的接口。为了增加一个新的模块,我们不需要确定直接的引用或者编译使用它的层。
- 可以使用在测试时使用STUBS/MOLES和MOCKS:这是一个真实的“替换模块”的场景。例如,用一个假的
数据访问模块替换一个真实的数据访问模块。依赖注入甚至允许在运行过程中进行替换,而不用重编译
解决方案。
2.4.- 依赖注入和控制反转
控制反转模式:这代表选择一个类的具体实现由外部的组件或代码决定。这个模式描述了一个支持“插件”
式的架构,对象可以搜寻需要或者依赖的实例。
依赖注入模式:实际上是控制饭庄的特例。模式里,对象/依赖提供给类,而不是类自己创建对象/依赖。
这个术语最早由 Martin Fowler提出。
我们不应该显式的实例化不同层间的依赖。可以使用一个基类或者接口(最好是接口)来实现,该接口
定义了一个共通的抽象来进行对象实例的注入。
最开始,对象注入可以用对象工厂,工厂在初始化时创建了依赖的实例。然而,如果我们想在需要依赖实例
的时候得到它,需要引入“依赖注入容器”(DI Container)。依赖注入容器注入需要的每个对象的依赖关系。
需要的依赖关系可以用代码或者XML来配置。
通常,应用由外部框架提供依赖注入容器(例如Unity,MEF,Castle-Windsor, Spring.NET等等)。所以,是应用
中的依赖注入容器实例化类。
开发人员将开发接口的实现类,用到容器来注入对象的实例。对象实例注入的技术有“接口注入”,“构造器注入”,
“属性注入”和“方法调用注入”。
当使用以来注入来进行对象间的解耦时,最终的设计会实现“依赖反转原则”。
一个有趣的在表现层使用依赖注入容器解耦的场景,为了在一个孤立的可配置的方式下执行模拟,stub/mole的
组件。例如,在MVC或MVVM的表现层,可能需要模拟Web服务来进行一个快速的单元测试。
当然,最好的解耦是为大多数层中的对象使用依赖注入容器和依赖反转。这将使我们可以在任何运行或者安装的
的时候注入不同的实现行为。
简言之,依赖注入容器和依赖注入增加了项目的灵活性,理解能力和可维护性,最后会当项目推进时更少的改代码。
表 4.- 依赖注入和对象的解耦
单一职责原则指出,每个对象应该有一个唯一的职责。
这个概念由Robert C. Martin引入。它规定改变的原因由于一个职责的改变,一个类必须有且只有
一个理由来改变。
这一原则在业界已被广泛接受,有利于设计和开发单一职责的类。这是直接关系到依赖的数量,即对象
依赖的类。如果一个类有一个职责,它的方法一般只有很少的依赖。如果一个类有很多依赖(假如有15个),
这表明这段代码有着“坏气味”。实际上,在构造函数中进行依赖注入,我们被迫在构造函数中声明所有的对象依赖。
在该例子中,我们将清楚地看到这个类没有遵照单一职责原则,因为一个类有单一职责不应该有这么多的依赖。
所以,依赖注入可以引导我们实现良好的设计和实现,提供了一个解耦的环境。
表 5.- 控制反转和依赖注入不仅有利于单元测试
这是至关重要的。依赖注入和控制反转不仅有助于单元测试和集成!说这就好像是说接口的主要目标是可以测试。
依赖注入和控制反转是关于解耦,使应用更灵活,有一个集中的地方可以维护。测试很重要,但不是使用依赖注入
或者控制反转的首要原因。
另一点需要澄清的是控制反转和依赖注入不是一个东西。
表 6.- 依赖注入和控制反转的区别
请记住依赖注入和控制反转是不同的。
依赖注入确实可以帮助测试,但它的主要用处是它把应用带向了单一职责原则和分离关注点原则。因此。依赖注入
是一个重点推荐的技术,是软件设计和开发的最佳实践。
由于我们自己实现依赖注入可能非常麻烦,我们使用控制反转容器来提供对象的依赖图形管理的灵活性。
表 7.- 设计原则 Nº D2
原则 #:D2。不同层之间对象的通信应该是解耦的,使用依赖注入和控制反转的模式。
o 原则
- 一般说来,这条原则应该在所有的多层架构的大中型应该使用。当然,应该被使用到对象的主要职责是逻辑执行
的时候。明显的例子时服务和仓储库,等等。另一方面,对实体类自己这么做没有任何意义,应该他们是POCO实体类,
并没有外部的依赖。
什么时候使用依赖注入和控制反转。
- 在几乎所有的大中型应用中都应该使用。在领域层、基础结构层和表现层都是特别有用的。
什么时候不要使用依赖注入和控制反转。
- 在解决方案级别,一般依赖注入和控制反转不会用在快速应用开发商。这类应用不需要一个灵活的多层架构,
也没可能引入解耦。这通常出现在小型的应用程序。
- 在对象级别,在没有依赖的类中使用依赖注入和控制反转没有意义。
使用依赖注入和控制反转的优点
- 以最小的代价替换层/模块。
- 易于使用STUBS/MOCKS/MOLES进行测试。
- 项目推进时,可以更少的动代码
- 可维护性更高,更易理解项目
使用依赖注入和控制反转的缺点
- 如果不是每个开发人员都熟悉依赖注入和控制反转,应用开始的阶段会增加难度。然而一旦理解了之后,会给
应用带来更好的灵活性,最终完成高质量的软件。
参考
依赖注入:
MSDN - http://msdn.microsoft.com/enus/library/cc707845.aspx
控制反转:
MSDN - http://msdn.microsoft.com/en-us/library/cc707904.aspx
依赖注入和控制反转模式 (By Martin Fowler) –
http://martinfowler.com/articles/injection.html
2.5.- 模块(代码的分割和组织)
领域模型往往在大型复杂的应用中大幅增加。模型将达到一个程度,很难把它作为一个“整体”讨论,可能很难
完全了解所有的关系和模型之间的相互作用。因此,需要将代码组织到几个模块中(使用程序集/包/命名空间)。
模块的概念实际上从软件开发依赖就被用到。如果我们细分不同的垂直模块,很容易看清楚系统的全貌。一旦
理解了模块之间的交互,容易更详细的关注每个模块。这是一个管理复杂性的有效方法。“分而治之”是对这最好的
定义。
模块用来组织概念和相关的任务的方法(通常不同的应用领域)。这使我们可以从外部的角度来减少复杂性。
一般一个模型(领域模型)可以和一个或多个模块相关。所以分离成一些模块基本上是代码分割。
很明显模块间应该高内聚低耦合,不只是把代码分成模块,更是概念上的分隔。
不能把模块和有界上下文混淆。有界上下文会在下面讨论。一个有界上下文包含了整个系统/子系统,甚至有数据库。
另一方面,一个模块通常和其他模块公用模型。模块是关于代码和概念的分隔。
像E.E说的,“如果一个模型像一本书在讲一个故事,模块就是章节”。选择可以讲述系统故事的,有相关概念的模块,
并且给模块按照领域专家的行业语言命名。了解更多领域驱动设计的语言,请阅读Eric Evan的“领域驱动设计”。
模块的划分的一个很好的例子是ERP(有可能是有界上下文的好例子,根据ERP实际的领域来定)。ERP一般通常划分为
垂直的模块,每个模块负责特定的业务领域。ERP模块的例子有工资,人力资源管理,计费,仓库等。正如前面提到的,
每个ERP的垂直模块根据它的大小和是否有它自己的模型,也可以是有界上下文,另一方面,如果所有的ERP公用一些
模型,这些方面就是模块。
另一个使用模块的原因是代码质量。代码应该高内聚低耦合是行业公认的原则。虽然内聚始于类和方法的级别,也可以
在模块的级别应用。因此,推荐把相关的类组织到模块中实现最大的内聚。有几种类型的内聚。最常用的两种是“通信内聚”
和“功能内聚“。通信内聚是关于操作相同数据集合的模块里的部分。把它们聚集起来非常有意义,因为这些代码有
比较强的联系。另一方面,功能内聚是关于模块中的全部都执行一个或一组定义的功能任务。这是内聚的最好的类型。
这样, 在设计时使用模块是一个好的增加内聚减少耦合的方法。通常模块会划分出不同的功能区域。然而,正常情况下
不同模块之间有一些通信;因此,应该为它们的通信定义接口。最好通过调用另一个模块的接口来增加/聚集一组功能。
这样也可以减少耦合。
建议的架构计划如下,考虑到应用中可能的不同模块:
表 8.- 设计原则
原则 #:D3. 定义和设计应用模块使共享相同模型的功能区域分隔开
原则
通常,这条原则必须在大部分有一定功能区域的应用中使用。
什么时候应该设计实现模块
应该在几乎所有的业务应用中实现,这样可以鉴别出不同的功能区域并实现模块间的低耦合。
什么时候不应该设计实现模块
应用中只有一个非常内聚的功能区域,也很难把它分开到解耦的功能模块。
使用模块的优点
- 设计中使用模块可以很好的增加内聚,减少耦合。
- 模块间的松耦合可以减少复杂性,显着增加应用的可维护性。
使用模块的缺点
- 假设一个模块的实体和其他模块的实体有很多的实体关系,或许应该是一个单一模块。
- 初期设计时需要额外定义模块间的通信接口。然而,只要模块的定义和分离非常适合,将对项目非常有益。
参考
Modules: DDD book – Eric Evans
Microsoft - Composite Client Application Library: http://msdn.microsoft.com/en-us/library/cc707819.aspx
2.6.- 模型细分和工作的上下文
在本节中我们会看到怎样处理大型的模型或者叫复合模型;我们会展示把一个大的模型分成几个具有明确的边界
小模型来实现模型的内聚。这就是有界上下文的概念。
注意:需要澄清的是有界上下文不是ORM工具的context或者sessions。这里,有界上下文是一个更广泛的概念,
我们讨论的上下文是关于独立子系统和独立开发小组的工作上下文,将在下文中看到。
2.7- 有界上下文
在大型复杂的应用中,无论是它们的数量还是它们的关系模型都在快速的增加。在大的模型维护内聚是很复杂的。
两个人对于同样的概念有不同的的诠释是很常见的,或者由于他们不知道已经实现了一个概念,而在另一个对象中
实现。为了解决这个问题,我们应该通过定义上下文来限制模型的大小。
对整个系统只有一个模型的想法是是诱人但不可行,因为维护这么大模型的内聚几乎是不可能的。事实上,我们
第一个应该问的问题是,当面对大型模型时,“我们需要完全整合系统所有的功能吗?”。这个问题的答案是在90%的
情况是否定的。
因此大型的项目/应用可以有多个模型,每个模型适用于不同的上下文中。但是如果我们有多个模型,问题就会
出现,因此我们需要显式的定义系统中模型的边界,这样一个模型会尽可能保持统一。上下文可能成为应用的一个
重要部分并且和其他区域独立开。
在每个上下文中,不同的模型使用不同的术语,不用的概念和规则,甚至有时处理相似的数据。
在大型系统中很难为所有的业务领域只提出一个模型。将会有一个巨大的模型试图满足不同情景的所有业务领域。
这就像试图取悦现实世界的每一个人,很难做到的。这就是我们需要上下文的一些模型来满足大型系统的业务领域。
避免在大型系统中只有一个模型处理很多垂直的业务领域。我们不能只创建一个模型来为所有的用户和相关人员服务。
“一个模型来统治他们"不是”一个戒指来统治他们“。
任何系统中的元素只有在上下文中定义才有意义。我们将专注于维护上下文的内聚,处理上下文的关系。上下文是
模型的分隔,旨在维护内聚,而不仅仅是简单的功能分隔。定义上下文的策略可以有多个,例如按照团队划分,按照
系统的高层功能划分等等。例如,一个项目中需要我们于一个已存在的系统并联,显然原有的系统有它的上下文,
我们不想让新系统也处于一样的上下文,因为这到影响新系统的设计。
下面的图展示了一些有界上下文,可能都是从头开始创建相同的应用。可以看到通常每个有界上下文都有一个单独
的模型,模型和其他有界上下文公用数据源。
实际上在大型系统中,根据需要每个有界上下文最好都都不同的架构。例如,下面可能是一个有几个有界上下文的
应用,每个有界上下文的架构和实现都不一样。这样做可以让每个应用都用最合适的方法构造。
注意:我们在最后一章中介绍了CQRS(命令查询的责任分离)架构(高扩展性云计算应用)。 虽然CQRS不必在云端
实现,但是由于其高扩展性和云很有关系。但这是另一个主题。
最重要的一点是根据不同应用的需要,使用不同的方法。例如,在示例中,如果任务是显示一些信息,和其他的
有界上下文没有关系,为什么要使用一个复杂的多层或者CQRS架构呢?。
尽管如此,在系统里实现分开的上下文有一个缺点:失去了统一的视图,当两个上下文应该通信实现功能时容易
混淆。因此,如果我们需要连接有界上下文,需要建立一个上下文图,这样会清楚的标明系统的不同上下文和它们
间的关系。这样就实现了内聚,上下文提供的内聚优势,通过明确建立上下文之间的关系保持了系统的全局视图的。
在不同的有界上下文的整合一定会涉及一些模型间的转化和数据转换。
同样地,有界上下文的通信应该是异步的。在这里使用轻量的服务总线可能是有用的。
表 9.- 设计原则
原则 #: D4.
大型系统中有多个模型时定义有界上下文
原则
- 在模型使用时显示的定义上下文
- 在团队组织,代码和数据库模式时显示的设定界限
- 严格把模型约束在边界内
参考
BOUNDED CONTEXTS:
DDD book – Eric Evans
COMPOSITE APPS and BOUNDED CONTEXTS:
Udi Dahan session „Domain Models and Composite Applications‟
(http://skillsm atter.com /podcast/design-architecture/talk-from-udi-dahan)
2.7.1.- 表现层,复合应用程序和有界上下文
当不同的开发团队开发不同的有界上下文时,在用户界面层出现了一个问题。这个例子中,一般只有一个
表现层,一个团队对表现层的修改会影响别的团队。
结果是有界上下文和复合应用的概念紧密相关,不同的团队独立开发同一应用的有界上下文和模型。然而,
最终都必须集成到用于界面中。为了让这种集成较少出问题,我们推荐使用”复合应用程序的表示层框架“。
那就是,为每个可视的区域(惨淡,内容区域,加载视觉模块,例如使用微软MEF and PRISM)定义接口,
这样集成会高度自动化而不费力。
参考
Microsoft - Composite Client Application Library: http://msdn.microsoft.com/en-us/library/cc707819.aspx
2.8.- 上下文间的关系
上下文间的关系视它们间的通信而定。例如,我们也许不能在一个上下文中执行更改,例如在一个离线的系统
中,或者我们的系统需要其他的系统支持才能正常工作。在下面,我们会看到有界上下文之间整合的可能性,但
是要理解不应该强制有这些关系除非是正常出现的。
2.8.1.- 共享内核
当我们有两个或多个上下文,并且开发的团队可以顺畅的沟通时,建立一个共享的上下文都频繁使用的责任对象
是不错的。这些对象成为上下文的共享内核。修改共享内核的对象时,需要得到所有开发团队的认可。推荐共同
建立一套对每个共享内核对象的单元测试。促进不同团队之间的沟通是关键,因此,一个很好的做法是让每个团队
的成员到其他团队中,这样在一个上下文中积累的知识被输送到其余的上下文中。
2.8.2.- 消费者/提供者 Customer/Supplier
很容易意识到开发的系统依赖于别的系统。例如,一个分析系统或一个决策系统。在这类系统里通常有两个
上下文,一个是我们的系统使用依赖系统的上下文,依赖系统在另一个上下文中。
两个上下文的依赖是单向的,从“消费者”上下文到”提供者“上下文,或者从依赖系统到被依赖的系统,
这类关系消费者会被提供者的功能限制。同时,提供者的上下文由于害怕引起消费者上下文的Bug而很少改变。
不同上下文的开发团队的沟通是解决这类问题的方法。消费者上下文的开发团队应该以消费者的身份参加提供者
的计划会议来决定提供者用例的优先级。另外,应共同为提供者系统创建一组验收试验,这样可以确认消费者
期望的接口,消费者上下文可以不用担心改变期望的接口而进行修改。
2.8.3.- 遵奉者
消费者/提供者的关系需要不同上下文团队的合作。这种情况往往是比较理想的,往往提供者上下文有自己的
优先级,而不是按照消费者的需要安排。这类情形中,我们的上下文依赖于别的我们不能控制的上下文,也没有
紧密的关系,可以使用遵奉者办法。这涉及到我们的模型来适应其他上下文。这限制了我们的模型完成额外的
任务,限制了我们模型的形态。然而,添加其他模型已基类的知识不是一个不靠谱的想法。决定是否遵从遵奉者
很大程度上取决于其他上下文模型的质量。如果它不适合,应遵循更具防御性的方法,例如防护层或其他的方法。
2.8.4.- 防护层
这可能是对于有界上下文集成的最重要的模式,尤其是处理传统的有界上下文集成。到目前为止我们看到的关系
假定两个团队直接有一个好的沟通,也有一个可被采用的有良好设计的上下文模型。但如果一个上下文的设计不佳,
我们不希望影响我们的上下文。对于这种情形,我们可以实现防护层,是一个上下文间执行转换的中间层。 一般来说
,这种通信由我们发起,虽然不是强制性的。
防护层有三种组件构成:适配器,转换器和门面。首先,门面是用来简化和其他上下文的通信,它暴露了我们的
上下文使用到的功能。需要明白门面应该的其他上下文定义;否则会混淆转化器和其他系统的访问。门面之后,
适配器用来使其他上下的接口适配我们的上下文。最后,我们使用转化器来映射我们的上下文元素和别的上下文。
2.8.5.- 其他的方法
集成被高估,花费的成本往往是不值得。因此,两组没有连接的功能可以在不同的上下文中开发而不必交互。如果
我们有功能需要两个上下文,可以在更高的级别来执行这个操作。
2.8.6.- 开放式主机
当我们开发一个系统,决定把它分成有界上下文时,一个常见的做法是在上下文间建立一个中间的转换层。
当上下文的数目很多时,创建中间转换层会花费相当大的额外工作量。当我们创建一个上下文时,它通常是
高内聚的,提供的功能可以看成是一组服务(不是Web服务)。
这种情况下,最好创建一组服务来定义通用的通信协议让别的上下文使用该上下文的功能。这项服务应保持
版本之间的兼容性,但可以逐渐增加所提供的功能。暴露的功能应该是概要的,如果其他的上下文需要具体的
功能,那应该在一个分开的转换层创建,这样我们的上下文协议就不会被破坏。
2.9.- 用.net实现有界上下文
正如我们在本节开始说过的,有界上下文是用来维护较大模型的内聚。为此,一个有界上下文可以对外部
系统展示系统功能的一部分,或者展示子系统功能的组件。实现有界上下文没有通用的规则,但是我们会提出
几个重要的方面并举几个典型的例子。
在架构中,我们把模型和功能分成大型的区域(有界上下文)。每个区域或有界上下文都被分配给了不同的
开发团队,它提供一组可以看作是服务的高内聚的功能。最合乎逻辑的事情是当我们处理一些业务领域时使用
不同方法的关系。每个模块反过来会变成一个“open host”,以服务的方式提供一组功能。因此,任何涉及到
几个模块的功能都会在更高的层实现。每个模块负责内部的对象模型,管理内部的持久化。当使用Entity Framework
时,我们在模型和有界上下文间是1对1的对应。
我们为每一个有界上下文分成更小的部分(模块)时都会足够复杂。然而,这些模块更多的和一个公用的模型相关。
这种情况,模块更多的是一个组织单元(代码分隔)而不是一个功能性的。有界上下文中不同的模块共享相同的实体
框架模型,但是关键对象的修改需要两个团队的同意。
最后,我们要找到系统中关于外部系统,遗留系统或第三方服务的方面。这些显然是不同的有界上下文。这里,
办法可以是接受外部系统模型,采用“遵奉者”的方法,或者通过一个防护层保护我们的领域。决定是否按照遵奉者
或者选择一个防护层根据其他上下文的模型质量和上下文间转换的成本。
2.9.1.- 使用把Entity Framework模型分开
一个有效的分隔EF模型的方法是找到最互连的模型,通过移除模型或所有的关联只留外键。通常最互联的实体是
那些对模型贡献较少语义的,可能是一个横切的模型,例如用户,通过关联到其他的模型的最后修改属性。
实体间并不总需要有关联,我们将看到为什么通过分析详细的关系。什么使得实体间需要有关系?通常一个
实体使用了另一个实体的功能。例如,考虑银行账户和客户的实体,客户的资产通过计算该客户所有账户的余额来
得到。
通常,两个实体间的关系可以用其中一个实体仓储的查询来代替。这个查询表示了该关系。在另一个实体的方法
里,可以增加额外的参数来包含查询结果的信息,可以用来替代关系。
两个实体之间的交互在服务级别实现,因为这类交互并不常见,逻辑并不复杂。如果需要修改一个关联
(增加或删除元素),我们在实体中查询的方法中返回布尔值来判断是否需要实现,而不是修改方法来适应删除的
关联。继续账户和客户的例子,让我们假设我们想计算给客户付的利息,这根据用户特定而定。该服务会根据客户
的资格把利息打到新的账户当利息超过一定数额时。该例中,我们将有下面接口的服务:
public interface IInterestRatingService
{
void RateInterests(intclientId);
}
public class InterestRatingService : IInterestRatingService
{
public InterestRatingService(IClientService clients,
IBankAccountService accounts)
{
…
}
public void RateInterests(intclientId)
{
Client client = _clients.GetById(clientId);
IEnumerableclientAccounts = accounts.GetByClientId(clientId);
double interests = 0;
foreach(var account in clientAccounts)
{
interests += account.calculateRate(client);
}
if(client.ShouldPlaceInterestsInaNewAccount(interests))
{
BankAccountnewAccount = new Account(interests);
accounts.Add(newAccount);
}
else
{
clientAccounts.First().Charge(interests);
}
}
}
在下面的章节我们会详细看到如何用表上技术实现架构的不同模式。
2.11.- 在 Visual Studio 2010 中实现多层架构
为了实现多层架构(按照领域驱动设计多层架构的风格),需要做以下的步骤:
1.- Visual Studio解决方案应该清楚的呈现每一层和层内的实现。
2.- 每层都应该正确的设计并包括设计模式和该层使用的技术。
3.- 项目中会有很多可重用的代码。这是迷你框架或者叫seedwork。最终,会变成在其他项目中重用的代码,
可能被一些有界上下文共享,也包括领域、应用、数据持久化层的基类。Seedwork这个词最初有Martin Fowler
提出:http://www.martinfowler.com/bliki/Seedwork.html
2.12.- 示例应用“面向领域多层.net 4.0 应用”
大多数的代码例子和解决方案结构都在示例应用中。我们强烈推荐下载这份源代码,按照该书查看它。
示例应用在CODEPLEX上发布,有开源的许可:http://microsoftnlayerapp.codeplex.com
2.13.- Visual Studio 解决方案设计
一旦创建了VS解决方案,我们就开始建立逻辑文件夹架构来分配不同的项目。大多数情况下我们为每层
或子层创建一个项目(DLL),以提供更高的灵活性,更容易进行解耦。然而,这会产生一个相当大数目的
项目,所以需要由VS里逻辑文件夹来对它们进行排序/分级。
最初的层次结构可能和下面的类似:
从最上面开始,第一个文件夹("0 –Modeling & Design‟)里面包括了不同的架构图和设计图,例如架构图,
内部设计的UML图。这些图用来展示我们的实现。
文件夹的数字是为了让他们以想要的顺序出现在VS解决方案中。
下一个文件夹,“1 Layers‟,包括了多层架构的层,如上面的层次结构图中所示。
表现层
第一层表现层,包括了不同种类的表现项目例如RIA (Silverlight), RIA-HTML5,传统的Web(ASP.NET), Windows Phone
等等。这些客户端只是展示了可能的种类,很可能真实的应用只有一个,
随后,在应用服务器会有组件层(我们指的是部署)。总之,会有面向领域的多层架构的主要的层,每个
子层都有不同的项目:
这些文件夹中,我们会按照每层的典型元素添加项目。这也和要实现的模式有关(在该指南后续逻辑和
实现章节介绍)。考虑划分每个子系统边界和职责的有界上下文的概念。如果只有一个有界上下文,前面的
层次结构是有用的。但如果有几个有界上下文,最好按照下面的层次结构:
Seedwork类似于可在项目,有界上下文等中重用的小型框架,但它不具有足够的分量开发团队称之为框架。
在我们的示例应用中,所有包含“Seedwork”词的项目都是可重用的程序集。就像之前提到的,Seedwork是由
Martin Fowler 提出的: http://www.martinfowler.com/bliki/Seedwork.html
每个有界上下文可能有相似的层,也可能没有,这样根据具体要求来定。
分发服务层(WCF 服务)
这层是为了实现应用服务器的远程访问的WCF服务。这层是可选的,因为在一些情况下是直接访问应用层和
领域层的。
如果我用使用分发服务进行远程访问,结构可能类似于下面:
我们需要一个存放WCF服务的项目,WCF进程运行在其中。这个项目/进程可以是IIS的网站,Windows服务或其他
类型的进程。但是Web服务的功能是在每个模块暴露的服务中。我们可以为每个模块的功能建立一个WCF服务类。
在我们的例子中,有一个模块叫"ERPModule‟,另一个叫“„BankingModule‟,当然这些代码的分割取决于你的设计。
另外,该层内需要一个单元测试的项目。
对于一个WCF服务,推荐在IIS站点上部署,最好是在有AppFabric的Windows服务器的IIS,这样可以有AppFabric
提供的监控和测试能力。
应用层
在本指南的逻辑架构部门,应用层不应该包含领域/业务逻辑。应用层应该协调应用方面的技术任务。这里我们
实现了应用“管道”的协调,例如事务协调,工作单元,仓储库和领域对象的调用。
有逻辑类的层都会有一个单元测试的项目。
领域层
这层从业务/领域的角度看是最重要的,由于在该层实现所有的领域逻辑、领域实体类等。该层的内部
有几个子层或组件。建议控制层内项目的数目。
我们可以重用由基类和其他可重用代码的“Seedwork‟项目,这样所有领域功能的每个有界上下文都可以使用。
对每个应用的功能性有界上下文(本例中,叫"MainBoundedContext”),我们会实现完整的子系统逻辑
(聚合,POCO实体,服务,查询和仓储契约)。
这是在我们示例应用中领域层一个有界上下文的内容:
有逻辑类的层都会有一个单元测试的项目。
领域层会在本指南中用一整个章节来介绍概念和具体实现。
数据持久化层
该层最重要的特征是仓储库的工作单元模式来进行数据访问和持久化。在该模块中我们会实现所有和映射
领域实体和数据库表,使用Entity Framework的API。
本层中最重要的元素如下:
- 工作单元/上下文 (UoW): 对每个有界上下文都实现一个抽象的Entity Framework上下文,这样我们建立了
一个清晰的契约声明了我们的工作单元模式,并且可以通过替换上下文来模拟来进行孤立的单元测试。
- 仓储库:和工作单元对象共同负责数据持久化逻辑的类
这儿也有模块的概念。模块只是和更好的组织代码分隔,共享相同的上下文和模型的代码职责有关系。
我们必须有一个测试整个项目的项目。
项目前以“Seedwork‟为前缀的是用来实现基类和扩展的,让不同的有界上下文甚至不同的应用重用。
数据持久化层也会在该指南的一整章来介绍概念和具体实现。
2.14.- VS.2010中的应用体系结构图层
为了更好地理解架构设计,用一张VS2010中的层图来查看多层架构。另外,这使我们可以让我们将层映射
到它们实际的命名空间。因此,这使该架构的验证针对实际的源代码,因此层间的访问/依赖之间是禁止的,
而是代码的行为。可以在工作流协作的引擎(TFS)中运行全局的编译进行验证,全局的测试或最后最后全局的
架构检查。
这是我们示例应用的多层架构图:
我们将在下面的章节介绍这些层的各个层,子层的逻辑和实现。这里,我们提出一些全局的维面。
我们可以看到在架构图中,整个架构的核心层是领域层。在依赖级别也是非常重要的。大部分依赖在
领域层结束。领域层,对数据访问是持久化透明的。领域层对其他层只有最少的依赖,如果有依赖都是通过
控制反转容器的抽象(接口)。这就是为什么依赖不是一个“箭头”。
另一方面,分发服务层是一个可选的。如果表现层在客户端运行(Silverlight, HTML5/JScript, WPF,
WinForms 或 OBA),分发服务层就是必须的。然而,在Web客户端(ASP.NET 或 ASP.NET MVC)的情况下,
表现层和业务组件是放在同意物理服务器上。这种情况,不用使用WCF服务,因为这会影响应用的性能,
增加不必要的延迟和序列化。
对于应用层,一般会是我们的“门面”层,应用服务协调关于领域层,数据持久化层和其他组件的调用。
2.15.- 使用UNITY实现依赖注入和控制反转
在本节中,我们会介绍实现层间组件解耦的技术。本节的目标是使用微软的模式和方法Unity,实现依赖注入
和控制反转。依赖注入和控制反转也可以用其他的工具实现,例如:
表 9.- 控制反转容器的实现
框架 制订人 信息
Unity
http://msdn.microsoft.com/en-us/library/dd203101.aspx
http://unity.codeplex.com/
微软模式与实践
这是目前实现依赖注入和控制反转最完整的轻量级微软框架。是有着微软发布许可的开源项目。
Castle 项目 (Castle Windsor)
http://www.castleproject.org/
CastleStronghold
Castle 是一个开源项目。这是依赖注入和控制反转最好的框架之一。
MEF (微软可扩展性框架)
http://code.msdn.microsoft.com/mef
http://www.codeplex.com/MEF
微软(.NET 4.0的一部分)
目前是为了应用和工具自动扩展的框架,并不很关注使用依赖注入和控制反转实现架构层间的解耦。
未来由于该框架的发展,可能替代Unity。
Spring.NET
http://www.springframework.net/
SpringSource
Spring.NET 是一个开源项目。这是最好的面向方面编程(AOP)的框架之一,也提供控制反转容器的能力。
StructureMap
http://structuremap.sourceforge.net/Default.htm
.NET 社区的一些开发者
开源项目
Autofac
http://code.google.com/p/autofac/
Several developers of the
.NET community
开源项目
LinFu
http://code.google.com/p/linfu/downloads/list
http://www.codeproject.com/KB/cs/LinFuPart1.aspx
.NET 社区的一些开发者
开源项目。提供控制反转容器,面向方面编程(AOP)和其他能力。
我们的示例应用选择了UNITY,因为这是微软提供的最完整的依赖注入和控制反转能力的框架。当然,
在商业框架架构中,可以使用任何控制反转容器的框架。
2.15.1.- 介绍Unity
该应用程序块叫做Unity,是一个可扩展的轻量级依赖注入容器。支持构造器注入,属性注入,方法调用
注入和嵌套容器。
Unity是一个注册类型(类,接口)的容器,也是在这种类型之间的映射(类似一个接口和实现该接口类的关系)。
Unity容器也可以按照请求实例化具体的类型。
Unity可以从微软的站点免费下载,Unity也包含在Enterprise Library 4.0/5.0和PRISM (复合应用框架)中,扩展
了Unity的功能。
通常在容器中记录类型和映射,制定接口,基类,特定类型的对象间的依赖关系。我们可以用代码来定义这些
记录和映射,也可以使用XML配置文件。同样地,依赖注入可以通过在类的标志中指明主要注入的属性和方法,
也可以使用构造函数的参数来指明需要自动注入的对象。甚至可以使用容器的扩展来支持“EventBroker”,它实现
了基于特性的发布/订阅机制。也可以建立自己的容器扩展。
Unity提供了应用开发的这些优点:
- 支持需要的抽象;可以让开发人员在运行时或安装时指定依赖,简化了横切管理的方面,例如用模拟执行
单元测试。
- 简化了对象的创建,尤其是那些有复杂分层结构和依赖的对象,最终简化了应用的代码。
- 通过把组件的配置移到控制反转容器中增加了灵活性。
- 它提供了服务定位能力;可以使客户端可以保存/缓存容器。这在ASP.NET Web应用中特别有用,可以把
容器存在session或ASP.NET应用中。
2.15.2.- 使用Unity的场景
Unity可以解决基于组件应用的开发难题。现代企业的应用包含的业务对象和组件执行在应用中的一般
或特定的任务;此外,也会有一些负责应用中水平方面的组件,例如跟踪,日志,验证,鉴权,缓存和异常管理。
成功构建这些多层应用的关键是实现松耦合的设计。松耦合的应用灵活性更好,容易维护,更重要的是在开发
阶段容易测试(单元测试)。可以模拟有较强依赖的对象。最终,可以对模拟或者真实的对象分别测试。依赖注入
是构建松耦合应用的重要技术。它提供了管理对象间依赖的方法。例如,一个处理客户信息的对象可能依赖于
访问数据库的对象,验证信息,验证用户是否授权。依赖注入可以使客户类实例化并执行这些它依赖对象,尤其当
是依赖是抽象时。
2.15.3.- 主要的模式
下面的设计模式定义了简化流程的架构和开发方法:
控制反转(IoC)。 该通用模式描述了一种支持“插件式”架构,其中对象可以搜寻需要的对象实例。
依赖注入模式(DI)。其实这是控制反转的一个特例。这是一种基于改变类的行为但不改变类的内部代码的技术。
对象实例注入技术有“接口注入”,“构造器注入”,“属性注入”和“方法调用注入”。
截取模式(Interception pattern)。该模式引入了另一种间接的级别。在客户端和真实对象中间加一个对象
(代理)。客户端的行为像是直接和真实对象交互,但是代理拦截并处理真实对象和其他对象的执行。
2.15.4.- 主要的方法
Unity在容器中暴露了两个注册类型和映射的方法:
RegisterType():该方法在容器中注册了一个类型。在合适的时候,建立具体类型的示例。这可能在依赖注入
通过类属性初始化或解析的方法调用时发生。对象的生命周期由方法的一个参数决定。不过不传该参数,类型
会被标记为透明的,意味着容器每次解析时都创建一个新的实例。
RegisterInstance(): 该方法在容器中注册了一个明确生命周期的指定类型的实例。容器在该生命周期返回
实例。不过不传生命周期,示例的生命周期由容器控制。
2.15.5.- 在Unity容器中注册类型
作为注册类型和解析方法的使用,下面的代码中,标记了ICustomerAppService接口,指定容器返回
CustomerAppService类的实例。
C#
//Register types in UNITY container
IUnityContainer container = new UnityContainer();
container.RegisterType();
...
...
//Resolution of type from interface
ICustomerAppService customerSrv =
container.Resolve();
为了理解构造器的注入考虑这样的场景,我们使用Unity容器的解析方法,该类的构造函数有一个或多个
参数(依赖其他类)。因此,Unity容器根据构造函数会自动创建需要的对象。
举例来说,不使用依赖注入或Unity的代码,我们希望修改代码,使用Unity以实现松耦合。这段代码使用
叫做CustomerAppService的业务类:
…
{
CustomerAppService custService = new CustomerAppService();
custService.SaveData(“0001”, “Microsoft”, “Madrid”);
}
C#
public class CustomerAppService
{
private ICustomerRepository _custRepository;
public CustomerAppService()
{
_custRepository = new CustomerRepository();
}
public SaveData()
{
_custRepository.SaveData("0001", "Microsoft", "Madrid");
}
}
C#
public class CustomerAppService
{
//Members
private ICustomerRepository _custRepository;
//Constructor
public CustomerAppService (ICustomerRepository customerRepository)
{
_custRepository = customerRepository;
}
public SaveData()
{
_custRepository.SaveData(“0001”, “Microsoft”, “Madrid”);
}
}
C# (In WCF service layer or in ASP.NET application)
… //(UnityInstanceProvider.cs in our SampleApp)
…
IUnityContainer _container = new UnityContainer;
…
//This is the only call to UNITY container in the whole solution
return _container.Resolve(_serviceType);
…
C#
public class ProductService
{
private Supplier supplier;
[Dependency]
public Supplier SupplierDetails
{
get { return supplier; }
set { supplier = value; }
}
}
3层
在3层设计模式中,用户和他机器上的客户端应用交互。该客户端应用和应用层通信,应用层有业务逻辑
和数据访问逻辑。这样,应用服务器访问第3层,数据库服务器。该模式在富客户端很场景。
下面是描述3层部署模型的图:
多层
在这种情况下,Web服务器和应用服务器是物理分隔的,应用服务器包括业务逻辑和数据访问层。对于网络
安全策略的原因,通常是这种分离是在专业的方式,Web服务器部署在外围网络中而访问应用服务器放在不同
的子网。通常在客户端层和Web层中有另一个防火墙。
下图描述了多层部署模式:
选择层
物理分隔逻辑层会影响性能,虽然这对于可扩展的分布负载的不同服务器是有益的。分隔应用的敏感组件
可以提高安全性。然而,需要考虑的是增加层会增加部署的复杂性,有时会影响性能,所以不要有不必要的层。
在许多情况下,应用中的所有代码都应在同一或负载均衡的服务器上。当你使用远程通信时,性能会影响
通信,数据也需要序列化来在网络上传输。然而,有些情况下我们可能因为安全层限制或扩展性的要求分割功能
到不用的层。
如果是下面的情况选择2层模式:
- Web应用:目标是开发典型的Web应用,具有高性能没有网络安全的限制。如果要增加可扩展性,Web服务器
应克隆到多个负载均衡平衡服务器。
- 客户端服务器应用(CS应用)。目标是开发直接访问数据库的客户端服务器应用。情况有很多种,由于所有
的逻辑层都放在客户端,该客户端可以是电脑。当有高性能要求时这种结构很有用,然而,CS程序架构有很
多可扩展性,维护和故障检测问题,因为所有的业务逻辑和数据访问都在用户电脑上,每个终端用户控制着
配置。这种情况在大多数场景中不推荐。
如果是下面的情况选择3层模式:
- 目标是开发在用户机器上有远程客户端访问的应用,应用服务器上发布有业务逻辑的Web服务。
- 所有的应用服务器都在同一网络中。
- “内网”应用程序,安全性的需求不要求表现层和业务层,数据访问层分割。
- 目标是开发典型的最大化性能的Web应用。
如果是下面的情况选择多层模式:
- 当有业务逻辑不能部署在表现层部署的外围网络中的安全性需求时。
- 有非常多的代码(使用很多服务器资源),需要增加可扩展性,并且这些业务组件和其他层是分开的。