今後の新サービスの立ち上げに向けて技術検証を行う技術推進課に所属している鈴木(@moomooya)です。今年度から新設の部署に(課長とふたりぼっちで)異動となりました。
最近マイクロサービスと関連してオブジェクト指向について取り組んでいるので、デメテルの法則に関する記事を書いてみようと思います。タイトルは1度くらい煽りタイトルをつけてみたかったので出来心です。
デメテルの法則って?
デメテルの法則(LoD: Law of Demeter)はノースイースタン大学で作成されたガイドラインが原典となり、達人プログラマーでも(あまり詳しくは書かれていませんが)紹介されています。
ちなみにみんな大好きWikipediaではこのように説明されています。
簡潔に言うと「直接の友達とだけ話すこと」と要約できる。基本的な考え方は、任意のオブジェクトが自分以外(サブコンポーネント含む)の構造やプロパティに対して持っている仮定を最小限にすべきであるという点にある。
―― Wikipedia - デメテルの法則より抜粋
なんとなくわかりますが、実装レベルでどうすればいいのか理解するにはもう少し説明がほしいところです。具体的な対応方法としては
「メンバを直接参照せずにアクセッサ(getter/setter)経由で参照すればいい」
とか
「オブジェクトのメンバを直接参照しない」 「オブジェクトのメンバのメンバを直接参照しない」
とよく言われています。
Javaのサンプルコードで見てみましょう。お題は乗り物です。
// Demeter1.java class Engine { // エンジンクラス String status; } class Vehicle { // 乗り物クラス Engine engine; Vehicle() { engine = new Engine(); } } public class Demeter { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.engine.status = "発進"; // ×××オブジェクトの内部を直接操作しない××× System.out.println(vehicle.engine.status); } }
これはvehicle.engine.status
で直接参照しているためデメテルの法則に違反している例です。
「メンバを直接参照せずにアクセッサ(getter/setter)経由で参照すればいい」んでしょ?
// Demeter2.java class Engine { protected String status; // ★getter/setter作った String getStatus() { return status; } void setStatus(String status) { this.status = status; } } class Vehicle { protected Engine engine; Vehicle() { engine = new Engine(); } // ★getter作った Engine getEngine() { return engine; } } public class Demeter { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.getEngine().setStatus("発進"); // ★getter/setter使って更新 System.out.println(vehicle.getEngine().getStatus()); } }
・
・
・
・
・
・
・
・
・
・
お可愛いこと……
メンバ変数名にそのままget/setをつけたメソッドを用意したところで本質的には何も変わっていません。
ただの素直か!
メンバをprotected
(またはprivate
でも)にしているのでリードオンリー/ライトオンリーなどの制御は可能ですが疎結合にはなりません。
このへんはIDEの功罪というかgetter/setterの自動生成によって洗脳されてしまっている人も結構いる気がします。
さらに言えばvehicle.getEngine().setStatus("発進");
の部分がオブジェクトの内部構造(Vehicle内のEngineの構造)を意識してしまっています。
「オブジェクトのメンバのメンバを直接参照しない」ようにすればいいの?
// Demeter3.java class Engine { protected String status; String getStatus() { return status; } void setStatus(String status) { this.status = status; } } class Vehicle { protected Engine engine; Vehicle() { engine = new Engine(); } Engine getEngine() { return engine; } // ★engine用のgetter/setter作った String getEngineStatus() { return engine.getStatus(); } void setEngineStatus(String status) { engine.setStatus(status); } } public class Demeter { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); vehicle.setEngineStatus("発進"); // ★Engineのメソッドを利用 System.out.println(vehicle.getEngineStatus()); } }
・
・
・
・
・
・
・
・
・
・
` ニ|ュ _二, ''''7 _二, | ヽヽ_」_」_ _|_ :/ ヘ
入呈 _ノ 、  ̄( ̄ _ノ |__丿 } .-' ノ よ {_,, .|
` ̄ `
,,...-=ミミミミミヽ、
/ミ////////////ハ
./ミ;;;;;:////////////}
{:::ミ `ヾ/////////リ
.{;;;;: : :.////////ノ
r、ミ_,,,_,,;;;::://////ソ‐,
{iハ〔i77777ハ77777!〕:/
ヽ.Yゝ___ノ ::ゝ_;;;;//ノ
', ::i 、,//////{ ,、
', ,ィ彡//////>、_____//〉
ノヽ´ ̄;///////////////////ァ-=ュ
─''"{: : :.\_;;;///////////////////////Y
、: : : |: : : :.Y /\ //: : / ..:::".:://///////ハ
: ヘ: :.|: : : : :{' `/: :/...:....::////////////リ
、: :ヽ:{: : : : :.} //: :\:://////////////ハ
: \: :{: : : : :.} /:::: : : : :\/////////////ハ
: : : ヽヘ: : : :.!/: :::::::::..: : : : :.ヽ////////////}
: : : : : \,ィ'": : :.::::::::::::::.: : : /////////////ハ
: : : :>'i: :::.:.:.: :::::::::::::::::: ://///////////////
: ://:./:∪:::::::: :::::::::::::::::///////////////////
:(;': /::: : ::::::::: ::::::::::::://///////////∠//////
Y: (:::: : : :::::: ::::::://///////////'''`ミミ:ヽ:////
.\: : : : : :::: :::////////////ノ: : :::::.: ::::://///
/ヽ; : : : /////////-'''":{::::.: : : ::::::::::://///
. /: r.、二、:{: : : >':::::: : : ::: : :ゝ: ::.:.:::::::://///
/: : ゝ-、 `ヽ-{: : : : : : : :::::.:.:.:.::::::///////
{: : :._/ ∨: : : ::::: : : : :.////////
Vehicle
とEngine
の関連をVehicle
内に移譲しているのでだいぶ良くなってはいます。これだとEngine
(原動機)がHuman
(人力)に変わったとしてもVehicle
の修正だけで済み、mainメソッドは内部構造(動力)を意識しなくてよくなりました。
しかしEngine
のステータスが"発進"
なのか1
なのかはmainメソッド側では意識する必要がないものです。
さらに現実的な実装として操作先のオブジェクトが準備完了かどうかを判定してからステータス更新を行うと思います。
// Demeter4.java class Engine { // ...略... } class Vehicle { // ...略... // ★動力があるか返す boolean isReady() { return engine != null; } } public class Demeter { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); if (vehicle.isReady()) { // ★動力があるかチェック vehicle.setEngineStatus("発進"); } System.out.println(vehicle.getEngineStatus()); } }
こんな感じ。
尋ねるな、命じろ (Tell, Don't Ask)
で、結局どうするんだ。という話ですが、こうするのが良いとされています。
// Demeter.java class Engine { // ...略... } class Vehicle { protected Engine engine; Vehicle() { engine = new Engine(); } // ★進むための処理 // engineへの操作はここで行う void start() throws Exception { if (engine != null) { engine.setStatus("発進"); } else { throw new Exception("エンジン盗まれた"); } } // ★Vehicleの状態として返す String getStatus() { // 必要に応じて整形(ここではそのまま返す) return engine.getStatus(); } } public class Demeter { public static void main(String[] args) { Vehicle vehicle = new Vehicle(); try { vehicle.start(); // ★ただ「進め」と命じる System.out.println(vehicle.getStatus()); // ★vehicleの状態として取得 } catch (Exception e) { System.out.println(e); // ★ダメなら例外処理(場合によってエラーコード処理でも) } } }
こうすることでmainメソッドからはVehicle
のみを意識すればよく、Engine
とのことはすべてVehicle
内に閉じることになります。
むかしばなし
ずいぶん昔に所属していたチームで「getter/setterを経由しなければならないんDA!!」というお可愛いコードを主張する方がいたのですが
int getA() { if (A == null) { return B; } return A; }
といったコード書いてたのを発見したときに、「[検閲削除]」「[検閲削除]」「[検閲削除]」といったことを心の中で叫んだ記憶が蘇りました。
まとめ
イケてるインタフェース設計を心がけましょう。
エンジニア中途採用サイト
ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
ご興味ありましたら是非ご確認をお願いします。
https://career-recruit.rakus.co.jp/career_engineer/カジュアル面談お申込みフォーム
どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
以下フォームよりお申込みください。
rakus.hubspotpagebuilder.comラクスDevelopers登録フォーム
https://career-recruit.rakus.co.jp/career_engineer/form_rakusdev/イベント情報
会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください!
◆TECH PLAY
techplay.jp
◆connpass
rakus.connpass.com