最小知識の原則


32

最小知識原理の背後にある動機は理解していますが、それを設計に適用しようとすると、いくつかの欠点があります。

この原則の例の1つ(実際には使用しない方法)は、Head First Design Patternsの本にあり、この原則の観点から、他のメソッドの呼び出しから返されたオブジェクトでメソッドを呼び出すのは間違っていると述べています。 。

しかし、そのような機能を使用することが非常に必要な場合があるようです。

たとえば、ビデオキャプチャクラス、エンコーダクラス、ストリーマークラスなどのいくつかのクラスがあり、それらはすべて基本的な他のクラスVideoFrameを使用します。また、相互作用するため、たとえば次のようなことができます。

streamer クラスコード

...
frame = encoder->WaitEncoderFrame()
frame->DoOrGetSomething();
....

ご覧のとおり、この原則はここでは適用されません。この原則をここで適用できますか、またはこの原則をこのような設計に常に適用できるわけではありませんか?




4
それはおそらく近い複製です。
ロバートハーヴェイ

1
関連:このようなコードは「列車事故」(デメテルの法則に違反)ですか?(完全開示:それは私自身の質問です)
CVn

回答:


21

関数のあなたが話している原則(デメテルの法則として知られている)は、次のような別のヘルパーメソッドをストリーマークラスに追加することで適用できます。

  {
    frame = encoder->WaitEncoderFrame()
    DoOrGetSomethingForFrame(frame); 
    ...
  }

  void DoOrGetSomethingForFrame(Frame *frame)
  {
     frame->DoOrGetSomething();
  }  

現在、各機能は「友人と話す」だけでなく、「友人と話す」だけです。

私見は、より厳密に単一の責任原則に従う方法を作成するのに役立つ大まかなガイドラインです。上記のような単純なケースでは、これが本当に手間をかける価値があり、結果のコードが本当に「きれい」であるか、または顕著な利益なしにコードを正式に拡張する場合、おそらく非常に意見が多いです。


このアプローチには、コードの単体テスト、デバッグ、および推論をはるかに簡単にするという利点があります。
-gntskn

+1。ただし、コードを変更しない非常に良い理由があります。それは、クラスインターフェイスが、他のいくつかのメソッドを1つまたは数回呼び出すだけのメソッドで(そして追加のロジックなしで)クラスインターフェイスが肥大化する可能性があるからです。C ++ COM(特にWICおよびDirectX)などの一部の種類のプログラミング環境では、他の言語と比較して、COMインターフェイスへの各単一メソッドの追加に関連するコストが高くなります。
-rwong

1
C ++ COMインターフェースの設計では、大きなクラスを小さなクラスに分解し(つまり、複数のオブジェクトとやり取りする可能性が高くなります)、インターフェースメソッドの数を最小限に抑えることが、深い理解に基づいた実際のメリット(コスト削減)の2つの設計目標です内部の仕組み(仮想テーブル、コードの再利用性、その他多くのこと)。したがって、C ++ COMプログラマーは通常、LoDを無視する必要があります。
-rwong

10
1:デメテルの法則を実際に名前を付ける必要がありデメテルの提案:YMMV
バイナリ心配性

デメテルの法則は、建築上の選択を行うためのアドバイスですが、化粧品の変更を提案することで、その「法則」に準拠しているように見えます。構文上の変更が突然すべてがうまくいくことを意味しないので、それは誤解だろう。私が知る限り、デメテルの法則は基本的に次のことを意味していました。OOPを行いたい場合は、どこでもゲッター関数を使用した手続き型コードの作成を停止します。
user2180613

39

最小知識の原則またはデメテルの法則は、レイヤーをレイヤーを横断する他のクラスの詳細とクラスを絡ませることに対する警告です。「友達の友達」と話すのではなく、「友達」とだけ話すのが良いことを教えてくれます。

輝くプレートアーマーの騎士の像にシールドを溶接するように頼まれたと想像してください。自然に見えるように、慎重にシールドを左腕に配置します。前腕、肘、上腕の3つの小さな場所で、シールドが偶然鎧に触れていることがわかります。接続が強いことを確認したいので、3つの場所すべてを溶接します。今、あなたの上司が鎧の肘を動かすことができないために怒っていると想像してください。あなたは、鎧は決して動かないだろうと仮定し、そのため、前腕と上腕の間に不動の接続を作成しました。シールドは、その友人である前腕にのみ接続する必要があります。友達を前腕にしないでください。触れさせるために金属の塊を追加する必要がある場合でも。

比phorはいいですが、実際には友人とはどういう意味ですか?オブジェクトが作成または検索する方法を知っているものはすべて友人です。あるいは、オブジェクトは他のオブジェクトを渡されるように単に要求することができますが、そのオブジェクトはインターフェースのみを知っています。これらを取得する方法への期待が課されていないため、これらは友人としてカウントされません。他の何かがそれを通過/注入したためにオブジェクトがどこから来たのかわからない場合、それは友人の友人ではなく、友人でさえありません。これは、オブジェクトが使用方法のみを知っているものです。それはいい。

このような原則を適用しようとするとき、何かを達成することを決して禁止しないことを理解することが重要です。これらは、同じことを達成するより良い設計を達成するためにより多くの作業を怠る可能性があるという警告です。

誰も理由もなく仕事をしたいとは思わないので、これに従うことで何が得られるかを理解することが重要です。この場合、コードの柔軟性が維持されます。変更を行うことができ、心配する変更によって影響を受ける他のクラスを減らすことができます。それは良さそうに聞こえますが、何らかの宗教的教義として受け取らない限り、何をすべきかを決定する助けにはなりません。

盲目的にこの原則に従うのではなく、この問題の単純なバージョンを取ります。原則に従わないソリューションと、従うソリューションを作成します。2つの解決策が用意できたので、両方で変更を試みることで、それぞれが変更に対する受容性を比較できます。

この原則を順守している間に問題を解決できない場合は、別のスキルが不足している可能性があります。

あなたの特定の問題に対する1つの解決策は、frameフレームと話す方法を知っている何か(クラスまたはメソッド)に注入することですフレームを取得します。

それは実際には別の原則に従います:構造から使用を分離します。

frame = encoder->WaitEncoderFrame()

このコードを使用することにより、何らかの方法でを取得する責任を負いますFrame。あなたはまだに話しかける責任を負いませんFrame

frame->DoOrGetSomething(); 

ここで、に話す方法を知る必要がありますが、それを次のようにFrame置き換えます。

new FrameHandler(frame)->DoOrGetSomething();

そして今、あなたはあなたの友人であるFrameHandlerと話す方法を知っているだけです。

これを達成するには多くの方法がありますが、これはおそらく最良の方法ではありませんが、原則に従うことで問題を解決できないことを示しています。あなたからもっと多くの仕事を要求しているだけです。

すべての良いルールには例外があります。私が知っている最良の例は、内部ドメイン固有言語です。DSLメソッドチェーンさまざまな型を返すメソッドを常に呼び出して直接使用しているため、Demeterの法則は常に悪用されているようです。なぜこれでいいですか?DSLで返されるものはすべて、直接話をするつもりの慎重に設計された友人だからです。設計上、DSLのメソッドチェーンが変更されないことを期待する権利が与えられています。見つけたものが何であれ、コードベースをランダムにつなぎ合わせていくだけなら、その権利はありません。最適なDSLは、他のオブジェクトへの非常に薄い表現またはインターフェイスであり、どちらも掘り下げてはいけません。DSLが優れた設計である理由を学んだ後、デメテルの法則をよりよく理解したことがわかったため、これについてのみ言及します。DSLがデメテルの本当の法則にさえ違反していないとまで言う人もいます。

別の解決策は、他の何かframeをあなたに注入させることです。frameセッターまたはできればコンストラクターから来た場合、フレームの構築または取得について一切責任を負いません。これは、あなたがここで役割を果たしているということですFrameHandlers。代わりに、あなたはおしゃべりをFrameして、他の何かを作る方法を見つけた人です。Frame ある意味で、これは同じ視点からの単純な変更を伴うソリューションです。

SOLID原則は、私は従うことをしようと大きなものです。ここで尊重されているのは、単一の責任と依存関係の逆転の原則です。これら2つを尊重し、デメテルの法則に違反することになります。

デメテルに違反する精神は、あなたが望むものを何でも手に入れるビュッフェレストランで食べるようなものです。前もって少し作業をするだけで、好きなものをお届けするメニューとサーバーを提供できます。座って、リラックスして、よく傾けます。


2
「それは、「友人」とではなく、「友人」とのみ話す方が良いことを教えてくれます」++
ラバーダック

1
2番目の段落、それはどこかから盗まれたのですか、それともそれを補ったのですか?それは私に概念を完全に理解させました。それは、「友人の友人」が何であるか、そしてあまりにも多くのクラス(ジョイント)をもつれさせることの欠点を露骨にさせました。A +引用。
死ぬマウス

2
@diemausそのシールドパラグラフをソースからどこかから盗んだ場合、それが私の頭から漏れています。当時、私の脳はそれが巧妙だと思っていました。私が覚えているのは、多くの賛成票の後に追加したので、検証されているのを見るのは素晴らしいことです。喜んでくれました。
candied_orange

19

機能設計はオブジェクト指向設計よりも優れていますか?場合によります。

MVVMはMVCよりも優れていますか?場合によります。

Amos&AndyまたはMartin and Lewis?場合によります。

何に依存していますか?選択する方法は、設計、パフォーマンス、および保守性の目標を十分に満たしながら、各手法またはテクノロジがソフトウェアの機能要件および非機能要件をどれだけ満たしているかによって異なります。

[ある本]は[あるもの]が間違っていると言っています。

本やブログでこれを読むときは、そのメリットに基づいて主張を評価してください。つまり、理由を尋ねてください。ソフトウェア開発には正しい手法も間違った手法もありません。「この手法は私の目標をどれだけうまく達成できますか。効果的か無効ですか。1つの問題を解決しますが、新しい問題を作成しますか?開発チームですか、それともわかりにくいですか?」

この特定の場合-別のメソッドによって返されたオブジェクトのメソッドを呼び出す行為-この慣行をコード化する実際のデザインパターン(ファクトリー)があるため、カテゴリに基づいていると主張する方法を想像することは困難です違う。

「最小知識の原則」と呼ばれる理由は、「低結合」がシステムの望ましい品質だからです。互いに緊密にバインドされていないオブジェクトはより独立して機能するため、個別に保守および変更するのが簡単です。しかし、例が示すように、オブジェクトがより効果的に作業を調整できるように、高結合がより望ましい場合があります。


2

Doc Brownの答えは、デメテルの法則の古典的な教科書の実装を示しています-そして、そのように多数のメソッドを追加することの煩わしさ/組織化されたコードの膨張は、おそらく、私自身が含めたプログラマーが、たとえそうであっても、そうすることを気にしない理由です。

オブジェクトの階層を分離する別の方法があります。

メソッドやプロパティを介して、interface型ではなく型を公開classします。

元のポスター(OP)の場合、encoder->WaitEncoderFrame()IEncoderFrame代わりにを返し、Frame許可される操作を定義します。


解決策1

最も簡単なケースではFrameEncoderクラスは両方とも制御下にIEncoderFrameあり、Frameは既に公開されているメソッドのサブセットであり、Encoderクラスは実際にそのオブジェクトに対して何をするかを気にしません。次に、実装は簡単です(c#のコード):

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Frame : IEncoderFrame {
    // A method that already exists in Frame.
    public void DoOrGetSomething() { ... }
}

class Encoder {
    private Frame _frame;
    public IEncoderFrame TheFrame { get { return _frame; } }
    ...
}

解決策2

Frame定義があなたの制御下にない、またはIEncoderFrameのメソッドをに追加するのが適切でない中間の場合、適切FrameなソリューションはAdapterです。それがCandiedOrangeの答えが議論していることnew FrameHandler( frame )です。重要:これを行う場合、クラスとしてではなくインターフェイスとして公開する方がより柔軟です。は知る必要がありますが、クライアントは知る必要があるだけです。または、私が名前を付けたように、-それがエンコーダのPOVから見た具体的なフレームであることを示すために:Encoderclass FrameHandlerinterface IFrameHandlerinterface IEncoderFrame

interface IEncoderFrame {
    void DoOrGetSomething();
}

// Adapter pattern. Appropriate if no access needed to Encoder.
class EncoderFrameWrapper : IEncoderFrame {
    Frame _frame;
    public EncoderFrameWrapper( Frame frame ) {
        _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame....;
    }
}

class Encoder {
    private Frame _frame;

    // Adapter pattern. Appropriate if no access needed to Encoder.
    public IEncoderFrame TheFrame { get { return new EncoderFrameWrapper( _frame ); } }

    ...
}

COST:毎回新しいオブジェクトEncoderFrameWrapperの割り当てとGC encoder.TheFrameが呼び出されます。(そのラッパーをキャッシュすることはできますが、コードが追加されます。エンコーダーのフレームフィールドを新しいフレームに置き換えることができない場合にのみ、確実に簡単にコーディングできます。)


解決策3

より難しいケースでは、新しいラッパーはEncoderとの両方について知る必要がありFrameます。そのオブジェクト自体がLoDに違反することになります-エンコーダーとフレームの間の関係を操作することは、エンコーダーの責任であるべきであり、おそらく正しくなるのは苦痛です。その道を始めた場合に起こりうることは次のとおりです。

interface IEncoderFrame {
    void DoOrGetSomething();
}

// *** You will end up regretting this. See next code snippet instead ***
class EncoderFrameWrapper : IEncoderFrame {
    Encoder _owner;
    Frame _frame;
    public EncoderFrameWrapper( Encoder owner, Frame frame ) {
        _owner = owner;   _frame = frame;
    }
    public void DoOrGetSomething() {
        _frame.DoOrGetSomething();
        // Hmm, maybe this wrapper class should be nested inside Encoder...
        _owner... some work inside owner; maybe should be owner-internal details ...
    }
}

class Encoder {
    private Frame _frame;

    ...
}

それはいです。ラッパーがその作成者/所有者(エンコーダー)の詳細に触れる必要がある場合、複雑度の低い実装があります。

interface IEncoderFrame {
    void DoOrGetSomething();
}

class Encoder : IEncoderFrame {
    private Frame _frame;

    // HA! Client gets to think of this as "the frame object",
    // but its really me, intercepting it.
    public IEncoderFrame TheFrame { get { return this; } }

    // This is the method that the LoD approach suggests writing,
    // except that we are exposing it only when the instance is accessed as an IEncoderFrame,
    // to avoid extending Encoder's already large API surface.
    public void IEncoderFrame.DoOrGetSomething() {
        _frame.DoOrGetSomething();
       ... make some change within current Encoder instance ...
    }
    ...
}

確かに、ここにたどり着くことがわかっていれば、これをしないかもしれません。LoDメソッドを記述するだけで、それを行うことができます。インターフェイスを定義する必要はありません。一方、インターフェイスが関連するメソッドをまとめてラップしているのが気に入っています。私は、フレームのように感じるものに対して「フレームのような操作」を行うことがどのように感じるかが好きです。


最終コメント

これを考慮してください:の実装者Encoderが公開Frame frameがアーキテクチャ全体に適切であると感じた場合、または「LoDを実装するよりもはるかに簡単」だった場合、代わりに最初に示したスニペットを実行した方がはるかに安全でした-の限定サブセットを公開しますインターフェイスとしてのフレーム。 私の経験では、それはしばしば完全に実行可能なソリューションです。必要に応じて、インターフェイスにメソッドを追加するだけです。(フレームに必要なメソッドがあることを「知っている」、または追加するのが簡単で議論の余地のないシナリオについて話しています。各メソッドの「実装」作業は、インターフェイス定義に1行追加することです。)最悪の将来のシナリオでも、そのAPIを動作させ続けることができることを知っています-ここで、IEncoderFrameFrameEncoder

また、に追加IEncoderFrameする権限がない場合Frame、または必要なメソッドが一般Frameクラスに適合せず、ソリューション2があなたに合わない場合、おそらく余分なオブジェクトの作成と破壊のために、ソリューション#3はEncoder、LoDを達成するためののメソッドを整理するための単なる方法と見なすことができます。何十ものメソッドをただ通過するだけではありません。それらをインターフェイスでラップし、「明示的なインターフェイスの実装」(c#を使用している場合)を使用して、そのインターフェイスを介してオブジェクトが表示されたときにのみアクセスできるようにします。

私が強調したい別の点は、機能をインターフェイスとして公開するという決定が、上記の3つの状況すべてを処理したことです。最初は、IEncoderFrame単にFrameの機能のサブセットです。2番目IEncoderFrameはアダプターです。3番目IEncoderFrameは、Encoders機能へのパーティションです。これらの3つの状況の間でニーズが変わっても問題ありません。APIは同じままです。


クラスを、具体的なクラスではなく、協力者の1人によって返されたインターフェイスに結合することは、改善の一方で、依然として結合の原因です。インターフェイスを変更する必要がある場合、クラスを変更する必要があることを意味します。取り上げる重要な点は、不安定なオブジェクトや内部構造への不必要な結合を避けるようにする限り、結合は本質的に悪くないということです。これが、デメテルの法則を大きな塩でとる必要がある理由です。状況に応じて、問題になる可能性のあるものとそうでない可能性のあるものを常に避けるように指示します。
ペリアタブレアッタ

@PeriataBreatta-私は確かにそうすることはできませんし、それに反対しません。しかし、1つのことを指摘したいと思います。定義により、インターフェイスは、2つのクラスの境界で知られる必要があるものを表します。それが「変更する必要がある」場合、それは基本です -代替アプローチは、何らかの方法で必要なコーディングを魔法のように回避できなかったでしょう。これとは対照的に、私が説明する3つの状況のいずれかを実行しますが、インターフェイスを使用せず、代わりに具体的なクラスを返します。1、フレーム、2、EncoderFrameWrapper、3、エンコーダー。あなたはそのアプローチに縛られます。インターフェイスはそれらすべてに適応できます。
ToolmakerSteve

@PeriataBreatta ...これは、インターフェイスとして明示的に定義する利点を示しています。最終的にはIDEを強化して非常に便利なものにし、インターフェイスがより頻繁に使用されるようにしたいと考えています。ほとんどのマルチレベルアクセスは何らかのインターフェイスを介して行われるため、変更の管理がはるかに簡単になります。(それがいくつかのケースで「過剰」である場合、コード分析は、わずかなパフォーマンスの向上と引き換えに、インターフェースを持たないリスクを負うことをいとわない場所についての注釈と組み合わせて、「コンパイルアウト」することができます-それを置き換える3つの「ソリューション」からの3つの具体的なクラスの1つ。)
ToolmakerSteve

@PeriataBreatta-「インターフェイスはすべてに適応できる」と言うとき、「ああ、この1つの言語機能がこれらのさまざまなケースをカバーできることは素晴らしい」と言っているわけではありません。インターフェイスを定義すると、必要な変更が最小限に抑えられると言っています。最良の場合、設計はインターフェイスをまったく変更せずに、最も単純なケース(ソリューション1)から最も難しいケース(ソリューション3)に変更できます。生産者の内部ニーズのみがより複雑になりました。そして、変更が必要なときでさえ、それらはあまり普及していない傾向があります、私見。
ToolmakerSteve
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.