iT邦幫忙

2021 iThome 鐵人賽

DAY 12
0
Modern Web

初階 Rails 工程師的養成系列 第 12

Day12. Class Method 與 MetaClass 的觀念

昨天提到的實體方法,是為了創建的物件提供方法使用,而與實體方法相對的另一個方法叫類別方法,是類別本身的方法。

與實體變數不一樣,類別方法多用在提供特定的方法解決問題。先忽略class << self,可以看到以下的三種方法appreciation_period, notification_sent_at, sf_sent_at都是用來提供解決特定用途的方式。

class Utility  
  class << self
    #==== 鑑賞期
    #==== 用法: Utility.appreciation_period
    def appreciation_period
      Rails.env.production? ? 2.days : 1.minute
    end

    #==== 推播發送時間
    #==== 用法: Utility.notification_sent_at
    def notification_sent_at
      send_time(8, 22)
    end

    #==== 物流收貨時間
    #==== 用法: Utility.sf_sent_at
    def sf_sent_at
      send_time(11, 17).strftime("%F %T")
    end

    def send_time(start_hour, end_hour)
      zero_am = DateTime.now.beginning_of_day
      started_at = DateTime.now.beginning_of_day.change(hour: start_hour)
      ended_at = DateTime.now.beginning_of_day.change(hour: end_hour)

      if Time.current.between?(zero_am, started_at)
        [false, started_at]
      elsif Time.current.between?(ended_at, DateTime.now.tomorrow.beginning_of_day)
        [false, DateTime.now.tomorrow.beginning_of_day.change(hour: start_hour)]
      else
        [true, Time.current]
      end
    end
  end
end

接著我們看下列的ApiClient 為打API 用途所設計的類別方法。這種寫法在許多專案內很常看到,而將打API流程包覆成新方法的好處為

  • 我們可以輕易的改寫打 Api 的方式,這邊使用的是RestClient::Request,若我們要改成其他方式如Net::HTTP, Faraday也是輕而易舉。使用上面的方法,我們不要改掉專案所有打Api的地方,只需改ApiClient即可。
  • 可以在包覆的方法中寫客製化的操作,例如印出請求跟回應的log,以及針對不同的資料回應錯誤做rescue

下列程式碼還有許多看點

  • 為了呼應 Day4 OpenStruct的方法,我們這邊進行實際應用
  • define_method 為一種動態宣告方法的一種方式,我們會在Day17介紹
class ApiClient
  DEFAULT_TIMEOUT = 200
  # 範例
  #
  # GET  => ApiClient.get 'https://jsonplaceholder.typicode.com/todos/1'
  # POST => ApiClient.post 'https://jsonplaceholder.typicode.com/posts', 
  #                         payload: { title: 'foo', body: 'bar', userId: 1 }
  class << self
    %i(get delete post put patch).each do |http_method|
      define_method(http_method) do |url, headers: { content_type: :json }, payload: {}, options: {}|
        begin
          Rails.logger.info("Request payload: #{payload}, 
                             request header: #{headers}, and url: #{url}")

          response = RestClient::Request.execute(method: http_method, url: url,
                                                 payload: payload, headers: headers,
                                                 timeout: (options[:timeout] || DEFAULT_TIMEOUT))

          Rails.logger.info("[Expected] Response code: #{response.code}, 
                             and response payload: #{response.body}")

          OpenStruct.new({ code: response.code, body: response.body })
        rescue RestClient::Exceptions::Timeout => e
          Rails.logger.info("超過等待時間: #{e}")

          OpenStruct.new({ code: nil, body: nil })
        rescue RestClient::ExceptionWithResponse => e
          Rails.logger.info("[Unexpected] Response code: #{e.response.code}, 
                             and response payload: #{e.response.body}")

          OpenStruct.new({ code: e.response.code, body: e.response.body })
        end
      end
    end
  end
end

看完上述實際的例子後,接著我們看下列的基本例子

ClassMethod

以下為類別方式的用法,簡單明瞭!

class Orangutan
  def self.bar
    'bar'
  end
end

Orangutan.bar   #=> bar

接著我們介紹在 Day11提及過的 << 方法

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
end

orangutan = Animal.new

class << orangutan
  def foo
    'foo'
  end
end

orangutan.foo

我們再宣告另外一隻紅色猩猩,並同樣的呼叫foo,發現這隻紅色猩猩不會使用foo方法

red_orangutan = Animal.new
red_orangutan.species
red_orangutan.foo       #=> NoMethodError (undefined method `foo' for #<Animal:0x00007fd065edf480>)

以下的故事內容,如果有看關於我轉成為史萊姆的那檔事可能會比較好理解:

上述的例子,我們只為 orangutan 給予 foo 方法,這種給予方法的方式稱為單體方法 singleton。單體的意思是,只有這個 orangutan 獨特技能 foo,其他猩猩除非被授與,不然都不會得到獨特技能。

我們可以使用singleton_methods查看 orangutan 的所有單體方法

orangutan.singleton_methods  #=> [:foo]

再看以下例子,把foo方法授與給Example 這個 class,即為 ClassMethod

MetaClass 的概念為 a class which defines classes

ClassMethodsingleton method 都有 metaclass的概念,原因在於Example 本質也是物件,在 orangutan 物件上,給予單體方法,只有這個黑猩猩才有這個獨特技能,而ClassMethod 本質上,更像是為所有的猩猩族加護,讓所有的猩猩族都獲得了bar技能~

class Orangutan
end

def Orangutan.bar
  'bar'
end

Orangutan.bar   #=> bar

ClassMethod,和JavascriptPrototype的概念很像。Javascript並沒有class的概念,取而代之的是原型鍊prototype。我們在MDN文件中常看到的一些方法,以reduce舉例好了,文章的開頭標題會以如下的方式呈現。

Array.prototype.reduce()

它的意思即為,只要你用了Array,就可以使用reduce這個方法。

由於這裡的主角為Ruby,所以Javascript的二三事,這裡就不便提太多,只是想說Array透過Prototype多了一個新方法,就像是Class 透過 MetaClass的方法多了ClassMethod

<< 的用法

<<只是一個看起來比較厲害的語法糖。不過當大家都在用的時候,也就沒那麼厲害了?

class << Orangutan
  def bar
    'bar'
  end
end

寫法1的寫法等同於寫法2

#=== 寫法1
class Orangutan
  def self.bar
    'bar'
  end
end

#=== 寫法2
class Orangutan
  class << self
    def bar
      'bar'
    end
  end
end

明天我們會開始介紹 module & mixin

參考資料


上一篇
Day11. 活用 Ruby Class
下一篇
Day13. class_eval & instance_eval - 解答什麼是 MetaClass & Singleton
系列文
初階 Rails 工程師的養成34

尚未有邦友留言

立即登入留言