実践で学ぶオブジェクト指向② 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メッセージを送る」という知識は持っています。


この変更により処理の流れはより当初のシーケンス図とはすこし違ったものになりました。
f:id:naito-coding0322:20190127021718p:plain
シーケンス図ではjudgeメッセージを送ったGameMasterがreturnを受けてPlayerにclear_opened(正しくはface_down)を送っていますが、現状はjudgeが直接playerへメッセージを送っています。