Image by ArthurHidden on Freepik
用 Ruby 的 Mixin 加入脈絡增加可讀性
大多數使用過 Ruby 的工程師都知道 Ruby 有一個特別的語言特性叫做 Mixin(混合)可以透過定義一個 Module(模組)然後被其他類別引用,如果從 DCI(Data Context Interaction)的角度來看,其實是一種脈絡的表現。
Mixin 的運作
Ruby 的 Mixin 運作機制大致上來說不複雜,我們可以使用 include
、 extend
以及 perpend
這三個方法根據情境「插入模組」到繼承鏈之中,以比較常用的 include
為例子會像這樣。
module Attackable
def attack(target)
# ...
end
end
class Actor
include Attackable
end
pp Actor.ancestors
# => [Actor, Attackable, Object, Kernel, BasicObject]
簡單來說 Mixin 的運作是將原本搜尋方法的順序改變,從 Actor
到 Object
的順序中,加入了Attackable
模組,因此就可以搶在 Object
之前找到 #attack
方法來使用,而 prepend
和 extend
則是跟 include
插入到不同的位置來達到不同的效果。
Ruby on Rails 的脈絡呈現
若是要舉例,用 Ruby on Rails 會容易些,因為在框架的設計上就有很深入的考慮到 DCI 的概念,如果還不清楚可以參考自然地在 Rails 中應用 Data Content Interaction 這篇文章的簡介。
大多數時候,我們的 Controller 就是作為「描述脈絡」的角色,因此通常會看到如下的實現。
class AttackController < ApplicationController
before_action :find_player
before_action :find_activate_battle
def create
@monster = @battle.monsters.find(params[:monster_id])
@battle.attack(
from: @player,
target: @monster
)
@battle.save!
render json: @battle.events
end
# ...
end
如果我們轉換成 Cucumber 的描述,就可以變成類似這樣的敘述
#language: zh-TW
功能: 戰鬥系統
# AttackController
場景: 玩家對指定怪物發起攻擊
# before_action :find_player
假定 這裡有一個玩家 "蒼時"
# before_action :find_activate_battle
而且 這裡有一個進行中的戰鬥
| name | monster_id | monster_type |
| Slime-1 | 1 | Slime |
# @battle.attack(...)
當 玩家對名為 "Slime-1" 的怪物攻擊
# render json: @battle.events
那麼 將會看到 "Attack Slime-1 success" 的結果
由此可見,像是 before_action
這些 DSL(Domain Specific Language,領域特訂語言)能夠用於「描述脈絡」而使用的。
如果想要擴充這些行為,在 Rails 可以使用基於 Ruby Mixin 所設計的 Concern 機制來增加可以用的「描述方式」
Mixin 的使用案例
要利用 Mixin 來增加脈絡的資訊,就用我最近針對 Feature Flag(特性切換)的實作來作為例子。
我使用的是 Flipper 這個套件,他能讓我使用類似這樣的方式保護某個尚未釋出的功能。
def index
raise UnreleasedError unless Flipper.enabled?(:preview, current_user)
# ...
end
假設不是指定的使用者,就無法使用「預覽版(Preview)」的功能,然而要在每個地方都重複寫 Flipper.enabled
? 其實是有點麻煩的,因此大多數人都會封裝成一個方法。
但是,如果考慮到「脈絡」跟「DSL」的特性,我們會實現像這樣的 Concern 模組。
module Previewable
class UnreleasedError < RuntimeError; end
extend ActiveSupport::Concern
included do
helper_method :preview?
end
class_methods do
def unreleased(**options)
before_action -> { raise UnreleasedError unless preview? }, **options
end
end
def preview?
Flipper.enabled?(:preview, current_user)
end
end
在這裡我們利用 extend
讓這個模組可以使用 ActiveSupport::Concern
的機制,這點大家應該不陌生,這其實就已經是一種脈絡的表現用來表明「這是一種 Concern」而我們會自然地使用 included
和 class_methods
這兩個 DSL 來描述。
首先,我們先將 Flipper.enabled?
封裝成 preview?
來加強語意,接下來用 included
透過 helper_method
聲明「可以被 View 使用」同時在 class_methods 中定義了新的 DSL 叫做 unreleased
(未釋出),他會在這個 Controller 開始前檢查「是否可以使用這個動作」
放到 Controller 裏面,就會發現「可讀性被提高」
class AutoAttackController < ApplicationController
# 表示「包含預覽」
include Previewable
# 描述未釋出的部分,除了 index 動作外
unreleased except: %i[index]
def index
# 如果是預覽模式,用不同畫面呈現
return render :index_v2 if preview?
# ...
end
# ...
end
基於 Mixin 的方式,我們會發現對於整個 Controller 的行為描述變的非常清晰,我們可以透過 DSL 的擴充對整個情境補充,或者方法來提供這個情境可以「做特定行為」
也許我們該反思,許多時候我們利用 Rails 的 Callback 機制(before_action)是否是在描述「前提」還是單純的想把過長的重複程式碼分離出去
本文引用自弦而時習之的用 Ruby 的 Mixin 加入脈絡增加可讀性
如果你喜歡這篇文章,想要更深入了解技術,我們有開設蒼時弦也的 📒 「開發必學的需求分析法」以及「Rails 開發者的進階實戰」課程唷 ❤️️ 歡迎報名!