- 服务层的定义
- Domain Model的设计与实现
- IRepository的设计与实现
- Document Message模式和Request-Response模式的探索
- DataContract的设计与实现
- Contracts的设计与实现
- Service的设计与实现
- Idempotent模式的探索
- ClientProxy的设计与实现
- 服务门面Facade的设计与实现
- 客户端访问
什么是服务层
服务层位于表示层和业务层之间,他提供一个接口来定义应用程序的边界以及可供客户端使用的操作,在服务层向客户端描绘的门面后,它将业务逻辑、验证和工作流封装起来并协调业务实体的持久化和和检索工作——《ASP.NET设计模式》
接下来,将以一个简单SOA的例来分析服务层的构建。
按开头所说的那样,服务接口位于表示层和业务层之间,它封装了业务领域逻辑,协调事物和响应,并将API定义成一组可供客户端访问的粗粒度方法,构建的解决方案大致如下:
解决方案:
Domain Model的设计与实现
首先建立领域模型,因为本篇博客不深究Domain Mode,故只贴出代码,仅供查考。
- 建立TicketReservation Domain Model,Domain Model项目将包含应用程序内的所有业务逻辑,如判断预订的票是否有效。
/// <summary> /// TicketReservation 领域模型 /// </summary> public class TicketReservation { public Guid Id { get; set; } /// <summary> /// 过期时间 /// </summary> public DateTime ExpiryTime { get; set; } /// <summary> /// 票数 /// </summary> public Event Event { get; set; } public int TicketQuantity { get; set; } /// <summary> /// 是否被回收 /// </summary> public bool HasBeenRedeemed { get; set; } public bool HasExpired() { return DateTime.Now > ExpiryTime; } public bool StillAction() { return !HasExpired() && !HasBeenRedeemed; } }
- 建立TicketPurchase领域模型
public class TicketPurchase { public Guid Id { get; set; } public Event Event { get; set; } public int TicketQuantity { get; set; } }
- 建立Event(赛事)领域模型
public class Event { public Event() { PurchaseTickets = new List<TicketPurchase>(); ReservedTickets=new List<TicketReservation>(); } public Guid Id { get; set; } public string Name { get; set; } public int Allocation { get; set; } public List<TicketPurchase> PurchaseTickets { get; set; } public List<TicketReservation> ReservedTickets { get; set; } public int AvailableAllocation() { int salesAndReservations = 0; //统计卖出了多少票 PurchaseTickets.ForEach(t=>salesAndReservations+=t.TicketQuantity); //统计预订出多少有效的票 ReservedTickets.FindAll(r=>r.StillAction()).ForEach(r=>salesAndReservations+=r.TicketQuantity); return Allocation - salesAndReservations; } /// <summary> /// 根据指定的预订Id判断是否存在某张票 /// </summary> /// <param name="reservationId"></param> /// <returns></returns> private bool HasReservationWith(Guid reservationId) { return ReservedTickets.Exists(r => r.Id == reservationId); } /// <summary> /// 能否购买某张票 /// </summary> /// <param name="reservationId"></param> /// <returns></returns> public bool CanPurchaseTicketWith(Guid reservationId) { if (HasReservationWith(reservationId)) { return GetReservationWith(reservationId).StillAction(); } return false; } /// <summary> /// 得到根据预订Id匹配的ticketReservation实例 /// </summary> /// <param name="reservatonId"></param> /// <returns></returns> public TicketReservation GetReservationWith(Guid reservatonId) { if (!HasReservationWith(reservatonId)) { throw new ApplicationException(string.Format("No reservation ticket with matching id of '{0}'",reservatonId.ToString())); } return ReservedTickets.FirstOrDefault(t => t.Id == reservatonId); } public TicketPurchase PurchaseTicketWith(Guid reservationId) { if (!CanPurchaseTicketWith(reservationId)) { throw new ApplicationException(DetermineWhyATicketCannotbePurchaseedWith(reservationId)); } TicketReservation reservation = GetReservationWith(reservationId); TicketPurchase ticket = TicketPurchaseFactory.CreateTicket(this, reservation.TicketQuantity); reservation.HasBeenRedeemed = true; PurchaseTickets.Add(ticket); return ticket; } public string DetermineWhyATicketCannotbePurchaseedWith(Guid reservationId) { string reservationIssue = string.Empty; if (HasReservationWith(reservationId)) { TicketReservation reservation = GetReservationWith(reservationId); if (reservation.HasExpired()) { reservationIssue = string.Format("Ticket reservation '{0}' has expired",reservationId.ToString()); } else if (reservation.HasBeenRedeemed) { reservationIssue = string.Format("Ticket reservation '{0}' has already been redeemed", reservationId.ToString()); } } else { reservationIssue = String.Format("There is no ticket reservation with the Id '{0}'", reservationId.ToString()); } return reservationIssue; } private void ThrowExceptionWithDetailsOnWhyTicketsCannotBeReserved() { throw new ApplicationException("there are no tickets available reserve."); } public bool CanReservTicket(int qty) { return AvailableAllocation() >= qty; } public TicketReservation ReserveTicket(int tktQty) { if (!CanReservTicket(tktQty)) { ThrowExceptionWithDetailsOnWhyTicketsCannotBeReserved(); } TicketReservation reservation = TicketReservationFactory.CreateReservation(this, tktQty); ReservedTickets.Add(reservation); return reservation; } }
IRepository的设计和实现
接着,再创建我们的持久化层,需要某种方式来持久化和检索Event聚合,添加Repository,因为在这儿我们主要考虑到是服务层的设计,所以Repository仓储层也不是侧重点,简单带过。
- 建立IEventRepository,提供了持久化和检索Event的契约
/// <summary> /// 仓储接口---持久化和检索Event聚合 /// </summary> public interface IEventRepository { Event FindBy(Guid guid); void Save(Event eventEntity); }
建立了应用程序的数据访问和业务逻辑之后,可以使用服务层来修饰,下图给出了服务层如何向客户端暴露API。
- Contracts:该项目存放用来定义服务契约的接口。
- Service:该项目包含服务契约的实现并协调业务逻辑的工作流以及实体持久化/检索。
- DataContract:该项目包含消息的DTO(传给客户的数据),使用了Document Message消息传送模式来交换数据。
- HTTPHost:该项目用来承载WCF服务。
补充:探索Document Message模式
Document Message(文档消息)模式能够采用一种统一、灵活的方法与服务通信,该模式并不使用传统的参数化方法来暴露服务API,而是采用消息对象:
- 传统的参数化方法
Customer[] RetrieveCustomers(string country); Customer[] RetrieveCustomers(string country, string postalCode); Customer[] RetrieveCustomers(string country, string postalCode, string stree);
很明显,这种方法很快会变得难以维护而且也不利于API客户代码调用。
- Document Message 模式通过将所有的信息封装到文档正文中形成一个更为简单和干净的服务签名,简化了通信
public class CusomerSearchRequest { public string Country { get; set; } public string PostalCode { get; set; } public string Street { get; set; } }
Customer[] FindBy(CusomerSearchRequest request);
Request-Response模式确保响应和请求一样均使用Document Message模式,Response可以继承一个基类BaseResponse,该基类提供通用的信息,就像下面这样,返回一个Response对象:
[DataContract] public abstract class Response { [DataMember] public bool Success { get; set; } [DataMember] public string Message { get; set; } }
PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest);
了解了Document Message和Request-Response模式之后,我们来设计消息的数据传输对象(Data Transfer Object)。
DataContract的设计与实现
DataContract项目存放着服务工作流中涉及的所有DTO对象,因为将使用WCF模型来暴露服务,所以添加相关的特性(Attribute)来修饰属性进行序列化。
- 所以的响应对象都继承自某个包含一些公共行为的基类Response:
[DataContract] public abstract class Response { [DataMember] public bool Success { get; set; } [DataMember] public string Message { get; set; } }
- PurchaseResponse、TicketResponse分别继承Response:
[DataContract] public class PurchaseTicketResponse:Response { /// <summary> ///票的Id /// </summary> [DataMember] public string TicketId { get; set; } /// <summary> /// 赛事名称 /// </summary> [DataMember] public string EventName { get; set; } /// <summary> /// 赛事Id /// </summary> [DataMember] public string EventId { get; set; } /// <summary> /// 票的数量 /// </summary> [DataMember] public int NoOfTickets { get; set; } }
- ReserveTicketResponse同样继承于Response:
[DataContract] public class ReserveTicketResponse:Response { /// <summary> /// 票的号码 /// </summary> [DataMember] public string ReserveTicketNumber { get; set; } /// <summary> /// 国期时间 /// </summary> [DataMember] public DateTime ExpirationDate{ get; set; } /// <summary> /// 赛事名称 /// </summary> [DataMember] public string EventName { get; set; } /// <summary> /// 赛事Id /// </summary> [DataMember] public string EventId { get; set; } /// <summary> /// 票数目 /// </summary> [DataMember] public int NoOfTickets { get; set; } }
接下来添加用于表示消息的数据传输对象(DTO)的请求部分:
- PurchaseTicketRequest:
[DataContract] public class PurchaseTicketRequest { /// <summary> /// 为每一次请求分配一个唯一的关联Id /// </summary> [DataMember] public string CorrelationId { get; set; } [DataMember] public string ReservationId { get; set; } [DataMember] public string EventId { get; set; } }
- ReserveTicketRequest:
[DataContract] public class ReserveTicketRequest { [DataMember] public string EventId { get; set; } [DataMember] public int TicketQuantity { get; set; } }
然后我们继续新建一个Contracts项目,包含服务要实现并且可供客户端访问的服务契约。
Contracts的设计与实现
[ServiceContract(Namespace = "EventTickets.Contract")] public interface ITicketService { //DTO传输对象 [OperationContract()] ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest); [OperationContract()] PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest); }
最后,添加Service项目实现定义的服务契约。
Service的设计与实现
添加两个新类TicketPurchaseExtensionMethods和TicketReservationExtensionMethods。这些扩展方法类可以让服务类流畅地把TicketReservation和TicketPurchase实体相应地转换成消息文档。
- TicketPurchaseExtensionMethods:按要求返回一个PurchaseTicketResponse:
public static class TicketPurchaseExtensionMethods { public static PurchaseTicketResponse ConvertToPurchaseTicketResponse(this TicketPurchase ticketPurchase) { PurchaseTicketResponse response = new PurchaseTicketResponse(); response.TicketId = ticketPurchase.Id.ToString(); response.EventId = ticketPurchase.Event.Id.ToString(); response.EventName = ticketPurchase.Event.Name; response.NoOfTickets = ticketPurchase.TicketQuantity; return response; } }
为了确保不会因为客户端(它使用将要构建的服务)的错误用法导致非预期问题,采用Idempotent消息传送模式,首先先要了解一下什么是Idempotent(幂):
Idempotent模式指使用相同的输入参数调用多次不会带来副作用的操作,因为服务不能控制它的客户端如何使用,所以确保重复调用不会对系统状态造成非预期的效果非常重要,Idempotent模式规定任何修改状态的请求都应该用一个唯一标志符标记(CorrelationId,关联Id)。这个唯一标识符应该接受某种存放响应结果的存储器的检查,以确保之前尚未处理过该请求。如果发现响应,则返回结果而不影响最初调用流程的状态。
- MessageResponseHistory把与给定关联标识符相关联的服务响应结果放到内存中。可以轻易地把该Response保存到某种数据存储中,为消息响应提供进程外存储。
public class MessageResponseHistory<T> { //将与给定关联标识符的服务响应结果存放在内存中,可能没有必要保存每一条的结果,因此可以处理只缓存最近N条响应,以确保业务逻辑只被调用一次。 private Dictionary<string, T> _responseHistory; public MessageResponseHistory() { _responseHistory=new Dictionary<string, T>(); } public bool IsAUniqueRequest(string correlationId) { return ! _responseHistory.ContainsKey(correlationId); } public void LogResponse(string correlationId, T response) { if (_responseHistory.ContainsKey(correlationId)) { _responseHistory[correlationId] = response; } else { _responseHistory.Add(correlationId,response); } } public T RetrievePreviousResponseFor(string correlationId) { return _responseHistory[correlationId]; } }
- 接着着重来分析一下服务类的实现方式:TicketService,正如前面所说的那样:包含服务契约的实现,并协调业务逻辑的工作流以及实体持久化和检索。
namespace EventTickets.Service { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class TicketService : ITicketService { private IEventRepository _eventRepository; private static MessageResponseHistory<PurchaseTicketResponse> _reservationResponse=new MessageResponseHistory<PurchaseTicketResponse>(); public TicketService(IEventRepository eventRepository) { _eventRepository = eventRepository; } public TicketService():this(new EventRepository()) { } public DataContract.ReserveTicketResponse ReserveTicket(DataContract.ReserveTicketRequest reserveTicketRequest) { ReserveTicketResponse response=new ReserveTicketResponse(); try { Event Event= _eventRepository.FindBy(new Guid(reserveTicketRequest.EventId)); TicketReservation reservation; //Tester-Doer模式 http://technet.microsoft.com/zh-cn/magazine/ms229009(VS.100).aspx if (Event.CanReservTicket(reserveTicketRequest.TicketQuantity)) { reservation = Event.ReserveTicket(reserveTicketRequest.TicketQuantity); _eventRepository.Save(Event); response = reservation.ConvertToReserveTicketResponse(); response.Success = true; } else { response.Success = false; response.Message = string.Format("There are {0} ticket(s) available.", Event.AvailableAllocation()); } } catch (Exception ex) { response.Success = false; response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex); } return response; } public DataContract.PurchaseTicketResponse PurchaseTicket(DataContract.PurchaseTicketRequest purchaseTicketRequest) { PurchaseTicketResponse response=new PurchaseTicketResponse(); try { if (_reservationResponse.IsAUniqueRequest(purchaseTicketRequest.CorrelationId)) { TicketPurchase ticket; Event Event = _eventRepository.FindBy(new Guid(purchaseTicketRequest.EventId)); if (Event.CanPurchaseTicketWith(new Guid(purchaseTicketRequest.ReservationId))) { ticket = Event.PurchaseTicketWith(new Guid(purchaseTicketRequest.ReservationId)); _eventRepository.Save(Event); response.Success = true; } else { response.Message = Event.DetermineWhyATicketCannotbePurchaseedWith(new Guid( purchaseTicketRequest.ReservationId)); response.Success = false; } _reservationResponse.LogResponse(purchaseTicketRequest.CorrelationId,response); } } catch (Exception ex) { response.Message = ErrorLog.GenerateErrorRefMessageAndLog(ex); response.Success = false; } return response; } } }
- ReserveTicket方法中的所有逻辑都打包到一个try---catch代码块中,以确保不会出现一些可能暴露服务内部工作机制的异常
- 静态的MessageResponseHistory对象负责存储和检索匹配相应。如果找到匹配响应则从MessageResponseHistory对象中检索出响应返回给客户端,以确保在客户端重复调用该服务时不会出现预料之外的问题。
- 最后别忘记承载WCF服务,怎样配置WCF终结点可以查阅相关的MSDN文档。
客户端代理TicketServiceProxy的设计与实现
- 为了能让客户能够使用该服务,需要创建一个代理,当然也可以利用Visual Studio添加服务引用自动为我们创建客户端代理。
/// <summary> /// 客户端代理 /// </summary> public class TicketServiceClientProxy : ClientBase<ITicketService>, ITicketService { public ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest) { return base.Channel.ReserveTicket( reserveTicketRequest); } public PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest) { return base.Channel.PurchaseTicket(purchaseTicketRequest); } }
TicketServiceClientProxy继承自ClientBase,Visual Studio自动替我们创建代理服务时正是使用该基类。
- 接着构建服务门面:TicketServiceFacade,Facade模式的应用,将复杂的接口隐藏起来,为应用程序提供一个一致的简化API。使用该模式把使用消息传送模式与服务端点通信的机制抽象出来(体现了分离关注点),为客户应用程序提供一个简化的接口。
- 建立TicketPresentation,从Service 获取的Response对象根据需要填充到Presentation中。
public class TicketPresentation { public string TicketId { get; set; } public string EventId { get; set; } public string Description { get; set; } public bool WasAbleToPurchaseTicket { get; set; } }
TicketServiceFacade
编写好代理服务之后,可以创建一个服务门面,用来与客户端Web应用程序通信。我们将创建一个门面,把与服务通信的复杂读隐藏起来(只提供简单API),并让客户端应用与服务松散耦合,从而有助于测试。这个服务门面将使用两个特定的Presentation模型类。Web应用程序只使用这两个类来显示从服务门面获取的数据。
/// <summary> /// 服务门面类 /// </summary> public class TicketServiceFacade { private ITicketService _ticketService; public TicketServiceFacade(ITicketService ticketService) { _ticketService = ticketService; } public TicketReservationPresentation ReserveTicketsFor(string EventId, int NoOfTkts) { //从Service 获取的数据 Response填充到显示的Presentation TicketReservationPresentation reservation = new TicketReservationPresentation(); //DTO:响应模型 Response ReserveTicketResponse response = new ReserveTicketResponse(); //DTO:请求模型 Request ReserveTicketRequest request = new ReserveTicketRequest(); request.EventId = EventId; request.TicketQuantity = NoOfTkts; //发送请求 response = _ticketService.ReserveTicket(request); //返回如果是成功 if (response.Success) { //填充至Presentation模型 reservation.TicketWasSuccessfullyReserved = true; reservation.ReservationId = response.ReserveTicketNumber; reservation.ExpiryDate = response.ExpirationDate; reservation.EventId = response.EventId; reservation.Description = String.Format("{0} ticket(s) reserved for {1}.<br/><small>This reservation will expire on {2} at {3}.</small>", response.NoOfTickets, response.EventName, response.ExpirationDate.ToLongDateString(), response.ExpirationDate.ToLongTimeString()); } else { reservation.TicketWasSuccessfullyReserved = false; reservation.Description = response.Message; } return reservation; } public TicketPresentation PurchaseReservedTicket(string EventId, string ReservationId) { TicketPresentation ticket = new TicketPresentation(); PurchaseTicketResponse response = new PurchaseTicketResponse(); PurchaseTicketRequest request = new PurchaseTicketRequest(); request.ReservationId = ReservationId; request.EventId = EventId; request.CorrelationId = ReservationId; response = _ticketService.PurchaseTicket(request); if (response.Success) { ticket.Description = String.Format("{0} ticket(s) purchased for {1}.<br/><small>Your e-ticket id is {2}.</small>", response.NoOfTickets, response.EventName, response.TicketId); ticket.EventId = response.EventId; ticket.TicketId = response.TicketId; ticket.WasAbleToPurchaseTicket = true; } else { ticket.WasAbleToPurchaseTicket = false; ticket.Description = response.Message; } return ticket; } }
服务门面的作用是简化客户端与服务之间的交互。客户端应用程序不需要了解消息传递模式以及与服务代理通信。 TicketServiceFacade的两个方法应该相当简单,这是因为它们遵循着相同的工作流:
1.生成一个请求。
2.将该请求传递给代理服务。
3.检索响应并构建Presentation模型。
客户端访问
- 创建于服务门面(然后通过代理与真正的服务层)通信的网站:
public partial class Shop : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { // TicketServiceFacade ticketService = new TicketServiceFacade(new TicketServiceClientProxy()); TicketReservationPresentation reservation = ticketService.ReserveTicketsFor(ddlEvents.SelectedValue, int.Parse(this.txtNoOfTickets.Text)); if (reservation.TicketWasSuccessfullyReserved) { //Todo //this.txtReservationId.Text = reservation.ReservationId; } } }
小结
本文探索了服务层在企业级开发的设计与实现,前前后后托了一个月了,终于静下心来写完了这篇博客,感谢《ASP.NET设计模式》这本书,让我收获不少,点击进行源代码下载 。