web-dev-qa-db-ja.com

不変クラスコンストラクターでの副作用の回避

最初に照会される特定のアカウントに関する情報に基づいて、データベースから一部のデータを照会する非常に長い方法を書き直しました。

アカウント情報を不変の内部クラスAccountInfoに分割しました。これは、コンストラクターの外観を大幅に簡略化および変更したバージョンです。

_private AccountInfo(String accountId, Optional<String> extraInfo) {
    final List<Project> projects;
    if (extraInfo.isPresent()) {
        // (I could probably improve this and remove the .get() call)
        projects = projectsDAO.fetchProjects(accountId, extraInfo.get());
        if (projects.isEmpty()) {
            this.projectIds = new ArrayList<>();
            this.projectNames = new ArrayList<>();
            this.unitIds = new ArrayList<>();
            return;
        }
    } else {
        projects = projectsDAO.fetchProjects(accountId);
    }

    projectIds = projects.stream().map(Project::getProjectId).collect(toList());
    projectNames = projects.stream().map(Project::getProjectName).collect(toList());

    final List<ProjUnit> units = unitsDAO.fetchUnitIds(projectIds);

    this.projectIds = projectIds;
    this.projectNames = projectNames;
    this.unitsIds = units.stream().map(ProjUnit::getUnitId).limit(1000).collect(toList());
}
_

ご覧のとおり、コンストラクターはDAOを介してデータベースに複数回クエリします(実際のコードでは、DAOは2つではなく4つです)。さて、私の知る限りでは、コンストラクター内で効果的な計算を行うことは悪い習慣です(それが間違っている場合は修正してください)。私の頭に浮かぶ4つの潜在的な解決策があります。

1.クラスを変更可能にする

コンストラクター内ですべてを行うことを主張しなかった場合は、最初に空のインスタンスを作成し、次にいくつかのメソッドを呼び出してそれを設定できます。

_accountInfo = new AccountInfo(accountId, extraInfo);
accountInfo.fetchProjects()
accountInfo.fetchUnits()
_

問題:

  • 不変クラスは一般に推奨されますが、これは特に関連があるように思われます。これは、一度照会すると変更してはならない情報を格納するクラスだからです。

  • 呼び出し元がコンストラクタを呼び出した後にfetchメソッドを呼び出すことは保証されません。

2.クエリされたデータをコンストラクタに提供する

コンストラクター内のデータベースを照会する代わりに、呼び出し元はパラメーターを介してコンストラクターにデータを提供できます。

問題:

  • これにより、最初にクラスを元のメソッドから分離することで得られる利点の一部が削除されます。これは、この機能を利用して、それを1つの単位として編成することがポイントであるためです。

  • 2番目のクエリは最初のクエリに依存するため、ロジックのほぼallを実行する必要があります前にコンストラクターが呼び出されるか、2つの個別の不変クラスが必要になります。1つはプロジェクトに関するクエリ情報を取得し、もう1つはそのオブジェクトとユニットに関するクエリ情報を取得します。 (おそらく実際のコードではさらに多くなります。)

3.静的なファクトリメソッドを使用する

コンストラクター内ではなく、静的ファクトリーメソッドでクエリを実行して、実際のフィールドへの割り当てのみをコンストラクターに残すことができます。

問題:

  • 静的ファクトリーメソッドはコンストラクターに対する責任が非常に似ているため、副作用がある方が良いかどうかはわかりません。

  • DAOオブジェクトは外部クラスインスタンスの一部であるため、静的メソッドはこれらを使用できるようにするためにパラメーターとしてこれらを受け取る必要がありますが、これはやや洗練されていないようです。それはそれらの使用を明示的にするので、それはおそらく利点として見ることができますが。

4.ビルダーのようなものを使用します

これは変更可能なオプションにいくぶん似ていますが、安全であるため厳密に優れているようです。それは次のようなことを可能にします

_AccountInfo accountInfo = AccountInfo.Builder(accountId, extraInfo, projectsDAO, unitsDAO).build()
_

また、Builder自体のタイプが間違っているため、呼び出し元がbuild()を呼び出さなければならず、すべての副作用を処理します。

問題:

  • 静的ファクトリメソッドオプションと同様に、build関数に副作用があることは悪い習慣のように思われます(おそらく、これはBuilderbuild同じ期待を呼び起こさないものに)

  • これには、ビルダーにDAOをパラメーターとして与えることも必要だと思います(これもできます利点として考えられます)

7
jbruenker

いくつかの考え:

さて、私の知る限り、コンストラクター内で効果的な計算を行うことは悪い習慣です

それは、あなたが「効果的」とはどういう意味かによって異なります。

コンストラクターの目的は、オブジェクトを「構築する」ことです。そのためには、多くのことが必要になる場合があります。あなたがしなければならないかもしれないことの一つは、新しいオブジェクトの内部にある状態を設定することです。それが「効果的」だと思いますか?

クラスの重要なポイントは、データ(「状態」)とそのデータを処理するコードを格納することです。これを「カプセル化」と呼びます。それがクラスの要点です。「プロパティ」と「メソッド」と呼ばれる明確に定義された境界を除いて、クラスの状態を外部の影響から保護し、状態が外部に影響を与えるのを防ぎます。このカプセル化により、システムの残りの部分を壊すことを心配する必要なく、データを自由に操作できます。

不変クラスは、可能であれば一般的に優先されます

不変クラスが提供する利点が必要な場合は、不変クラスが推奨されます

コンストラクター内のデータベースを照会する代わりに、呼び出し元はパラメーターを介してコンストラクターにデータを提供できます。

問題:

  • これは、最初にクラスを元のメソッドから分離することで得られる利点の一部を取り除きます。その目的は、この機能を利用してそれを1つの単位として編成することであったためです。

直感的には、このアプローチには意味があります。 1つのクラスはデータの収集を担当し、もう1つのクラスは計算の実行を担当します。テストが容易になり、ロジックが理解しやすくなり、SRPの精神に従います。

コンストラクター内ではなく、静的ファクトリーメソッドでクエリを実行して、実際のフィールドへの割り当てのみをコンストラクターに残すことができます。

ファクトリメソッドの目的は、より柔軟性のあるオブジェクトを構築する代替方法を提供することです。たとえば、1つの基本型から派生する型のファミリーから1つのオブジェクトを返すことができます。あなたのシステムについて、そのような柔軟性が必要かどうかを知るには十分な知識がありません。

ビルダーのようなものを使用

... Builder自体のタイプが間違っているため、呼び出し側がすべての副作用を処理するbuild()を呼び出す必要があります。

ビルダーを作ることはとても複雑な仕事です。ビルダーが提供する利点が絶対に必要な場合にのみ、それを行ってください。ビルダーを呼び出す人にインテリセンスの形でガイダンスを提供したり、呼び出し側に柔軟な構造を提供したり、ビルドステップに特定の順序を強制したりする場合にのみ、ビルダーを使用します。

結論?

セレモニーや「正解」について少し心配しすぎていると思います。 「コンストラクターで計算を実行しない」というルールを無視できるとしたら、どうでしょうか。クラスが不変である必要がない場合、どうしますか? 「ベストプラクティス」の概念に関係なく、最も気に入ったアプローチを採用できるとしたら、どのアプローチを選択しますか?

利益とコストの最適なバランスを提供する手法に焦点を当てます。

11
Robert Harvey

「クラスを変更可能にする十分な理由がない限り、クラスは不変でなければなりません。」
Joshua Bloch-Effective Java

「原則に従うべきなのはあなたが理解しているからです。有名な本から引用できるのではありません。」
インターネット上の匿名の男

不変かどうか、カプセル化されているかどうか、 あまりにも多くの作業を行うコンストラクタ かどうか、この設計の本当の制限は、コンストラクタがこのデータの取得方法について意見を持っていることです。つまり、そのデータがどこか別の場所にある場合、このクラスは役に立たなくなります。

このデータを取得するために実行する必要がある作業がある場合、コンストラクターでその機能を実行するコードをパックする必要はありません。データを受け取るコンストラクターを作成できます。その後、データを渡すだけでかまいません。依存性注入はそのための特別な用語ですが、これまでは参照渡しと呼んでいたため、その名前で問題を解決しないでください。

利点は、クラスがこのデータのコンシューマーになり、データの収集者にならないことです。したがって、そのデータが他の場所に移動して準備ができていれば、このクラスは準備ができています。欠点は、データ収集コードを配置する場所を考える必要があることです。この原則は 構造とは別に使用 と呼ばれます。

実用的に言えば、このデータ取得動作をどこに置くかわからないだけかもしれません。多くの人はあなたが怠惰であるためにこの問題を単に無視しますが、それは本当の問題です。コードベースは組織的なスタイルをあなたに課しており、後で人々が見つけられる場所に物を置く場所を見つける必要があります。したがって、おそらくファクトリクラス、ビルダー、DSL、またはその他の作成パターンは、実際的ではありません。それは問題ありませんが、それでも、このクラスが常にこの作業を行うように強制する必要があるという意味ではありません。

最も怠惰な方法(怠惰は良いことかもしれません)毎回この作業を強制的に行わないようにするには、単純に 別のコンストラクターを作成 にして、このデータ収集作業の方法を知っている方法から呼び出します。これで、データを収集する別の方法がある場合は、データ収集をバイパスできます。データ収集は、単にオーバーライド可能なデフォルトの動作になりました。

そのためのコストは、クラスが必要なものを収集する方法をまだ知っている必要があるため、そのために必要なものにハードコードされた依存関係があることです。これは分離されている可能性があるため、そのようなものがなくなってもクラスを再利用できます。 [〜#〜] ocp [〜#〜] に違反している場合にのみ気にし、必要な場合にクラスを直接書き換えることが問題になります。それで、これは広く使用されているライブラリで公開されているのですか、それともそれを直接見る唯一の開発グループですか? OCPに違反することによる本当の代償は、書き直した後、クラスをもう一度テストする必要があるということです。なぜなら、書き直したために、手つかずの、テスト済みのコードを失うからです。

ああ、それからこの方法でやると、まだ不変であることがわかります。きちんと、あなたがそれを気にするなら。

これらは、これらの原則を適用する前に考慮すべきことです。あなたが気にする理由を常に知ってください。

また、このようなコードを本当に作成したい場合は、

accountInfo = new AccountInfo(accountId, extraInfo);
accountInfo.fetchProjects()
accountInfo.fetchUnits()

注文を課し、AccountInfoを不変にすることを許可しながら、 Joshua Bloch's Builder のアイデアを活用する内部ドメイン固有言語( iDSL )を作成できます。例については、Javaの標準 StringBuilder を参照してください。 Joshのビルダーは不変の結果を提供しますが、呼び出されるメソッドに順序を課しません。 DSLは、構築に注文を課している間、不変の結果を提供します。使用コードは次のようになります。

 AccountInfo accountInfo = new AccountInfo.Builder(accountId, extraInfo)
     .fetchProjects()
     .fetchUnits()
     .build()
 ;

これらのフェッチを順不同で呼び出すことはできません。このDSLでは、さまざまなタイプでハングしていて、さまざまなタイプを返すからです。 AccountInfoが存在すると、完全に不変です。

これはもう1つの極端です。 DSLの設定は大変な作業です。別のコンストラクタに取り組むよりもはるかに。 AccountInfoがコードベースで何度も構築されているのが分からない場合を除いて、あまりお勧めしません。その場合は、この作業で多くの痛みを軽減できます。

これを見て考えているなら、「これは元のコンストラクタと同じくらいデータ収集の実装に結びついている」、まあ正しい。必要に応じて、構築順序の知識をデータ収集実装の知識から分離できます。

public AccountInfo accountInfoFactoryMethod(AccountInfoBuilder accountInfoBuilderImp) {
    accountInfoBuilderImp
        .builder(accountId, extraInfo)
        .fetchProjects()
        .fetchUnits()
        .build()
    ;
}
6
candied_orange

このコンストラクターからprojectsDAO.fetchProjectsを移動したくないと言ったので、「...コンストラクターが呼び出される前に、ほぼすべてのロジックを実行する必要があります...」このメソッドの外側のロジックは、呼び出すfetchProjectsメソッドを決定するものとまったく同じです。

つまり、extraInfoが渡されるかどうかを決定するこのクラスのロジックoutsideがあるということです。これは、呼び出すfetchProjectsメソッドを決定する唯一のロジックであるため、より直接的な責任を負うこと。

だからあなたが挙げた問題はオプションによって引き起こされた問題ではなく、むしろ選択されたオプションに関係なく存在するので、私はオプション2を選びます。

1
Daniel T.