実践で学ぶオブジェクト指向② if分岐のネスト/デメテルの法則
繰り返されるif分岐
今回はまず、GameMasterクラスのopenメソッドについて考えてみようと思います。
このメソッドはアプリケーションの中で一番行数が多く、まず着手すべき部分であることは自分でもすぐに分かりました。
以下が現状のコードです。
GameMasterクラス
Judgeクラス
GameMasterクラスのopenメソッドは、長い上にインデントも深いです。
openメソッドのみ抜粋
def open(card_id) if counter.limit?(opened) board.face_down(opened) player.face_down else i = card_id * 3 if cards[i] == "-" board.face_up(i) player.face_up(i) end end if counter.limit?(opened) i = card_id * 3 if Judge.new.judge(cards[opened[0]+2], cards[i+2]) player.face_down end end end
Judgeオブジェクトにメッセージを送り、その返り値でif分岐している部分をなんとかしましょう。
if分岐を他人にブン投げる
def open(card_id) if counter.limit?(opened) board.face_down(opened) player.face_down else i = card_id * 3 if cards[i] == "-" board.face_up(i) player.face_up(i) end end if counter.limit?(opened) Judge.new.judge(self, card_id) end end
class Judge def judge(game, card_id) i = card_id * 3 if game.cards[opened[0]+2] == game.cards[i+2] game.player.face_down end end end
if分岐をopenメソッドから消し去ることだけを考えました。
そして「2枚のカードが一致しているか判定する」「その結果によってplayerオブジェクトにメッセージを送る」という両方の仕事をJudgeクラスに任せてしまうことにしました。
たしかにopenメソッドからif分岐が1つ無くなりました。が、「その結果によってplayerオブジェクトにメッセージを送る」というのはJudgeオブジェクトの責任ではありません。
さらに、judgeメソッド内で
game.player.face_down
というようにドットが2つ使われています。
これは「デメテルの法則」に違反してはいないでしょうか。
Judgeが2つの責任を負っているという点はひとまず置いておき、この問題について考えたいと思います。
(デメテルの法則がどうしても頭の中で「デムルメの法則」と変換されてしまってなかなか正しく覚えられません)
ちなみにcard_idを3倍するとカードのインデックス番号になるということをJudgeオブジェクトが知っているのも良くないですね。
さて、これらの問題を回避しようと考えたのが以下です。
class Game_Master def face_down player.face_down end def index(card_id) card_id * 3 end end
GameMasterクラスに、playerへface_downメッセージを送るface_downメソッドと、カードのインデックス番号を返すindexメソッドを追加
def open(card_id) -- 略 -- if counter.limit?(opened) Judge.new.judge(self, index(card_id)) end end
card_idを送っていたのを、GameMasterの方で処理をしてindexを送るように変更
class Judge def judge(game, i) if game.cards[opened[0]+2] == game.cards[i+2] game.face_down #ドットが1つ減った! end end end
card_idの処理に関する問題はこれで片付いたと思います。
しかし、このように表面上だけでデメテルの法則を回避して良いのでしょうか。
たしかにドットの数が1つに減りましたが、GameMasterクラスにはメソッドが1つ増えました。
デメテルの法則の本質
p111に「デメテルの法則はオブジェクトを疎結合にするためのコーディング規則の集まり」とあります。
コードは『TRUE』であるべきで、デメテルの法則を破ったコードはこれに違反している可能性を示しているとのことです。
具体例もあるので見てみましょう。
class Trip def depart customer.bicycle.wheel.tire #上記を含む何らかの処理 end end
wheelがtireを変更したときに、Tripはwheelとは無関係であるにも関わらず変更が強制される恐れがある。これは不必要に変更コストを上げており、合理的であるとは言えない。
tireへの変更はTripのdepart内の何かを壊す可能性もある。
Tripを再利用する場合、wheelとtireを持つbicycleを持つcustomerにアクセスできるようにする必要があり、利用性が低い。
このようなコードは模範的でないにも関わらず他人によって複製され増殖していってしまう。
つまり、隣人ではない「遠くのオブジェクト(の振る舞い)を実行してしまっている」という点が重要なんだと読み解けます。
改定後のコードでは、メソッドが1つ増えはしましたが、GameMaster→Judge→GameMaster→Playerという流れに変わりました。
ドットが1つ減っただけ、というよりは
JudgeがPlayerに対してメッセージを送っていたのが、GameMasterを中継するようになった、要は話しかける相手が変わりました。
これはつまり、Judgeが「face_downをどのように実行すればよいか(game_master.player)」という知識をひとつ失ったということです。密結合が若干解消されました。
しかし依然として「2枚のカードが一致していた場合にはface_downメッセージを送る」という知識は持っています。
この変更により処理の流れはより当初のシーケンス図とはすこし違ったものになりました。
シーケンス図ではjudgeメッセージを送ったGameMasterがreturnを受けてPlayerにclear_opened(正しくはface_down)を送っていますが、現状はjudgeが直接playerへメッセージを送っています。