セッターのチェインが型破りなのはなぜですか?


46

Beanにチェーンを実装すると非常に便利です。コンストラクター、メガコンストラクター、ファクトリーをオーバーロードする必要がなく、読みやすさが向上します。オブジェクトを不変にしたい場合を除き、欠点は考えられません。その場合、とにかくセッターはありません。これがOOPの慣例ではない理由はありますか?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
Javaはセッターが人間にとって忌み嫌われない唯一の言語であるかもしれないからです。
Telastyn

11
戻り値は意味的に意味がないため、スタイルが悪いです。誤解を招く。唯一の利点は、非常に少ないキーストロークを保存することです。
usr

58
このパターンはまったく珍しいことではありません。名前もあります。流れるようなインターフェイスと呼ばれます
フィリップ

9
ビルダーでこの作成を抽象化して、より読みやすい結果にすることもできます。myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();セッターは無効であるという一般的な考えと矛盾しないように、私はそうします。

9
@フィリップは、技術的には正しいが、それnew Foo().setBar('bar').setBaz('baz')は非常に「流fluent」だとは思わないでしょう。つまり、まったく同じ方法で実装できますが、次のようなものを読むことを非常に期待していますFoo().barsThe('bar').withThe('baz').andQuuxes('the quux')
ウェインワーナー

回答:


50

それでは、これがOOPコンベンションではない理由はありますか?

最良の推測:CQSに違反するため

コマンド(オブジェクトの状態を変更する)とクエリ(状態のコピー、この場合はオブジェクト自体を返す)が同じメソッドに混在しています。それは必ずしも問題ではありませんが、いくつかの基本的なガイドラインに違反しています。

たとえば、C ++では、std :: stack :: pop()はvoidを返すコマンドであり、std :: stack :: top()はスタックの最上位要素への参照を返すクエリです。古典的には、この2つを組み合わせたいと思いますが、それを行うことはできず例外に対して安全です。(Javaの割り当て演算子はスローしないため、Javaの問題ではありません)。

DTOが値型である場合、同様の目的を達成できます

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

また、戻り値の連鎖は、継承を処理する際に非常に面倒です。「不思議な繰り返しテンプレートパターン」を参照してください

最後に、デフォルトのコンストラクターが有効な状態にあるオブジェクトを残す必要があるという問題があります。オブジェクトを有効な状態に復元するために一連のコマンドを実行する必要がある場合、何かが非常に間違っています。


4
最後に、ここに本当のカタイン。継承が本当の問題です。チェーンは、コンポジションで使用されるデータ表現オブジェクトの従来のセッターになる可能性があると思います。
ベン

6
「オブジェクトから状態のコピーを返す」-それはしません。
user253751 16

2
これらのガイドラインに従う最大の理由は(イテレータのようないくつかの注目すべき例外はありますが)、コードの推論が非常に簡単になることです。
jpmc26 16

1
なつみ いや。私は主にJavaを話しますが、高度な「流体」APIでよく見られます-他の言語にも存在するはずです。基本的に、自分自身を拡張する型で型をジェネリックにします。次にabstract T getSelf()、ジェネリック型を返すメソッドを追加します。これで、thisセッターから戻る代わりに、ユーザーreturn getSelf()とオーバーライドするクラスは、型をそれ自体でジェネリックにし、thisから戻りますgetSelf。このようにして、セッターは宣言型ではなく実際の型を返します。
ボリス・スパイダー

1
なつみ 本当に?C ++のCRTPに似た音...-
役に立たない

33
  1. いくつかのキーストロークを保存することは魅力的ではありません。いいかもしれませんが、OOPの規則では、キーストロークではなく概念と構造を重視しています。

  2. 戻り値は無意味です。

  3. ユーザーが戻り値に意味があると期待する場合があるため、戻り値は無意味であるだけでなく、誤解を招く可能性があります。彼らはそれが「不変のセッター」であると期待するかもしれません

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }

    実際には、セッターはオブジェクトを変更します。

  4. 継承ではうまく機能しません。

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 

    私は書くことができます

    new BarHolder().setBar(2).setFoo(1)

    だがしかし

    new BarHolder().setFoo(1).setBar(2)

私にとって、#1から#3は重要なものです。適切に記述されたコードは、快適に配置されたテキストに関するものではありません。適切に記述されたコードは、基本的な概念、関係、および構造に関するものです。テキストは、コードの真の意味を外に反映したものにすぎません。


2
ただし、これらの引数のほとんどは、流れるような/チェーン可能なインターフェイスに適用されます。
ケーシー

@Casey、はい。ビルダー(つまりセッターを使用)は、私がチェーンでよく見かけるケースです。
ポールドレーパー

10
流interfaceなインターフェイスの概念に精通している場合は、戻り値型から流なインターフェイスが疑われる可能性が高いため、#3は当てはまりません。また、「よく書かれたコードは、心地よく配置されたテキストに関するものではない」ことにも同意する必要があります。それだけではありませんが、きちんと配置されたテキストは、プログラムの人間の読者が少ない労力でそれを理解できるようにするため、かなり重要です。
マイケルショー

1
セッターチェーンとは、テキストを快適に配置したり、キーストロークを保存したりすることではありません。識別子の数を減らすことです。セッターチェーンを使用すると、単一の式でオブジェクトを作成および設定できます。つまり、変数に名前を付ける必要がある変数に保存する必要がない場合があり、スコープの終わりまでそこにとどまります。
イダンアリー

3
ポイント#4については、次のようにJavaで解決できるものです。public class Foo<T extends Foo> {...}セッターが戻りFoo<T>ます。これらの「セッター」メソッドも、「with」メソッドと呼ぶことを好みます。作品の罰金に過負荷をかける場合は、単にFoo<T> with(Bar b) {...}、そうでありませんFoo<T> withBar(Bar b)
ヨーヨー

12

これはOOPの慣習ではないと思います。言語設計とその慣習により関連しています。

Javaを使用したいようです。Javaには、セッターの戻り値の型をvoidに指定するJavaBeans仕様があります。つまり、セッターのチェーンと競合しています。この仕様は広く受け入れられ、さまざまなツールに実装されています。

もちろん、なぜ仕様の一部を連鎖させないのかと尋ねるかもしれません。答えはわかりませんが、このパターンは当時知られていないか、人気がなかったかもしれません。


ええ、JavaBeansはまさに私が念頭に置いていたものです。
ベン

JavaBeansを使用するほとんどのアプリケーションは気にすることはないと思いますが、(リフレクションを使用して 'set ...'という名前のメソッドを取得し、単一のパラメーターを持ち、void returnである)かもしれません。多くの静的解析プログラムは、何かを返すメソッドからの戻り値をチェックしないことについて不満を述べており、これは非常に面倒な場合があります。

Javaでメソッドを取得する@MichaelTリフレクション呼び出しでは、戻り値の型を指定しません。そのようにメソッドを呼び出すと、メソッドが戻り型として指定されている場合、Object型のシングルトンが返される可能性があります。他のニュースでは、たとえ「コメントを残して投票しない」とは、たとえそれが良いコメントであっても、「最初の投稿」監査に失敗する理由のようです。私はこれをシャグのせいにします。Voidvoid

7

他の人々が言っ​​たように、これはしばしば流interfaceなインターフェースと呼ばれます

通常、セッターは、アプリケーションのロジックコードに応じて変数を渡す呼び出しです。DTOクラスはこの例です。セッターが何も返さない従来のコードは、これに最適です。他の答えは方法を説明しました。

ただし、流なインターフェイスが良い解決策になる場合がいくつかありますが、これらは共通しています。

  • 定数は主にセッターに渡されます
  • プログラムロジックは、セッターに渡されるものを変更しません。

fluent-nhibernateなどの構成のセットアップ

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

特別なTestDataBulderClasses(Object Mothers)を使用して、単体テストでテストデータを設定する

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

しかし、優れた流fluentなインターフェイスを作成するのは非常に難しいので、多くの「静的な」セットアップがある場合にのみ価値があります。また、流なインターフェイスを「通常の」クラスと混在させないでください。したがって、ビルダーパターンがよく使用されます。


5

セッターを次々とチェーンするのが慣例ではない理由の多くは、コンストラクターでオプションオブジェクトまたはパラメーターを表示するのがより一般的だからだと思います。C#にも初期化子構文があります。

の代わりに:

DTO dto = new DTO().setFoo("foo").setBar("bar");

書くかもしれません:

(JSで)

var dto = new DTO({foo: "foo", bar: "bar"});

(C#で)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(Javaで)

DTO dto = new DTO("foo", "bar");

setFooそしてsetBar、初期化に必要なくなり、後で突然変異に使用できます。

連鎖性は状況によっては便利ですが、改行文字を減らすためだけにすべてを1行に詰め込まないようにすることが重要です。

例えば

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

何が起こっているのかを読んで理解するのが難しくなります。再フォーマット:

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

理解しやすく、最初のバージョンの「間違い」をより明確にします。コードをその形式にリファクタリングすると、次のような利点はありません。

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1.読みやすさの問題には本当に賛成できません。各呼び出しが明確にならない前にインスタンスを追加します。2. jsとC#で示した初期化パターンはコンストラクターとは関係ありません。jsで行ったことは1つの引数に渡されます。 C#のようなゲッターセッターシュガーはありません。
ベン

1
@Benedictus、私はOOP言語がこの問題を連鎖せずに処理するさまざまな方法を示していました。ポイントは同一のコードを提供することではなく、チェーンを不要にする代替案を示すことでした。
zzzzBov 16

2
「各呼び出しの前にインスタンスを追加しても、明確になるわけではありません」各呼び出しの前にインスタンスを追加すると、明確になると主張したことはありません。
zzzzBov 16

1
VB.NETには、たとえば[ /改行を表すために使用する] With Foo(1234) / .x = 23 / .y = 47がと同等になるように、コンパイラ一時参照を作成する使用可能な[With]キーワードもありますDim temp=Foo(1234) / temp.x = 23 / temp.y = 47。このような構文はあいまいさを生じさせません。なぜなら.x、それ自体は、すぐに囲む "With"ステートメントにバインドする以外に意味を持たないからです(ない場合、またはオブジェクトにメンバーがないx場合、.x意味がありません)。OracleにはJavaにはそのようなものは含まれていませんが、そのような構造は言語にスムーズに適合します。
supercat

5

この手法は、実際にはBuilderパターンで使用されます。

x = ObjectBuilder()
        .foo(5)
        .bar(6);

ただし、一般的にはあいまいであるため回避されます。戻り値がオブジェクトであるか(他のセッターを呼び出すことができるか)、または戻りオブジェクトが割り当てられたばかりの値であるか(これも共通のパターンであるか)は明らかではありません。したがって、Least Surpriseの原則では、オブジェクトのデザインの基本でない限り、ユーザーが1つのソリューションまたは他のソリューションを見たいと思い込まないでください。


6
違いは、ビルダーパターンを使用する場合、通常は読み取り専用(不変)オブジェクト(構築するクラス)を最終的に構築する書き込み専用オブジェクト(ビルダー)を作成することです。その点で、メソッド呼び出しの長いチェーンを持つことは、単一の式として使用できるため望ましいです。
ダークホッグ

「または、返されるオブジェクトが割り当てられたばかりの値である場合」私はそれが嫌いです。渡した値を保持したい場合は、最初に変数に入れます。ただし、以前に割り当てられた値を取得すると便利な場合があります(一部のコンテナインターフェイスはこれを行うと思います)。
JAB

@JAB私は同意します、私はその表記法のそのようなファンではありませんが、その場所があります。Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); 思い浮かぶのは、それがミラーリングされているために人気を得たと思いますa = b = c = dが、人気が十分に確立されているとは確信していません。いくつかのアトミック操作ライブラリで、前の値を返す、あなたが言及したものを見てきました。この話の教訓?音を立てるよりもさらに混乱しています=)
Cort Ammon

私は学者が少しつまらないものになると信じています:イディオムに本質的に悪いことは何もありません。特定の場合に明確さを追加するのに非常に便利です。たとえば、文字列のすべての行をインデントし、その周りにascii-artボックスを描画する次のテキストフォーマットクラスを取り上げますnew BetterText(string).indent(4).box().print();。その場合、私は大量のgobbledygookをバイパスし、文字列を取得し、インデントし、ボックス化して出力しました。カスケード内の複製メソッド(例:など.copy())を使用して、後続のすべてのアクションでオリジナルを変更できないようにすることもできます。
tgm1024

2

これは答えというよりはコメントですが、コメントすることはできませんので...

ちょうど私がこのように表示されていないので、この質問は、私を驚かせたことを言及したかった珍しいすべてで。実際、私の作業環境(Web開発者)では非常に一般的です。

エンティティは自動-生成するコマンド:生成します。例えば、これはどのようにsymfonyの教義であるすべて settersデフォルトで

jQueryのは、ちょっとチェーン非常によく似た方法で、そのメソッドのほとんどを。


Jsは憎むべきです
ベン

@Benedictus PHPはより大きな憎悪だと思います。JavaScriptは完全に優れた言語であり、ES6機能を含めることで非常に良くなりました(CoffeeScriptまたはバリアントを好む人もいると思いますが、個人的には、CoffeeScriptが外側のスコープをチェックする際に変数スコープを処理する方法のファンではありませんPythonのように非ローカル/グローバルとして明示的に示されていない限り、ローカルスコープで割り当てられた変数をローカルとして扱うのではなく、最初に)。
JAB

@Benedictusに同意しJABます。私は大学時代にJSを嫌悪感のあるスクリプト言語として見下していましたが、数年間それを使って、正しい使い方を学んだ後、実際にそれを愛するようになりました... 。そして、ES6によって、本当に成熟した言語になると思います。しかし、私が頭を回すのはですか... 最初に私の答えはJSとどう関係しますか?^^ U
xDaizu

私は実際にjsで数年働いていて、その方法を学んだと言えます。それにもかかわらず、私はこの言語を間違い以外の何物でもないと考えています。しかし、私は同意します、Phpははるかに悪いです。
ベン

@Benedictus私はあなたの立場を理解し、それを尊重します。それでも私はそれが好きです。確かに、それはその癖と特典を持っています(どの言語にはないのですか?)が、それは着実に正しい方向に進んでいます。しかし... 私は実際にそれを好まないので、私はそれを守る必要があります。私は笑また、...愛するか、それを嫌う人から何かを得ることはありませんすべてで、これはそのための場所ではありませんでもJSについての私の答えの波平を。
-xDaizu
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.