読者です 読者をやめる 読者になる 読者になる

Rails Webook

自社のECを開発している会社で働いています。Rails情報やサービスを成長させる方法を書いていきます

リファクタリング: デメテルの法則(Law of Demeter, LoD)

Ruby リファクタリング 設計

f:id:nipe880324:20150922205235j:plain:w320
Photo by Wonderlane | Flickr - Photo Sharing!

デメテルの法則について勉強したのでまとめてみました。間違っているかもしれませんのでコメントいただけると助かります。

目次

  1. デメテルの法則とは
  2. デメテルの法則に反している例
  3. デメテルの法則の適用
  4. まとめ


1. デメテルの法則とは

wikipediaより引用
デメテルの法則 - Wikipedia

デメテルの法則 、または最小知識の原則とは、ソフトウェアの設計、特にオブジェクト指向プログラムの設計におけるガイドラインである。
簡潔に言うと「直接の友達とだけ話すこと」と要約できる。
あるオブジェクトAは別のオブジェクトBのサービスを要求してもよい(メソッドを呼び出してもよい)が、オブジェクトAがオブジェクトBを「経由して」さらに別のオブジェクトCのサービスを要求してはならない。
これが望ましくないのは、オブジェクトAがオブジェクトBに対して、オブジェクトB自身の内部構造以上の知識を要求してしまうためである。
このような場合には、クラスBを変更し、クラスAがクラスBに対して行った要求を適切なBのサブコンポーネントに伝播させるようにすればよい。または、AがCへのリファレンスを持つようにして、AがCを直接呼ぶようにしてもよい。この法則に従えば、オブジェクトBが知っているのは自分自身の内部構造だけになる。


2. デメテルの法則に反している例

クラス図としては次のような例で考えてみます。

[Result] =1:n=> [Answer] <=1:n= [Question]

テスト結果(Result)には、回答(Answer)が複数あります。
そして、回答から質問(Question)が辿れるようになっています。

ここで、テスト結果を計算するscoreメソッドを実装してみます。

class Result
  has_many :answers

  def score
    answers.inject(0) do |result, answer|
      # resultからquestionまで辿っているのでデメテルの法則に反している
      question = answer.quesiton
      result + question.score(answer.text)
    end
  end
end

class Answer
  belongs_to :question
  belongs_to :result
end

class Question
  has_many :answers

  def score(text)
    # correct_textは正解のtext
    text == correct_text ? 10 : 0
  end
end

result = Result.find(1)
result.score #=> Answerに応じてスコアが算出される

ここで、scoreメソッドのブロック内で、デメテルの法則であったように、
「あるオブジェクトA(Result)は別のオブジェクトB(Answer)のサービスを要求してもよい(メソッドを呼び出してもよい)が、オブジェクトA(Result)がオブジェクトB(Answer)を「経由して」さらに別のオブジェクトC(Question)のサービスを要求してはならない。」に合致しています。
つまり、デメテルの法則に違反しています。


3. デメテルの法則の適用

そのため、
「クラスB(Answer)を変更し、クラスA(Result)がクラスB(Answer)に対して行った要求を適切なBのサブコンポーネントに伝播させるようにすればよい。」をしてみます。

具体的には、scoreメソッド内の処理をAnswerクラスに委託します。

class Result
  has_many :answers

  def score
    answers.inject(0) do |result, answer|
      # answerに委託しているのでQuestionについて知らなくてもよい
      # つまり、Questionの変更に対して強くなる
      result + answer.score
    end
  end
end

class Answer
  belongs_to :question

  def score
    question.score(text)
  end
end

class Question
  has_many :answers

  def score(text)
    text == correct_text ? 10 : 0
  end
end

result = Result.find(1)
result.score #=> Answerに応じてスコアが算出される


4. まとめ

これは、簡単な例なので分かりづらいですが、デメテルの法則に反した実装をしていると、規模が大きくなるにつれて、クラス同士の余計な依存関係が多くなり、クラス同士の関連が複雑になります。
それにより、修正に対する影響範囲が大きくなり、保守が辛い、予想外のバグを生みやすくなってしまうのではと思いました。