服务层位于表示层和业务层之间,他提供一个接口来定义应用程序的边界以及可供客户端使用的操作,在服务层向客户端描绘的门面后,它将业务逻辑、验证和工作流封装起来并协调业务实体的持久化和和检索工作——《ASP.NET设计模式》
接下来,将以一个简单SOA的例来分析服务层的构建。
按开头所说的那样,服务接口位于表示层和业务层之间,它封装了业务领域逻辑,协调事物和响应,并将API定义成一组可供客户端访问的粗粒度方法,构建的解决方案大致如下:
解决方案:
首先建立领域模型,因为本篇博客不深究Domain Mode,故只贴出代码,仅供查考。
/// <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;
}
}
public class TicketPurchase
{
public Guid Id { get; set; }
public Event Event { get; set; }
public int TicketQuantity { get; set; }
}
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;
}
}
接着,再创建我们的持久化层,需要某种方式来持久化和检索Event聚合,添加Repository,因为在这儿我们主要考虑到是服务层的设计,所以Repository仓储层也不是侧重点,简单带过。
/// <summary>
/// 仓储接口---持久化和检索Event聚合
/// </summary>
public interface IEventRepository
{
Event FindBy(Guid guid);
void Save(Event eventEntity);
}
建立了应用程序的数据访问和业务逻辑之后,可以使用服务层来修饰,下图给出了服务层如何向客户端暴露API。
Document Message(文档消息)模式能够采用一种统一、灵活的方法与服务通信,该模式并不使用传统的参数化方法来暴露服务API,而是采用消息对象:
Customer[] RetrieveCustomers(string country);
Customer[] RetrieveCustomers(string country, string postalCode);
Customer[] RetrieveCustomers(string country, string postalCode, string stree);
很明显,这种方法很快会变得难以维护而且也不利于API客户代码调用。
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项目存放着服务工作流中涉及的所有DTO对象,因为将使用WCF模型来暴露服务,所以添加相关的特性(Attribute)来修饰属性进行序列化。
[DataContract]
public abstract class Response
{
[DataMember]
public bool Success { get; set; }
[DataMember]
public string Message { get; set; }
}
[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; }
}
[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)的请求部分:
[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; }
}
[DataContract]
public class ReserveTicketRequest
{
[DataMember]
public string EventId { get; set; }
[DataMember]
public int TicketQuantity { get; set; }
}
然后我们继续新建一个Contracts项目,包含服务要实现并且可供客户端访问的服务契约。
[ServiceContract(Namespace = "EventTickets.Contract")]
public interface ITicketService
{
//DTO传输对象
[OperationContract()]
ReserveTicketResponse ReserveTicket(ReserveTicketRequest reserveTicketRequest);
[OperationContract()]
PurchaseTicketResponse PurchaseTicket(PurchaseTicketRequest purchaseTicketRequest);
}
最后,添加Service项目实现定义的服务契约。
添加两个新类TicketPurchaseExtensionMethods和TicketReservationExtensionMethods。这些扩展方法类可以让服务类流畅地把TicketReservation和TicketPurchase实体相应地转换成消息文档。
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)。这个唯一标识符应该接受某种存放响应结果的存储器的检查,以确保之前尚未处理过该请求。如果发现响应,则返回结果而不影响最初调用流程的状态。
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];
}
}
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;
}
}
}
/// <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自动替我们创建代理服务时正是使用该基类。
public class TicketPresentation
{
public string TicketId { get; set; }
public string EventId { get; set; }
public string Description { get; set; }
public bool WasAbleToPurchaseTicket { get; set; }
}
编写好代理服务之后,可以创建一个服务门面,用来与客户端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设计模式》这本书,让我收获不少,点击进行源代码下载 。