Rubyでcase文の使用をやめる理由――ポリモーフィズムでより良いコードを書く方法
オブジェクト指向プログラミング(OOP)の力を最大限に引き出せていますか?それとも、その恩恵を逃しているのでしょうか?
もしオブジェクトの型(クラス)に基づいて処理を分岐させているなら、OOPの重要な機能であるポリモーフィズム(多態性)を見逃してしまっています。
型による判定は通常、case文の中で行われがちですが、これはオブジェクト指向的とは言えません。この記事では、case文を取り除いてより良いコードを書く方法を解説します。
型チェックを行う従来のコード
まずは、ポリモーフィズムを活用していないコード例から見ていきましょう。
ここでは「じゃんけん(Rock, Paper, Scissors)」ゲームを実装します。Gameクラスを1つと、それぞれの手(グー・チョキ・パー)ごとに個別のクラスを用意することにしました。
勝敗を判定するために、Gameクラスにplayメソッドを実装します:
class Game
def self.play(move1, move2)
return :tie if move1 == move2
move1.wins_against?(move2)
end
end
そして、手のクラスの一例がこちらです(他の手も同じパターンになります):
class Rock
def wins_against?(other_move)
case other_move
when Paper then false
when Scissors then true
end
end
end
これで、2つの手を渡してplayメソッドを呼び出せば、最初の手が勝つかどうかがわかります。
p Game.play(Rock.new, Paper.new)
# false
確かに動作していますが、もっと良くできるはずです。あの醜いcase文をなくせないでしょうか?
型チェックの代わりにポリモーフィズムを使う
答えはイエスです。OOPの基本原則を活用すれば、型チェックのためのcase文は取り除けます。
アイデアはこうです。自分自身のクラスが何であるかは自明なので、相手の手オブジェクトに「あなたは自分(このクラス)に勝てるか?」と尋ねるのです。
そのために、自分のクラスに固有のメソッド名を使います。たとえばRockなら、do_you_beat_rock?のような名前になります。
Rubyにおけるポリモーフィズムとは、オブジェクトのクラスを確認することなく、任意のオブジェクトに対して任意のメソッド呼び出し(OOPの用語では「メッセージ」とも呼ばれます)を送れる能力のことです。「ダックタイピング」とも呼ばれますが、筆者はこの用語はあまり好きではありません 🙂
Javaには(少しお付き合いください…)「インターフェース」という仕組みがあり、コンパイラレベルで一連のメソッドの実装をクラスに強制できます。
Rubyにはそれがありません(おそらくそれが良い方向ですが)。そのため、代わりにテストによって契約を担保することになります。
それでは、新しい実装がどのようになるか、コード例で見てみましょう:
class Rock
def wins_against?(other_move)
other_move.do_you_beat_rock?
end
def do_you_beat_paper?
false
end
def do_you_beat_scissors?
true
end
end
case文が消えていることに注目してください。それは1つのメソッド呼び出しと2つのメソッド定義に置き換えられました。
更新:読者の方からのコメントで、このパターンを実装するとロジックが逆になってしまうことに気づきました。Daniel P. Clark氏による解決策のひとつは、
playメソッド内の呼び出し順序をmove2.wins_against?(move1)と入れ替えるだけというものです。
ずっとすっきりしませんか?いかがでしょうか。
新しい手を追加するときは?
次に、新しい手を追加したくなったとしましょう。何を変更する必要があるでしょうか?少し考えてみてください…
case文を使ったアプローチでは、すべての手のクラスに新しい分岐を追加しなければなりません。つまり、問題なく動いているメソッドを、新しい機能を導入するために変更せざるを得ないのです。
一方、OOP指向のバージョンではそれは問題になりません。必要なのは新しいメソッドを追加することだけです。これこそが、開放/閉鎖原則(SOLIDの「O」)の実践です。
もうひとつの選択肢として、メタプログラミング(method_missingやdefine_method)を使う方法もあります。
実際に筆者がこれにメタプログラミングを使うかというと?おそらく使いません。手の種類が頻繁に変わる場合や、手の数が非常に多い場合を除けば、です。
メタプログラミングにはコストが伴います。一部の人々が信じるような銀の弾丸ではありません。パフォーマンスと可読性を犠牲にして、わずかな柔軟性と引き換えにすることになるのです。
まとめ
この記事では、クラスの型チェックにcase文を使うのは避けるべきだと学びました。代わりにポリモーフィズムを活用しましょう。
そうすれば、既存のコードを変更するのではなく、新しいコードを追加するだけで拡張できる、より良いコードを作れるようになります。さあ、今度はあなたが行動を起こして、リファクタリングに挑戦してみましょう 🙂
ちなみに、case文をツールボックスから完全に追放すべきだと言っているわけではありません。ただ、case文を書いている自分に気づいたときは一度立ち止まるべきです。それがその問題に対する本当に最善の解決策なのか、必ず確認してください。
このような記事をもっと読みたい方は、ぜひこの記事をシェアしてください!
-
Rubyのcaseステートメントの多彩な活用法と仕組みを徹底解説
Rubyでif / elsifを使おうとしている場面では、代わりにcaseステートメントを使うことを検討してみてください。この記事では、caseステートメントのさまざまな活用例と、その内部で実際にどのような仕組みで動作しているのかを解説します。 補足:他のプログラミング言語では、これはswitch文として知られています。 Rubyにおけるcaseステートメントの構成要素は以下の通りです。 キーワード 説明 case caseステートメントの定義を開始します。処理対象となる変数を受け取ります。 when マッチ可能な各条件が、1つのwhen句に相当します。 else どの
-
Rubyの配列(Array)クラスの使い方徹底解説!基本操作と便利なメソッド一覧
配列(Array)とは? 配列はRubyに組み込まれたクラスで、0個以上の要素をリスト形式で保持することができます。さらに、要素の追加・アクセス・繰り返し処理を簡単に行えるメソッドも多数用意されています。 もし配列が存在しなければ、値ごとに個別の変数を用意しなければならず、管理が非常に大変になります。 例: a = 1 b = 2 c = 3 しかし、配列を使えば次のように書けます: numbers = [1, 2, 3] さらに嬉しいことに、配列の中には何でも入れられます! 例えば: 数値 文字列 さらに別の配列!(これは多次元配列になります) それでは、配列を最大限に活用できる