分层架构中的服务层-服务层的相关模式
2010-09-07 07:14 Virus-BeautyCode 阅读(3944) 评论(10) 编辑 收藏 举报服务层的相关模式
1 引言
我们把服务层看做是暴露给用户界面的一个服务集合。大多数时候,我们会发现服务层的方法很容易满足用户的行为。在大多数企业应用中,CRUD是常用的操作。有的时候在一次操作中会处理多个实体。
服务层包括角色管理,数据验证,通知,调整返回给用户界面的数据,或者是整合系统可能的需求。
在谈到这些的时候,一些设计模式可能会有帮助。下面是一些在实现服务层的过程中有帮助的模式。
2 远程外观模式Remote Facade Pattern
远程外观模式是用来修改已经实现的方法的粒度的,外观模式没有实现任何新功能。它只是在原有的API之上包装一层不同的外观。为什么会需要外观模式呢?
2.1 使用外观模式的动机
外观模式改变一个已经存在的对象的访问方式。举一个货运商在线服务的例子。每一个货运商对于注册运输的货物、跟踪货物和其他服务都有自己的API,他们的细节在你的应用中可能都用不着。通过建立一个统一的API,你可以隐藏货运商API的细节,使得你的应用有一个清晰的接口。
换句话说,如果你想要你的接口处理一个简单的接口,这些必要性强迫你创建另外一个外观。实际上,这就是经典的远程外观模式的精确定义。
外观模式的好处就是允许你在一系列定义好的细粒度对象之上,定义粗粒度的接口。
面向对象使得你创建了很多小的对象,职责分离,细粒度对象。但是这些对象不适合分布式。为了是这些对象有效,你需要做出一些调整。你不想改变细粒度对象的任何实现,但是你需要通过这些对象可以执行一批操作。在这种情况下,你需要创建新的方法,移动更大的数据。当你这么做的时候,实际上就是修改了已经存在的接口粒度,也就是创建了外观模式。让我们回到货运订单的例子。通过你自己的API你可以发送多个订单,不用使用货运商的API来发送单个的订单,这里我们假设货运商不支持多个订单的API。
2.2 远程外观模式和服务层
经过设计,服务层本质上拥有了粗粒度的接口,因为它更趋向于抽象一定数量的细小操作,来给客户端使用。在这点上,服务层已经是一种位于业务逻辑层和领域层对象之上的外观模式。
{
void Create(Order o);
List<Order> FindAll();
Order FindByID(int orderID);
}
如果你使用WCF,需要在服务层接口上添加协议attribute。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public interface IOrderService
{
[OperationContract]
void Create(Order o);
[OperationContract]
List<Order> FindAll();
[OperationContract]
Order FindByID(int orderID);
}
所有非基本类型的数据都需要添加datacontract标记。在WCF运行的时候,这些标记会为指定的类型自动创建数据传输对象DTO。
public class Order
{
[DataMember]
int OrderID {get; set;};
[DataMember]
DateTime OrderDate {get; set;};
}
如果你使用asp.net xml web service。上面的类不要添加任何标记,只需要在服务层类的方法上添加WebMethod标记就可以了。
在使用外观模式的时候,你可能会需要更粗粒度的接口,你也可能会用到DTO,或者你都会用到。下面的接口就和领域对象解耦了,更加聚焦在和表现层的交互上。
{
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对象。在复杂的领域模型中,循环引用很常见。
循环引用:
循环引用就是两个对象相互引用,每个对象的都有对方类型的属性存在,这样就造成了循环引用。也就是下面的情况。
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
public class Order
{
public string OrderSeqNo
{
get;
set;
}
public Customer Customer
{
get;
set;
}
}
{
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到领域对象。