Ruby method 的參數設計
設計 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, 2From: /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
endaaa(a: false, c: "WG")
# 希望的結果: { :a => false, :b => false, :c => "WG" }
那麼,我只要去看看我 傳進去的 hash 有沒有 相對應的 key,如果有就 assign value,沒有就使用 default value
於是我只要用 Ruby hash 的 has_key?
就可以了啊
於是我把 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
endprivatedef 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
endprivatedef assign_value(args, default, key)
args.has_key?(key) ? args[key] : default[key]
end
更聰明的解法
這樣的寫法看來不錯,但有沒有更簡潔的寫法呢?我們可以直接用 hash 的 merge
來實作
先簡單看一下 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,哪些是必填。
相關文章