业务逻辑层是系统的核心,业务逻辑层的设计的选择将影响到其它层---特别是持久化层数据访问层。这两层对项目成败产生决定性的作用
- 业务逻辑层负责表现层和数据层之间的信息交换。业务逻辑层的输入和输出不一定是业务对象。很多时候架构师更倾向于使用数据迁移对象(DTO)。
- 业务对象(Business Object,BO):数据+行为
- 数据迁移对象(Data-Transfer Object, DTO):只有数据(业务对象去掉行为),没有逻辑行为。业务对象是所需数据的投影。
- DTO与DO的取舍一直存在争议:
正面:DTO能减少层之间的耦合,使系统更加整洁干净。
反对:数百个业务对象,不能仅仅为了系统的干净而增加不必要的对象。这种情况,DTO
- Tier:物理位置:代码运行的位置
Layer:一般用来组织代码。
多个Layer可以共存于同一个Tier。
- 每层都可以看成一个黑盒子,这个黑盒子对输入和输出有定义清晰的契约(接口)
- 良好的分层封装,可用测试驱动开发工具,用“模拟”层临时替代尚未构造的真实组件。 这样也可以并行开发两个有依赖关系的层
- 物理层:系统中多个物理层并不算是个好的特性
穿越进程边界比进程内调用要慢100倍。
- 远程软件最好不要提供太过详细的接口,
软件内部最好有细颗粒度的接口
- 远程使用的业务逻辑层必须由一个粗颗粒度的接口封装---一般使用门面(Facade)模式。
服务层?
- 现实中,本应属于业务逻辑层的逻辑可能与其他层交织在一起。导致这些的原因:
系统功能上的灰色地带导致的,即不知道某些功能应该放在那一层。
常见的灰色地带:
[1.]数据格式化:电话、货币的显示格式这个属于业务逻辑还是界面?倾向于放在业务逻辑层中。
如果客户端格式化,表现层必须知道很多上下文和系统的逻辑
如果是业务逻辑层必须提供合适的方法,供客户端调用并获取直接显示的字符串。
那原始数据如何存储,最佳选择—存储原始数据,在需要时在格式化化。
[2]CRUD操作
数据访问层包含了逻辑。
例如删除客户时,一些跟该客户相关的信息怎么处理,这些本属于业务逻辑层的逻辑。
不要能这样做。
访问层应该仅仅用来处理数据库的相关操作,甚至不应该了解“客户”这个实体的存在。
[3]存储过程:
坦率的说,将业务逻辑写到存储过程中,常见的原因是这样做更简单快速。但是如果能将业务逻辑从存储过程中提取出来,那么逻辑将极大地方便更新、测试和调试,提高数据库的可以移植性。
如果决定使用存储过程,也要注意不要添加太多是逻辑。
- 四种组织业务逻辑的模式:
事务脚本模式
表模块模式:为每个数据库表定义一个业务组件,封装成一个类表示,并将行为封装在这个类中,关注的是方法而不是对象。没有太多的关注业务,而是关注数据表。抹平了面向对象和关系模型之间的差别,不啻一场噩梦。这个问题由来已久,因此产生了各种转梦的O/RM工具。
活动记录模式:活动记录就是一个封装了数据库表或视图的一行的对象,对像中可以同时包含数据和行为(逻辑方法),
若你想根据Orders表创建活动记录对象,那么就要提供一个Order类,该类的属性应该与Orders表中的列一一对应。
框架:LINQ-to-SQL
领域模型模式:
以上三种模式是以数据位核心,因此驱动业务模型设计的并不是业务本身,而是业务中使用的数据。以数据为中心的方法并不能改很好地适应规模的增加,因为当系统的复杂性达到摸个极限后,哪怕在增加一些新的需求,都成为难以处理的问题。领域模型正是为此类场景准备的。
领域驱动设计是一种设计软件设计方法,是一种考虑问题的方法,一种将问题领域放在所有事情中的首要位置的方法。
领域驱动设计并不一定需要使用领域模型模式,但是领域模型模式是最佳的选择。
领域模型模式力求让对象模型能够和系统的概念模型匹配起来,遮掩那个的对象模型就叫做领域模型。
领域模型描述系统中涉及的实体,捕获实体之间的关系及数据在其中的交流过程。
领域模型中的实体将成为一系列彼此相连的对象,一系列带有方法和属性的类,领域模型中的每个松溪都需要用一个带行为的类来表示,这样每个方法调用的顺序图都是一些系列对象之间的调用的组合。
领域模型完全与数据库独立,是一个尽可能贴近真实流程的模型。
架构师和领域专家间的紧密合作,保证企业流程的各个方面都被所有人理解,需求可以被正确完整的提出。
将抽象的模型映射到领域模型和数据模型,是领域模型实现的困难所在,也是它比其他模型请打更加强大。
优势:
概念是用类来建模,充分利用的面向对象设计的优势
无需收到数据库结构的限制,实体不会察觉底层使用的持久化,机制这样日后就是替换数据访问层,也不必担心会影响到业层。
模型是和业务相关的,且仅和业务相关。
劣势:
优势就是其劣势,对象和关系型数据库之间的不匹配。创建两者的转化代价非常高。如果没用O/R映射工具,例如NHibernate或Entity Framework,,那么将很难实现领域模型模式。
实战:
将抽象模型中实体用类表示,这与活动记录模式的区别在于,领域模型中是完全与数据库无关的。
在于领域专家讨论时遇到的每个流程和实体用类来表示。其中包括
参与者(Customer)和辅助实体(Address)
领域对象不包含:任何将其状态保存到存储介质中的逻辑,所有创建领域对象的代码都是在领域模型之外的业务逻辑层的其他类型中。
领域对象中的方法仅用来实现业务逻辑,需要什么的方法取决于领域对象在整体模型中的角色。
持久化透明(Persistent Ignorance,PI):属于使用O/RM工具来设计领域模型并将类型映射到关系型数据库时,持久化透明才是领域模型中需要考虑的特性。若领域模型和类型映射到关系型数据由手工完成,那么讨论该主题没多大的意义。
PI是单一职责的应用,即领域对象中不应该包含持久化代码。
持久化透明的类(Plain Old CLR Object, POCO):在EF1.0中,领域类型并不完全的POCO,
,因为必须要让类型派生自一个框架提供的基类(或实现一系列的接口)
这样的弊端:
如果框架要求你必须派生自一个持久化不透明的类:不能继承其它类,为你的领域类型添加新的职责。
问题:如何持久化。Net类型,如何才不影响性能?
- 实体和对象:
值对象是实体的一面,仅能依赖某个实体存活。
- 仓储模式:
领域模型中的类包含数据和行为,
但是行为仅仅用来表示实体的逻辑,不包含加载数据的相关逻辑(访问数据库)。故引入仓储模式。是领域模型和数据访问层之间的一个抽象层。
Public interface IRepository<T>
{
Void Save(T obj);
Void Add(T obj);
T FinBy(Guid id);
List<T> FindAll();
}
好处:
领域模型和持久化逻辑拆分,再为仓储提取统一的接口,用工厂模式封装各钟仓储,这样领域模型可以配合任意的数据访问层及数据提供器:
UserRepository rep = Repository.GetFactory<Cunstoner>();
List<User> user = rep. FindAll ();
仓储实现:
在领域模型模式总,执行数据库操作的代码叫做数据映射器(Data Mapper, 是使用一系列的类将逻辑实体及其所有的关系映射到数据表和记录中(通常在关系型数据库)),
在最坏的情况下,映射器的实现有手工完成,这也是领域模型的最主要的难点。非常困难并且耗时,所有借助工具是不二之选。O/RM工具也应运而生。
注意:
实体和对象的区别:仅为实体对象提供仓储对象和映射器,在处理实体时自然处理实体的值对象。
- 特例模式:
Public sealed Class MissingUSer :User
{
//查找失败或抛出异常,返回改对象,不返回null
}
- 工作流
可以有不同层次的工作流,业务逻辑层的工作流可以位于领域对象之间,或服务层中。工作流是必须的,不过如何实现业务逻辑层中取决于工作流的复杂性和问题领域本身,这是需要平衡的问题。通常比较难以抉择,这时,架构师的经验和知识又再一次变成了项目的发展关键。
- 业务逻辑层是系统的核心,业务逻辑层的设计的选择将影响到其它层---特别是持久化层数据访问层。这两层对项目成败产生决定性的作用
- 通常,服务层为表现层定义了一个接口,让表现层触发系统的预定义的系统操作。
- 服务层是表现层的结束,业务逻辑层的开始的边界。降低两层间的耦合。
- 服务层的功能是组织各个业务对象,为表现层提供粗颗粒度的操作。
- 服务的目的:
降低表现层和业务逻辑层的耦合
组织系统的行为:
服务层将组织业务逻辑层中的组件---服务、工作流、领域模型对象、并根据需要调用数据访问层?(本人不明白为什么要访问数据访问层)
业务逻辑层中需要使用数据访问层的有:业务服务、工作流等。
业务逻辑层中唯一需要完全和数据库细节分离的那部分就是领域模型。
表现层---DTO—服务层—DTO 转换器—领域模型、专门的应用服务、工作流等
实践:
数目庞大,不为每个操作定义一个DTO,而是将领域模型去掉行为或直接使用领域模型。
- SOA
在SOA的世界里,应用程序是有一系列独立的服务经过组合集成得到的。
最小的构件是服务,而不是组件。
- SOA中的服务的定义:
服务=封装(类+运行环境)
运行环境提供的额外的功能让类得到这些额外的功能并没有包含在类中。
运行环境:注入搜索、绑定、安全、日志、可靠的消息传递、事务、与其他服务的消息交互。
这些运行环境通过一些声明的方式给出,因此整个系统的架构都变得灵活、完全可以定制。
- 服务层中的服务
好处:
没有服务层,表现层直接调用逻辑层,这就会造成太多细颗粒度的远程调用,导致过多的交互,影响性能。
- 宏服务和微服务
宏服务:粗粒度的服务。是按照用例来组织操作,不包含任何核心业务相关的领域逻辑。
微服务:表示特定的服务、领域逻辑的功能
实践:
推荐使用服务层、但不是非得这样做,如果某个操作部需要组织太多的步骤,表现层就理所当然的可以直接调用应用程序服务(绕过服务层)。
表现层和服务层都不(或者是不应该)包含业务逻辑
表现层仅仅了解服务层提供的粗粒度接口
服务层仅仅了解一系列可用的交互方式,并关注一些具体细节(必要的事务、资源管理、协调、数据转换)
- 何时使用服务层
有多个前端且应用逻辑复杂:最好通过应用逻辑而不是为每个钱的换重写一遍。
如果需要将服务分为宏服务和微服务,那么就应该使用服务层
- 服务层的优势:
如果服务层是通过服务(例如:WCF服务)来实现,可以得到额外的好处:
方便让该层在远程调用和通过配置修改绑定设置等。
- 服务层的劣势:
抽象是服务层的主要长处,在简单的系统中服务层有过度设计之嫌。
服务层并不一定要使用WCF等专门的服务技术来实现。如果表现层和服务层在同一台服务器上,使用WCF得留意性能指标,若性能影响过大,有必要考虑另一种技术。
- 服务层实践:
创建服务就是个普通的类加上一些特性修饰而已。
WCF ---------DataContract
WebService---------- WSDL
设计服务层的类:
基于用例准备方法,随后分成逻辑相关的组,为每组封装成类或服务。
用例有变化,那么需要修改服务层,不过业务逻辑可能不受影响。
总之,对于服务层的编程接口,先查看用例,使用常识组织方法到类中。
服务层类的实现:
强烈建议为每个服务类都提取一个接口.
- 处理角色和安全
处理角色和安全,除非特殊需要,没必要将角色检查放在业务逻辑层中。
如果确实需要放在业务逻辑层中,最好抽取到服务层中。
- 对于复杂的场景,服务层可以很好的将领域逻辑和应用逻辑分开。
- 领域逻辑和应用逻辑
领域逻辑是用来表达业务概念、保证业务规则、存放业务数据和状态,由领域对象来实现,且不知道这些对象的持久化方式。
应用逻辑组织业务对象和应用程序服务,以便满足表现层的需要,并不需要了解业务规则,只是协调任务将工作委托给下层的一系列协作的领域对象即可。
应用逻辑处理问题的工作流程,实现于服务层中。
为什么要在需要两种逻辑:
应用逻辑很明显要依赖于应用程序的用例和用户界面,将这些逻辑放在领域模型中,那么领域模型的类型自然不会有太好的重用性。
不过这是有代价的,并不是使用所有的情况。
本人认为最特别注意的话:
领域逻辑的实体大多是独立的对象,对其他的实体没有太多的了解。
实体中的交互(例如:创建、删除、或命名其它实体执行工作)一般由服务层组织,并由领域逻辑管理。
领域逻辑中实体可以直接交互的唯一情况就是实体之间存在在逻辑上的依赖,例如:Order和OrderDetail之间。
服务层所提供的应用逻辑一般包括:
角色管理、数据验证、通知、数据转换修改。
- 服务层中的相关模式
远程门面(Remote Facede):需要一套简化的接口。
数据迁移对象(DTO)模式:
DTO(只有get,set)就是一个携带数据穿越应用程序边界的对象,主要用于减少数据交换的次数。
主要用于2个场景:
一是降低远程调用的交互次数;
二是降低前端和领域模型中类的耦合。(领域模型不一定都满足界面的数据或界面的数据是几个领域模型中的某部分的组合,还有的就是DTO可能还包括非领域模型中的其它信息,如果使用领域模型作为DTO,必须修改,两层间增加了耦合)
表现层直接用领域模型的条件:
仅表现层和服务层部署在同一位置(例如,web表现层中),如果服务层位于另一个物理层,有可能无法使用领域模型进行数据交换。
其主要的原因是领域模型对象之间可能存在彼此依赖甚至是循环依赖,这影响到对象的可序列化能力。
适配器模式:
每个数据迁移对象都需要一个双向的适配器(实现领域对象和DTO的转换)。
平衡点:
不打算用DTO,可以把领域模型放在同一的程序集中。
在面向服务的世界中,一个完全使用DTO的解决方案可以称为完美。但现实中这样过于偏激,不切实际。
- SOA:
原则:边界清晰、服务自治、使用契约,而不是类。
目标:互操作性
- 数据库独立性:
将数据访问层作为一个黑盒子。提供统一的接口,从配置文件汇总动态获取当前数据库访问组件的细节,
- 对象/关系阻抗失调 :对象模型构造出的一张对象图,对象模型与关系型数据库就需要映射。
数据访问层不仅需要将类持久化到多种数据库,还要能够到不同结构的数据表。
- 数据访问层的4个职责:
CRUD服务
查询服务
事务管理
处理并发
整合以上的高层次的类叫数据上下文(Data Context)
CRUD服务
为对象模型中的没个类型,都为其创建专门的映射类并实现接口实现该CRUD等数据库操作。
CRUD服务的实现:数据映射器(DataMapper)模式
查询服务
CRUD服务中的R表示读取。有些高级的查询需求CRUD的查询应付不了。
现实问题:
以Id查询、以名字查询、以地址查询等用户界面需求。就等提供好多这样的方法。
更好的做法是:
定义一个通用的查询对象,适应UI界面动态生成查询。
不是SQL的替代品,而是更加面向对象灵活的动态生成数据库所需要的SQL代码。
可以看做是一个基于SQl语句的StringBuilder的替代品。
查询对象(QueryObject)模式的应用:
查询对象类---查询条件类结合;
查询条件类---属性名、值、逻辑操作符(表明属性名和值的逻辑关系)
仓储模式内部可以用这查询对象。
查询服务
现实问题:
指导原则:尽量减少与数据库的交互
不是每次应用程序的数据变化都要去操作数据库。
应该有个机制去跟踪在一个工作单元内对应的应用程序数据所有修改,这样以后统一一次性保存到数据库。
做法:
数据访问层中引入工作单元的概念。
处理并发
现实问题:
事务性—工作单元模式解决;
并发------多用户问题(最后写入者获胜)
解决:
乐观的并发处理:自由更新,若已被修改,更新失败。
乐观的并发处理需要专门的数据映射器,以便为特定的实体创建专门的代理,例如
UserProxy 来替代User,前包含该条记录在从数据库中读取时的元素数据,而后者去不包含此类信息,这样将实例写回数据库前,讲在SQL语句中添加Where语句。
- 数据访问层和其它层
与业务层:
领域模型模式中,数据访问层属于业务层的一个补充,一般有服务层使用。
与服务层
特别注意:
数据访问层和领域模型并不直接通信,而是靠服务层来协调,这也是架构上的关键之处。
服务层(也叫应用层)通过DTO(或其他方法)获取表现层输入,然后组织业务层中的领域模型、应用服务、工作流等实现所有用例。
同时,服务层使用数据访问层初始化化领域模型的数据、跟踪修改、识别区分对象和更新提交到底层存储。
与表现层
表现层直接与数据访问层唯一理由是:接受开发时间。但是,不应该这样做。
- 数据访问策略:
Repository模式 :将业务实体和底层数据基础设施完全隔离
Data Acesss Object模式:与Repository模式不同的是:没有隐藏接口背后是数据表这一事实,而且通常会为数据库中的每张表创建一个DAO类。
由于DAO与数据库之间的一对一的匹配关系,因此DAO很好的匹配了Active r Recordhe
和Transaction Script业务模式。
- 数据访问层的实现:
提取接口,方便切换不同的实现。
O/RM或其他DBMS执行访问任务模块都实现该接口。
同时提供一个工厂返回当前的数据访问层。
为了确保完全不依赖于数据库结构,数据访问层和其它使用者将使用业务实体(领域模型对象、活动记录模型中的对象、强类型的DataSet或普通的数据迁移对象)来通讯。
这样的优势是:
支持多种数据源,切换数据库(如换到Oracle,只需添加一个Oracle数据访问层)
分离接口模式要求:接口和实现在不同的包中(dll中)。
各种实现:
SQL Server DAL , NHibermate DAL等实现DAL接口
DTO要放在一个dll中吧
使用者定位如何那个数据库访问层,有两种模式:
插件模式、控制反转模式。
- 插件模式:
服务层应用只引用DAL接口的程序集,不引用实现DAL接口的数据库访问层组件的程序集。数据库访问层组件的程序集根据配置动态加载的内存,在。Net中这需要反射功能 +工厂模式+配置文件,切换无需重新编译代码。
本人:插件模式:使用者New个工厂,通过工厂创建出实现了DAL接口的实例(具体是什么实例,使用者不知道。
服务定位器:使用者有一个服务定位器的引用。
可以看到插件模式和服务定位器模式中使用者都自己管理自己的依赖
依赖注入:连这个工厂都不需要了,容器将在使用者外部创建,由容器创建使用者并注入使用者的依赖部分。用3种依赖注入方式。使用者不用管理自己的依赖。
服务定位器VS插件模式
难以区分,效果一样管他呢。
- 控制反转模式:
比插件模式更好的选择:
<unity>
<containers>
<!--容器-->
<container name="defaultContainer">
<!--映射关系-->
<register type="ThreeKindIocByUnity.IA, ThreeKindIocByUnity" mapTo="ThreeKindIocByUnity.A, ThreeKindIocByUnity"/>
<register type="ThreeKindIocByUnity.IB, ThreeKindIocByUnity" mapTo="ThreeKindIocByUnity.B, ThreeKindIocByUnity"/>
<register type="ThreeKindIocByUnity.IC, ThreeKindIocByUnity" mapTo="ThreeKindIocByUnity.C, ThreeKindIocByUnity"/>
<register type="ThreeKindIocByUnity.ID, ThreeKindIocByUnity" mapTo="ThreeKindIocByUnity.D, ThreeKindIocByUnity"/>
<register type="ThreeKindIocByUnity.IE, ThreeKindIocByUnity" mapTo="ThreeKindIocByUnity.E, ThreeKindIocByUnity"/>
</container>
</containers>
</unity>
推荐使用控制反转的理由:
【1】 插件模式使得使用者和工厂之间创建了依赖,进而可能导致问题。
【2】 太过复杂的功能,插件模式难以实现。
【3】 控制反转最大的优势是:
有现成的框架支持,接管很多底层操作,
提供其他的服务和辅助功能(例如,面向切面
- 数据上下文接口(CRUD服务、查询服务、事务管理(这里没并发部分,并发奖状每个持久化方法中具体实现中管理))