web-dev-qa-db-ja.com

構築と初期化の分離

Mark Seemanによる post で混乱しています。

そして、以下のIInitializableに関する彼のコメント:

Initializeメソッドの問題は、プロパティインジェクション(A.K.A.セッターインジェクション)と同じです。これは、Initializeメソッドとクラスの他のすべてのメンバーとの間に一時的な結合を作成します。最初にInitializeメソッドを呼び出さずにクラスの他のメンバーを本当に呼び出すことができない場合を除き、そのようなAPI設計は欺瞞的であり、実行時例外が発生します。また、オブジェクトが常に一貫した状態にあることを確認することははるかに困難になります。

彼が書くと同時に:

この問題は、コンストラクターから仮想メンバーを呼び出す問題に似ています。概念的には、注入された依存関係は仮想メンバーと同等です。

このステートメントは、構築された!=初期化を認めた場合にのみ当てはまると思います。

私たちが今得ているもの:
依存関係はコンストラクターに注入されますが、使用することはお勧めしません。
初期化フェーズは複雑さをもたらすため、回避する必要があります。

矛盾していませんか?

クラスが提供された依存関係を使用してその状態を設定する必要があると想像してください。たとえば、保存した設定を読み込みます。
初期化が悪く、コンストラクタが悪いので、この操作をどこで実行するのですか?

そして別のポイント:
Connection.Open()などのメソッドは、Initializeの別の名前ではありませんか?

質問:
だから、マークシーマンが提起する懸念に対処する依存性注入のコンテキストで、良い初期化パターンをだれでも説明できますか?

5
Pavel Voronin

専用のInitializeメソッドは不適切です-これを使用する場合は、オブジェクトを作成する必要があり、Initが正常に呼び出されるまで、オブジェクトをまったく使用しない、および常にInit呼び出しが失敗した場合にそれを破棄します。これは、コンストラクターでより適切に処理される初期化の混乱です。

作業を開始するために必要なすべてを含む、正常に構築されたオブジェクトのみを返す場合、そのクラスを使用するプログラマーとしての時間ははるかに楽になります。

同様に、注入された依存関係が構築中に解決されれば問題はないはずです。

しかし、それは構築中にDIオブジェクトを設定することを意味します-'コンストラクターインジェクション'、そして私は彼が 'プロパティインジェクション' について話していると仮定します。これは、Initメソッドの場合と同じ問題ですが、SetConfigXメソッドが追加されました。名前は明らかに異なりますが、原則は同じです-作成されたオブジェクトが最終的に作成され使用する前に残りの状態を入力します。

4
gbjbaanb

このステートメントは、構築された!=初期化を認めた場合にのみ当てはまると思います。

どうやらポイントが足りない。コンストラクタから仮想メソッドを暗黙的に呼び出すとは、オブジェクトがまだ完全に構築(および初期化)されていないことを意味します。概念は同じです。すべてのコンストラクタが実行される(オブジェクトが完全に初期化される)まで、仮想メソッドを呼び出すことはできません。同様に、投入される(オブジェクトが完全に初期化される)まで、注入された依存関係を使用するメソッドを呼び出すことはできません。

Dependecnieはコンストラクターに挿入されますが、使用することはお勧めしません。

それは記事が言っていることとはまったく違います。それは、コンストラクターが依存関係を受け入れることに限定されるべきであり、それらを調べたり構成したりするのではなく、実際には何か他のものに限定されるべきだということです。また、コンストラクターの注入は一部のニーズ(循環依存関係)には不十分であり、他のニーズには不便である(コンストラクターの定義が大きな継承階層を強制する)とも述べています。

記事の執筆者の口には言葉を入れたくありませんが、コンストラクターインジェクションを使用することをお勧めしますしない理由が見つかるまで。実装が最も簡単です。デバッグが最も簡単です。最も読みやすいです。

では、マークシーマンが提起する懸念に対処する依存性注入のコンテキストで、優れた初期化パターンについて誰かが説明できますか?

個人的には、依存関係がすべて入力されたオブジェクト(またはオブジェクトのセット)を起動するファクトリーが好きです。これにより、コンポーネントのコンストラクタとファクトリー自体に注意を払う必要がある場所が制限されます。依存関係が見つからない場合は、すぐにエラーが発生します。オブジェクトを取り戻すと、オブジェクトの状態は良好です。

それはすべての問題に(うまく)適用できるものではありませんが、私の経験では、ほとんどの問題にうまく適用でき、複雑さを少し切り離します。

3
Telastyn

依存関係はコンストラクターに注入されますが、使用することはお勧めしません。

著者は疎結合に焦点を当てて、その反対を言っていると思います。彼は言っていると引用されています:

「コンストラクターインジェクションのデザインパターンは、疎結合を実装するための非常に有用な方法です。」

疎結合は、優れたソフトウェア設計の中核です。クラスを再利用可能、拡張可能、テスト可能にします。

初期化フェーズは複雑さをもたらすため、避ける必要があります。

はい、ありますが、それは常に回避できるという意味ではありません。コンストラクターには戻り値がなく、すべての言語が例外のスローをサポートしているわけではありません。一部の書籍では、コンストラクタは常に成功するはずであると主張しており、私はこのルールに同意します。

次に、両方のスタイルのソースコードの例をいくつか示します。どちらのアプローチも機能しますが、どちらが優れていますか?

_try
{
    FileReader f = new FileReader("something.txt");
    String str = f.read();
}
catch(FileNotFound e) {...}
_

または

_FileReader f = new FileReader("something.txt");
if(f.exists())
{
    String str = f.read();
}
_

最初の例には、外部リソースに依存するFileReaderオブジェクトがあります。アクセスできない場合、コンストラクター中に失敗します。これにより、プログラマーのコントローラーを超えた依存関係が作成され、単体テストも行われます。

2番目の例では、依存関係はありません。リソースが存在しない場合でも、オブジェクトの作成は許可されます。依存関係は、今やプログラマーが強制することです。

これは、オブジェクトとinitializingをどのように関連付けますか。とても簡単です。誰が責任を負うべきですか?オブジェクトまたはプログラマー。それはオブジェクトの作者次第です。彼/彼女はそれを使用するプログラマーにオブジェクトを構築することの制御をあきらめる正当な理由があるかもしれません。

initializingオブジェクトが複雑なタスクである場合は、そのコードをファクトリクラスにローカライズして、変更を行うための1つの場所を用意します。

Connection.Open()のようなメソッドは単にInitializeの別の名前ではありませんか?

メソッドConnection.Open()は、Connection.Read()が呼び出されなかった場合にOpen()が失敗した場合にのみ初期化子になります。

これが作者が話している問題です。

_Connection con = new Connection();
con->Read(); // this will fail, Open() was not called
_

上記のコードを修正するには。あなたはこれを書かなければなりません、そしてこれは悪いデザインです。

_Connection con = new Connection();
con->Open("192.168.1.1");  // bug fix, forgot to call Open()
con->Read();
_

「バグ修正、X(...)の呼び出しを忘れた」と書いたプログラマによるソースコードのコメントをたくさん読んだことがあります。議論は、バグはそもそも回避可能だったということです。 Connectionクラスの作成者が初期化子を使用していませんでした。

これが問題の解決策です。

_Connection con = new Connection("192.168.1.1");
con->Read();
_

今、失敗した接続をどのように処理するかは、私の回答の上位で回答されています。コンストラクターが例外をスローするか、プログラマーがisOpen()の前にread()を呼び出す必要があります。

では、マークシーマンが提起する懸念に対処する依存性注入のコンテキストで、優れた初期化パターンについて誰かが説明できますか?

理解しにくいかもしれませんが、答えは_Single Responsibility Principle_にあります。

私の例では、Connectionオブジェクトを使用しています。 SRPルールに違反しています。 Connectionオブジェクトが開き、リソースから読み取ります。それは2つの異なる責任です。これを修正するには、オブジェクトを複数の部分に分割し、それぞれに独自の責任を持たせます。

以下に例を示します。

_try
{
    SocketAddress addr = new SocketAddress("192.168.1.1");
    try
    {
        Socket s = new Socket(addr);
        try
        {
            SocketReader r = new SocketReader(s);
            if(r != null)
            {
                String str = r->Read();
            }
        } catch(ReadFailure e) {..}
    } catch(ConnectionFailure e) {..}
} catch(BadAddress e) {..}
_

各オブジェクトが担当するのは1つだけです。

  • SocketAddressは、アドレスが有効な場合にのみ正常に構築されます。
  • Socketは、アドレスに接続できる場合にのみ構築されます。
  • SocketReaderは、読み取り可能な場合にのみ機能します。

ご覧のように。もっと多くのソースコードを書かなければならないので、依存性注入が避けられることがよくあります。それはプログラマーの側の余分な仕事です。

0
Reactgular

柔軟性のために、コンストラクターで初期化を行わないでください。

簡単にするために、つまり、余裕があり、確実であれば、後で頭痛の種になることはありません。コンストラクタ(RAII)で初期化してください。次に、(モノリシック?)割り当て解除について、通常の安全なRAIIの仮定を行うことができます。

すべてのプログラミングは、イベントの正しい順序に依存しています。プログラマーに期待することについて、「deceitful」は何もありません。他の特定の操作の前に特定の操作を実行します(この場合、構築->すべてのメンバーの初期化->オブジェクトを使用)。シーマンの「欺瞞」という考えは明らかにばかげている。

メンバーの個々のインジェクション/初期化がもたらす柔軟性を必要とすることにより、何度も焼き尽くされてきたため、私は最近、柔軟性(前者)を好む傾向があります。 (同じ理由で、私はC++よりもCを優先します:柔軟性。)コンストラクターインジェクションはRAIIです。 RAIIは、(まさにその名において)懸念の融合であると認めています。その結果、オブジェクトをプールに入れて再構成/再利用することはできません。代わりに、オブジェクトを再構築する必要があります。これは、追加のランタイム割り当てを意味します。命令の順序を実際に制御することはできません(動的DIの懸念)。そしてもちろん、長くて醜いパラメータリストがあります。

0
Engineer