Upgrade to Pro — share decks privately, control downloads, hide ads and more …

20230916 - DDDTW - 導入 Domain-Driven Design 的最佳時機

20230916 - DDDTW - 導入 Domain-Driven Design 的最佳時機

蒼時弦や

October 05, 2023
Tweet

More Decks by 蒼時弦や

Other Decks in Programming

Transcript

  1. 導入 Domain-Driven Design 的最佳時機
    The best time to use Domain-Driven Design in your project

    View full-size slide

  2. 蒼時弦也
    Software Developer
    https://blog.aotoki.me

    View full-size slide

  3. 困難
    敏捷
    架構
    導入

    View full-size slide

  4. 困難
    敏捷
    架構
    導入

    View full-size slide

  5. 困難
    敏捷
    架構
    導入

    View full-size slide

  6. 困難
    敏捷
    架構
    導入

    View full-size slide

  7. 起點
    為何開始領域驅動設計

    View full-size slide

  8. Ruby on Rails 社群很少直接討論 Domain-Driven Design 自己在
    2020 年之前,完全不知道有這一套理論

    View full-size slide

  9. 另一方面 Ruby on Rails 有不少「慣例」讓開發變得簡單的同時,
    更難以將 Domain-Driven Design 的觀念放到裡面

    View full-size slide

  10. 慘痛經驗
    因為設計失誤造成問題

    View full-size slide

  11. 2019 年參與一套非常複雜的系統開發,業主說資料表都設計好,只
    要解決他們最複雜客戶其他就能支援,結果完全無法支援其他客戶

    View full-size slide

  12. 在沒有了解使用者需求下設計系統代價極大,這次慘痛的經驗讓自
    己下定決心更關注系統設計的議題

    View full-size slide

  13. 挑戰
    實踐過程中要克服的事情

    View full-size slide

  14. Knowledge
    純自學的前提,從讀完書到能夠熟練應用,大致上花費兩年時間實作、補充知識

    View full-size slide

  15. Team
    說服團隊使用需要所有人都具備知識,並且積極的進行討論才有機會成功

    View full-size slide

  16. Legacy Code
    想要修改現有系統,在大型專案中很容易互相影響,要先切出邊界

    View full-size slide

  17. Meetings
    Event Storming 總是進行很久,總是有考慮不完的問題

    View full-size slide

  18. Standard
    Rails 沒有官方標準和正式寫法,Golang 能找到三種以上的實作樣板

    View full-size slide

  19. 想要一次性解決問題非常困難,需要漸進式的把問題解決才有機會

    View full-size slide

  20. 敏捷
    小增量的進行改進

    View full-size slide

  21. Epic 1 - Q1 Epic 2 - Q2
    過去在規劃專案時,時間較長範圍也較多,做系統分析時就會很費時

    View full-size slide

  22. Epic 1 - Q1 Epic 2 - Q2
    Feat 1 - W1 Feat 2 - W3 Feat 3 - W1 Feat 4 - W2
    從敏捷的角度,我們把一次改版切割成以功能單位來看

    View full-size slide

  23. Epic 1 - Q1 Epic 2 - Q2
    Feat 1 - W1 Feat 2 - W3 Feat 3 - W1 Feat 4 - W2
    Feat 5 - W3
    也可以很彈性對應變化,調整某個功能的優先順序

    View full-size slide

  24. 當需要討論範圍變小,耗費時間的設計會議就能夠縮短時間

    View full-size slide

  25. 重構
    當一切快速進行,必有代價

    View full-size slide

  26. 快速迭代必然會需要持續的重構,我們最終實作跟預測差異多少,
    是否更有價值?

    View full-size slide

  27. Event Storming
    我們使用 Event Storming 對系統開發的幫助是什麼?

    View full-size slide

  28. 透過 Event Storming 能夠幫助我們全面的了解系統,避免在開發
    過程中失去控制

    View full-size slide

  29. 然而,在快速變化、有許多不確定性的情境中,還能在(前期)有
    幫助嗎?

    View full-size slide

  30. 敏捷開發要能夠成立所需的條件非常多,也包括工程師的自律以及
    需要有一定水準的能力等問題

    View full-size slide

  31. 規格
    透過約定確保底線

    View full-size slide

  32. 利用「驗收」規格來確保成果是可預期的,讓 User Interface 跟使
    用者、客戶期待的一致

    View full-size slide

  33. UI Application
    即使系統實作完全失控,至少使用者還能順利操作跟使用

    View full-size slide

  34. Command Process / System Event
    從 Event Storming 的角度看是很初期的實現,然而關鍵的 Command / Event 有被找出來

    View full-size slide

  35. 我們是否能夠將 Event Storming 分階段進行,是否可以從 User
    Story 中推導出 Event Storming 的內容?

    View full-size slide

  36. 介面
    保持系統的彈性

    View full-size slide

  37. 經常跟 Domain-Driven Design 一起談的是 Clean Architecture,
    這背後是如何保持彈性的技巧

    View full-size slide

  38. Input Function Output
    在生活經驗中,我們大多可以意識到一個動作會有「輸入」跟「輸出」成對

    View full-size slide

  39. Command Process / System Event
    對應到 Event Storming 上也可以得到相同的結構

    View full-size slide

  40. // Golang


    // ...
    package
    type interface
    error

    error

    {

    (context.Context)
    (context.Context)
    }


    daemon


    Service
    Start
    Stop

    View full-size slide

  41. // Golang


    package
    type struct
    func * error
    return
    func * error
    return
    {

    http.Server

    }


    (s HttpServer) (ctx context.Context) {

    s. ()

    }


    (s HttpServer) (ctx context.Context) {

    s. (ctx)

    }
    main


    HttpServer
    Start
    Stop
    ListenAndServe
    Shutdown

    View full-size slide

  42. // Golang


    // ...

    package
    import
    func
    :=
    &
    &
    if := !=
    (

    )


    () {

    d daemon. (

    HttpServer{ Addr: },

    GrpcServer{ Addr: },

    )


    err d. (context. ()); err {

    (err)

    }

    }
    main


    myapp/pkg/daemon
    main
    " "

    ":8080"
    ":8081"
    New
    Run Background nil
    panic

    View full-size slide

  43. 在這個例子,我們約定了 Daemon 會使用 Service 介面來啟動服
    務,如此一來我們只要符合條件就能夠被 Daemon 使用

    View full-size slide

  44. Scenario
    When
    Then
    :
    I click
    I can see
    When user click "Toggle" button and show "Hello"

    "Toggle"


    "Hello"
    # 介面:Button()

    # 輸入:ClickEvent(Label="Toggle")

    # 輸出:String("Hello")

    View full-size slide

  45. 從文件、規格的角度來看,我們實際上都是在定義介面。

    View full-size slide

  46. 架構
    有了介面就能劃分出邊界

    View full-size slide

  47. 介面反應了邊界(Boundary)因此我們有了架構上的邊界,或者在
    Domain-Driven Design 中的上下文(Context)邊界

    View full-size slide

  48. Presenter
    Application
    Domain Model
    從 Layered Architecture 作為例子,每一層之間都有一個約定的介面存在

    View full-size slide

  49. User
    HTTP Protocol
    Presenter
    以網站來說,使用者跟 Presenter 約定的介面可能會是 HTTP 協定

    View full-size slide

  50. Presenter
    User Flow
    Application
    Presenter 跟 Application 共同約定的介面是使用者流程

    View full-size slide

  51. Application
    Business Logic
    Domain Model
    Application 跟 Domain Model 共同約定的介面是商業邏輯

    View full-size slide

  52. 介面有點像是「黏著劑」想要將金屬跟玻璃緊密的黏在一起,就需
    要使用正確的黏著劑,軟體系統也是類似的

    View full-size slide

  53. 然而,如果黏著劑想要對應多種材質就很可能造成不牢固、難以清
    理等問題(跨層的依賴)

    View full-size slide

  54. 依賴
    用介面來控制依賴

    View full-size slide

  55. 想實現快速迭代的目標,就需要容易重構、擴充,那麼將物件的耦
    合(依賴)控制在最小就變得很重要

    View full-size slide

  56. Presenter
    Application
    Domain Model
    Infrastructure
    以 Layered Architecture 常見的介紹方式,很難說明 Infrastructure 的依賴關係

    View full-size slide

  57. Presenter
    Application
    Domain Model
    Infrastructure
    我認為這張圖的樣子更加合理一些,然而 Domain Model 該依賴其他物件嗎?

    View full-size slide

  58. Presenter
    Application
    Infrastructure
    Domain Model
    在我的經驗裡面,Domain Model 是沒有依賴的

    View full-size slide

  59. 大多數時候,只要遵循「只依賴相鄰的類型」以及「保持單向依
    賴」兩個原則,加上善用介面,大多能在必要時很好的進行抽換

    View full-size slide

  60. 導入
    有策略地進行應用

    View full-size slide

  61. 在經驗中,初期就開始引入 Domain-Driven Design 的概念會讓專
    案更容易維護,然而有許多限制,因此需要轉換成工作上的原則

    View full-size slide

  62. 案例
    近期的案例分享

    View full-size slide

  63. 這是一個活動的報到 App 後端,當參加者查詢狀態時會顯示活動資
    訊,並且紀錄上一次使用的時間

    View full-size slide

  64. Attendee Get Status AttendeeInfo
    GET /status?token=[TOKEN]

    View full-size slide

  65. Attendee Get Status AttendeeInfo
    Actor 是 Attendee,透過 `token` 參數識別

    View full-size slide

  66. Attendee Get Status AttendeeInfo
    我們要實作一個功能,查詢以及更新資訊

    View full-size slide

  67. Attendee Get Status AttendeeInfo
    回傳的結果是 AttendeeInfo 並且包含「名稱」和「票種」

    View full-size slide

  68. #language:zh-TW

    功能
    場景
    假設
    | token | name | type |

    | 1234567890 | 蒼時 | 一般票 |


    那麼
    :
    :
    有一張票券

    我發出 GET 請求到
    我會看到 JSON 格式的回應

    票券資訊

    當查詢票券資訊時,可以看到名稱和票種

    "/status?token=1234567890"

    """

    {

    "name": "蒼時",

    "type": "一般票"

    }

    """

    View full-size slide

  69. Presenter
    Application
    Infrastructure
    Domain Model
    從 Presenter(使用者介面) 開始處理

    View full-size slide

  70. interface extends
    :
    interface
    :
    :
    export async function : :
    return
    {

    ;

    }


    {

    ;

    ;

    }


    ( )
    < > {

    {

    name: ,

    type: ,

    };

    }
    AttendeeInfoRequest HttpRequest
    AttendeeInfoResponse
    statusHandler AttendeeInfoRequest
    Promise AttendeeInfoResponse
    token
    name
    type
    request
    string
    string
    string
    // ...

    '蒼時'
    '一般票'

    View full-size slide

  71. Presenter
    Application
    Infrastructure
    Domain Model
    接著再處理 Application(流程)的機制

    View full-size slide

  72. interface
    :
    :
    &
    export async function : :
    const =
    const = await
    return
    {

    ;

    } HttpRequest


    ( )
    < > {

    { , } request;

    attendeeUsecase. (token);


    {

    name: attendee.name,

    type: attendee.type,

    }

    }
    AttendeeInfoRequest
    AttendeeUsecase

    statusHandler AttendeeInfoRequest
    Promise AttendeeInfoResponse
    getAttendee
    token
    attendeeUsecase
    request
    string
    token attendeeUsecase
    attendee
    // ...

    // ...


    View full-size slide

  73. interface
    :
    :
    export class
    async : :
    return
    {

    ;

    ;

    }


    {

    ( ) < > {

    {

    name: ,

    type: ,

    };

    }

    }
    AttendeeInfo
    AttendeeUsecase
    getAttendee Promise AttendeeInfo
    name
    type
    token
    string
    string
    string
    // ...

    '蒼時'
    '一般票'

    View full-size slide

  74. Presenter
    Application
    Infrastructure
    Domain Model
    根據分析的結果,實作 Domain Model(領域模型)必要的部分

    View full-size slide

  75. // ...

    // ...


    export class
    public readonly :
    public readonly :
    private ?:
    constructor : :
    =
    =
    = new
    {

    ;

    ;


    ;


    ( , ) {

    .name name;

    .type type;

    }


    () {

    ._lastUsedAt ();

    }

    }
    Attendee
    AttendeeType
    Date
    AttendeeType
    touch
    name
    type
    _lastUsedAt
    name type
    string
    string
    this
    this
    this Date

    View full-size slide

  76. Presenter
    Application
    Infrastructure
    Domain Model
    進一步把 Domain Model 提供的功能加入到 Use Case 中

    View full-size slide

  77. export class
    private :
    async : :
    const : =
    await
    await
    return
    {

    ;


    ( ) < > {



    .attendees. (token);

    attendee. ();

    .attendees. (attendee);


    {

    name: attendee.name,

    type: attendee.type,

    };

    }

    }
    AttendeeUsecase
    AttendeeRepository
    getAttendee Promise AttendeeInfo
    Attendee
    findByToken
    touch
    save
    // Next

    // ...


    attendees
    token string
    attendee
    this
    this

    View full-size slide

  78. Presenter
    Application
    Infrastructure
    Domain Model
    將底層依賴最後再做判斷,也能減少實作受到底層依賴的限制(如:資料表)

    View full-size slide

  79. type =
    :
    :
    export class
    async : :
    const =
    return new
    {

    ;

    ;

    };


    {

    ( ) < > {

    .database

    . ( , [token])

    . < >(token);


    ({

    name: res.name,

    type: res.type,

    });

    }

    }

    AttendeeSchema
    PostgresAttendeeRepository
    findByToken Promise Attendee
    prepare
    first AttendeeSchema
    Attendee
    name
    type
    token
    string
    number
    string
    res this
    // ...

    '[SQL]'

    View full-size slide

  80. AttendeeInfoRequest / AttendeeInfoResponse
    Application
    Infrastructure
    Domain Model
    Presenter 的介面是 API Interface 的定義

    View full-size slide

  81. AttendeeInfoRequest / AttendeeInfoResponse
    getAttendee(token) / AttendeeInfo
    Infrastructure
    Domain Model
    Application 的介面是 UseCase 的方法和回傳

    View full-size slide

  82. AttendeeInfoRequest / AttendeeInfoResponse
    getAttendee(token) / AttendeeInfo
    Infrastructure
    Attendee
    Domain Model 的介面是 Entity、Service 等等物件

    View full-size slide

  83. AttendeeInfoRequest / AttendeeInfoResponse
    getAttendee(token) / AttendeeInfo
    AttendeeRepository
    Attendee
    Infrastructure 的介面會照 Presenter / Application 需要而定,

    沒辦法契合時則會實作 Adapter 來對應

    View full-size slide

  84. 綜合敏捷、Clean Architecture 和 Domain-Driven Design 的優
    點,在一些地方作出讓步,能得到不錯的平衡

    View full-size slide

  85. 從另一個角度來看,開發初期加入 Domain-Driven Design 似乎會
    多耗費一點開發的時間,然而隨著架構完善實作的速度反而能加快

    View full-size slide

  86. 開發初期就開始導入跟敏捷並不太衝突,需要的是視情況而定做一
    些取捨跟調整,反覆檢查是否有偏離即可

    View full-size slide

  87. Blog https://blog.aotoki.me
    歡迎關注我的部落格,目前週更的主題都圍繞在今天的分享內容上

    View full-size slide