軟體架構設計:有『題』- API 設計原則以『線上售票系統為例』

軟體架構設計:有『題』- API 設計原則以『線上售票系統為例』



前言

先前,我有篇文章是以讀過【Get Your Hands Dirty on Clean Archiecture】後的心得感想,這本書的中文名稱是:【Clean Architecture 實作篇:在整潔架構上弄髒你的手】,讀完這本書大約是去年 2022/10 月左右,也是我當時在開發 API 新版框架大爆發的時候,書中給我不少的啟發,在配合實際的專案開發的實作經驗 + 框架設計所需要的框架設計便引入六角架構的設計方式,藉以改良原有的舊版的框架,而這篇『軟體架構設計:無題 (http://gelis-dotnet.blogspot.com/2022/08/blog-post.html)』中便是以完整的〔六角架構 (Hexagonal Architecture)〕並加以解釋如何在六角架構中落實單元測試 Unit Test,這些都是我讀完書後,實際落實在專案上的實際做法 + 分享。

當時的六角架構 + Unit Test 的範例程式在這:https://github.com/wugelis/AdapterInOutLayerSapmle

上次是無題,今天,我以一個X宏線上售票系統為例子,我們從頭到尾地的,Step By Step 的來介紹,這個 API 售票系統的建置過程,而這裡,我也會搭配 『API 設計原則』一書裡的一些做法,也就是說,這裡開發的 API 會使用 API First 的設計樣式,因為這裡的 API 設計的出發點為該企業、在這個商業模式上所能夠提供之『商業能力』回出發點考量,而不是過往傳統的單一客戶(UI)思維的客製化模式考量,所謂的 API First 是轉向為產品化思惟的導向模式,如此也才能發揮 API 在市場上的價值,這也才是 API First 的思維模式。

需求來源

圖(一)、線上售票系統需求 線上售票系統需求

今天的需求來源,是一個線上售票系統,先前我曾在專案中製作過一個售票系統的雛形,這裡我就拿這個範例來以 DDD 來進行戰略建模,並搭配使用我在新竹顧問時所開發的新版 API Framework 來實作並支撐這個售票系統 API 的 Infrastructure Layer,沒錯,所以言下之意,我們以 DDD 來驅動這個售票系統 API 的開發,所以這裡的 Core Domain 就是這個 API 主要的商業能力。

從事件風暴到領域模型

圖(二)、DDD 的戰略建模 - Bounded Context DDD 的戰略建模 - Bounded Context

從圖(一)需求得知在購票流程裡,除了可以線上購票外,購票成功後會發送簡訊通知,這裡我們找到 3 個 Contexts 並確定其系統邊界,這 3 個 Contexts 分別是:購票、管理票卷、簡訊發送。整個購票作業還需要 Generic Subdomain 來支撐,像是我前面提到的 API 的 Framework 會提供 OAuth2 與 JWT Token 的 Infrastructure 的底層能力。

一次只看一種業務問題即是 Problem Space 的主要目標,透過這個方式來看我們的這個(產品/專案)的商業目標,我可以將這些商業目標拆分如下的 Subdomain:

  • 購票:Core Domain
  • 登入購票網站:Generic Subdomain
  • 簡訊通知:Generic Subdomain
  • 管理票卷:Supporting Domain

讓領域模型關連到程式碼

圖(三)、透過事件風暴(Event Storming) - 進行建模 透過事件風暴(Event Storming) - 進行建模

這邊管理票卷劉在下次,我先針對 Core Domain 來進行 API First 的建模,這也是售票系統主要提供的商業能力。

圖(四)、DDD 戰術建模 - Domain Modeling DDD 戰術建模 - Domain Modeling

到這裡,我們已經可以進行戰術建模 + 繪製領域模型 Domain Modeling 了,因為經由事件風暴可以得知 UI 應該提供『選擇座位』、『選擇演唱會場次』、『選擇票種』的 Command,這些即可以直接繪製 Domain Modeling 並清楚的表達出劃位 (Seat Reservation) 動作、產生 Ticket 的購票作業,以及相關的 Value Object

程式碼實作

這裡以整潔架構為基底,先刻劃出 Domain Layer:

public interface IAggregateRoot
{
}

public class Entity
{	
}

public abstract class ValueObject
{
    
}

public class Ticket: Entity, IAggregateRoot
{
    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() };
    }
}
// 預定位 Entity
public class SeatReservation: Entity
{
    public Guid Id { get; protected set; }
    public string ReserveName { get; protected set; }
    public ConcertVenue ReserveConcertVenue { get; protected set; }
    public DateTime? ShowTime { get; set; }
    // 確認與預訂票卷
    public Ticket SetReservat(string name)
    {
        return new Ticket();
    }
    public string[] GetReserveByDate(TimeSpan reserveRange)
    {
        return new string[] {};
    }
    public static SeatReservation Create(string ReserveName)
    {
        return new SeatReservation() { Id = Guid.NewGuid() };
    }
}

public class Membership: Entity
{
}

public class Account: Entity, IAggregateRoot
{
}

public class ConcertVenue: ValueObject
{
    
}

接著是 Application Services 的實作,這對應到 UX 也可以是一個 Use Case ,我們先以購票這個 Domain Event 來進入,這裡的 Command 有些是 Query,不過因為有購票人就要有 Account 會員身分,最後要建立 Ticket 演唱會票卷,這些就有很多事情要做了。在上方 Domain Layer 我們先建好 Account, Ticket, SeatReservation, ConcertVenue 等 Entity 與 ValueObject 物件,接著,下面的 Application Services 便會參照到這些物件。

Application Services 設計的程式碼如下:

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)
        {
            // 檢核購票時間
            
            // 檢核選擇票種是否還有位子?
            
            // 若選擇時間有票種,進行購票作業(此預定保留 10 分鐘、若未付款,10分鐘取消資格,將釋放此預訂票種給其他訂票作業的 Transaction)
            SeatReservation seat = SeatReservation.Create(ticketDto.ReserveID);
            
            // 預訂票卷(此預定預設保留 10 分鐘)
            Ticket ticket = seat.SetReservat(ticketDto.ReserveID);
            
            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 int? ShowTime {get;set;}
    }
}

這裡即為一個透過事件風暴 (Event Stroming) 進行建模、到如何對應到實際程式碼設計的一個過程,這之中包括了整潔架構的軟體架構設計。

因為內容太多了,待續...

範例程式

https://github.com/wugelis/CleanArchitectXConcertLINQv2

留言

這個網誌中的熱門文章

什麼是 gRPC ?

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

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