ADZ 學習筆記

Ruby/Rails, Startup, Life

rails use case - mixin & concerns

| Comments

了解 concerns 前,應該先講講 ruby mixin,因為 concerns 的設計加強了 ruby mixin 技巧,並額外解決 module 間的相依相問題,範例可參考 這篇,而使用 concerns 也讓 code 變得更簡潔了。

mixin

ruby 語言裡面可以把共同邏輯寫在 module,在讓不同 class 來 includeextend,除了能達成多重繼承外讓 code 更 DRY 外,也可以拿來擴充已存在的物件、或基本物件如 String Boolean 等等,規則如下:

  1. class include module 會把該 module 的 methods 變成 instance method
  2. class extend module 會把該 module 的 methods 變成 class method

(這裡指的 extend 不是指繼承)

include
# app/models/concerns/departs.rb

module Departs
  def from_site
    I18n.t("departs.#{from_id}")
  end
  
  def to_site
    I18n.t("departs.#{to_id}")
  end
end

# app/models/thsr_groupbuys.rb

class ThsrGroupbuys < ActiveRecord::Base
  # 假設這個 model 有 from_id & to_id column

  include Departs
end

# app/models/thsr_groupbuys.rb

class ThsrTickets < ActiveRecord::Base
  # 假設這個 model 有 from_id & to_id column

  include Departs
end

這樣一來把 ThsrTicketsThsrGroupbuys 都擁有了 from_site to_site 的 instance method,而不用分別重寫在兩個 model 裡,所以你可以這樣操作。

@group = ThsrGroupbuys.find(1)
@group.from_site # 左營

@group.to_site # 台北


@ticket = ThsrTicket.find(1)
@ticket.from_site # 左營

@ticket.to_site # 台北
extend
module TranslateModelName
  def model_name
    I18n.t("models_name.#{self.to_s.downcase}")
  end
end

class ThsrGroupbuy < ActiveRecord::Base
  extend TranslateModelName
end

這樣一來 model_name 就會變成 ThsrGroupbuy 的 class level method,所以你就可以這樣用:

ThsrGroupbuy.model_name 

(以上這個例子不是真實情況,ActiveRecord 已經有類似的 method)

同時需要 class methods instance methods

拿 departs module 的例子,如果我希望可以使用 ThsrGroupbuy.sites_list ThsrTicket.sites_list 可以得到車站的陣列,則必須修改成以下:

# app/models/concerns/departs.rb

module Departs

  def self.included(base)
    base.class_eval do 
      def self.sites_list
        %w(左營 台南 嘉義 台中 桃園 新竹 板橋 台北)
      end
    end
  end

  def from_site
    I18n.t("departs.#{from_id}")
  end
  
  def to_site
    I18n.t("departs.#{to_id}")
  end
end

以上的 code 只要任何 class include 這支 module 則會執行這裡的 self.included,並傳入是誰 include 了這支 module 到第一個參數 base,然後再用 class level eval 去創建該 class 的 method。

以上是基本的 mixin 範例,另外還可以搭配各種設計方式來達到不同的需求 (等等會提到)

Concderns

剛剛 mixin 的語法感覺不是那麼簡潔,如果用 concerns 寫法則會變成:

# app/models/concerns/departs.rb

module Departs
  extend ActiveSupport::Concern

  included do
    # 可以在這裡放當 include 時要執行的東西

    # 你可以存取所有 class level 的東西

    # ex1: 宣告 shared scope

    # ex2: 可寫 shared validation

  end

  module ClassMethods
    def sites_list
      %w(左營 南台 嘉義 台中 桃園 新竹 板橋 台北)
    end
  end

  def from_site
    I18n.t("departs.#{from_id}")
  end
  
  def to_site
    I18n.t("departs.#{to_id}")
  end
end

不需要 class eval 直接宣告一個 ClassMethods module,裡面的 methods 全部會變成 class level method,是不是很方便呢? (記得 ClassMethods 大小寫不要打錯了)

相依性

如果你的 concerns 跟 concerns 間有相依關係,例如有一個 concerns 叫做 HightSpeedRail 相依 Departs 和其他 modules, concerns 並組合出一個新功能,則可以這樣使用:

# app/models/concerns/high_speed_rail.rb

module HighSpeedRail
  extend ActiveSupport::Concern
  include Departs
  include ... ()
  include ... ()
  include ... ()


  # ... (略)

end

最重要的使用情境

工具只是工具,重要的是如何使用、如何設計出彈性、和容易維護,除了拿 concerns 來寫一些 shared virtual attributes 外,還有一些其他的技巧:

  1. concerns 實作 class 決定行為 (TODO: 未完成)
  2. 搭配 meta-programming (TODO: 未完成)
  3. 擴充已存在的物件 (TODO: 未完成)

Tips

  1. mixin & concerns 本身特性就有些錯綜,所以搞清楚你現在的 scope 是在 class level 還是 instance level 非常重要。
  2. 越 DRY 的 code 則有可能出現維護的問題,一些教學中指出 concerns 的問題是,只單看 concerns 內容無法得知上下文 (哪些 class 使用了),如果過度使用反而可能造成更難維護的情況。

source from: https://github.com/afunction/rails-use-cases

Comments

comments powered by Disqus