Slide 1

Slide 1 text

Rethink Rails Architecture The good part and challenges of Rails

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Model-View Controller Pattern & Style Apply to Rails Rethink

Slide 6

Slide 6 text

Model-View Controller Pattern & Style Apply to Rails Rethink

Slide 7

Slide 7 text

Model-View Controller Pattern & Style Apply to Rails Rethink

Slide 8

Slide 8 text

Model-View Controller Pattern & Style Apply to Rails Rethink

Slide 9

Slide 9 text

軟體架構

Slide 10

Slide 10 text

規範

Slide 11

Slide 11 text

關於軟體結構的推論

Slide 12

Slide 12 text

一系列關於軟體結構的推論,來規範軟體的系統組成

Slide 13

Slide 13 text

Problem Solution 當遇到 要解決時,我們會提出一個 問題 解決方案

Slide 14

Slide 14 text

Problem Solution Solution Solution 要解決問題,通常會有好幾種方案,就需要選擇最佳的方案

Slide 15

Slide 15 text

Problem Architecture Solution Solution Solution 當有架構做為指引,可以讓我們更容易篩選適當的方案

Slide 16

Slide 16 text

架構不定義細節,會以模式、風格的理論來構成

Slide 17

Slide 17 text

The Model-View-Controller

Slide 18

Slide 18 text

一種軟體設計模式(Software Design Pattern)

Slide 19

Slide 19 text

用於開發使用者介面的設計模式

Slide 20

Slide 20 text

Model MVC 的核心,動態的資料結構,獨立與使用者介面。直接管理應用 的資料、邏輯和規則。

Slide 21

Slide 21 text

View 是任何資訊的表現(Representation)以 Rails 為例子,就是 HTML 的部分,同一種資訊可以有多種表現。

Slide 22

Slide 22 text

Controller 接受事件,轉換成命令給 Model 或者 View。以 Rails 為例子,會 呼叫 Controller 的方法與相關的 Model 互動再用 View 回傳。

Slide 23

Slide 23 text

MVC 的概念不複雜,適合處理資料跟呈現的對應

Slide 24

Slide 24 text

ActiveRecord

Slide 25

Slide 25 text

一種架構模式(Architecture Pattern)

Slide 26

Slide 26 text

物件的屬性會跟資料表直接對應(Object-Relation Mapping)

Slide 27

Slide 27 text

資料庫操作 應用邏輯 跟 耦合在一起,違反單一職責跟關注點分離

Slide 28

Slide 28 text

在開發時會變成 Data-Driven 的形狀,對複雜行為不容易處理

Slide 29

Slide 29 text

Model Driven

Slide 30

Slide 30 text

根據領域知識(Domain Knowledge)來設計概念模型(或稱領域 模型,Domain Model),來描述行為跟資料

Slide 31

Slide 31 text

先定義資料、行為,再決定如何儲存到資料庫

Slide 32

Slide 32 text

# Domain Model # Value Object class def new end end . (@currency, @amount) Wallet balance Money

Slide 33

Slide 33 text

amount . ( , ) @wallet . @wallet.deposit(amount) @wallet.save = new = new Money :TWD 100 Wallet 以 AcitveRecord 的角度,我們的 Money 物件該如何對應資料表欄位?

Slide 34

Slide 34 text

id 1 2 2 currency TWD USD TWD amount 100 3.3 150

Slide 35

Slide 35 text

Data-Driven 的設計,很難對應有複雜問題的系統

Slide 36

Slide 36 text

class < end composed_of , , { , } Wallet ActiveRecord::Base :balance class_name: mapping: currency: :currency amount: :amount "Money" Rails 中提供了 composed_of 來做出對應,然而無法限制直接存取屬性

Slide 37

Slide 37 text

Data Context Interaction

Slide 38

Slide 38 text

透過區分資料、脈絡、互動來對 MVC 進行補全

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

# 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 的狀態

Slide 42

Slide 42 text

Interaction 在不同語言有不同的方式,通常會是樣板或者 DSL

Slide 43

Slide 43 text

DCI 其中一個目的是將商業邏輯和業務行為切分開來

Slide 44

Slide 44 text

Aggregate Root

Slide 45

Slide 45 text

Domain-Driven Design 提出的一種模型

Slide 46

Slide 46 text

是 Entity 的一種,會聚合 Value、Entity 統一處理

Slide 47

Slide 47 text

一個系統的最小操作單位

Slide 48

Slide 48 text

Order OrderItem OrderItem OrderItem 訂單中的「品項」不會 於訂單出現操作 獨立

Slide 49

Slide 49 text

def end .create( @order) update OrderItem order: # or def build end @order.items. (...) @order.save update 兩種不同的寫法,後者在模型的設計上更加合理

Slide 50

Slide 50 text

當我們可以任意對應資料表時,很容易忽略 的問題 操作單位

Slide 51

Slide 51 text

Clean Architecture

Slide 52

Slide 52 text

處理「依賴」的技巧

Slide 53

Slide 53 text

Controller Low-Level View Low-Level Model High-Level MVC 的依賴關係很單純

Slide 54

Slide 54 text

當系統變複雜的時候,物件該如何被安排?

Slide 55

Slide 55 text

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

Slide 56

Slide 56 text

Controller View Context Model 加入 DCI 的概念,用 Context 來增加一層抽象

Slide 57

Slide 57 text

Architecture Framework Design 粗略 詳細 以「框架」的定位,是選擇某種架構但沒有詳細設計的階段

Slide 58

Slide 58 text

當我們想將各種概念套用到 Rails 時會有架構上的衝突

Slide 59

Slide 59 text

Aggregate Root in Rails

Slide 60

Slide 60 text

Aggregate Root 看似沒問題,然而還有不少難以界定的情況

Slide 61

Slide 61 text

class < end has_many has_many has_many User ActiveRecord::Base :payments :orders :carts # ... User 關聯了非常多不同相關的 Model

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

大多數 SaaS 最後都會得出 User 是 Aggregate Root 的結果

Slide 64

Slide 64 text

如果沒有區分出操作單位,最後 User 的「職責」會變得非常大

Slide 65

Slide 65 text

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 的合理性

Slide 66

Slide 66 text

class < end has_many has_many , has_many , User ActiveRecord::Base :memberships :owned_stores class_name: :stores through: :memberships # ??? # ... 'Store' Store 作為 Aggregate Root 時,不使用能更清楚地表達界線

Slide 67

Slide 67 text

要實現「店家查詢會員列表」那麼 Membership 由誰負責?

Slide 68

Slide 68 text

ActiveRecord 的限制讓我們無法將資料存取跟商業邏輯區分開來

Slide 69

Slide 69 text

Data Context Interaction in Rails

Slide 70

Slide 70 text

Ruby 的語言特性,以及 Rails 的設計能滿足 DCI 的期望嗎?

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

不依靠 DSL 的前提下,沒辦法很好滿足 DCI 的要求

Slide 75

Slide 75 text

Model Concern 是最容易的方案,但 Role 無法依照 Context 改變

Slide 76

Slide 76 text

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 仍是非常有價值,容易演進成讀寫分離的架構

Slide 77

Slide 77 text

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 使用相同的邏輯

Slide 78

Slide 78 text

Form / Service Object 是 Context 的一種表現

Slide 79

Slide 79 text

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 兩種物件職責

Slide 80

Slide 80 text

Rethink Architecture Rails

Slide 81

Slide 81 text

Rails 框架採用 MVC 的方式在問題不複雜時非常容易使用

Slide 82

Slide 82 text

當問題變複雜時,原有的架構不足以細分職責

Slide 83

Slide 83 text

導入 Clean Architecture 可以在經常變動的地方讓修改更容易

Slide 84

Slide 84 text

以模型驅動方式思考,並且區分出處理單位能更好對應複雜問題

Slide 85

Slide 85 text

加入 DCI 的設計和現有大多方法不衝突,但能更清楚描述意圖

Slide 86

Slide 86 text

仍有許多問題還沒有探討,如:Event、View、Rails Engine 等

Slide 87

Slide 87 text

在 Rails 框架下,如何形成可行的模式、風格仍有許多討論空間

Slide 88

Slide 88 text

Blog https://blog.aotoki.me 歡迎關注我的部落格