コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

Rubyにてハッシュを用いたキーワード引数(**引数)を使いたくなった場合は本当のそれが必要かを一息おいて考えてほしい

ハッシュを用いたキーワード引数(アスタリスクを二重につける記法のもの)を使ってしまうことによるデメリットと代替案に関してまとめます。

今回扱うケースとRubyのバージョン

一息おいて考えてほしいケースのサンプルコードは以下

def put_english_text(**params)
  params_times = params[:repeat_times].to_i
  repeat_times = params_times < 1 ? 1 : params_times

  puts(params[:before_text])
  repeat_times.times do
    puts("#{params[:s_word]} #{params[:v_word]} #{params[:o_word]}")
  end
  puts(params[:after_text])
end

サンプルコードの確認につかったRubyのバージョンは以下

$ ruby -v
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin19]

この書き方によるデメリット

引数として何を扱っているかがわかりにくい

**params としてしまっているので、このメソッドが引数として何を必要として、どう使うのかというのがメソッド内のプログラムコードを参照しないとわからない。

今回のサンプルコードでは少なからず6つの要素を引数的に扱っているが、それを把握するにはparams[:hogehoge]と使っている部分をコードから拾っていかなければいけない。

paramsIDEなどの検索で拾っていけばいい」という考え方ものあるが、メソッドの引数として愚直に並べた場合と比べると把握スピードに差がでるし、IDEの補完も効きにくくなるのでメリットのほうが少ないと思われる。

関係ない値をメソッドの引数のように記述できる

通常のキーワード引数の場合は、足りない場合はエラーになるし、想定外のものが引数に設定されていればエラーになる。だがこの形式の場合メソッド上に使わない値をセットしてもエラーにはならない。

# 下記のように全く関係ないsurpriseをセットしても問題ないし、repeat_timesなどを入れなくても問題がない
put_english_text(surprise: "yeah!",after_text: "see you!")

作り上は問題がないが、コードリーディングするときにノイズになるためリーディングコストがあがる。

定義済みのハッシュの転用が可能になるのでコードリード難易度が上がる

このサンプルコードのメソッドを呼び出す場合以下のような書き方ができます

## 普通のキーワード引数のように利用する
put_english_text(before_text: "hello!", s_word: "I", v_word: "like", o_word: "dog")

## ハッシュの中身をそのまま転用する
params_hash = {s_word: "I", v_word: "like", o_word: "sushi",repeat_times: "5" }
put_english_text(**params_hash)

後者のように「定義済のハッシュをそのまま利用する」ということが可能なので、他の場所から取得したハッシュをそのままセットもできる。そのためRailsのControllerなどでは params の actionが受け取ったパラメータをそのままメソッドに送るということも可能になる。

一見すると便利なように見えるが、メソッドの引数がパラメータ名に依存するので、パラメータの名称が変更になった場合にメソッドの実態処理にも変更の影響を受けるので変更容易性が損なわれる。

代替案

多くは引数の多さを回避するためなので、少なくするためのアプローチをご紹介します。

引数をまとめる

例えば今回のケースだと「SVOの文」に関しては1つの文字列にまとめられそうなので、3つの引数が1つにできる。以下変更例です。

def put_english_text(repeat_time: ,before_text: ,svo_text: ,after_text:nil)
  repeat_times = repeat_time < 1 ? 1 : repeat_time

  puts(before_text)
  repeat_times.times do
    puts(svo_text)
  end
  puts(after_text)
end

svoのテキストは事前に組み立てて置けばいいので呼び出しは下記のようになる。

main_text = "#{s_word} #{v_word} #{o_word}"

put_english_text(repeat_time: 1, before_text: "hello!", svo_text: main_text)

コンテキストの塊でクラス化する

引数をまとめると基本的な目線は同じだが、考え方を変えます。今回の put_english_text のメソッドは3つの英文をputsするものなので、英文の塊を1つのオブジェクトとして捉えてクラス化してしまえばいいという発想もできる。

今回のケースの場合は以下のような考え方

class EnglishText
  def initialize(text:, repeat_time: 1)
    @text = text.to_s
    set_repeat_time = repeat_time.to_i <= 0 ? 1 : repeat_time.to_i
    @repeat_time = set_repeat_time
  end

  def output
    output_text = "#{@text}\n"
    output_text * @repeat_time
  end
end

def put_english_text(before_text:, main_text:, after_text:)
  puts(before_text.output)
  puts(main_text.output)
  puts(after_text.output)
end

英文本体と繰り返しがあるという部分の機能をクラスに定義し、メソッド自体は表示だけに集中させた。このメソッドを使う場合は以下のようになる。

s_word = "I"
v_word = "like"
o_word = "sushi"

set_before_text = EnglishText.new(text: "Hello!")
set_main_text = EnglishText.new(text: "#{s_word} #{v_word} #{o_word}", repeat_time: 10)
set_after_text = EnglishText.new(text: "See you!")

put_english_text(before_text:set_before_text,main_text:set_main_text,after_text: set_after_text)

このようにすると責務を分離できるので、メソッドの内容も明快にしやすいです。ただ今回のサンプルコードだとやりたいことが単純なためやや冗長にも見えるため、パラメータの性質を見極めながらこちらの例を参考にしていただきたいです。

この状態を利用したくなるケース

その時どう考えればいいか、というところを最後にまとめておきます。

RubocopのMetrics/ParameterListsで怒られたから回避策として使いたい

安易にRubocopの警告を回避するために利用するのはオススメできません。原則引数が多いということは「そのメソッドに多くのことをやらせようとしすぎている」という前兆なので、そこに対してアプローチをしていくのが良いかと思います。以下アプローチの例です。

  • そのメソッドに引数としてすべて渡す必要があるか、事前に処理を加えてまとめられないか?
  • 複数の引数で1つの意味を構成するようなものがないか、もしあればプレーンなRubyのクラスや構造体を作れないか?
  • そのメソッドに複数のことをやらせようとしていないか。その処理は分けられないか?

他のハッシュ要素をそのまま当てはめたい

「定義済みのハッシュの転用が可能になるのでコードリード難易度が上がる」のパートで紹介した、別のハッシュを埋め込みたいというニーズを満たすために使うケースもあると思うが、よほどのことがない限りそのまま渡すことはオススメできないです。以下理由です。

  • 前述で紹介したとおり、関係ない内容もセットできるので関係がない値も渡るので要不要の判断が利用先を見ないとわからない
  • 渡ってきたハッシュの中身がどうあるべきかが定まらないので、テストコードが書きにくい
  • 送り元のハッシュのキー名が変更になるとメソッド内の記述も変更しないといけない

基本的には「コードの見通しが悪い」「依存が強くなりすぎて変更の難易度が上がる(=変更容易性が著しく下がる)」という2点なので、代替案のパートで紹介したやり方を元にきっちり必要なものを引数として明示的にセットしたほうが良いかと思います。

参考サイト