軟體架構設計:有『題』(二)- 為X宏網路售票系統加入 API First 網站

軟體架構設計:有『題』(二)- 為X宏網路售票系統加入 API First 網站

前言

在上一篇文章〔軟體架構設計:有『題』- API 設計原則以『線上售票系統為例〕裡面,我們幾乎在 LINQPad 上完成了一個線上購票的雛形,而這一次,一個部分是我們會再持續的建模的過程中補足一些上一次的缺失部分、第二部分則是加入網站設計部分,而這個網站會使用先前我在某客戶端所開發的 API Framework 框架為基底來設計,除了完全支援 >NET 6 外,這個框架封裝的許多在 Clean Architecture 裡屬於 Infrastructure 的部分,包括 【Role Security Access Controll Layer】與【Notification】以及【Log】等功能,更細部的部份我們下面再來談。

持續的建模

針對上一次,我們在X宏網路售票系統中用 UML 所呈現的 DDD 的 Domain Modeling 與 對照 Scenario 可以發現幾個小缺失。

(1). 加入 ShowTime 的 ValueObject (2). 建立 SeatReservation 實體的 Create 方法 (3). 調整 ConcertVenue 音樂會場地物件實作

另外,也針對 Application Services 的 購票作業 Reservation 方法調整程式碼邏輯,因應票卷 Ticket 由 Ticket.Create 方法來產生,並需要傳入(訂位代號:ReserveID; 場次:ShowTime)

程式碼中使用的術語,完全參照 Domain Modeling 與 事件風暴 中所精練出的通用語言 (Ubiquotius Language),以確保程式碼完全可以對照領域模型,建模的過程也完全可以撰寫程式碼,並確保在最短的週期裡完成程式碼的設計與修改。

程式碼調整如下:

Domain Layer 的程式碼

public interface IAggregateRoot
{
}

public class Entity
{	
}

public abstract class ValueObject
{
    
}
// 票卷實體
public class Ticket: Entity, IAggregateRoot
{
    private SeatReservation _seatReservation;
    public SeatReservation SeatReservationInfo => _seatReservation;
    public Guid Id { get; set; }
    public string ReservatName { get; protected set;}
    // 確認購票
    public int SaveTicket()
    {
        return 0;
    }
    // 建立票卷實體
    public static Ticket Create(SeatReservation reserve)
    {
        return new Ticket() { Id = Guid.NewGuid(), _seatReservation = reserve };
    }
}
// 預定位 Entity
public class SeatReservation: Entity
{
    public Guid Id { get; protected set; }
    public string ReserveName { get; protected set; }
    public ConcertVenue ReserveConcertVenue { get; protected set; }
    public ShowTime ShowTime { get; set; }
    // 確認與預訂票卷
    public Ticket SetReservat(Ticket ticket)
    {
        return ticket;
    }
    public string[] GetReserveByDate(TimeSpan reserveRange)
    {
        return new string[] {};
    }
    public static SeatReservation Create(string ReserveName, ShowTime ReserveShowTime)
    {
        return new SeatReservation() { Id = Guid.NewGuid(), ShowTime = ReserveShowTime };
    }
    // 檢核購票時間、並檢核選擇票種是否還有位子?(如預定是空位,則傳回:true; 否則傳回:false)
    public bool CheckVenueIsExist()
    {
        return true;
    }
}

public class Membership: Entity
{
}

public class Account: Entity, IAggregateRoot
{
}
// 演唱會場次
public class ConcertVenue: ValueObject
{
    public int ShowTimeNum { get; set; }
    public string ShowTimeName {get;set;}

}
// 演出時間
public class ShowTime: ValueObject
{
    public ShowTime(DateTime? startShowTime, DateTime? endShowTime) 
    {
        if(!startShowTime.HasValue || !endShowTime.HasValue)
        {
            throw new ShowTimeNotDefinedException(@"票種的開始時間 startShowTime 與 結束時間 endShowTime 不可為空白!");
        }
        _startShowTime = startShowTime;
        _endShowTime = endShowTime;
    }
    private DateTime? _startShowTime;
    public DateTime? GetStartShowTime()
    {
        return _startShowTime;
    }
    private DateTime? _endShowTime;
    public DateTime? GetEndShowTime()
    {
        return _endShowTime;
    }
}
// 票種場次未定義的自訂錯誤 Exception
public class ShowTimeNotDefinedException: Exception
{
    public ShowTimeNotDefinedException(string message) : base(message) {}
}
// 無效的訂位紀錄(可能該座位已經被預訂;或者是資料輸入有誤)
public class InvalidSeatReservationException: Exception
{
    public InvalidSeatReservationException(string message) : base(message) {}
}

Application Services

在應用層部分,調整(建立/產生)票卷 Ticket 實體的流程,先完成預定票卷的實作。

namespace Application.ConcertTickets
{
    public interface IReserveRepository
    {
        int SaveConcertReservation(Ticket ticket);
    }
    
    public class ConcertTicketAppService
    {
        private IReserveRepository _reserveRepository;
        public ConcertTicketAppService(IReserveRepository ticketRepository)
        {
            _reserveRepository = ticketRepository;
        }
        // 購票作業流程
        public int Reservation(ReserveDTO ticketDto)
        {
            // 建立票種、若選擇時間有票種,進行購票作業
            SeatReservation seat = SeatReservation.Create(ticketDto.ReserveID, ticketDto.ShowTime);
            
            // 產生新的票卷(此預定保留 10 分鐘)
            Ticket ticket = Ticket.Create(seat);
            
            // 檢核購票時間、並檢核選擇票種是否還有位子?
            if(!ticket.SeatReservationInfo.CheckVenueIsExist())
            {
                throw new InvalidSeatReservationException("可能該座位已經被預訂, 或者是資料輸入有誤!");
            }
            
            // 預訂票卷(此預定預設保留 10 分鐘、即便執行了 SeatReservat() 但若未付款,10分鐘取消資格,將釋放此預訂票種給其他訂票作業的 Transaction)
            ticket = seat.SetReservat(ticket);
            
            return 0;
        }
        // 確認定位
        public int SaveReservation(Ticket ticket)
        {
            return _reserveRepository.SaveConcertReservation(ticket);
        }
    }
}
// DTO 組件
namespace Application.DTO
{
    public class ReserveDTO
    {
        public string ReserveID { get; set; }
        public DateTime? ReserveTime { get; set; }
        public ShowTime ShowTime {get;set;}
    }
}

加入 API First 網站設計部分

這邊的場景,我們切換到 Visual Studio 裡,使用我預先撰寫好的 API Framework Template 進行套入,原先在 LINQPad 裡的程式碼我們就直接貼為各 Assembly 並切開耦合性,完全符合在 [API 設計原則-API與微服務傳遞價值之道]一書裡,先切分 (Package) 再 切分 (Layer) 設計技巧。

(1). 安裝 EasyArchitectVSIXProject 並新增一個 EasyArchitectV2CoreWebHost

圖(一)、安裝 EastArchitectVSIXProject 套件 安裝 EastArchitectVSIXProject 套件

這裡,圖示為齒輪的為支援 .NET 6 版本的 API Framework、未來也會支援 .NET 7/8

(2). 如果熟悉 TDD 的開發者,這裡就可以針對 Domain Layer 寫紅燈測試

(3). 撰寫 XConcertTicketsController 的 SeatReservation 的 API 方法

這裡,Infrastructure 全由 API Framework 框架所提供,所以開發人員能夠『專注』在訂票的商業邏輯上的設計,這裡我直接引用我 API Framework 裡的 API First 範本:EasyArchitectV2ApiHostController

這裡 EasyArchitectV2ApiHostController 更名為 XConcertTicketsController

所有的 API Controller 程式碼均由 Templates 自動產生,這裡我只撰寫 SeatReservation 的 API 方法,完整 API Controller 程式碼如下:

using Application.ConcertTickets;
using Domain.ConcertTickets;
using EasyArchitect.OutsideApiControllerBase;
using EasyArchitect.OutsideManaged.AuthExtensions.Attributes;
using EasyArchitect.OutsideManaged.AuthExtensions.Filters;
using EasyArchitect.OutsideManaged.AuthExtensions.Services;
using Infrastructure.ConcertTickets;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Mxic.FrameworkCore.Core;
using System;

namespace Web.XConcertTickets.Controllers
{
    /// <summary>
    /// X宏網路購票系統
    /// </summary>
    public class XConcertTicketsController : OutsideBaseApiController
    {
        private readonly IUserService _userService;
        private readonly IUriExtensions _uriExtensions;

        /// <summary>
        /// X宏網路購票系統範例
        /// </summary>
        /// <param name="logger"></param>
        /// <param name="userService"></param>
        /// <param name="httpContextAccessor"></param>
        public XConcertTicketsController(
            ILogger<OutsideBaseApiController> logger, 
            IUserService userService, 
            IUriExtensions uriExtensions,
            IHttpContextAccessor httpContextAccessor) 
            : base(logger, userService, httpContextAccessor)
        {
            _userService = userService;
            _uriExtensions = uriExtensions;
        }

        /// <summary>
        /// 訂票作業
        /// </summary>
        /// <param name="ReserveName"></param>
        /// <param name="startShowTime"></param>
        /// <param name="endShowTime"></param>
        /// <returns></returns>
        [NeedAuthorize]
        [HttpGet]
        [APIName("SeatReservation")]
        [ApiLogException]
        [ApiLogonInfo]
        public async Task<ReserveResponseDTO> SeatReservation(string ReserveName, DateTime? startShowTime, DateTime? endShowTime)
        {
            Application.ConcertTickets.ConcertTicketAppService app = new ConcertTicketAppService(new ReserveRepository());
            ReserveResponseDTO result = app.Reservation(
                new ReserveDTO()
                {
                    ReserveID = "XXXXX10001",   //訂位代號(流水號)
                    ReserveName = ReserveName,  //定位大名
                    ReserveTime = DateTime.Now, // 訂位時間、不等於票種時間 或 演唱會場時間 (the showtime for Concert Venue)
                    ShowTime = new ShowTime(startShowTime, endShowTime)
                }
            );

            return await Task.FromResult(result);
        }
    }
}

以上,我們大致完成網路的訂票作業系統的初步設計,這已經算是比較完整的設計了,也完全遵循 API First 的設計方法,下次,我們再來探討搭配 TDD 的實作技巧。

待續...

文章範例程式:

https://github.com/wugelis/CleanArchitectXConcert


# 關於我:

Gelis 吳俊毅 - 資深 .NET 技術顧問

FB 社團 (軟體開發之路)

https://www.facebook.com/groups/361804473860062/

FB 粉絲團 (Gelis 的程式設計訓練營)

https://www.facebook.com/gelis.dev.learning/

我講授過的課程 SlideShare

https://www.slideshare.net/GelisWu

GitHub

https://github.com/wugelis

留言

這個網誌中的熱門文章

什麼是 gRPC ?

什麼是 gRPC(二)- 來撰寫第一個 Hello World 吧!

常見的程式碼壞味道(Code Smell or Bad Smell)