web-dev-qa-db-ja.com

別の関数を呼び出すだけの関数、設計上の選択の誤り?

建物を表すクラスの設定があります。この建物には、境界のある平面図があります。

私がそれを設定する方法は次のとおりです:

public struct Bounds {} // AABB bounding box stuff

//Floor contains bounds and mesh data to update textures etc
//internal since only building should have direct access to it no one else
internal class Floor {  
    private Bounds bounds; // private only floor has access to
}

//a building that has a floor (among other stats)
public class Building{ // the object that has a floor
    Floor floor;
}

これらのオブジェクトには、さまざまなことを行うために存在する独自の理由があります。ただし、建物のローカルポイントを取得したい場合があります。

この状況では、私は本質的にやっています:

Building.GetLocalPoint(worldPoint);

これは次のようになります:

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

これは私のFloorオブジェクトでこの関数につながります:

internal Vector3 GetLocalPoint(Vector3 worldPoint){
    return bounds.GetLocalPoint(worldPoint);
}

そしてもちろん、boundsオブジェクトは実際に必要な計算を行います。

ご覧のとおり、これらの関数は下位の別の関数に渡されるだけなので、かなり冗長です。これは私には賢く感じられません-それはコード混乱の行のどこかでお尻に噛み付く悪いコードのようなにおいがします。

または、以下のように自分のコードを記述しますが、公開したくないので、やめたくありません。

building.floor.bounds.GetLocalPoint(worldPoint);

これはまた、多くのネストされたオブジェクトに移動すると少しばかげてしまい、特定の関数を取得するために大きなウサギの穴につながり、どこにあるか忘れてしまう可能性があります-これも悪いコード設計のようなにおいがします。

これをすべて設計する正しい方法は何ですか?

53
WDUK

デメテルの法則 を忘れないでください:

デメテルの法則(LoD)または最小知識の原則は、ソフトウェア、特にオブジェクト指向プログラムを開発するための設計ガイドラインです。一般的な形式では、LoDは疎結合の特定のケースです。このガイドラインは、1987年の終わり頃にノースイースタン大学のIan Hollandによって提案され、次の方法のそれぞれで簡潔に要約できます。[1]

  • 各ユニットは、他のユニットについて限られた知識しか持っていない必要があります。現在のユニットに「密接に」関連しているユニットのみです。
  • 各ユニットは、その友達とのみ会話する必要があります。見知らぬ人と話をしないでください。
  • 直接の友達とのみ話してください

基本的な概念は、与えられたオブジェクトが他のもの(のサブコンポーネントを含む)に従って、構造またはプロパティについて可能な限りほとんど想定してはならないということです。 「情報隠蔽」の原則。
これは、モジュールが正当な目的に必要な情報とリソースのみを所有することを指示する、最小特権の原則の当然の結果と見なされる場合があります。


building.floor.bounds.GetLocalPoint(worldPoint);

このコードはLODに違反しています。あなたの現在の消費者は何とかして知る必要があります:

  • 建物にfloorがあること
  • 床がboundsであること
  • 境界にGetLocalPointメソッドがあること

しかし実際には、コンシューマーはbuildingのみを処理し、建物の内部は処理しません(サブコンポーネントを直接処理するべきではありません)。

これらの基礎となるクラスのanyが構造的に変化する場合、彼があなたのクラスからいくつかのレベルにある場合でも、突然このコンシューマを変更する必要があります実際に変更されました。
これは、変更が複数のレイヤーに影響を与えるため(直接隣接するレイヤー以上のもの)、レイヤーの分離に違反し始めます。

public Vector3 GetLocalPoint(Vector3 worldPoint){    
    return floor.GetLocalPoint(worldPoint);
}

床のない2つ目のタイプの建物を導入するとします。私は実際の例を考えることはできませんが、一般化されたユースケースを表示しようとしているので、EtherealBuildingがそのようなケースであると仮定しましょう。

building.GetLocalPointメソッドを使用すると、建物の利用者が気づくことなく、その動作を変更できます。例:

public class EtherealBuilding : Building {
    public Vector3 GetLocalPoint(Vector3 worldPoint){    
        return universe.CenterPoint; // Just a random example
    }
}

これを理解するのを難しくしているのは、床のない建物の明確な使用例がないことです。ドメインがわからないので、発生するかどうか、どのように発生するかを判断することはできません。

しかし、開発ガイドラインは、特定のコンテキストアプリケーションを放棄する一般化されたアプローチです。コンテキストを変更すると、例がより明確になります。

// Violating LOD

bool isAlive = player.heart.IsBeating();

// But what if the player is a robot?

public class HumanPlayer : Player {
    public bool IsAlive() {
        return this.heart.IsBeating();
    }
}

public class RobotPlayer : Player {
    public bool IsAlive() {
        return this.IsSwitchedOn();
    }
}

// This code works for both human and robot players, and thus wouldn't need to be changed when new (sub)types of players are developed.

bool isAlive = player.IsAlive();

これは、現在の実装が些細なものであっても、Playerクラス(またはその派生クラス)のメソッドに目的がある理由を証明します


サイドノート
例として、継承へのアプローチ方法など、いくつかの接線的な議論を省略しました。これらは答えの焦点では​​ありません。

110
Flater

ときどきそのような方法がある場合、それは一貫した設計の副作用(または、支払う場合の代償)にすぎない可能性があります。

それらがたくさんある場合、このデザイン自体に問題があることを示しています。

あなたの例では、たぶんあるべきではない建物の外から「建物にローカルにポイントを取得する」方法であり、代わりに建物のメソッドはより高い抽象化レベルにあり、そのようなもので動作するはずです内部のみを指します。

21

有名な「デメテルの法則」は、どのようなコードを記述するかを規定する法律ですが、有用なものについては説明していません。 Flaterの答えは例を示しているので問題ありませんが、私はこれらを「デメテルの法則の違反/遵守」とは呼びません。 「デメテルの法則」が施行されている場合は、最寄りのデメテル警察署に連絡してください。問題を整理します。

あなたは常にあなたが書いたコードのマスターであることを忘れないでください。したがって、「委任関数」を作成することと作成しないことの間では、それはあなた自身の判断の問題です。くっきりとした線はないので、明確なルールを定義することはできません。それどころか、Flaterのように、そのような関数を作成してもまったく役に立たない場合や、そのような関数を作成した方が便利な場合があります。 (スポイラー:前者の場合、修正は関数をインライン化することです。後者の場合、修正は関数を作成することです)

委任関数を定義することが役に立たない例には、唯一の理由が次の場合があります。

  • メンバーがカプセル化されるべき実装の詳細でない場合に、メンバーによって返されたオブジェクトのメンバーにアクセスするため。
  • インターフェイスメンバーは.NETの quasi-implementation によって正しく実装されています
  • Demeterに準拠

委任関数を作成すると便利な例は次のとおりです。

  • 何度も繰り返される呼び出しチェーンを除外する
  • 言語があなたを強制するとき、例えば。別のメンバーに委任するか、単に別の関数を呼び出すことにより、インターフェイスメンバーを実装する
  • 呼び出す関数が同じレベルの他の呼び出しと同じ概念レベルにない場合(たとえば、プラグインのイントロスペクションと同じレベルのLoadAssembly呼び出し)
1

少しの間、Buildingの実装を知っていることを忘れてください。他の誰かがそれを書いた。コンパイルされたコードのみを提供するサプライヤーかもしれません。または、実際に来週書き始める請負業者もいます。

知っているのは、Buildingのインターフェースとそのインターフェースに対して行う呼び出しだけです。それらはすべてかなり合理的に見えるので、あなたは大丈夫です。

ここで、別のコートを着て、突然、Buildingの実装者になります。あなたはフロアの実装を知りません、あなたはインターフェースを知っています。 Floorインターフェイスを使用して、Buildingクラスを実装します。あなたは、Floorのインターフェースと、Buildingクラスを実装するためにそのインターフェースに対して行う呼び出しを知っており、それらはすべてかなり合理的に見えるので、再び元気です。

全体として、問題ありません。すべて順調。

1
gnasher729

building.floor.bounds.GetLocalPoint(worldPoint);

悪い。

それ以外の場合はシステムを変更するのが非常に難しいため、オブジェクトは隣接するオブジェクトのみを処理する必要があります。

0
kiwicomb123

関数を呼び出すだけでも問題ありません。アダプターやファサードなど、その手法を使用している多くのデザインパターンが存在しますが、デコレーター、プロキシなどの拡張パターンもあります。

それはすべて抽象レベルの問題です。異なる抽象化レベルの概念を混在させないでください。これを行うには、クライアントが自分でそれを強制されないように、内部オブジェクトを呼び出す必要がある場合があります。

例(車の例はより簡単になります):

ドライバー、車、ホイールのオブジェクトがあります。現実の世界では、車を運転するために、ドライバーは車輪で直接何かをしているのですか、それとも全体として車とのみやり取りしていますか?

何かが良くないことを知る方法:

  • カプセル化が壊れており、内部オブジェクトはパブリックAPIで使用できます。 (例えば、car.Wheel.Move()のようなコード)。
  • SRPの原則が崩れ、オブジェクトがさまざまなことを実行している(たとえば、電子メールメッセージテキストを準備して実際に同じオブジェクトで送信する)。
  • 特定のクラスを単体テストすることは困難です(たとえば、多くの依存関係があります)。
  • 同じクラスで扱うもの(販売やパッケージの配達など)を扱うさまざまなドメインエキスパート(または会社の部門)がいます。

デメテルの法則を破るときの潜在的な問題:

  • ハードユニットテスト。
  • 他のオブジェクトの内部構造への依存。
  • オブジェクト間の高い結合。
  • 内部データの公開。
0
0lukasz0