web-dev-qa-db-ja.com

そのフィールドを満たすために多くの計算を必要とする良い/読みやすいコンストラクタを書く

たくさんの計算で連続的にしか入力できないフィールドがいくつかあるクラスがあります。 1番目のフィールドは非常に簡単に設定できます。 2番目のフィールドを埋めるために、1番目のフィールドのコンテンツを取得し、たくさん計算します。 3番目、4番目、および5番目のフィールドは、field1とfield2から一緒に計算する必要があります(深刻なパフォーマンス損失なしで互いに分離された3、4、および5を計算することはできません)。 6番目のフィールドは、フィールド1、3、および4から計算されます。

すべてのコード例にC#構文を使用します。

class Foo
{
    private Type1 f1; // obtained directly from input parameter of constructor
    private Type2 f2; // from f1 we can calculate f2
    private Type3 f3; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type4 f4; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type5 f5; // from f1 and f2 we can calculate the triple (f3,f4,f5)
    private Type6 f6; // from f1, f3 and f4 we can calculate f6

    public Foo(Type1 input)
    {
        ... // What is a good way to write it?
    }
}

このようなコンストラクターを実装するための適切な方法に関する情報を入手したいと思います。私は実際のソフトウェアエンジニアリングに非常に慣れていないため(以前に大学でサンドボックスプロジェクトをいくつか作成したばかりです)、最も重要な側面が何であるかまだわかりません。

ここに私が考えている2つの方法があります。それらについてコメントして、私が考えたことのない長所と短所について教えてください。あるいは、そのようなコンストラクターをどのように書くかの例を挙げてください!

最初のバージョン:静的な副作用のないメソッド

public Foo(Type1 input)
{
    f1 = input;
    f2 = GetF2(f1);
    Tuple<Type3, Type4, Type5> bar = GetF3F4F5(f1, f2);
    f3 = bar.Item1;
    f4 = bar.Item2;
    f5 = bar.Item3;
    f6 = GetF6(f1, f3, f4);
}

private static Type2 GetF2(Type1 field1)
{
    Type2 field2 = new Type2();

    ... // calculating a lot, setting the correct value for field2

    return field2;
}

private static Tuple<Type3, Type4, Type5> GetF3F4F5(Type1 field1, Type2 field2)
{
    Type3 field3 = new Type3();
    Type4 field4 = new Type4();
    Type5 field5 = new Type5();

    ... // calculating a lot, setting the correct values for field3, 4 and 5

    return new Tuple<Type3, Type4, Type5>(field3, field4, field5);
}

private static Type6(Type1 field1, Type3 field3, Type4 field4)
{
    Type6 field6 = new Type6();

    ... // calculating a lot, setting the correct value for field6

    return field6;
}

プロ:

  • コンストラクターのすべての行で、どのフィールドが既に設定され、どのフィールドが設定されていないかを正確に把握し、それを明確に読み取り可能にします。
  • 計算は副作用のない方法でカプセル化され、簡単に変更できます。
  • どの方法でも、計算に必要なフィールドが明確に示されます。

con:

  • タプルの作成にはいくつかのオーバーヘッドがあります。
  • フィールドを直接設定するのではなく、フィールドをこの値に設定するためだけに変数が定義され、初期化されて返されることは、読者にとっては役に立たないように見えるかもしれません。

2番目のバージョン:フィールドを直接操作する非静的メソッド

public Foo(Type1 input)
{
    f1 = input;
    GetF2();
    GetF3F4F5();
    GetF6();
}

private void GetF2()
{
    f2 = new Type2();

    ... // calculating a lot with f1, setting the correct value for f2
}

private void GetF3F4F5()
{
    f3 = new Type3();
    f4 = new Type4();
    f5 = new Type5();

    ... // calculating a lot with f1 and f2, setting the correct values for f3, f4 and f5
}

private void Type6()
{
    f6 = new Type6();

    ... // calculating a lot with f1, f3, f4, setting the correct value for f6
}

プロ:

  • 少し短い。
  • タプルのオーバーヘッドはありません。
  • 迂回 "initialize-> return-> set"がないため、おそらく少し高速です。 (単なる推測)

con:

  • コンストラクターで何らかの行を取っている場合、どのフィールドが既に初期化されており、どのフィールドが初期化されていないかはわかりません。 (または、メソッド名のせいで可能かもしれませんが、私の目では、最初のバージョンほど明確ではありません。)
  • メソッドは、計算に使用するフィールドを知らせません。
7
Kjara

最初のアプローチは、2番目のアプローチよりもはるかに優れています。

ソフトウェアの最も重要な品質は読みやすさです。これは、コード行を書き込んだり書き換えたりする回数が、コード行を読み取る回数よりもはるかに少ないためです。 (コードを書いて、それを二度と読まないことはありますか?私は望んでいません。そして、コード行を書き直す前に、少なくとも一度は読まれるでしょうね?)

メソッド呼び出しから複数の値を返すことに関連するパフォーマンスのペナルティはまったく重要ではありません。これは、ユーザー自身の許可により、関連する計算が広範囲にわたるためです。したがって、結果を返すことは非常に安価であるだけでなく、実行する必要があることをすでに知っている他の操作よりもはるかに安価です。

また、計算方法はプライベートになるため、コンパイラーはそれらを最適化することさえできるため、パフォーマンスが低下することはまったくありません。

(いずれの場合も、タプルの作成を本当に避けたい場合、およびC#構文を使用しているので、outパラメーターを使用することもできます。)

(また、オブジェクトを不変にすることを望んでいた場合、2番目のアプローチはオプションにならないことも覚えておいてください。)

パフォーマンスの問題には、アルゴリズムの問​​題と「クロックサイクルを節約する」タイプの問題の2種類があります。

アルゴリズムのパフォーマンスの問題は、バブルソートではなくクイックソートを使用し、線形検索ではなくバイナリサーチを使用し、順次ではなく並列に実行できるように操作の同期を解除し、ラウンドトリップを必要としないように通信プロトコルを改善するなどです。 。

「クロックサイクルを保存する」タイプのパフォーマンスの懸念は、クロックサイクルをあちこちに保存するための、ハッキング、調整、ねじれに関するものです。

ソフトウェアの構成方法を決定するときは、「クロックサイクルを保存する」タイプのパフォーマンスの問題を「プロ」または「コン」と見なさないようにしてください。この種の問題は決して違いを生むことはなく、いつか違いが見つかる可能性が非常に低い場合でも、常にそれを修正する非常に簡単な方法があります。

本当に別のアプローチの提案が必要な場合は、factory methodパターンを検討してください:

  1. コンストラクターをプライベートにして、初期化する各メンバー変数に対してパラメーターを1つだけ受け入れるようにします。 (f1、f2、f3、f4、f5、およびf6。)

  2. _Type1 input_パラメーターを受け入れ、必要なすべての計算を実行して結果をローカル変数 f1、f2、f3、f4、f5、およびf6という名前で保存し、末尾がreturn new Foo( f1, f2, f3, f4, f5, f6);

8
Mike Nakis

使用と構造を分ける を検討してください。

Foo foo = new FooInjector(f1).build();

Fooを不変にするのは簡単です。これで、f#の構築に煩わされることなく、Fooの動作を確認できます。確かにFooには処理する長いコンストラクタがありますが、これはFooInjectorに隠されており、コードベース全体に広がっていません。

多くのクラス(Fooだけでなく)がf#ファミリーにアクセスするためにこれを実行する必要がある場合は、 パラメーターオブジェクトの導入 リファクタリングを調べます。

また、コンストラクターが実際の作業を行うべきではない理由についても説明しますが、これには既に回答 here があります。

2
candied_orange

それが重要だとは思わない。すべての処理はfooクラスの内部/プライベートです。いずれにしても、これは大きな副作用の1つです。コンストラクタはFooオブジェクトの状態を設定しています。 Fooが自分自身を信頼できない場合はどうすればよいですか?状態を保持せず、すべてのfxオブジェクトを毎回再計算しませんか?

それでも、副作用の管理のためではなく、明示的にパラメーターを渡すことでオブジェクトを構築するための依存関係を表すため、1番目のオプションが好きです。これにより、必要な構築順序と参加オブジェクトが明確になります。そして、何が起こっているのかを理解するために、2番目の例のようにたくさんのコードを読む必要がなくなります。

1
radarbob