走向ASP.NET架構設計——第七章:階段總結,實踐篇(上篇)
示例說明
本篇的例子的是一個在線訂票的服務系統。這個系統向外界暴露了一些可以通過Http協議訪問的API,在這個訂票服務下面允許任意多個隸屬機構來使用服務API進行真正的售票活動。如下圖所示:
就好比銀行外面的那些自動取款機(對應圖中的Affiliate A, B, C),可以把它們看成是銀行系統的隸屬機構,我們就是通過這些取款機來進行存取活動的,其實這些取款機是調用了銀行系統的一些服務來進行數據操作,當然我們也可以直接到銀行柜臺(對應圖中的Ticket Shop)去進行存取款操作。本例中的售票例子和這個有點類似。
在本例中,在我們將會在上圖中的Application和Internal Client之間采用Reservation模式來約定票,通過采用Idempotent模式來確保訂票的每個交易只進行一次。
下面就開始進入實戰:
解決方案建立如下:
為了演示的方便,上面的Solution把客戶端和服務端程序建立在了一起。
Domain Model
首先,我們來建立這個系統中所涉及到的一些業務類和一些輔助的類。

其中:
Event類代表了一次購票的活動。
Event類包含了兩個集合:一個TicketPurchase集合代表了真實的要購買的票;另外一個TicketReservation集合代表了Reservation模式中的預約票據,或者大家理解為標識,或者令牌,概念類似于ASP.NET中的驗證票據 。
另外兩個工廠類提供了一些簡單的接口來創建TicketPurchase和TicketReservation。
下面我們就來看看上面提及的一些類的具體的定義:
{
public Guid Id { get; set; }
public Event Event { get; set; }
public DateTime ExpiryTime { get; set; }
public int TicketQuantity { get; set; }
public bool HasBeenRedeemed { get; set; }
public bool HasExpired()
{
return DateTime.Now > ExpiryTime;
}
public bool StillActive()
{
return !HasBeenRedeemed && !HasExpired();
}
}
public class TicketPurchase
{
public Guid Id { get; set; }
public Event Event { get; set; }
public int TicketQuantity { get; set; }
}
為了簡化創建票據類的方面,我們添加兩個工廠類如下:
{
public static TicketReservation CreateReservation(Event Event, int tktQty)
{
TicketReservation reservation = new TicketReservation();
reservation.Id = Guid.NewGuid();
reservation.Event = Event;
reservation.ExpiryTime = DateTime.Now.AddMinutes(1);
reservation.TicketQuantity = tktQty;
return reservation;
}
}
public class TicketPurchaseFactory
{
public static TicketPurchase CreateTicket(Event Event, int tktQty)
{
TicketPurchase ticket = new TicketPurchase();
ticket.Id = Guid.NewGuid();
ticket.Event = Event;
ticket.TicketQuantity = tktQty;
return ticket;
}
}
上面兩個工廠的方法都是很直觀,簡單。在TicketReservationFactory中創建ReservationTicket類的時候,設置這個標識票據的默認過期時間是一分鐘。也就是說,整個訂票的交易要在一分鐘之內完成,當然一分鐘只是例子而已,便于例子的測試。可能時間太長了一是耗費太多的資源,二是在安全方面也存在一些隱患。
下面就來一起看看比較核心的Event類:(下面的代碼有點多,在代碼的后面我會詳細講述類中每個方法的意思)
{
public Event()
{
ReservedTickets = new List<TicketReservation>();
PurchasedTickets = new List<TicketPurchase>();
}
public Guid Id { get; set; }
public string Name { get; set; }
public int Allocation { get; set; }
public List<TicketReservation> ReservedTickets { get; set; }
public List<TicketPurchase> PurchasedTickets { get; set; }
public int AvailableAllocation()
{
int salesAndReservations = 0;
PurchasedTickets.ForEach(t => salesAndReservations += t.TicketQuantity);
ReservedTickets.FindAll(r => r.StillActive()).ForEach(r => salesAndReservations += r.TicketQuantity);
return Allocation - salesAndReservations;
}
public bool CanPurchaseTicketWith(Guid reservationId)
{
if (HasReservationWith(reservationId))
return GetReservationWith(reservationId).StillActive();
return false;
}
public TicketPurchase PurchaseTicketWith(Guid reservationId)
{
if (!CanPurchaseTicketWith(reservationId))
throw new ApplicationException(DetermineWhyATicketCannotbePurchasedWith(reservationId));
TicketReservation reservation = GetReservationWith(reservationId);
TicketPurchase ticket = TicketPurchaseFactory.CreateTicket(this, reservation.TicketQuantity);
reservation.HasBeenRedeemed = true;
PurchasedTickets.Add(ticket);
return ticket;
}
public TicketReservation GetReservationWith(Guid reservationId)
{
if (!HasReservationWith(reservationId))
throw new ApplicationException(String.Format("No reservation ticket with matching id of '{0}'", reservationId.ToString()));
return ReservedTickets.FirstOrDefault(t => t.Id == reservationId);
}
private bool HasReservationWith(Guid reservationId)
{
return ReservedTickets.Exists(t => t.Id == reservationId);
}
public string DetermineWhyATicketCannotbePurchasedWith(Guid reservationId)
{
string reservationIssue = "";
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 to reserve.");
}
public bool CanReserveTicket(int qty)
{
return AvailableAllocation() >= qty;
}
public TicketReservation ReserveTicket(int tktQty)
{
if (!CanReserveTicket(tktQty))
ThrowExceptionWithDetailsOnWhyTicketsCannotBeReserved();
TicketReservation reservation = TicketReservationFactory.CreateReservation(this, tktQty);
ReservedTickets.Add(reservation);
return reservation;
}
}
下面,我們就來看看每個方法的作用:
AvailableAllocation():這個方法計算現有還有多少票可以賣;用總的票數減去已經賣出的票數和已經預定了的票數。
CanReserveTicket(int qty):這個檢查是否還有足夠數量的票供預定。
ReserveTicket(int qty):這個方法創建一個新的TicketReservation,并且指定在這個標識票據中有多少張真實的票要購買的,并且將標識票據添加到集合中
HasReservationWith(Guid reservationId):這個方法判斷給定Id的TicketReservation是否存在。
GetReservationWith(Guid reservationId):通過Id標識,獲取一個TicketReservation。
CanPurchaseTicketWith(Guid reservationId):這個方法判斷可以基于給定的標識Id來購買真實的票。
PurchaseTicketWith(Guid reservationId):基于給的預約標識來創建一個新的真實的票TicketPurchase.
DetermineWhyTicketCannotBePurchase(Guid reservationId):這個方法返回一個字符串結果,說明一下為什么不能基于給定的預約標識來購買票,可以因為標識過期或者我們規定一個標識所代表的一次交易最多只能買指定數量的真實票。
業務類建立完成之后,下面我們就來創建一個類來進行存取這些業務類所需要的數據。
Repository
添加一個IEventReposistory接口,如下:
{
Event FindBy(Guid id);
void Save(Event eventEntity);
}
為了演示的簡潔,這個接口定義的很簡單。下面就用ADO.NET的方式來實現一個EventRepository.(當然,大家可以采用自己喜歡的數據訪問技術)
下面的代碼很多,但是很容易理解:
{
private string connectionString = @"Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\EventTickets.mdf;Integrated Security=True;User Instance=True";
public Event FindBy(Guid id)
{
Event Event = default(Event);
string queryString = "SELECT * FROM dbo.Events WHERE Id = @EventId " +
"SELECT * FROM dbo.PurchasedTickets WHERE EventId = @EventId " +
"SELECT * FROM dbo.ReservedTickets WHERE EventId = @EventId;";
using (SqlConnection connection =
new SqlConnection(connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = queryString;
SqlParameter Idparam = new SqlParameter("@EventId", id.ToString());
command.Parameters.Add(Idparam);
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
if (reader.HasRows)
{
reader.Read();
Event = new Event();
Event.PurchasedTickets = new List<TicketPurchase>();
Event.ReservedTickets = new List<TicketReservation>();
Event.Allocation = int.Parse(reader["Allocation"].ToString());
Event.Id = new Guid(reader["Id"].ToString());
Event.Name = reader["Name"].ToString();
if (reader.NextResult())
{
if (reader.HasRows)
{
while (reader.Read())
{
TicketPurchase ticketPurchase = new TicketPurchase();
ticketPurchase.Id = new Guid(reader["Id"].ToString());
ticketPurchase.Event = Event;
ticketPurchase.TicketQuantity = int.Parse(reader["TicketQuantity"].ToString());
Event.PurchasedTickets.Add(ticketPurchase);
}
}
}
if (reader.NextResult())
{
if (reader.HasRows)
{
while (reader.Read())
{
TicketReservation ticketReservation = new TicketReservation();
ticketReservation.Id = new Guid(reader["Id"].ToString());
ticketReservation.Event = Event;
ticketReservation.ExpiryTime = DateTime.Parse(reader["ExpiryTime"].ToString());
ticketReservation.TicketQuantity = int.Parse(reader["TicketQuantity"].ToString());
ticketReservation.HasBeenRedeemed = bool.Parse(reader["HasBeenRedeemed"].ToString());
Event.ReservedTickets.Add(ticketReservation);
}
}
}
}
}
}
return Event;
}
public void Save(Event Event)
{
// Code to save the Event entity
// is not required in this senario
RemovePurchasedAndReservedTicketsFrom(Event);
InsertPurchasedTicketsFrom(Event);
InsertReservedTicketsFrom(Event);
}
public void InsertReservedTicketsFrom(Event Event)
{
string insertSQL = "INSERT INTO ReservedTickets " +
"(Id, EventId, TicketQuantity, ExpiryTime, HasBeenRedeemed) " +
"VALUES " +
"(@Id, @EventId, @TicketQuantity, @ExpiryTime, @HasBeenRedeemed);";
foreach (TicketReservation ticket in Event.ReservedTickets)
{
using (SqlConnection connection =
new SqlConnection(connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = insertSQL;
SqlParameter Idparam = new SqlParameter("@Id", ticket.Id.ToString());
command.Parameters.Add(Idparam);
SqlParameter EventIdparam = new SqlParameter("@EventId", ticket.Event.Id.ToString());
command.Parameters.Add(EventIdparam);
SqlParameter TktQtyparam = new SqlParameter("@TicketQuantity", ticket.TicketQuantity);
command.Parameters.Add(TktQtyparam);
SqlParameter Expiryparam = new SqlParameter("@ExpiryTime", ticket.ExpiryTime);
command.Parameters.Add(Expiryparam);
SqlParameter HasBeenRedeemedparam = new SqlParameter("@HasBeenRedeemed", ticket.HasBeenRedeemed);
command.Parameters.Add(HasBeenRedeemedparam);
connection.Open();
command.ExecuteNonQuery();
}
}
}
public void InsertPurchasedTicketsFrom(Event Event)
{
string insertSQL = "INSERT INTO PurchasedTickets " +
"(Id, EventId, TicketQuantity) " +
"VALUES " +
"(@Id, @EventId, @TicketQuantity);";
foreach (TicketPurchase ticket in Event.PurchasedTickets)
{
using (SqlConnection connection =
new SqlConnection(connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = insertSQL;
SqlParameter Idparam = new SqlParameter("@Id", ticket.Id.ToString());
command.Parameters.Add(Idparam);
SqlParameter EventIdparam = new SqlParameter("@EventId", ticket.Event.Id.ToString());
command.Parameters.Add(EventIdparam);
SqlParameter TktQtyparam = new SqlParameter("@TicketQuantity", ticket.TicketQuantity);
command.Parameters.Add(TktQtyparam);
connection.Open();
command.ExecuteNonQuery();
}
}
}
public void RemovePurchasedAndReservedTicketsFrom(Event Event)
{
string deleteSQL = "DELETE PurchasedTickets WHERE EventId = @EventId; " +
"DELETE ReservedTickets WHERE EventId = @EventId;";
using (SqlConnection connection =
new SqlConnection(connectionString))
{
SqlCommand command = connection.CreateCommand();
command.CommandText = deleteSQL;
SqlParameter Idparam = new SqlParameter("@EventId", Event.Id.ToString());
command.Parameters.Add(Idparam);
connection.Open();
command.ExecuteNonQuery();
}
}
}
今天就到這里。下一篇接著講述!