web-dev-qa-db-ja.com

なぜセッターを連鎖させることが型破りなのですか?

Beanにチェーンを実装すると非常に便利です。コンストラクタ、メガコンストラクタ、ファクトリをオーバーロードする必要がなく、読みやすさが向上します。オブジェクトをimmutableにしたくない場合を除いて、私はどのような欠点も考えられません。これが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");
46
Ben

これが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);
}

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

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

50
VoiceOfUnreason
  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は重要なものです。上手に書かれたコードは、心地よく配置されたテキストではありません。適切に作成されたコードは、基本的な概念、関係、および構造に関するものです。テキストは、コードの真の意味を外部に反映したものにすぎません。

33
Paul Draper

これはOOPの規則ではないと思います。言語の設計とその規則にもっと関係しています。

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

もちろん、なぜ仕様のチェーニングの一部ではないのかと尋ねるかもしれません。私は答えを知りません、おそらくこのパターンは当時知られていない/人気がなかったのかもしれません。

12
qbd

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

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

ただし、流暢なインターフェイスが良い解決策となる可能性があるfewの場合があり、これらは共通しています。

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

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

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

7
Ian

あるセッターを次々にチェーンするのが慣例ではない理由の多くは、これらのケースでは、コンストラクターでオプションオブジェクトまたはパラメーターを見るのがより一般的だからです。 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");

setFoosetBarは初期化に不要になり、後で変更に使用できます。

連鎖可能性は状況によっては便利ですが、改行文字を減らすためだけに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");
5
zzzzBov

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

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

ただし、あいまいであるため、一般的には回避されます。戻り値がオブジェクトであるか(他のセッターを呼び出すことができるか)、または戻りオブジェクトが割り当てられたばかりの値であるか(一般的なパターン)かは明らかではありません。したがって、最小サプライズの原則では、オブジェクトの設計の根本的なものでない限り、ユーザーがいずれかの解決策を見たいと思ってはならないことをお勧めします。

5
Cort Ammon

これは回答というよりコメントですが、コメントはできないので...

uncommonとはまったく思えないので、この質問が私を驚かせたことに触れたかっただけです。実際、私の職場環境(Web開発者)では非常に一般的です。

たとえば、これはSymfonyのdoctrine:generate:entitiesコマンドがallsettersデフォルトではを自動生成する方法です。

jQuerykindaは、ほとんどのメソッドを非常に類似した方法でチェーンします。

2
xDaizu