Ruby method 的參數設計

NickWarm
Nickwarm Journey
Published in
11 min readAug 3, 2018
Photo by Med Badr Chemmaoui on Unsplash

設計 method 時,你可以決定給他帶一個參數,還是數個參數,i.e:

def writing(name, email, id, work_address, phone_number)
... (略)
end

每次調用 writing method 時,我都要給他傳參數,i.e:

writing("學姊",  "beauty@gmail.com", 1234, "110台北市信義區市府路1號", 0912345678)

那如果,我給 writing method 設計 default value 呢?它會長成

def writing(name = "學姊", email, id, work_address, phone_number, citizen = true)

但是我在調用 writing method 時,我一定要照著這順序填參數,如果填的順序不對,結果就不如我預期,i.e:

# hhh.rb
def tttt(a, b = 3, c)
binding.pry
a + b + c
end
# 進到 pry 裡面
[4] pry(main)> load 'hhh.rb'
=> true
[5] pry(main)> tttt 1
ArgumentError: wrong number of arguments (1 for 2..3)
from hhh.rb:144:in 'tttt'
[6] pry(main)> tttt 1, 2
From: /Users/nick.chen/Desktop/hhh.rb @ line 146 Object#tttt: 144: def tttt(a, b = 3, c)
145: binding.pry
=> 146: a + b + c
147: end
[1] pry(main)> a
=> 1
[2] pry(main)> b
=> 3
[3] pry(main)> c
=> 2
[8] pry(main)> tttt a = 1, c = 2, b = 4From: /Users/nick.chen/Desktop/hhh.rb @ line 146 Object#tttt: 144: def tttt(a, b = 3, c)
145: binding.pry
=> 146: a + b + c
147: end
[1] pry(main)> a
=> 1
[2] pry(main)> b
=> 2
[3] pry(main)> c
=> 4

如果 method 裡面需要的參數又複雜又長時,這樣的 method 設計就不是個好設計。

我們可以反思,rails 的 method 是怎樣設計的,i.e:

user = User.first
user.update(name: "學姊", work_address: "110台北市信義區市府路1號")

可以看到 update method 是設計成 傳hash argument進去method 的

method 設計成傳 hash argument 的好處是,當我在調用時,我一定知道,我傳進去的 value 是用在什麼地方,讓我們不用去理解該 method 內部是如何實作的。

設計 method時,如何讓 hash argument 有 default value

通常我們要設定 default value 時,你可能會想到用 ||=,i.e:

$ pry
[1] pry(main)> a = 1
=> 1
[2] pry(main)> a ||= 3
=> 1
[3] pry(main)> a
=> 1
[4] pry(main)> b
NameError: undefined local variable or method 'b' for main:Object
from (pry):4:in '__pry__'
[5] pry(main)> b ||= 3
=> 3
[6] pry(main)> b
=> 3

於是,你可能會想到 method 要設計成

# hhh.rb
def aaa(args = {})
args[:a] ||= true
args[:b] ||= false
args[:c] ||= "happy"
binding.pry
args
end
# 進 pry 玩了後發現結果不如預期
[8] pry(main)> aaa a: false, b: "WG"
From: /Users/nick.chen/Desktop/hhh.rb @ line 96 Object#aaa: 91: def aaa(args = {})
92: args[:a] ||= true
93: args[:b] ||= false
94: args[:c] ||= "happy"
95: binding.pry
=> 96: args
97: end
[1] pry(main)> args
=> {:a=>true, :b=>"WG", :c=>"happy"}

這是因為 ||= 其實是做下面這件事,i.e:

[10] pry(main)> c = c || 4
=> 4
[11] pry(main)> c
=> 4

而我想要給 default value 設定成 true 或 false 時就會發生像是

# args[:a] 最初傳進去時是 false# 執行 args[:a] ||= true,等同於
args[:a] = args[:a] || true
# 等同於
args[:a] = false || true

所以不管你怎麼弄,只要 args[:a] 是傳 boolean 進來,一定會變成 true

那我們要怎麼設計才好呢?讓我們回頭看一下現在的 code

def aaa(args = {})
args[:a] ||= true
args[:b] ||= false
args[:c] ||= "happy"
binding.pry
args
end
aaa(a: false, c: "WG")
# 希望的結果: { :a => false, :b => false, :c => "WG" }

那麼,我只要去看看我 傳進去的 hash 有沒有 相對應的 key,如果有就 assign value,沒有就使用 default value

於是我只要用 Ruby hash 的 has_key? 就可以了啊

ref:https://stackoverflow.com/a/9108723

於是我把 code 改成

def bbb(args)
default = { a: true, b: false, c: "happy" }

args[:a] = assign_value(args, default, :a)
args[:b] = assign_value(args, default, :b)
args[:c] = assign_value(args, default, :c)

args
end
privatedef assign_value(args, default, key)
args.has_key?(key) ? args[key] : default[key]
end

然後進 pry 測試,會發現一切都如我預期

$ pry
[1] pry(main)> load 'hhh.rb'
=> true
[2] pry(main)> bbb a: false, b: true
=> {:a=>false, :b=>true, :c=>"happy"}
[3] pry(main)> bbb c: "WG"
=> {:c=>"WG", :a=>true, :b=>false}

上面的 code 還可以寫得更簡潔,寫成:

def ccc(args)
default = { a: true, b: false, c: "happy" }
default.keys.each { args[key] = assign_value(args, default, key) }
args
end
privatedef assign_value(args, default, key)
args.has_key?(key) ? args[key] : default[key]
end

更聰明的解法

這樣的寫法看來不錯,但有沒有更簡潔的寫法呢?我們可以直接用 hash 的 merge 來實作

ref: https://github.com/airbnb/ruby#no-default-args

先簡單看一下 merge 的用途

$ pry
[1] pry(main)> h1 = {a: "a", b: "b"}
=> {:a=>"a", :b=>"b"}
[2] pry(main)> h2 = { b: "bb", c: "c"}
=> {:b=>"bb", :c=>"c"}
[3] pry(main)> h3 = h1.merge h2
=> {:a=>"a", :b=>"bb", :c=>"c"}
[4] pry(main)>

我們可以看到,被 merge 傳進來的 h2,會把 h1 相同的 key value pair 蓋掉,產生新的 h3 object

我們查看 object id 就可以知道這件事:

[8] pry(main)> h1.object_id
=> 70212047007800
[9] pry(main)> h2.object_id
=> 70212047056400
[10] pry(main)> h3.object_id
=> 70212072426820

若不想生成新的 obejct ,可以直接用 merge!

[12] pry(main)> h1.merge! h2
=> {:a=>"a", :b=>"bb", :c=>"c"}
[13] pry(main)> h1
=> {:a=>"a", :b=>"bb", :c=>"c"}

於是,我們的 method 傳 hash 在設計 default value 時可以寫成

# hhh.rb
def ddd(args = {})
args = { a: true, b: false, c: "happy" }.merge!(args)
args
end
# pry
[1] pry(main)> load 'hhh.rb'
=> true
[2] pry(main)> bbb a: false, b: true
=> {:a=>false, :b=>true, :c=>"happy"}
[3] pry(main)> bbb c: "WG"
=> {:c=>"WG", :a=>true, :b=>false}

這樣又更加簡潔了

不用 hash 如何設計 method 的參數:keyword argument

前面都是用 hash 的角度思考,那麽 best practice 是什麼呢

早在 Ruby 2.1 引入了 keyword argument。讓我們可以用更簡單的方式設計 method 的參數

ref: Ruby’s Powerful Method Arguments & How To Use Them Correctly

讓我們回到前面的範例:

def ddd(args = {})
args = { a: true, b: false, c: "happy" }.merge!(args)
args
end

有的是必填的值,有的是 default value ,透過 keyword argument 可寫成:

# hhh.rb
def ddd(a: true, b: false, c:) # c 是必填得值
binding.pry
end

然後我們一樣進 pry 裡玩玩

$ pry
[1] pry(main)> load 'hhh.rb'
=> true
[2] pry(main)> ddd
ArgumentError: missing keyword: c
from (pry):2:in '__pry__'
[3] pry(main)> ddd c: "WG"
From: /Users/nick.chen/Desktop/hhh.rb @ line 47 Object#ddd: 45: def ddd(a: true, b: false, c:)
46: binding.pry
=> 47: end
[1] pry(main)> a
=> true
[2] pry(main)> b
=> false
[3] pry(main)> c
=> "WG"
[4] pry(main)> exit
=> nil
[4] pry(main)> ddd a: false, b: true, c: "WG"
From: /Users/nick.chen/Desktop/hhh.rb @ line 47 Object#ddd: 45: def ddd(a: true, b: false, c:)
46: binding.pry
=> 47: end
[1] pry(main)> a
=> false
[2] pry(main)> b
=> true
[3] pry(main)> c
=> "WG"
[4] pry(main)> exit
=> nil
[5] pry(main)>

使用 keyword arguments 的好處:

  • 更簡單的 default value 設定
  • 必填的參數,在調用 method 時沒有填就會噴掉

這是非常好的防錯設計,讀 method source code 時就很清楚知道哪些參數有 default value,哪些是必填。

相關文章

--

--

NickWarm
Nickwarm Journey

Rubyist。Nicholas aka Nick. Experienced in Ruby, Rails. I like to share the experiences what I learned.