# 為什麼 2.days.ago 在內建的 irb 會找不到這個方法？這不是 Ruby 語法嗎？

> 

Published: 2016-04-26
URL: https://kaochenlong.com/open-class

---

在公開演講或是校園推廣 Rails 的時候，我常會開 rails console 出來秀一下 Rails 快速開發的威力：

```ruby
$ rails console
# 看，這樣寫就可以印出 2 天前的時間
&gt;&gt; 2.days.ago
=&gt; Sun, 24 Apr 2016 00:56:51 UTC +00:00

# 這樣可以印出 10 Megabyte 是多大
&gt;&gt; 10.megabyte
=&gt; 10485760
```

這真的很驚人，不只程式碼 `2.days.ago` 本身看起來好寫，而且就算連不懂程式語言的人看了也大概猜得出來是什麼意思，台下觀眾看到這裡，有的就會開始發出「喔喔喔」的讚嘆聲了。

&lt;!--more--&gt;

但打開標準的 `irb` 來試試：

```ruby
&gt;&gt; 2.days.ago
NoMethodError: undefined method `days&#39; for 2:Fixnum
	from (irb):1
	from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `&lt;main&gt;&#39;
&gt;&gt; 10.megabyte
NoMethodError: undefined method `megabyte&#39; for 10:Fixnum
	from (irb):2
	from /Users/user/.rvm/rubies/ruby-2.3.0/bin/irb:11:in `&lt;main&gt;&#39;
```

一樣的語法在 Ruby 竟然不會動了!

原來，這些看起來很厲害的語法是 Rails 幫 Ruby 做的擴充功能，讓開發者在開發網站應用程式的時候可以很快的把功能完成。

## 怎麼做的?

先來看一段範例：

```ruby
class Cat
  def sleep
    puts &quot;ZZZZZZ&quot;
  end
end

class Cat
  def eat
    puts &quot;Yammy!&quot;
  end
end

kitty = Cat.new
kitty.eat         # =&gt; Yammy!
kitty.sleep       # =&gt; ???
```

這裡我不小心定義了兩次 Cat 類別，如果這是你剛接觸 Ruby 不久，你也許會認為後面定義的類別會蓋掉前面的類別，所以執行 `eat` 方法沒問題，但執行 `sleep` 就會壞掉了。

但實際執行的時候發現其實兩個方法都可以正常運作! 所以，上面這段範例到底發生了什麼事?

事實上，當 Ruby 遇到兩個類別重複定義的時候，後面的定義並不會「覆蓋」掉前面的，反而比較像是「合併」。這個設計在 Ruby 裡面稱之「Open Class」，可以「打開」已經存在的類別，並再加料進去，甚至連內建的類別(像是數字、字串、陣列之類的)也可以。

我們來試試看內建的字串類別：

```ruby
class String
  def say_hello
    &quot;hello, #{self}&quot;
  end
end
```

在這個定義之後，所有的字串就都會 `say_hello` 了

```ruby
puts &quot;kitty&quot;.say_hello  # =&gt; &quot;hello, kitty&quot;
puts &quot;snoopy&quot;.say_hello  # =&gt; &quot;hello, snoopy&quot;
puts &quot;くまモン&quot;.say_hello  # =&gt; &quot;hello, くまモン&quot;
```

這在其它程式語言是很少見的設計，特別是連內建的類別也可以這樣玩。讓我們再來玩一個!

```ruby
puts 1 + 2    # =&gt; 3
```

1 加 2，不就等於 3 啊，這小學生都會!

但你知道其實這個簡單的加法可能跟你想的不太一樣，Ruby 的四則運算，事實上並不是一般的運算元(operator)。以上面這個 1 + 2 的例子來說，在 Ruby 裡實際上是：

&gt; 數字物件 1 呼叫了 `+` 這個方法，並且把數字物件 2 傳進去當做參數

所以 1 + 2 事實上是長這樣：

```ruby
puts 1.+(2)  # =&gt; 3
```

執行之後會得到一樣的結果。既然知道加法其實也是一個 Ruby 的 method 的話，那就可以來試著這樣玩：

```ruby
class Fixnum
  def +(n)
    1000
  end
end

puts 1 + 2
```

這樣一來，不管是 1 + 2 或是 3 + 4，得到的結果都會是 1000。

不過這樣做有點風險，因為這樣等於是改寫了加法的行為，如果有其它方法有用到這個方法的話也會跟著得到不正確的結果。上面這個例子可以再改寫成：

```ruby
class Fixnum
  alias :original_plus :+

  def +(n)
    puts &quot;hey hey hey&quot;
    original_plus(n)
  end
end

puts 1 + 2
```

執行上面這段範例可以發現，1 + 2 還是等於 3，但除此之外還會默默的輸出 `hey hey hey` 字樣，表示除了原來的加法之外，還可以做一些事。至於可以做什麼，這就靠大家自己發揮想像力了。

## 所以，Rails 是怎麼做的?

Rails 使用了 Open Class 的手法來幫 Ruby 加了不少功能，就以 `2.days.ago` 來說：

```ruby
class Numeric
  # .. 略

  def days
    ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
  end
  alias :day :days

  # .. 略
end
```

這裡打開了 `Numeric` 類別(它是 `Fixnum` 的上層類別)，幫它加上了 `days` 方法，而且還很貼心的幫 `days` 做了單數的 alias `day`，萬一只有 1 天也可以寫出 `1.day.ago` 這樣更像英文的語法。

實作的程式碼請看[這裡](https://github.com/rails/rails/blob/v5.0.0.beta3/activesupport/lib/active_support/core_ext/numeric/time.rb)。

## 安全嗎?

這種「猴子補丁 (Monkey Patching)」的手法，有些人覺得很不嚴謹 (但我自己個人覺得還好)，在 Ruby 2.0 之後推出了一個叫做 Refinement 的設計，可以稍微控制一下 Open Class 影響範圍。以前面那個 `&quot;kitty&quot;.hello` 的範例，重新用 Refinement 改寫會長像這樣：

```ruby
module MyHelloString
  refine String do
    def say_hello
      &quot;hello, #{self}&quot;
    end
  end
end
```

上面這段範例建立了一個叫 `MyHelloString` 的模組，在裡面使用 `refine` 方法在 `String` 類別上面進行「提煉」。但這並不會像之前 Open Class 的一樣寫完馬上有效果，要使用這個提煉過的方法，則是使用 `using` 方法：

```ruby
using MyHelloString

puts &quot;kitty&quot;.say_hello  # =&gt; &quot;hello, kitty&quot;
```

## 小結

各位看官會覺得 Ruby 的 Open Class 很隨便嗎? 我自己是還滿愛這樣的設計的 :)

Ruby 是一款非常有彈性的程式語言，但彈性本身也是雙面刃，你得知道學會怎麼運用這個彈性，而且你最好知道自己在做什麼。


