メソッド連鎖とカプセル化


17

メソッドチェーンと「単一アクセスポイント」メソッドの古典的なOOP問題があります。

main.getA().getB().getC().transmogrify(x, y)

main.getA().transmogrifyMyC(x, y)

最初の方法には、各クラスがより小さな操作セットのみを担当し、すべてをよりモジュール化するという利点があるようです-Cにメソッドを追加しても、A、B、またはCでそれを公開する労力は必要ありません。

もちろん、欠点はカプセル化が弱いことです。これは2番目のコードで解決されます。これで、Aはそれを通過するすべてのメソッドを制御し、必要に応じてフィールドに委任できます。

単一の解決策はなく、もちろんコンテキストに依存することはわかっていますが、2つのスタイルのその他の重要な違いについて、またどのような状況でどちらを好むべきかについて、いくつかの意見を聞きたいと思います。いくつかのコードを設計するために、引数を使用してどちらかの方法を決定していないように感じます。

回答:


25

デメテル法則はこの点で重要なガイドラインを提供すると思います(その長所と短所は、通常どおり、ケースごとに測定する必要があります)。

デメテルの法則に従うことの利点は、結果として得られるソフトウェアの保守性と適応性が高まる傾向があることです。オブジェクトは他のオブジェクトの内部構造への依存度が低いため、オブジェクトコンテナーは呼び出し元を変更せずに変更できます。

デメテルの法則の欠点は、メソッド呼び出しをコンポーネントに伝達するために、多数の小さな「ラッパー」メソッドを記述する必要がある場合があることです。さらに、クラスのインターフェイスは、含まれるクラスのメソッドをホストするため、かさばる可能性があり、結果として、凝集したインターフェイスのないクラスになります。しかし、これはまた、OO設計が悪いことの兆候かもしれません。


私はその法律を忘れていました、私に思い出させてくれてありがとう。しかし、私がここで聞いてるのよは主にどのような長所と短所がより正確に、私は他の上で一つのスタイルを使用することを決定する方法ですか。
オーク

@Oak、長所と短所を説明する引用を追加しました。
ペテルトレック

10

私は一般に、メソッドの連鎖を可能な限り制限するようにしています(デメテル法則に基づいて)

唯一の例外は、流なインターフェイス/内部DSLスタイルのプログラミングです。

Martin FowlerはDomain-Specific-Languagesでも同じような区別をしていますが、違反の理由によりコマンドクエリの分離を示しています。

すべてのメソッドは、アクションを実行するコマンドか、呼び出し元にデータを返すクエリのいずれかである必要がありますが、両方ではありません。

70ページの彼の本のファウラーは次のように述べています。

コマンドとクエリの分離は、プログラミングにおいて非常に価値のある原則であり、チームが使用することを強くお勧めします。内部DSLでメソッドチェーンを使用する結果の1つは、通常この原則を破ることです。各メソッドは状態を変更しますが、チェーンを継続するためにオブジェクトを返します。コマンドとクエリの分離に従わない人々を非難する多くのデシベルを使用してきました。しかし、流れるようなインターフェースは異なるルールに従っているので、ここでそれを許可させていただきます。


3

問題は、適切な抽象化を使用しているかどうかだと思います。

最初のケースでは、

interface IHasGetA {
    IHasGetB getA();
}

interface IHasGetB {
    IHasGetC getB();
}

interface IHasGetC {
    ITransmogrifyable getC();
}

interface ITransmogrifyable {
    void transmogrify(x,y);
}

mainはタイプIHasGetAです。問題は、その抽象化が適切かどうかです。答えは簡単ではありません。そして、この場合、それは少し見た目が悪いですが、とにかく理論的な例です。しかし、別の例を作成するには:

main.getA(v).getB(w).getC(x).transmogrify(y, z);

しばしばより良い

main.superTransmogrify(v, w, x, y, z);

後者の例では、両方のためthismainの種類に依存するvwxyおよびz。また、すべてのメソッド宣言に6つ以上の引数がある場合、コードの外観はそれほど良くありません。

実際にサービスロケーターには最初のアプローチが必要です。サービスロケーターを介して作成されたインスタンスにアクセスする必要はありません。

そのため、オブジェクトを介して「到達」すると、実際のクラスのプロパティに基づいている場合は、依存関係が多くなります。
ただし、抽象化の作成、つまりオブジェクトを提供することは、まったく別のものです。

たとえば、次のことができます。

class Main implements IHasGetA, IHasGetA, IHasGetA, ITransmogrifyable {
    IHasGetB getA() { return this; }
    IHasGetC getB() { return this; }
    ITransmogrifyable getC() { return this; }
    void transmogrify(x,y) {
        return x + y;//yeah!
    }
}

mainインスタンスはどこですかMain。クラスmainが依存関係をのIHasGetA代わりにに減らすと、Main実際に結合が非常に低くなることがわかります。呼び出し元のコードは、元のオブジェクトの最後のメソッドを実際に呼び出していることすら知らず、実際に分離の程度を示しています。
実装の内部に深く入るのではなく、簡潔で直交する抽象化のパスに沿って到達します。


パラメータ数の大幅な増加に関する非常に興味深い点。
オーク

2

@PéterTörökが指摘しているように、Demeter法則は「コンパクトな」形式を示唆しています。

さらに、コードで明示的に言及するメソッドが増えると、クラスが依存するクラスが増え、メンテナンスの問題が増えます。この例では、コンパクトなフォームは2つのクラスに依存していますが、長いフォームは4つのクラスに依存しています。長い形式は、デメテルの法則に違反するだけではありません。また、4つの参照メソッド(コンパクト形式の2つではなく)のいずれかを変更するたびにコードを変更します。


一方、盲目的にその法律に従うことは、メソッドの数Aが爆発することを意味し、多くのメソッドAがとにかく委任したいと思うかもしれません。それでも、依存関係に同意します。これにより、クライアントコードに必要な依存関係の量が大幅に削減されます。
オーク

1
@オーク:盲目的に何かをするのは決して良くない。長所と短所を見て、証拠に基づいて決定を下す必要があります。これには、デメテルの法則も含まれます。
-CesarGon

2

私は自分でこの問題に取り組んできました。さまざまなオブジェクトに深く「到達」することのマイナス面は、リファクタリングを行うと、非常に多くの依存関係があるため、非常に多くのコードを変更しなければならなくなることです。また、コードが少し肥大化し、読みにくくなります。

一方、単純にメソッドを「渡す」クラスを持つことは、複数の場所で複数のメソッドを宣言しなければならないというオーバーヘッドも意味します。

これを軽減し、場合によっては適切なソリューションは、適切なクラスからデータ/オブジェクトをコピーすることにより、ある種のファサードオブジェクトを構築するファクトリクラスを持つことです。そうすれば、ファサードオブジェクトに対してコードを作成でき、リファクタリングするときは、ファクトリのロジックを変更するだけです。


1

私はしばしば、プログラムのロジックがチェーン化されたメソッドで簡単に理解できることを発見します。私にとっては、customer.getLastInvoice().itemCount()よりも私の脳にフィットしcustomer.countLastInvoiceItems()ます。

それが余分なカップリングを持っていることのメンテナンスの頭痛の価値があるかどうかはあなた次第です。(私は小さなクラスの小さな関数も好きなので、連鎖する傾向があります。それが正しいと言っているわけではありません。私がしていることです。)


IMO customer.NrLastInvoicesまたはcustomer.LastInvoice.NrItemsである必要があります。そのチェーンは長すぎないので、組み合わせの量がいくぶん大きい場合はおそらく平坦化する価値はありません
Homde
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.