Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
2023 - RubyConfTW - Rethink Rails Architecture
Search
蒼時弦や
December 17, 2023
Programming
0
100
2023 - RubyConfTW - Rethink Rails Architecture
蒼時弦や
December 17, 2023
Tweet
Share
More Decks by 蒼時弦や
See All by 蒼時弦や
2024 - COSCUP - Clean Architecture in Rails
elct9620
2
120
20230916 - DDDTW - 導入 Domain-Driven Design 的最佳時機
elct9620
0
370
2023 - WebConf - 選擇適合你的技能組合
elct9620
0
580
20230322 - Generative AI 小聚 ft. Happy Designer
elct9620
0
310
2022 - 默默會 - 重新學習 MVC 的 Model
elct9620
1
410
MOPCON 2022 - 從 Domain-Driven Design 看網站開發框架隱藏
elct9620
1
430
2022 - COSCUP - 我想慢慢寫程式該怎麼辦?
elct9620
0
220
2022 - COSCUP - 打造高速 Ruby 專案開發流程
elct9620
0
240
2021 - RubyKaigi - It is time to build your mruby VM on the microcontroller?
elct9620
0
230
Other Decks in Programming
See All in Programming
Recoilを剥がしている話
kirik
5
6.6k
Keeping it Ruby: Why Your Product Needs a Ruby SDK - RubyWorld 2024
envek
0
180
短期間での新規プロダクト開発における「コスパの良い」Goのテスト戦略」 / kamakura.go
n3xem
2
160
create_tableをしただけなのに〜囚われのuuid編〜
daisukeshinoku
0
240
HTTP compression in PHP and Symfony apps
dunglas
2
1.7k
fs2-io を試してたらバグを見つけて直した話
chencmd
0
220
nekko cloudにおけるProxmox VE利用事例
irumaru
3
420
LLM Supervised Fine-tuningの理論と実践
datanalyticslabo
3
950
生成AIでGitHubソースコード取得して仕様書を作成
shukob
0
180
創造的活動から切り拓く新たなキャリア 好きから始めてみる夜勤オペレーターからSREへの転身
yjszk
1
130
Jakarta EE meets AI
ivargrimstad
0
230
return文におけるstd::moveについて
onihusube
1
730
Featured
See All Featured
Put a Button on it: Removing Barriers to Going Fast.
kastner
59
3.6k
Navigating Team Friction
lara
183
15k
The Straight Up "How To Draw Better" Workshop
denniskardys
232
140k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
38
1.9k
Code Reviewing Like a Champion
maltzj
520
39k
The Language of Interfaces
destraynor
154
24k
Music & Morning Musume
bryan
46
6.2k
Scaling GitHub
holman
458
140k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
28
900
Typedesign – Prime Four
hannesfritz
40
2.4k
Making the Leap to Tech Lead
cromwellryan
133
9k
Raft: Consensus for Rubyists
vanstee
137
6.7k
Transcript
Rethink Rails Architecture The good part and challenges of Rails
蒼時弦也 Software Developer https://blog.aotoki.me @elct9620v
None
None
Model-View Controller Pattern & Style Apply to Rails Rethink
Model-View Controller Pattern & Style Apply to Rails Rethink
Model-View Controller Pattern & Style Apply to Rails Rethink
Model-View Controller Pattern & Style Apply to Rails Rethink
軟體架構
規範
關於軟體結構的推論
一系列關於軟體結構的推論,來規範軟體的系統組成
Problem Solution 當遇到 要解決時,我們會提出一個 問題 解決方案
Problem Solution Solution Solution 要解決問題,通常會有好幾種方案,就需要選擇最佳的方案
Problem Architecture Solution Solution Solution 當有架構做為指引,可以讓我們更容易篩選適當的方案
架構不定義細節,會以模式、風格的理論來構成
The Model-View-Controller
一種軟體設計模式(Software Design Pattern)
用於開發使用者介面的設計模式
Model MVC 的核心,動態的資料結構,獨立與使用者介面。直接管理應用 的資料、邏輯和規則。
View 是任何資訊的表現(Representation)以 Rails 為例子,就是 HTML 的部分,同一種資訊可以有多種表現。
Controller 接受事件,轉換成命令給 Model 或者 View。以 Rails 為例子,會 呼叫 Controller 的方法與相關的
Model 互動再用 View 回傳。
MVC 的概念不複雜,適合處理資料跟呈現的對應
ActiveRecord
一種架構模式(Architecture Pattern)
物件的屬性會跟資料表直接對應(Object-Relation Mapping)
資料庫操作 應用邏輯 跟 耦合在一起,違反單一職責跟關注點分離
在開發時會變成 Data-Driven 的形狀,對複雜行為不容易處理
Model Driven
根據領域知識(Domain Knowledge)來設計概念模型(或稱領域 模型,Domain Model),來描述行為跟資料
先定義資料、行為,再決定如何儲存到資料庫
# Domain Model # Value Object class def new end
end . (@currency, @amount) Wallet balance Money
amount . ( , ) @wallet . @wallet.deposit(amount) @wallet.save =
new = new Money :TWD 100 Wallet 以 AcitveRecord 的角度,我們的 Money 物件該如何對應資料表欄位?
id 1 2 2 currency TWD USD TWD amount 100
3.3 150
Data-Driven 的設計,很難對應有複雜問題的系統
class < end composed_of , , { , } Wallet
ActiveRecord::Base :balance class_name: mapping: currency: :currency amount: :amount "Money" Rails 中提供了 composed_of 來做出對應,然而無法限制直接存取屬性
Data Context Interaction
透過區分資料、脈絡、互動來對 MVC 進行補全
# Data # ... # ... class attr_reader def +=
end end (amount) @balance amount BankAccount increment_balance :balance 概念上非常接近 Model,反應使用者、領域專家心中的資訊結構
# Context # ... # Roles # ... # def
source # ... # end class def end end source.transfer(@amount) MoneyTransferContext execute 用於描述某個脈絡,以及定義參與的角色,可以在不同情境被使用
# 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 的狀態
Interaction 在不同語言有不同的方式,通常會是樣板或者 DSL
DCI 其中一個目的是將商業邏輯和業務行為切分開來
Aggregate Root
Domain-Driven Design 提出的一種模型
是 Entity 的一種,會聚合 Value、Entity 統一處理
一個系統的最小操作單位
Order OrderItem OrderItem OrderItem 訂單中的「品項」不會 於訂單出現操作 獨立
def end .create( @order) update OrderItem order: # or def
build end @order.items. (...) @order.save update 兩種不同的寫法,後者在模型的設計上更加合理
當我們可以任意對應資料表時,很容易忽略 的問題 操作單位
Clean Architecture
處理「依賴」的技巧
Controller Low-Level View Low-Level Model High-Level MVC 的依賴關係很單純
當系統變複雜的時候,物件該如何被安排?
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Controller View Context Model 加入 DCI 的概念,用 Context 來增加一層抽象
Architecture Framework Design 粗略 詳細 以「框架」的定位,是選擇某種架構但沒有詳細設計的階段
當我們想將各種概念套用到 Rails 時會有架構上的衝突
Aggregate Root in Rails
Aggregate Root 看似沒問題,然而還有不少難以界定的情況
class < end has_many has_many has_many User ActiveRecord::Base :payments :orders
:carts # ... User 關聯了非常多不同相關的 Model
def = end def = end @order current_user.orders.create(...) @order .create(
current_user.id) create create # or Order user_id: 假設 Order 是操作的基礎單位,那麼後者的實作更為合理
大多數 SaaS 最後都會得出 User 是 Aggregate Root 的結果
如果沒有區分出操作單位,最後 User 的「職責」會變得非常大
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 的合理性
class < end has_many has_many , has_many , User ActiveRecord::Base
:memberships :owned_stores class_name: :stores through: :memberships # ??? # ... 'Store' Store 作為 Aggregate Root 時,不使用能更清楚地表達界線
要實現「店家查詢會員列表」那麼 Membership 由誰負責?
ActiveRecord 的限制讓我們無法將資料存取跟商業邏輯區分開來
Data Context Interaction in Rails
Ruby 的語言特性,以及 Rails 的設計能滿足 DCI 的期望嗎?
class def end def new end end (amount) source.transfer(amount) ::
. ( , @source) TransferContext execute source # ... # where is receiver? Transfer Source self 這是 C++ 的方式,然而 C++ 能透過 Template 自動產生程式碼
class def end def extend end end (amount) source.transfer(destination, amount)
@source. :: TransferContext execute source # ... Transfer Source 符合 Ruby 語言特性的解法,然而 Role 無法參考 Context
class < include end class def end end :: (amount)
@source.transfer(destination, amount) BankAccount ApplicationRecord TransferContext execute Transfer Source # ... Rails 的用法,符合慣例卻違反 Clean Architecture
不依靠 DSL 的前提下,沒辦法很好滿足 DCI 的要求
Model Concern 是最容易的方案,但 Role 無法依照 Context 改變
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 仍是非常有價值,容易演進成讀寫分離的架構
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 使用相同的邏輯
Form / Service Object 是 Context 的一種表現
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 兩種物件職責
Rethink Architecture Rails
Rails 框架採用 MVC 的方式在問題不複雜時非常容易使用
當問題變複雜時,原有的架構不足以細分職責
導入 Clean Architecture 可以在經常變動的地方讓修改更容易
以模型驅動方式思考,並且區分出處理單位能更好對應複雜問題
加入 DCI 的設計和現有大多方法不衝突,但能更清楚描述意圖
仍有許多問題還沒有探討,如:Event、View、Rails Engine 等
在 Rails 框架下,如何形成可行的模式、風格仍有許多討論空間
Blog https://blog.aotoki.me 歡迎關注我的部落格