14

私は最近、興味深い質問を思いつきました。流暢なメソッドは何を返す必要がありますか? 現在のオブジェクトの状態を変更するか、新しい状態で新しいオブジェクトを作成する必要がありますか?

この短い説明があまり直感的でない場合は、(残念ながら) 長い例を次に示します。電卓です。非常に重い計算を実行するため、非同期コールバックを介して結果を返します。

public interface ICalculator {
    // because calcualations are too lengthy and run in separate thread
    // these methods do not return values directly, but do a callback
    // defined in IFluentParams
    void Add(); 
    void Mult();
    // ... and so on
}

パラメータとコールバックを設定する流暢なインターフェースは次のとおりです。

public interface IFluentParams {
    IFluentParams WithA(int a);
    IFluentParams WithB(int b);
    IFluentParams WithReturnMethod(Action<int> callback);
    ICalculator GetCalculator();
}

このインターフェースの実装には、2 つの興味深いオプションがあります。私はそれらの両方を示してから、それぞれの良い点と悪い点を書きます。

したがって、最初は通常のもので、これを返します:

public class FluentThisCalc : IFluentParams {
    private int? _a;
    private int? _b;
    private Action<int> _callback;

    public IFluentParams WithA(int a) {
        _a = a;
        return this;
    }

    public IFluentParams WithB(int b) {
        _b = b;
        return this;
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _callback = callback;
        return this;
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_a, _b);
    }

    private void Validate() {
        if (!_a.HasValue)
            throw new ArgumentException("a");
        if (!_b.HasValue)
            throw new ArgumentException("bs");
    }
}

2 番目のバージョンはより複雑で、状態が変化するたびに新しいオブジェクトを返します。

public class FluentNewCalc : IFluentParams {
    // internal structure with all data
    private struct Data {
        public int? A;
        public int? B;
        public Action<int> Callback;

        // good - data logic stays with data
        public void Validate() {
            if (!A.HasValue)
                throw new ArgumentException("a");
            if (!B.HasValue)
                throw new ArgumentException("b");
        }
    }

    private Data _data;

    public FluentNewCalc() {
    }

    // used only internally
    private FluentNewCalc(Data data) {
        _data = data;
    }

    public IFluentParams WithA(int a) {
        _data.A = a;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithB(int b) {
        _data.B = b;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _data.Callback = callback;
        return new FluentNewCalc(_data);
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_data.A, _data.B);
    }

    private void Validate() {
        _data.Validate();
    }
}

それらはどのように比較されますか:

プロの最初の (この) バージョン:

  • より簡単に、より短く

  • 一般的に使用される

  • よりメモリ効率が良いようです

  • ほかに何か?

プロセカンド () バージョン:

  • データを別のコンテナーに格納し、データ ロジックとすべての処理を分離できるようにする

  • データの一部を簡単に修正してから、他のデータを入力して個別に処理することができます。見てみましょう:

        var data = new FluentNewCalc()
            .WithA(1);
    
        Parallel.ForEach(new[] {1, 2, 3, 4, 5, 6, 7, 8}, b => {
            var dt = data
                .WithB(b)
                .WithReturnMethod(res => {/* some tricky actions */});
    
            // now, I have another data object for each value of b, 
            // and they have different callbacks.
            // if I were to do it with first version, I would have to create each 
            // and every data object from scratch
            var calc = dt.GetCalculator();
            calc.Add();
        });
    

2 番目のバージョンでさらに良くなることはありますか?

  • 次のような WithXXX メソッドを実装できます。

    public IFluentParams WithXXX(int xxx) {
        var data = _data;
        data.XXX = xxx;
        return new FluentNewCalc(data);
    }
    

    _data を読み取り専用 (つまり、不変) にします。これは、一部の賢い人が良いと言っています。

質問は、どちらの方法が優れていると思いますか? また、その理由は何ですか? PS私はc#を使用しましたが、これはJavaにも当てはまります。

4

4 に答える 4

4

流暢なメソッドはこれを返すと思いがちです。ただし、テスト中に私を捕まえた可変性に関して良い点を挙げています。あなたの例を使用して、私は次のようなことができます:

var calc = new Calculator(0);
var newCalc = calc.Add(1).Add(2).Mult(3);
var result = calc.Add(1);

コードを読むと、結果は calc + 1 のようになると考える人が多いと思い1ます。ミュータブルな流暢なシステムでは当然、 が適用されるため、答えは異なりAdd(1).Add(2).Mult(3)ます。

ただし、不変の流暢なシステムは実装が難しく、より複雑なコードが必要になります。不変性のメリットが、それらを実装するために必要な作業を上回るかどうかについては、非常に主観的なことのようです。

于 2013-11-07T17:14:36.140 に答える
3

型推論がなければFluentThing、API で定義された不変クラスだけでなく、 へのFluentThingInternalUseOnly拡大変換をサポートする別の可変クラスを実装することで、「両方の長所を活かす」ことができますFluentThing。の Fluent メンバーFluentThingは、の新しいインスタンスを構築し、FluentThingInternalUseOnly後者の型を戻り値の型として持ちます。のメンバーはFluentThingInternalUseOnlyを操作し、 を返しthisます。

これは、 と言えばFluentThing newThing = oldFluentThing.WithThis(4).WithThat(3).WithOther(57);WithThisメソッドは新しい を構築しFluentThingInternalUseOnlyます。その同じインスタンスが変更され、 and によって返されWithThatますWithOtherFluentThingそこからのデータは、参照が に格納されるnew にコピーされnewThingます。

このアプローチの主な問題は、誰かが , と言った場合dim newThing = oldFluentThing.WithThis(3);newThingimmutableFluentThingではなく mutableへの参照を保持し、FluentThingInternalUseOnlyそれへの参照が永続化されたことを知る方法がないことです。

概念的にはFluentThingInternalUseOnly、パブリック関数からの戻り値の型として使用できるように十分にパブリックにする方法が必要ですが、外部コードがその型の変数を宣言することを許可するほどパブリックではありません。残念ながら、これを行う方法はわかりませんが、おそらくObsolete()タグを含むいくつかのトリックが可能かもしれません.

それ以外の場合、操作対象のオブジェクトが複雑であるが操作が単純である場合、実行できる最善の方法は、流暢なインターフェイス メソッドに、呼び出されたオブジェクトへの参照を保持するオブジェクトと、そのオブジェクトに関する情報を返すことです。そのオブジェクトに対して実行する必要があります[流暢なメソッドを連鎖すると、リンクされたリストが効果的に構築されます]、およびすべての適切な変更が適用されたオブジェクトへの遅延評価された参照。が呼び出されnewThing = myThing.WithBar(3).WithBoz(9).WithBam(42)た場合、新しいラッパー オブジェクトが途中の各ステップで作成さnewThingれ、モノとして使用する最初の試行でThingは、3 つの変更が適用されたインスタンスを構築する必要がありますが、元のオブジェクトは変更されmyThingず、 3 つではThingなく1 つの新しいインスタンスを作成するだけで済みます。

于 2013-11-12T19:59:26.880 に答える