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

2023 - RubyConfTW - Rethink Rails Architecture

2023 - RubyConfTW - Rethink Rails Architecture

蒼時弦や

December 17, 2023
Tweet

More Decks by 蒼時弦や

Other Decks in Programming

Transcript

  1. # Domain Model # Value Object class def new end

    end . (@currency, @amount) Wallet balance Money
  2. amount . ( , ) @wallet . @wallet.deposit(amount) @wallet.save =

    new = new Money :TWD 100 Wallet 以 AcitveRecord 的角度,我們的 Money 物件該如何對應資料表欄位?
  3. class < end composed_of , , { , } Wallet

    ActiveRecord::Base :balance class_name: mapping: currency: :currency amount: :amount "Money" Rails 中提供了 composed_of 來做出對應,然而無法限制直接存取屬性
  4. # Data # ... # ... class attr_reader def +=

    end end (amount) @balance amount BankAccount increment_balance :balance 概念上非常接近 Model,反應使用者、領域專家心中的資訊結構
  5. # Context # ... # Roles # ...
 # def

    source # ... # end class def end end source.transfer(@amount) MoneyTransferContext execute 用於描述某個脈絡,以及定義參與的角色,可以在不同情境被使用
  6. # Interaction # ... # or (discuss later) # or

    (discuss later) class def new extend end end :: . ( , @source) @source. :: @source MoneyTransferContext source Transfer Source self Transfer Source 根據 Context 定義行為來改變 Data 的狀態
  7. def end .create( @order) update OrderItem order: # or def

    build end @order.items. (...) @order.save update 兩種不同的寫法,後者在模型的設計上更加合理
  8. class < end has_many has_many has_many User ActiveRecord::Base :payments :orders

    :carts # ... User 關聯了非常多不同相關的 Model
  9. def = end def = end @order current_user.orders.create(...) @order .create(

    current_user.id) create create # or Order user_id: 假設 Order 是操作的基礎單位,那麼後者的實作更為合理
  10. def = end def = = end @stores @user.stores store_ids

    @user.memberships.pluck( ) @stores .where( store_ids) index index # or :store_id Store id: 我們應該思考使用 has_many through 的合理性
  11. class < end has_many has_many , has_many , User ActiveRecord::Base

    :memberships :owned_stores class_name: :stores through: :memberships # ??? # ... 'Store' Store 作為 Aggregate Root 時,不使用能更清楚地表達界線
  12. class def end def new end end (amount) source.transfer(amount) ::

    . ( , @source) TransferContext execute source # ... # where is receiver? Transfer Source self 這是 C++ 的方式,然而 C++ 能透過 Template 自動產生程式碼
  13. class def end def extend end end (amount) source.transfer(destination, amount)

    @source. :: TransferContext execute source # ... Transfer Source 符合 Ruby 語言特性的解法,然而 Role 無法參考 Context
  14. class < include end class def end end :: (amount)

    @source.transfer(destination, amount) BankAccount ApplicationRecord TransferContext execute Transfer Source # ... Rails 的用法,符合慣例卻違反 Clean Architecture
  15. class < def end end class < def = =

    = = new end end (amount); source_id transfer_params[ ] destination_id transfer_params[ ] amount transfer_params[ ] command :: . (source_id, destination_id) command.execute(amount) Transfer::Command Context execute Wallet::TransferController ApplicationController create :source_id :destination_id :amount Transfer Command 即使如此 DCI 仍是非常有價值,容易演進成讀寫分離的架構
  16. class < def end end class < def = new

    end end (amount); (source_id, destination_id, amount) command :: . (source_id, destination_id) command.execute(amount) Transfer::Command Context execute Subscription::RenewJob ApplicationJob perform Transfer Command 也能很好的重複在 Controller / Job 使用相同的邏輯
  17. class < include def end end :: attribute , attribute

    , validates , valid! Transfer::Command Context execute ActiveModel Model :source_id :string :amount :integer :source_id presence: true # ... # ... Command 會做輸入的檢查,滿足 Form + Service 兩種物件職責