web-dev-qa-db-ja.com

デメテルの法則の違反を解決する方法は?

同僚と私はお客様のためにシステムを設計し、私たちの意見では、すてきなクリーンな設計を作成しました。しかし、私たちが導入したいくつかのカップリングに問題があります。私たちのデザインと同じ問題を含むサンプルデザインを作成することもできますが、ご容赦いただければ、質問をサポートするためにデザインの抜粋を作成します。

私たちは、患者の特定の治療法を登録するためのシステムを開発しています。画像へのリンクが壊れないようにするために、概念的なUMLクラス図をc#スタイルのクラス定義として説明します。

class Discipline {}
class ProtocolKind 
{ 
   Discipline; 
}
class Protocol
{
   ProtocolKind;
   ProtocolMedication; //1..*
}
class ProtocolMedication
{
   Medicine;
}
class Medicine
{
   AdministrationRoute;
}
class AdministrationRoute {}

デザインについて少し説明しようと思います。プロトコルは新しい治療法のテンプレートです。また、プロトコルには特定の種類があり、投与する必要のある薬があります。プロトコルごとに、同じ薬の投与量が異なる可能性があるため(とりわけ)、ProtocolMedicationクラスに保存されます。 AdministrationRouteは、薬を投与する方法であり、プロトコル管理とは別に作成/更新されます。

デメテルの法則に違反する次の場所を見つけました。

デメテルの法則の違反

BLLの内部

たとえば、ProtocolMedicationのビジネスロジック内には、医薬品のAdministrationRoute.Solubleプロパティに依存するルールがあります。コードは

if (!Medicine.AdministrationRoute.Soluble)
{
   //validate constrains on fields
}

リポジトリ内

特定の分野のすべてのプロトコルを一覧表示するメソッドは、次のように記述されます。

public IQueryable<Protocol> ListQueryable(Discipline discipline)
{
    return ListQueryable().Where(p => (p.Kind.Discipline.Id == discipline.Id)); // Entity Frameworks needs you to compare the Id...
}

ユーザーインターフェイスの内部

システムのインターフェイスにはASP.NET(MVCなし)を使用していますが、私の意見では、このレイヤーの違反は現在最悪です。文字列であるグリッドビュー(プロトコルの分野を表示する必要がある列はKind.Discipline.Nameにバインドする必要があります)のデータバインディングしたがってコンパイル時エラーはありません

<asp:TemplateField HeaderText="Discipline" SortExpression="Kind.Discipline.Name">
   <ItemTemplate>
      <%# Eval("Kind.Discipline.Name")%>
   </ItemTemplate>
</asp:TemplateField>

それで、実際の質問は、それをデメテルの提案としてもっと見ても大丈夫なのはいつか、そしてデメテルの法則の違反を解決するために何ができるかということだと思います。

私は自分自身のアイデアをいくつか持っていますが、コメントと投票が別々にできるように、回答として投稿します。 (これがSOの方法かどうかはわかりません。そうでない場合は、回答を削除して質問に追加します)。

36
Davy Landman

デメテルの法則の結果についての私の理解は、DrJokepuのものとは異なるようです-オブジェクト指向コードに適用するたびに、手続き型コードのコントラクトパスに追加のゲッターを追加するのではなく、より緊密なカプセル化と凝集をもたらしました。

ウィキペディアには次のようなルールがあります

より正式には、関数のデメテルの法則では、オブジェクトOのメソッドMは、次の種類のオブジェクトのメソッドのみを呼び出すことができます。

  1. O自体
  2. Mのパラメータ
  3. m内で作成/インスタンス化されたオブジェクト
  4. Oの直接コンポーネントオブジェクト

'kitchen'をパラメーターとして受け取るメソッドがある場合、Demeterは、キッチンのコンポーネントを検査することはできず、直接のコンポーネントのみを検査できると言います。

このようにデメテルの法則を満たすためだけにたくさんの関数を書く

Kitchen.GetCeilingColour()

私にとっては時間の無駄のように見えますが、実際に取得するのが私の方法です

Kitchenの外部のメソッドがキッチンに渡された場合、厳密なDemeterによって、GetCeilingColour()の結果に対してメソッドを呼び出すこともできません。

しかし、いずれにせよ、重要なのは、構造の表現を一連の連鎖メソッドからメソッドの名前に移動するのではなく、構造への依存を取り除くことです。 DogクラスでMoveTheLeftHindLegForward()などのメソッドを作成しても、Demeterの実行には何の効果もありません。代わりに、dog.walk()を呼び出して、犬に自分の足を処理させます。

たとえば、要件が変更され、天井の高さも必要になった場合はどうなりますか?

部屋と天井を操作できるように、コードをリファクタリングします。

interface RoomVisitor {
  void visitFloor (Floor floor) ...
  void visitCeiling (Ceiling ceiling) ...
  void visitWall (Wall wall ...
}

interface Room { accept (RoomVisitor visitor) ; }

Kitchen.accept(RoomVisitor visitor) {
   visitor.visitCeiling(this.ceiling);
   ...
}

または、天井のパラメータをvisitCeilingメソッドに渡すことで、さらに進んでゲッターを完全に排除することもできますが、それによって結合が脆弱になることがよくあります。

これを医療の例に適用すると、SolubleAdminstrationRouteで薬を検証できるか、検証に必要な薬のクラスにカプセル化された情報がある場合は、少なくとも薬のvalidateForSolubleAdministrationメソッドを呼び出すことができると思います。

ただし、DemeterはOOシステム(データを操作するオブジェクト内にデータがカプセル化されている)に適用されます。これは、データが異なるレイヤー間で受け渡される異なるレイヤーを持つシステムではありません。ダムのナビゲート可能な構造のレイヤー。Demeterがモノリシックまたはメッセージベースのシステムのように簡単にそのようなシステムに適用できるとは限りません(メッセージベースのシステムでは、にないものにナビゲートすることはできません)。メッセージのグラム、それであなたはそれが好きかどうかにかかわらずデメテルで立ち往生しています)

30
Pete Kirkham

トータルアニヒレーションに反対するつもりですが、デメテルの法則が嫌いだと言わざるを得ません。確かに、

dictionary["somekey"].headers[1].references[2]

本当に醜いですが、これを考慮してください:

Kitchen.Ceiling.Coulour

私はこれに反対するものは何もありません。このようにデメテルの法則を満たすためだけにたくさんの関数を書く

Kitchen.GetCeilingColour()

私にとっては時間の無駄のように見えますが、実際に得ることが物事を成し遂げるための私の方法です。たとえば、要件が変更され、天井の高さも必要になった場合はどうなりますか?デメテルの法則を使用すると、天井の高さを直接取得できるように、キッチンで他の関数を作成する必要があります。最終的には、どこにでも小さなゲッター関数がたくさんあります。これはかなり混乱していると思います。

編集:私のポイントを言い換えさせてください:

このレベルの抽象化は非常に重要なので、3-4-5レベルのゲッター/セッターを書くことに時間を費やしますか?それは本当にメンテナンスを容易にしますか?エンドユーザーは何かを得ますか?それは私の時間の価値がありますか?私はそうは思いません。

21
Tamas Czinege

デメテル違反に対する従来の解決策は、「教えて、聞かないで」です。言い換えると、状態に基づいて、管理対象オブジェクト(保持しているオブジェクト)に何らかのアクションを実行するように指示する必要があります。これにより、自身の状態に応じて、要求したことを実行するかどうかが決定されます。

簡単な例として、私のコードはロギングフレームワークを使用しており、デバッグメッセージを出力することをロガーに伝えています。次に、ロガーは、その構成に基づいて(おそらくデバッグが有効になっていない)、メッセージを出力デバイスに実際に送信するかどうかを決定します。この場合のLoD違反は、オブジェクトがメッセージに対して何かを行うかどうかをロガーに尋ねることです。そうすることで、コードをロガーの内部状態の知識に結合しました(もちろん、この例を意図的に選択しました)。

ただし、この例の重要なポイントは、ロガーがbehaviorを実装していることです。

LoDが壊れると思うのは、dataを表すオブジェクトを処理するときで、動作なしです。

この場合、オブジェクトグラフをトラバースするIMOは、XPath式をDOMに適用することと同じです。また、「isThisMedicationWarranted()」などのメソッドを追加することは、より悪いアプローチです。これは、オブジェクト間でビジネスルールを分散しているため、オブジェクトが理解しにくくなっているためです。

11
kdgregory

The Clean Code Talksのセッションを見るまで、多くの人と同じようにLoDに苦労していました。

"物事を探さないでください"

このビデオは、依存性注入をより適切に使用するのに役立ちます。これにより、本質的にLoDの問題を修正できます。デザインを少し変更することで、親オブジェクトを作成するときに多くの下位レベルのオブジェクトまたはサブタイプを渡すことができるため、親が子オブジェクトを介して依存関係チェーンをたどる必要がなくなります。

あなたの例では、AdministrationRouteをProtocolMedicationのコンストラクターに渡す必要があります。理にかなっているようにいくつかのことを再設計する必要がありますが、それがアイデアです。

そうは言っても、LoDに不慣れで専門家がいないので、私はあなたとDrJokepuに同意する傾向があります。ほとんどのルールと同様に、LoDにはおそらく例外があり、デザインには適用されない場合があります。

[数年遅れているので、この回答はおそらく発信者の助けにはならないでしょうが、それが私がこれを投稿する理由ではありません]

4
goku_da_master

Solubleを必要とするビジネスロジックには他のものも必要であると想定する必要があります。もしそうなら、その一部を意味のある方法で医学にカプセル化することができますか(Medicine.isSoluble()よりも意味があります)?

別の可能性(おそらく、やり過ぎであり、同時に完全な解決策ではない)は、ビジネスルールをそれ自体のオブジェクトとして提示し、ダブルディスパッチ/ビジターパターンを使用することです。

RuleCompilator
{
  lookAt(Protocol);
  lookAt(Medicine);
  lookAt(AdminstrationProcedure) 
}

MyComplexRuleCompilator : RuleCompilator
{
  lookaAt(Protocol)
  lookAt(AdminstrationProcedure)
}

Medicine
{
  applyRuleCompilator(RuleCompilator c) {
    c.lookAt(this);
    AdministrationProtocol.applyRuleCompilator(c);
  }
}
2
user3458

含まれているすべてのオブジェクトのすべてのメンバーにゲッター/セッターを提供する代わりに、将来の変更に柔軟に対応できる簡単な変更を1つ行うと、代わりに含まれているオブジェクトを返すメソッドをオブジェクトに与えることができます。

例えば。 C++の場合:

class Medicine {
public:
    AdministrationRoute()& getAdministrationRoute() const { return _adminRoute; }

private:
    AdministrationRoute _adminRoute;
};

次に

if (Medicine.AdministrationRoute.Soluble) ...

になります

if (Medicine.getAdministrationRoute().Soluble) ...

これにより、将来getAdministrationRoute()を次のように変更できる柔軟性が得られます。オンデマンドでDBテーブルからAdministrationRouteをフェッチします。

1
j_random_hacker

LoDのraisond'êtreを覚えておくと役立つと思います。つまり、関係のチェーンの詳細が変更された場合、コードが破損する可能性があります。あなたが持っているクラスは抽象化であるため問題に近いドメイン、問題が同じままである場合、関係は変更されない可能性があります。たとえば、プロトコルはその作業を完了するために規律を使用しますが、抽象化高レベルであり、変更される可能性はありません。情報隠蔽について考えてみてください。プロトコルが規律の存在を無視することは不可能ですよね?たぶん私はドメインモデルの理解に取り掛かっています...

プロトコルと規律の間のこのリンクは、リストの順序、データ構造の形式など、パフォーマンス上の理由で変更される可能性のある「実装」の詳細とは異なります。確かにこれはやや灰色の領域です。

ドメインモデルを作成した場合、C#クラス図にあるものよりも多くの結合が見られると思います。 [編集]次の図に、問題のあるドメインの関係を破線で追加しました。

UML Diagram of Domain model

一方、 教えて、聞かないでくださいメタファー :を適用することで、いつでもコードをリファクタリングできます。

つまり、オブジェクトに何をしてほしいかを伝えるように努める必要があります。彼らに彼らの状態について質問し、決定を下し、そして彼らに何をすべきかを言わないでください。

answer で最初の問題(BLL)をすでにリファクタリングしました。 (BLLをさらに抽象化する別の方法は、ルールエンジンを使用することです。)

2番目の問題(リポジトリ)をリファクタリングするには、内部コード

    p.Kind.Discipline.Id == discipline.Id

おそらく、コレクションの標準APIを使用したある種の.equals()呼び出しに置き換えることができます(私はJavaプログラマーなので、正確なC#の同等物はわかりません) 。アイデアは、一致を判断する方法の詳細を非表示にすることです。

3番目の問題(UI内)をリファクタリングするために、私もASP.NETに精通していませんが、tell Kindオブジェクトに(尋ねるのではなく)分野の名前を返す方法がある場合Kind.Discipline.Nameのような詳細については、それがLoDを尊重する方法です。

1
Fuhrmanator

BLLの場合、私のアイデアは、Medicineに次のようなプロパティを追加することでした。

public Boolean IsSoluble
{
    get { return AdministrationRoute.Soluble; } 
}

これは、デメテルの法則に関する記事で説明されていると思います。しかし、これはクラスをどれだけ混乱させるでしょうか?

1
Davy Landman

3番目の問題は非常に単純です。Discipline.ToString()Nameプロパティを評価する必要があります。そうすれば、Kind.Disciplineのみを呼び出すことができます。

1
Guillermo

「可溶性」特性を持つ最初の例に関して、私はいくつかの意見を持っています:

  1. 「AdministrationRoute」とは何ですか。開発者はなぜそれから薬の可溶性特性を期待するのでしょうか。 2つの概念は完全に無関係のようです。これは、コードがあまりうまく通信しないことを意味し、おそらくあなたがすでに持っているクラスの分解が改善される可能性があります。分解を変更すると、問題に対する他の解決策が表示される可能性があります。
  2. 可溶性は、理由のために医学の直接のメンバーではありません。直接アクセスする必要がある場合は、おそらく直接メンバーである必要があります。追加の抽象化が必要な場合は、その追加の抽象化を薬から返します(直接またはプロキシまたはファサードによって)。可溶性の特性を必要とするものはすべて抽象化に取り組むことができ、基質やビタミンなど、複数の追加のタイプに同じ抽象化を使用できます。
1