RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

お前のデメテルの法則は間違っている ~getter/setterの必要性~

今後の新サービスの立ち上げに向けて技術検証を行う技術推進課に所属している鈴木(@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.、二、:{: : : >':::::: : : ::: : :ゝ: ::.:.:::::::://///
/: : ゝ-、 `ヽ-{: : : : : : : :::::.:.:.:.::::::///////
{: : :._/    ∨: : : ::::: : : : :.////////

VehicleEngineの関連を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;
}

といったコード書いてたのを発見したときに、「[検閲削除]」「[検閲削除]」「[検閲削除]」といったことを心の中で叫んだ記憶が蘇りました。


まとめ

イケてるインタフェース設計を心がけましょう。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Copyright © RAKUS Co., Ltd. All rights reserved.