デメテルの法則
原文と訳
Law of Demeter (General Formulation)
Law of Demeter (LoD) - デメテルの法則
The Law of Demeter was originally formulated as a style rule for designing object-oriented systems. "Only talk to your immediate friends" is the motto. The style rule was discovered at Northeastern University in the fall of 1987 by Ian Holland.
デメテルの法則は、もともとオブジェクト指向システムを設計するためのスタイルルールとして策定されました。「身近な友人とだけ話す」がモットーです。このスタイルルールは、1987年秋に Northeastern 大学の Ian Holland によって発見されました。
A more general formulation of the Law of Demeter is: Each unit should have only limited knowledge about other units: only units "closely" related to the current unit. Or: Each unit should only talk to its friends; Don't talk to strangers.
デメテルの法則をより一般的に定式化すると次のようになります。各ユニットは他のユニットについて限られた知識しか持たないようにします。つまり、現在のユニットに「密接に」関連するユニットのみとします。または、各ユニットは友人とのみ話す必要があります。見知らぬ人とは話さないでください。
In this general form, the LoD is a more specific case of the Low Coupling Principle well-known in software engineering. The Low Coupling Principle is very general and we tried to make it more specific. The benefit of the specific Law of Demeter shown below is that it makes the notion of unnecessary coupling very explicit.
この一般的な形式では、LoD はソフトウェアエンジニアリングでよく知られている低結合原則のより具体的なケースです。低結合原則は非常に一般的なものですが、私たちはそれをより具体的にしようとしました。以下に示す具体的なデメテルの法則の利点は、不要な結合の概念が非常に明確になることです。
The main motivation for the Law of Demeter is to control information overload; we can only keep a limited set of items in short-term memory and it is easier to keep them in memory if they are closely related. The definition of "closely related" is intentionally left vague so that it can be adapted to particular circumstances.
デメテルの法則の主な目的は、情報過多を制御することです。短期記憶に保持できるのは限られた項目のセットのみであり、密接に関連している項目であれば、それらを記憶に保持する方が簡単です。「密接に関連している」の定義は、特定の状況に適応できるように意図的に曖昧にされています。
要約
- 「ユニット」とはオブジェクトのこと
- 「友人」とはオブジェクトが持つ他のオブジェクトのこと(直接の友人)
- 「見知らぬ人」とは友人の友人のこと(直接の友人でない友人)
- 「話す」とはメソッドやプロパティを呼び出すこと
つまり、オブジェクト指向において、あるレシーバ(インスタンス)から呼び出すメソッドは連続的にせずに友人までにしなさいという原則です。レシーバのコンテキストがころころ変わるようなメソッドの連鎖はデメテルの法則に反する可能性が高いということです。
// Bad Code
class Main {
public static void main(String[] args) {
Game game = new Game();
int mainCharacterId = 0;
String memberName = game.getParty().getMember(mainCharacterId).getName();
String weaponName = game.getParty().getMember(mainCharacterId).getEquipments().getWeapon().getName();
}
}
// Good Code
class Main {
public static void main(String[] args) {
Game game = new Game();
int mainCharacterId = 0;
String memberName = game.getMemberName(mainCharacterId);
String weaponName = game.getMemberEquipment(mainCharacterId, EquipmentType.WEAPON).getName();
}
}
Game クラスが Facade (ファサード)パターンで設計されている、つまり Game クラスまたは Game インスタンスを呼び出して使う側が Game の内部で呼び出されている他のクラス情報を知らなくてもよい設計になっているのであれば、Game クラスは内部で使っているプロパティやメソッドを public にするべきではありません。Game クラスは呼び出し側で使われる API のみを提供すべきです(Game.getParty() のようなメソッドは private にしておくか、そもそも Getter メソッドを用意すべきではありません)。
Method Chaining はデメテルの法則に反するか
Method Chaining(メソッドチェイン、メソッドチェーン)という用語は定義に揺れがあります。まずは定義を明確にする必要があります。
広義な定義:メソッドを連続的に書き連ねたもののこと。以下のようなコードを指します。
foo.a().b().c().d();
狭義な定義:Fluent Interface パターンによるメソッドの連鎖のこと。以下のようなコードを指します。
Person person = new Person();
person.setName("Alice").setAge(20).setSex(SexType.FEMALE).show();
DBRecord record = db.select("name, age, sex").from("Users").where("name = ?", "Alice").getResult();
結論として、先にデメテルの法則に反するかどうかの表を掲載しておきます。
| 実装パターン | 広義の Method Chaining | 狭義の Method Chaining | デメテルの法則に反する |
|---|---|---|---|
| Trainwreck パターン | ⭕️ | ❌️ | ⭕️ |
| Fluent Interface パターン | ⭕️ | ⭕️ | ❌️ |
ここでは、Method Chaining には広義の定義と狭義の定義があるものとした上で、Method Chaining に関しては「必ずしもデメテルの法則に反するとは言えない」と結論付けられることになります。
デメテルの法則に反するのは Trainwreck パターンの場合のみです。
それでは、Trainwreck と Fluent Interface について見ていきましょう。
Trainwreck パターン
class Main {
public static void main(String[] args) {
Game game = new Game();
int mainCharacterId = 0;
String memberName = game.getParty().getMember(mainCharacterId).getName();
String weaponName = game.getParty().getMember(mainCharacterId).getEquipments().getWeapon().getName();
}
}
memberName を取得するのに getParty() で Party インスタンスを取得しています。また、weaponName を取得するのに Party, Member, Equipments インスタンスを連鎖的にアクセスしています。これを Trainwreck(トレイン・レック)パターンと呼びます。
Trainwreck パターンでは、呼び出し元の main メソッドを記述する際に game インスタンスの内部設計を知っていなければなりません。つまり、Game クラスのインスタンスメソッドの他に、Party クラス、Member クラス、Equipments クラスのメソッドを知らなければコードが書けないのです。
内部設計を知っていれば問題ないのでは?と思うかもしれませんが、問題はそれだけではありません。もし何らかの理由で内部設計を変更したらどうなるでしょうか。機能追加によって新しい概念が登場したときに、各クラスが持つプロパティ、メソッドの設計が変わるかもしれません。そのような際に、このような内部設計を辿る実装が行われていると変更が容易にできなくなってしまいます。
Trainwreck を解消するには、次のような設計にする必要があります。
class Main {
public static void main(String[] args) {
Game game = new Game();
int mainCharacterId = 0;
String memberName = game.getMemberName(mainCharacterId);
String weaponName = game.getMemberEquipment(mainCharacterId, EquipmentType.WEAPON).getName();
}
}
main メソッドから呼び出している友人である game のメソッドに処理を任せるのです。ただし、どこまで処理を任せるかは設計に依ります。変更容易性への影響が無くなればよいので、例えば game.getMember(mainCharacterId).getName() のようにしてもよいでしょう。weaponName の方では EquipmentName だけを取得するメソッドは使い勝手が悪いと判断してそのように実装した例を示しています。
Fluent Interface パターン
広義の Method Chaining はアンチパターンなのかというと、そうでもありません。メソッド呼び出しが何個も連続していても Trainwreck にならないケースがあります。
それが狭義の Method Chaining である Fluent Interface パターンです。
Fluent Interface という言葉は Martin Fowler と Eric Evans によって生み出されました。
Fluent Interface - 流れるようなインタフェース
class Main {
public static void main(String[] args) {
Person person = new Person();
person.setName("Alice").setAge(20).setSex(SexType.FEMALE).show();
}
}
enum SexType {
NULL("null"), MALE("Male"), FEMALE("Female");
private final String label;
private SexType(final String label) { this.label = label; }
public String getString() { return label; }
}
class Person {
private String name;
private int age;
private SexType sex;
Person() { name = ""; age = 0; sex = SexType.NULL; }
public Person setName(final String name) { this.name = name; return this; }
public Person setAge(final int age) { this.age = age; return this; }
public Person setSex(final SexType sex) { this.sex = sex; return this; }
public void show() { System.out.println(String.format("Name: %s\nAge: %d\nSex: %s", name, age, sex.getString())); }
}
上記は動作する Java の全体コードですが Person クラスの Setter メソッドの実装が肝になっています。一般的に Setter メソッドはインスタンスプロパティを更新するだけでメソッドの返り値は void にします。しかし Fluent Interface パターンでは Setter メソッドの返り値を this にします。これにより、Setter メソッドに続けてインスタンスメソッドを呼び出せるようになり、Fluent Interface が実現できます。
改めて書くと、次のような書き方ができます。Fluent Interface パターンではない例から示します。
Person person = new Person();
person.setName("alice");
person.setAge(20);
person.setSex(SexType.FEMALE);
person.show();
一般的な実装では、このようにインスタンス変数に対してメソッドを何度も異なる文として呼び出す必要があります。これが Fluent Interface パターンの場合、次のように書くことができます。
Person person = new Person();
person.setName("alice").setAge(20).setSex(SexType.FEMALE).show();
person インスタンス変数を用意していますが、場合によってはこれすら不要です。次のようなコードが考えられるかもしれません。
Member party = new Party().add(new Member("Cecil")
.equipment(EquipmentType.WEAPON, Weapon.create("Ragnarok"))
.equipment(EquipmentType.SHIELD, Shield.create("CrystalShield"))
.equipment(EquipmentType.ARM, Arm.create("CrystalRing"))
.equipment(EquipmentType.HEADGEAR, Headgear.create("Ribbon"))
.equipment(EquipmentType.ARMOR, ARMOR.create("AdamantArmor"))
);
Method Chaining という用語は、狭義の定義である Fluent Interface を指す場合があります。その前提であれば、「Method Chaining はデメテルの法則に反しない」と結論付けられることになります。
結論の表を再掲しておきます。
| 実装パターン | 広義の Method Chaining | 狭義の Method Chaining | デメテルの法則に反する |
|---|---|---|---|
| Trainwreck パターン | ⭕️ | ❌️ | ⭕️ |
| Fluent Interface パターン | ⭕️ | ⭕️ | ❌️ |
悪しきパターンは Trainwreck であって、Fluent Interface は問題ありません。そして Method Chaining という用語は定義に揺れがあるため、定義を明らかにしないうちは「Method Chaining はデメテルの法則に反するか」という問いはナンセンスです。
参考文献
- Random Thoughts on Java Programming
- Train wrecks vs. fluent interfaces
- Fluent Interface
- Expression Builder
"Method Chaining" vs "Trainwreck" vs "Fluent Interface" に関する日本語記事が見当たらなかったので書いてみました。