オブジェクト指向プログラミングにおける有名な法則の一つにデメテルの法則 (Law of Demeter, LoD) というものがある。これは、簡単に言えば「戻り値として返されたオブジェクトのメソッドを呼び出してはいけない」という法則であり、例えば次のようなコードを書くと、「それ、デメテルの法則に違反しているよ」などと言われたりするので注意が必要だ。
int id = projects.Find(projectName).GetID(); // プロジェクト名から ID を取得
そして、このコードをデメテルの法則に違反しないように書き換えると、次のようになる。
int id = projects.GetID(projectName); // プロジェクト名から ID を取得
変更後のコードは Find が返すオブジェクト(ここでは多分 Project みたいなクラス)に依存していないので、オブジェクト間の結合度が下がったことになる。結合度の低いプログラムは良いプログラムであり、デメテルの法則に従えば良いプログラムになるというわけだ。
でも、ちょっと待って欲しい。
様々な Project を一括して管理する Projects クラスがあったとして、その Projects を扱うようなプログラムが Project に依存するのは自然な流れだと思う。また、その方がコードがスッキリする場合だってあるはずだ。
例えば、Project クラスに次のようなメンバ関数が含まれていたとする。
class Project {
public:
const std::string& GetName() const; // プロジェクト名
int GetID() const; // ID
const std::string& GetDescription() const; // プロジェクトの説明
//...
};
先ほどは GetID だけで済んだが、当然の流れとして GetDescription も Projects のメンバにすべきという話になるはずだ。
class Projects {
public:
int GetID(const std::string& name) const; // プロジェクト名 → ID
const std::string& GetDescription(const std::string& name) const; // プロジェクト名 → プロジェクトの説明
//...
};
つまり、デメテルの法則に従って Projects クラスを定義していくと、Project にメンバが追加される度に Projects メンバの追加を検討することになる。
実際に Project に「メンバ」を足してみることにしよう。
class Member {
public:
const string& GetName() const;
//...
};
class Project {
public:
const std::string& GetName() const; // プロジェクト名
int GetID() const; // ID
const std::string& GetDescription() const; // プロジェクトの説明
const Member& GetLeader() const; // リーダは一人だけ
const std::vector<Member>& GetMembers() const; // 全メンバ(リーダも含む)
//...
};
メンバ違いなのはさておき、Projects からプロジェクトリーダやメンバの名前を取得するようなコードは、デメテルの法則に従えば次のようなものになるはずだ。
string leaderName = projects.GetLeaderName(projectName);
vector<string> membersName = projects.GetMembersName(projectName);
// or
string memberName = projects.GetMemberName(projectName, index); // 一人ずつ取得しても良い
これを実現するためには、Projects クラスに GetLeaderName, GetMembersName あるいは GetMemberName といった関数を追加しなければならない。
class Projects {
public:
int GetID(const std::string& name) const; // プロジェクト名 → ID
const std::string& GetDescription(const std::string& name) const; // プロジェクト名 → プロジェクトの説明
// プロジェクト名 → プロジェクトリーダ名
const std::string& GetLeaderName(const std::string& name) const;
// 全メンバ(リーダも含む)の名前を一括で取得する
// (他にも色々な実装が考えられるが、今回の話題ではこれで充分)
const std::vector<std::string>& GetMembersName(const std::string& name) const;
// こちらの方が「戻り値のメンバを使わない」という観点から、より LoD 的な実装
std::size_t GetMemberCount(const std::string& name) const; // メンバの数
const std::string& GetMemberName(const std::string& name, int index) const; // メンバ名
//...
};
以降、同様に Projects のメンバは肥大化の一途をたどることになる。私が Projects クラスのユーザなら「もう、いっそのこと Project に依存させてくれ!」と叫んでいるところだ。(そして多分 Member クラスも同じ理由で依存したくなるはず。)
さらに、Project や Member のインタフェースだけが肥大化の要因ではない。
プロジェクト名から何らかの情報を得るというインタフェースの他に、プロジェクト ID からも情報を得たいという要求が挙がった時のことを考えてみよう。デメテルの法則に従えば、プロジェクト ID をキーにした一連のメソッド群を用意すべし、となる。
class Projects {
public:
int GetID(const std::string& name) const; // プロジェクト名 → ID
const std::string& GetDescription(const std::string& name) const; // プロジェクト名 → プロジェクトの説明
const std::string& GetLeaderName(const std::string& name) const; // プロジェクト名 → プロジェクトリーダ名
const std::vector<std::string>& GetMembersName(const std::string& name) const; // プロジェクト名 → 全メンバ名
std::size_t GetMemberCount(const std::string& name) const; // プロジェクト名 → メンバの数
const std::string& GetMemberName(const std::string& name, int index) const; // プロジェクト名 → メンバ名
public:
int GetName(int id) const; // ID → プロジェクト名
const std::string& GetDescription(int id) const; // ID → プロジェクトの説明
const std::string& GetLeaderName(int id) const; // ID → プロジェクトリーダ名
const std::vector<std::string>& GetMembersName(int id) const; // ID → 全メンバ名
std::size_t GetMemberCount(int id) const; // ID → メンバの数
const std::string& GetMemberName(int id, int index) const; // ID → メンバ名
//...
};
以降、検索のキーが増える度に、同様のインタフェースが追加されることになる。
もし、デメテルの法則に従わなければ、先ほどのコードは次のようになっていたはずだ。
class Projects {
public:
const Project& Find(const std::string& name) const; // プロジェクト名 → Project クラス
const Project& Find(int id) const; // ID → Project クラス
//...
};
これなら Find の種類が増えても Projects のメンバ関数はそれに応じて増えるだけだし、Project のメンバが増えても Projects に影響はない。先ほどよりも遥かに直交性の高いプログラムになっている。
そもそも Projects というクラスは Project を一括して管理するという役割を担っていたはずだ。リーダ名を取得するメソッドが「Project を一括して管理する」という役割に沿ったものだろうか。いや、とてもそうは思えない。こういった、クラスの役割とは異なるメンバ関数が多く含まれるクラスのことを凝集度が低いクラスと呼び、デメテルの法則に従って改変した Projects クラスは凝集度が低いクラスに変化したことになる。
ちなみに、Projects が Find によって Project を直接返すなら、プロジェクトリーダの名前を取得するコードは次のようになるだろう。
string leaderName = projects.Find(projectName).GetLeader().GetName();
// あるいは
string leaderName = projects.Find(projectID).GetLeader().GetName();
まさに「デメテルの法則違反」なコードだ。でも、私は Projects が肥大化するよりはこの方がマシだと思えるのだが、そういった感覚はおかしいのだろうか? 直交性を犠牲にしてまで従うべき法則なのだろうか?
デメテルの法則に従うと得られるメリットは以下の通り。
- 依存するクラスの数を減らすことができる(結合度の低下)
- 中間部分のインタフェースが変更されても、呼び出し側のコードを変更する必要がなくなる
- 中間クラスが持つ不要なインタフェースを隠蔽できる(むしろ隠蔽すべき)
これに対するデメリットは以下の通り。
- クラスの役割があいまいになる(凝集度の低下)
- 場合によってはラッパーメソッド(委譲に必要なメンバ関数)が大量に必要となる
結局のところ、メリットとデメリットのトレードオフなだけであって、闇雲にデメテルの法則に従う必要はないはずだ。「それ、デメテルの法則に違反してるよ」と言われた時に、「それでは、直交性や凝集度を考慮した上でクラスのインタフェースを再検討してみましょう」と言ってみるのも面白いだろう。
極端な話、以下のコードだってデメテルの法則に違反している。
vector<Member> members;
//...
for (size_t i = 0; i < members.size(); ++i)
cout << members[i].GetName() << endl;
そう見えなければ、こう書き換えてもいい。
vector<Member> members;
//...
for (size_t i = 0; i < members.size(); ++i)
cout << members.operator[](i).GetName() << endl; // ドットが2つ! LoD 違反!
これをこう書き換えればデメテルの法則を違反せずに済むが......
for (size_t i = 0; i < members.size(); ++i) {
const Member& member = members[i];
cout << member.GetName() << endl; // OK. ドットは一つだけだ
}
それを強制させられるような職場なら、本気で転職を考えたほうが良いだろう。
参考文献
The Paperboy, The Wallet, and The Law of Demeter (PDF)
このエントリが LoD に批判的なものになってしまったので、肯定的な文献を参考に挙げておく。英文だが、現実の世界をモチーフにしたサンプルコードが分かりやすく、英語そのものも読みやすいのでお奨め。
今回は意図的に LoD に不利な条件を作り出したが、隠蔽すべきものは隠蔽するというカプセル化の大原則を忘れてはならない。トレードオフを考えつつ、LoD に従うべきときは素直に従うべし。
- Newer: static_cast をいい加減な気持ちで適当に付けないと決めた日
- Older: 仮想関数を generic にする
コメントを表示する前に、このブログのオーナーによる承認が必要になることがあります。コメントが表示されない場合は、承認されるまでしばらくお待ちください。