🪓

Rubyアンチパターン: なんでもHash

2022/04/10に公開

ソフトウェアの内部では、データを様々な形に加工しつつ、あちこちに受け渡して再利用します。そのためのデータ構造として、RubyではHashクラスのオブジェクト(他の言語ではMapだったりDictionaryだったり連想配列だったりします)を使うことはよくあります。
単体の数値や文字列ではなく、それらを組み合わせたデータを扱う際には、Hashオブジェクトが便利です。

しかし、あらゆる場面でHashを駆使しようとすると、利便性よりも弊害が大きくなります。

典型的な症状

  • あちこちで同じだったりちょっと違ったりするHashオブジェクトを引き回していたりします
  • インスタンス変数が少なく(あるいはまったくなく)、その代わりとしてメソッド引数のHashオブジェクトにいろんなデータを渡していたりします
  • 1つのHashオブジェクトのキーとして、文字列やシンボル、さらにはそれ以外のクラスのオブジェクトが使われていたりします
  • JSONで送受信するデータをコードの中でもそのまま扱っていたりします

生じる問題

ざっくり言うと、「理解しづらい&変更・修正しづらい」というのが問題です。

  • 何が入っているか分からない

とりあえずキーは見れば分かるにしても、値に一貫性がないとオブジェクトの内容を理解するのが困難になります。

また、Rubyの場合、キーを文字列にするかSymbolにするか(あるいは両方許すか)で揺れがちになる、という問題もあります。

  • いろんな場所で適当に扱ってしまってしまい、どこで何が書き換わるか分からない

通常はmutableなままなので、影響範囲が把握しづらくなります。

とりあえずfreezeすることもできますが、結局Hashを何度も再生成して引き回すようにしていれば、挙動の理解は困難なままです。

  • 機能追加や修正が難しい

何も考えずにHashを加工するメソッドを追加することは可能ですが、どこに何のメソッドを追加するかが難しくなります。

クラスになっていれば「そのクラスに追加する」「そのクラスと関連するクラスに追加する(必要であれば関連するクラスごと追加する)」といったように、基点となるクラスが存在しますが、Hashの場合はそのようなクラスが存在しません。そのため、どこにでも追加されてしまう可能性があります。

解決策

  • クラスにする

悪いことは言わないので、素直にクラスを作ってそれを使うのがおすすめです。

カジュアルにHashを使うのと同じくらいのカジュアルさで、使い捨ての小さいクラスを作ってみるのも良いでしょう。最初はそのファイル内でしか使わない、くらいのところから始めるのでも構いません。他のファイルでも使いたくなったら、別ファイルに切り出して使いましょう。案外そのクラスが大きく育ち、複雑になってしまうかもしれませんが、Hashが同様に育ってしまった場合よりはずっと扱いが容易なはずです。

  • Structにする

クラスにするまでもなさそう…という場合は、Structを使うのも良いかもしれません。ブロックでメソッドを追加することもできるため、実質(アクセサの生えた)クラスと同等になります。

Structの場合、newする際にkeyword_init: trueも指定することも考慮してみるべきです。

ポイントとしては、Structの値についてもちゃんとStructなりクラスなりにするところです。Rubyの場合はある程度duck typing的な扱いがされる値も多く、Hashを元に適宜クラスにしようとすると、いろんなクラスやStructを値として持つこともあるかと思います。が、共通のスーパークラスやモジュールがなくても(duck typingなのでそういうものですよね)、どんなキー・値を持つか不明なHashオブジェクトになってしまうよりは扱いやすくなるはずです。

将来的にはRBSのUnion TypeInterface declarationを導入し、より明確な型付けを行うことになるかもしれません。それに備える意味でも、どういうクラスがあると良いのかを考えることは役に立つでしょう。

例外

  • JSONを扱う

JSONを扱う場合は、ある程度JSONのObjectを操作することになり、どうしてもHashを含んだオブジェクトを扱うことになります。
その場合でも、内部でもそのままHashを引き回すよりも、そのJSONをラップするクラスを作り、そのクラスに用意されたメソッドから操作させるようにした方が、ずっと扱いやすくなるはずです。

関連するアンチパターン

  • なんでもユーテリティクラス

Discussion