http://www.uml.org.cn/zjjs/201009141.asp
简介
引言
服务层不直接执行任何任务。它所做的就是合理的安排一些列你提供的业务对象。服务层很清楚业务逻辑层,也很清楚领域模型。例如:你使用数据库表模型模式的业务逻辑层,服务层会通过DataSet来进行交互。
很显然,服务层合理的安排业务组件,同时也合理的安排应用的服务、工作流和业务逻辑的其他组件。
服务层的职责
服务层是一个额外的层,是在两个层之间设置一个边界。
服务层的目的是什么?
在业界有很多的应用原则都很重要,在设计软件的时候要注意:分离关注、低耦合、高内聚。当我们讨论在修改一个已有对象的时候,如何才能使得它暴露低耦合,首先想到的是增加更多的抽象。在下图中我们看到了类Action依赖于具体类Strategy,因为Action的一个方法中引用了Strategy类。
如何降低依赖呢?而且还有一条原则:针对接口编程,不要针对实现编程。在设计的时候,应该在Action类中隐藏Strategy类。为Strategy类定义一个接口,然后定义一个工厂类来创建具体的Strategy实例,这样Strategy的修改不再影响Action类,降低了Action类对Strategy类的依赖。
除了高级别抽象的工厂模式,服务层还有很多模式。服务层位于一对接口层之间,保持他们的低耦合和相对的独立,但是可以很好的和他们进行通信。大多数时候,我们看到的服务层都是在表现层和业务逻辑层之间定义一个边界。这是常见的解决方案。
合理的安排系统行为
正如你在上图中看到的,服务层想表现层屏蔽了业务逻辑层的实现细节。但是,他不仅做了这些工作。上图是正确的,但不是全部。
在他的核心中,每一次用户驱动的交互都有两个参与者:表现层实现的用户接口,服务层实现的响应用户行为的模块。这也证明了服务层不仅仅是合理的安排业务逻辑,而且它也和表现层打交道。
任何来源于表现层的交互,在服务层都会找到一个响应。基于收到的输入数据,服务层控制业务组件,包括:服务、工作流、和领域模型,当然了访问数据库访问层也是必须的。
是否服务层只是直接进行数据库操作呢?不是十分准确,业务逻辑会包括工作流和业务服务。业务逻辑应该独立于任何数据库细节。
服务层是表现层和其它层之间的边界,它获取和返回数据传输对象,然后将数据传输对象转换为合适的领域模型类的实例。每一个通过服务层暴露的方法都整合了其他服务、工作流和通过ORM进行了数据库操作。
什么是服务?
抛开相关的技术,服务一词简单的代表一层发送给另外一层的软件请求。服务层,就是一系列服务的集合,在两个相互通信的层中间的一个层。
什么是面向服务?
面向服务是一种将业务处理看做是一系列相互连接的服务的设计方式。面向服务不是技术本身,只是一个完成描述业务如何操作的不同的方式。
服务 vs. 类
在应用的设计中,面向服务允许你使用固定接口的独立组件。这些组件随时可以替换,只要保持接口的协议不发生变化,都不会影响系统。
使用服务层的好处
服务层在用户接口和其它层之间提供了唯一的协议,使得你可以专注于应用逻辑。应用逻辑是业务逻辑的一部分,直接产生于用例。
在服务端,被执行的服务层方法,合理的安排了需要的逻辑,通过协调领域模型、特殊的应用服务、工作流。
没有服务层,表现层就会直接调用业务逻辑层。完成一个任务,就可能需要进行多次的远程调用。对于性能来说,这不是一件好事。
服务层实战
引言
服务层是在交互的两个层中间又定义了另外一个层,典型的是在表现层和业务逻辑层之间。这个中间层只是实现应用的用例的类集合。
服务和面向服务的出现,使得整个解决方案更有价值、更加成功。与表现层相比,服务层提供了松散的耦合,服务层提供商定的协议,可重用性,跨平台的部署。服务向其他类一样,允许你调整你需要的抽象总数。
真实世界的表现层,主要是用户前端。用户做的每一件事都通过表现层和用户界面。
企业级的应用,可能会有多种数据表现接口。一个接口可能就是一个用户界面,也可能是每一个支持的平台,例如:移动应用、web、WPF、windows、Silverlight,或者是其他软件平台。另一个接口可能是后端应用,传入数据或者是获取数据,并且转变他。可能还有一个接口是一个使用应用的连接者代理-系统整合方案中的内部处理逻辑。
服务层响应来自表现层的输入。相应的,表现层不关心另外一端的操作和模块。重要的是模块声明了它能做什么。
表现层和服务层都不包含业务逻辑。表现层只知道服务层提供的粗粒度接口,服务层只知道一系列可行的相互作用的协议,处理本质的细节:事务,资源管理,协调,数据消息。
服务层在现实生活中的例子
SOA的出现,与服务层的出现一致,加强了服务层的概念,使他更吸引人。一些人争论说在多层架构中使用SOA是有创造力的。争论这些是毫无意义的,就好像争论是先有鸡,还是先有蛋。
在实际的使用中,使用服务层的目的,背后的理由,很多程序员和架构师还未能理解。下面,让我们分析一个现实生活中的例子。
我们中的很多人都有做初级程序员的经历。在某些时候,我们还会碰到一个目中无人的老板。老板可能会说:嗨,我们需要马上为客户定制一个系统。
你听到了吗?老板就是表现层。老板关心的是向经理人发送一个简单的命令。在他的眼里,经理人暴露了一个任务和责任的列表。老板不关心经理人实际如何完成任务,但是他知道公司和经理人之间的协议中规定的经理人应该做什么(协议中也会提到,如果经理人不满足要求,就会被替换掉)。经理人就是服务层。最终,经理人协调其他资源来完成这个任务。
如果你左右看看,在现实生活中你会找到很多服务层模式的例子。例如:孩子向父母要零花钱,编辑要求修改文稿,你从ATM中取现金,等等。
什么时候使用服务层
在任何有点复杂的应用中都应该使用服务层。如果在一个简单的文档管理系统,或者是一个快速建立的网站,可能只是存在几个星期的网站中建立服务层很可能会没有回报。
在一个分层系统中,没有理由不使用服务层。一个可能的例外就是简单的前端和一系列只是满足用例的应用服务。在这种情况下,服务层很可能只是一个发报机,没有任务组合工作。简单的服务层还不如直接调用业务逻辑层。
相反的,在你拥有多个前端,而且是大量的应用逻辑,将应用逻辑存放在一个地方,而不是在每个应用接口中都保留副本会更加好。
服务层的优点
服务层增加抽象,解耦两个交互的层。在任何你想获得一个更好的系统的时候,你都应该构建一个服务层。服务层使用粗粒度的、远程接口最小化表现层和业务层之间的通信次数。
通过服务(例如:WCF)来实现服务层的时候,你能感觉到其他的好处,例如:通过配置来改变绑定信息。
服务层的缺点
因为抽象是服务层的主要优点,对于简单的系统可能有点过头了。
服务层不是必须使用例如WCF这样的服务技术。在ASP.NET中的表现层,你可以把code-behind类叫做服务层。这时候使用WCF代替普通的类,可能有点过头了,很可能会降低性能。考虑在你的系统中使用WCF,需要考虑性能,如果性能下降的无法忍受,请选择其他服务层技术。
服务层适用于什么样的场景
表现层调用服务层。是一个远程调用,还是一个本地调用?
Flower关于分布式对象设计的推荐是:不要分散你的对象。我们可以理解为:“除非是必须的,或者是有好处”。就想你所知道的,必要性和好处实际容易变化的,难以量化,但是在一些特殊的方案中,他们很容易识别。
因此,在什么场景适合服务层呢?通常来说,如果你有一个服务层,可以很容易的跨层移动,那是一件好事。在这点上,例如WCF这样的服务技术是一个正确的工具。
如果客户端是web网页,服务层最好是位于本地的web服务器上。如果站点成功了,你可以将服务层分离到独立的应用服务器,来增加扩展性。
如果客户端是桌面应用,服务层会部署到一个独立的物理层,并且通过远程来访问。这个方法类似于Software+Services的架构,客户端除了GUI什么都没有,全部的应用逻辑都在远端。如果客户端使用Silverlight,服务层会发布在Internet上,你可以建立一个完美的RIA(Rich Internet Application)应用。
实战服务层模式
实现服务层依赖于两个技术选择。第一个选择就是那什么方法或者是调用来作为服务层的基础。使用普通的类还是服务?如果选择服务,又该选择哪种服务的实现技术?在windows或者是.NET平台,你的选择比较少。你可以选择WCF service、asp.net xml web service,或者是类似于REST之类的服务。
如果你对于.NET框架有一些了解,你应该知道创建一个wcf service或者是web service,就好像创建普通的类,然后添加一些attribute。当然,还有很多细节需要考虑,例如:web service的WSDL(web service description language)web服务描述语言,WCF的配置和数据协议。服务最终是一个包含其他内容的类。
设计服务层的类
服务层使用的类应该暴露一个协议,无论是WCF的协议还是实现接口。实现接口是一个比较好的做法,因为它更清晰的描述了一个类可以做什么,将会做什么。接口使用DTO接收和返回数据,推荐粗粒度的方法,以便它可以最小化网络传输,最大化网络吞吐量。
如何将需要的方法映射为接口和类呢?在用例的基础上,列出一系列所需的方法,然后将他们分为逻辑组。每个组建立自己的服务或者是类。
大多数情况,你的结果是问题域的每个实体建立一个服务类,OrderService,CustomerServcie等等。这些都是应用需要的。但是,如果用户的行为相对比较小,行为比较相似,这时候一个服务类可能就够用了。否则,一个单一的服务类会迅速变大,会很难以维护和变化。
通常来说,我们认为没有严格定义的的准则,例如:每个实体都要有自己的service,或者是一个service需要满足用户所有实体。服务层在系统的表现层和其他部分之间进行调节。服务层包括了粗粒度的服务(也是用例驱动的),在他们的编程接口中,实现了用例。
服务层和系统的其他部分相比,是独立的,对于表现层来说只是一个调用内部处理流程的接口。如果用例有变化,你很可能只是修改服务层而不用修改业务逻辑。在一个相对较大的应用中,对于服务层的编程接口来说,你应该先看一下你的用例,然后使用通用的方法组织类中的方法。
实现一个服务层的类
我们推荐每个类应该实现一个接口。如果你选择WCF,这是严格要求的,而且从整体上来讲是一个好方法。
代码
[ServiceContract (Namespace="http://www.dsn.com/wcf")]
public interface IOrderService
{
[OperationContract]
bool Submit(BeautyCode.Entity.Order order,BeautyCode.Entity.CommunUser user,out BeautyCode.Entity.CException exception);
[OperationContract]
List<BeautyCode.Entity.Order> FindOrders(BeautyCode.Entity.OrderFind find, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception);
[OperationContract]
BeautyCode.Entity.Order GetByOrderSeqNo(string orderSeqNo, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception);
}
代码
[ServiceBehavior (InstanceContextMode= InstanceContextMode.PerCall )]
[AspNetCompatibilityRequirements (RequirementsMode=AspNetCompatibilityRequirementsMode .Allowed )]
public class OrderService:ServiceBaseImpl , IOrderService
{
private void ThrowException(BeautyCode.Entity.CommunUser user)
{
}
public bool Submit(BeautyCode.Entity.Order order, BeautyCode.Entity.CommunUser user, out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
public List<BeautyCode.Entity.Order> FindOrders(BeautyCode.Entity.OrderFind find, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
public BeautyCode.Entity.Order GetByOrderSeqNo(string orderSeqNo, BeautyCode.Entity.CommunUser user,
out BeautyCode.Entity.CException exception)
{
throw new NotImplementedException();
}
}
首先假设接口直接使用领域模型对象。在上面的例子中的Order类,代表我们在领域模型中建立的order实体。如果我们使用实际的领域模型对象,我们假设在业务逻辑中使用领域模型的模式。如果你使用数据表模型的模式,上面代码中的order类应该替换为DataTable。我们一会在回到DTO的讨论上来。
submit方法需要整合应用内部的服务,检查用户的账户状态,检查订单中商品的有效性,同步厂商的商品信息。submit方法是一个典型的服务层方法,它对表现层提供了单一的协议,与不同的领域模型和业务逻辑进行多个步骤的操作。
FindOrders方法返回一个order集合,GetByOrderSeqNo返回一个特定的order。此外,假定我们在业务逻辑层使用领域模型的模式,没有专门的数据传输对象。很多的架构师推荐在服务层不出现Create、Read、Update、Delete(CRUD)方法。FindOrders和GetByOrderSeqNo方法本质上来说就是CRUD中的Read方法。
而且,方法依赖于你的用例。如果用例中有用户点击一个地方显示订单列表,或者是单个订单的详细信息的需要,那么这些方法就必须要有。
处理角色和安全
FindOrders方法应该只是返回当前用户可以看到的订单。
如果你把安全当回事来考虑的话,就应该在服务层的每一个方法中检查调用者的身份,对未授权的用户拒绝方法的调用。
如果你不想在每个方法中重复验证用户的身份,那就需要在服务层的方法上面添加attribute来实现身份验证。
服务层起到一个看门人的作用,通常不需要将role信息传输到业务逻辑层中去验证,除非有好的理由。但是,如果有这么一个好的理由,使得你不得不将role信息传到业务逻辑层中,也是不错的。
服务层的相关模式
1 引言
我们把服务层看做是暴露给用户界面的一个服务集合。大多数时候,我们会发现服务层的方法很容易满足用户的行为。在大多数企业应用中,CRUD是常用的操作。有的时候在一次操作中会处理多个实体。
服务层包括角色管理,数据验证,通知,调整返回给用户界面的数据,或者是整合系统可能的需求。
在谈到这些的时候,一些设计模式可能会有帮助。下面是一些在实现服务层的过程中有帮助的模式。
2 远程外观模式Remote Facade Pattern
远程外观模式是用来修改已经实现的方法的粒度的,外观模式没有实现任何新功能。它只是在原有的API之上包装一层不同的外观。为什么会需要外观模式呢?
2.1 使用外观模式的动机
外观模式改变一个已经存在的对象的访问方式。举一个货运商在线服务的例子。每一个货运商对于注册运输的货物、跟踪货物和其他服务都有自己的API,他们的细节在你的应用中可能都用不着。通过建立一个统一的API,你可以隐藏货运商API的细节,使得你的应用有一个清晰的接口。
换句话说,如果你想要你的接口处理一个简单的接口,这些必要性强迫你创建另外一个外观。实际上,这就是经典的远程外观模式的精确定义。
外观模式的好处就是允许你在一系列定义好的细粒度对象之上,定义粗粒度的接口。
面向对象使得你创建了很多小的对象,职责分离,细粒度对象。但是这些对象不适合分布式。为了是这些对象有效,你需要做出一些调整。你不想改变细粒度对象的任何实现,但是你需要通过这些对象可以执行一批操作。在这种情况下,你需要创建新的方法,移动更大的数据。当你这么做的时候,实际上就是修改了已经存在的接口粒度,也就是创建了外观模式。让我们回到货运订单的例子。通过你自己的API你可以发送多个订单,不用使用货运商的API来发送单个的订单,这里我们假设货运商不支持多个订单的API。
2.2 远程外观模式和服务层
经过设计,服务层本质上拥有了粗粒度的接口,因为它更趋向于抽象一定数量的细小操作,来给客户端使用。在这点上,服务层已经是一种位于业务逻辑层和领域层对象之上的外观模式。
public interface IOrderService
{
void Create(Order o);
List<Order> FindAll();
Order FindByID(int orderID);
}
如果你使用WCF,需要在服务层接口上添加协议attribute。
代码
[ServiceContract]
public interface IOrderService
{
[OperationContract]
void Create(Order o);
[OperationContract]
List<Order> FindAll();
[OperationContract]
Order FindByID(int orderID);
}
所有非基本类型的数据都需要添加datacontract标记。在WCF运行的时候,这些标记会为指定的类型自动创建数据传输对象DTO。
[DataContract]
public class Order
{
[DataMember]
int OrderID {get; set;};
[DataMember]
DateTime OrderDate {get; set;};
}
如果你使用asp.net xml web service。上面的类不要添加任何标记,只需要在服务层类的方法上添加WebMethod标记就可以了。
在使用外观模式的时候,你可能会需要更粗粒度的接口,你也可能会用到DTO,或者你都会用到。下面的接口就和领域对象解耦了,更加聚焦在和表现层的交互上。
public interface IOrderService
{
void Create(OrderDto o);
List<OrderDto> FindAll();
OrderDto FindByID(int orderID);
List<OrderDto> SearchOrder(QueryByExample query);
}
OrderDto可能是领域类Order的子集或者是包含Order(可能会整合依赖的对象OrderDetail),QueryByExample是一个专门的类,会传输用户界面的一些条件给服务层,我更喜欢定义写Find类,例如OrderFind。里面就是一些查询条件,例如:时间、编号、金额等等。
实际上,如果用户界面改动比较大的话,就需要修改服务层。你可能会需要重构服务层,或者是在外面再次的使用外观模式来包装。
3 数据传输对象模式
DTO(Data Transfer Object数据传输对象)仅仅是一个跨越应用边界传输数据的对象,主要的目的是最小化网络的往返。
3.1 使用DTO模式的动机
有两个主要的动机。一个是在你调用远程对象的时候,最小化网络的往返;另外一个是在前端显示和后端的领域模型之间维护一个松散的耦合关系。
在多数情况,DTO都只有属性,没有操作。DTO在领域模型方案中扮演着重要的角色。不是在所有的情况,表现层都可以直接使用本地的领域对象。如果是服务层和表现层在同一个物理层的话,例如表现层是web网页的形式,可以直接使用。如果服务层和表现层不在同一个物理层,最好不要通过领域对象来交换数据。主要的原因是你的领域对象之间可能是彼此依赖的,甚至是循环引用的关系,这会严重的影响它们的序列化能力,甚至是序列化失败的问题。
关于为什么需要DTO以及什么时候需要DTO的个人理解,仅供参考:
都在同一层的话,不存在对象通过网络传输的问题,所以可以直接使用领域模型,而且效率还高,没有网络传输和延迟。
不在同一层的话,就存在网络传输的问题,领域模型中既包含了数据,也包含了操作,数据可以在层之间传输,可是操作时不能传输的。而且为了解耦领域模型中的对象和传输的对象,而且领域模型的粒度较小,传输领域模型的话,可能需要多次调用,才可以满足一次界面的显示,这样会增加网络的往返次数。
同时,界面显示的内容可能来自几个领域模型对象,也可能是一个领域模型对象中的几个属性。所以才单独建立DTO用来在层之间传输数据。
独立出来的话,就可能就会涉及到序列化的问题。
例如,要考虑到WCF和asp.net xml web service的xml序列化都不能处理循环引用的情况。同时,如果你使用领域模型,你会发现每一个Customer对象都有多个Order对象,每一个Order对象都会对应一个Customer对象。在复杂的领域模型中,循环引用很常见。
循环引用:
循环引用就是两个对象相互引用,每个对象的都有对方类型的属性存在,这样就造成了循环引用。也就是下面的情况。
代码
public class Order
{
public string OrderSeqNo
{
get;
set;
}
public Customer Customer
{
get;
set;
}
}
public class Customer
{
public List<Order> Orders
{
get;
set;
}
}
DTO可以帮助你避免这种风险,使你的系统更加整洁。但是,它也引入和新的复杂等级。我们需要额外的层,DTO适配层。
3.2 数据传输对象和服务层
当你在开始的架构会议中讨论DTO的时候,你经常会听到反对使用DTO的声音。数据传输对象可能会浪费开发时间和资源。问题是DTO的存在有他的必要性。可以不使用它们,但是他们在企业级架构中任然扮演重要的角色。
在理论上,我们提倡在两个层之间发生通信的时候使用DTO,包括表现层和服务层的通信。另外,我们还提倡在每一个截然不同的用户界面使用不同的DTO,甚至是不同的DTO请求和相应。
在实际的使用中,事情可能有所不同。DTO意味着在服务层添加新的一层代码,随着而来的是复杂性。只要没有更好的选择,这样做是可以接受的。我们在强调DTO的花销的时候很容易低估它的花销。当你拥有上百个领域对象的时候,2-3倍的类就会使噩梦了。
只在使用DTO的好处很明显,而且必要性很明显的时候再用DTO,否则就直接使用领域对象。
使用DTO我们还需要内部的、项目定制的ORM层。
对于ORM层,我们有很多工具可以选择,商业的和开源的。WCF和asp.net xml web service在序列化数据的时候生成DTO,但是对于数据格式的控制,它们提供的功能有限。通过 WCF中的[IgnoreDataMember]和asp.net xml web service中的[XmlIgnore ],你可以将一些属性从DTO中删除,也就是在DTO对象中没有这些属性。但是在请求和相应需要不同的类的时候,或者是不同的用户界面使用不同的DTO的时候,你没有自动产生特定的DTO的方法。到目前为止,还不存在这样的向导。需要我们手动来完成,自己定义DTO。
4 适配器模式
当你在分层系统中使用DTO的时候,你可能会为不同的接口而调整领域模型。你需要实现适配器模式,一个经典而且很流行的模式。适配器的本质是将一个类的接口转换为用户希望的另外一个接口。
4.1 使用适配器模式的动机
适配器的职责是将数据表现为另外一种格式。例如:适配器会将从数据库中读取的bit格式的列,转换为用户接口中的boolean,来方便使用。
在一个分层的方案中,适配器模式被用于将一个领域对象转换为DTO对象,或者是反过来。在适配器类中没有复杂的逻辑,但是你需要一些适配器类来赋值DTO对象。
4.2 适配器模式和服务层
给每一个DTO配置一个适配器类,必然会增加开发的成本。
在评估DTO的时候,对于将要产生的一大丢类,你应该持续的考虑维护他们的问题。你要记住,在每一个适配器类中, 需要有两个功能,一个是从领域对象到DTO;一个是从DTO到领域对象。