私はデザインに取り組んでいますが、障害を突き続けています。私は特定のクラス(ModelDef)を持っています。このクラスは、本質的に、XMLスキーマ(DOMなど)を解析することによって構築された複雑なノードツリーの所有者です。優れた設計原則(SOLID)に従い、作成されたシステムが簡単にテスト可能であることを確認します。 DIを使用して依存関係をModelDefのコンストラクターに渡すつもりです(テスト中に必要に応じて、これらを簡単に交換できるようにするため)。
しかし、私が苦労しているのは、ノードツリーの作成です。このツリーは完全に単純な「値」オブジェクトで構成され、独立してテストする必要はありません。 (ただし、抽象ファクトリをModelDefに渡して、これらのオブジェクトの作成を支援することもできます。)
しかし、私はコンストラクターが実際の作業を行うべきではない(例 Flaw:コンストラクターが実際の作業を行う )ことを読み続けます。これは、「実際の作業」が、後でテストのためにスタブ化したい、重い依存オブジェクトを構築することを意味する場合、私には完全に理にかなっています。 (DI経由で渡される必要があります。)
しかし、このノードツリーなどの軽量の値オブジェクトはどうでしょうか。ツリーはどこかに作成する必要がありますよね? ModelDefのコンストラクター(たとえば、buildNodeTree()メソッドを使用)を使用しないのはなぜですか?
スキーマを解析してノードツリーを作成するには、かなりの量の複雑なコード(徹底的にテストする必要があるコード)が必要になるため、ModelDefの外部でノードツリーを作成してそれを(コンストラクターDIを介して)渡したくありません。 。私はそれを「接着」コードに委任したくありません(比較的簡単なはずであり、おそらく直接テストされないでしょう)。
ノードツリーを作成するコードを別の「ビルダー」オブジェクトに配置することを考えましたが、それを「ビルダー」と呼ぶのはためらいます。これは、ビルダーパターンに実際には一致しないためです(伸縮を排除することにさらに関心があるようです)コンストラクタ)。しかし、別の名前(NodeTreeConstructorなど)を呼び出したとしても、ModelDefコンストラクターがノードツリーを構築するのを避けるために、ちょっとしたハックのように感じられます。どこかに構築する必要があります。それを所有しようとしているオブジェクトにないのはなぜですか?
そして、Ross Pattersonの提案に加えて、正反対の位置を考えてみてください。
「あなたはあなたのコンストラクタで実際の仕事をしてはならない」のような格言を一粒の塩で味わってください。
コンストラクターは、実際には静的メソッドにすぎません。したがって、構造的には、次のような違いはほとんどありません。
a)単純なコンストラクターと複雑な静的ファクトリーメソッドの束、および
b)単純なコンストラクターとより複雑なコンストラクターの束。
コンストラクターで実際の作業を行うことに対する否定的な感情のかなりの部分は、例外がコンストラクター内でスローされた場合にオブジェクトがどのような状態になるか、およびこのようなイベントでは、デストラクタを呼び出す必要があります。 C++の歴史のその部分は終わり、問題は解決しましたが、Javaのような言語では、最初からこの種の問題はありませんでした。
私の意見では、コンストラクターでnew
を単に使用しない場合は(依存性注入を使用する意図が示すように)、問題ないはずです。 「コンストラクターの条件付きまたはループロジックは欠陥の警告サイン」のようなステートメントを笑います。
それ以外に、個人的には、コンストラクターからXML解析ロジックを削除します。これは、コンストラクターに複雑なロジックがあるのは悪いことではなく、「関心の分離」の原則に従うのが良いためです。したがって、XML解析ロジックを、ModelDef
クラスに属するいくつかの静的メソッドではなく、いくつかの別個のクラスに移動します。
修正
XMLからModelDef
を作成するModelDef
以外のメソッドがある場合、動的な一時ツリーデータ構造をインスタンス化し、XMLを解析してデータを入力し、次に作成する必要があると思います新しいModelDef
構造体をコンストラクタパラメータとして渡します。したがって、これは「ビルダー」パターンのアプリケーションと考えることができます。やりたいこととString
とStringBuilder
のペアの間には、非常に類似したものがあります。しかし、私には不明確な理由のために、このQ&Aに同意できないようです: Stackoverflow-StringBuilderおよびBuilderパターン 。したがって、ここでStringBuilder
が「ビルダー」パターンを実装するかどうかについての長い議論を避けるために、私はStrungBuilder
がどのように機能するかを考えて自由に発想を得られると思いますニーズに合ったソリューションを使用し、その詳細を解決するまで「ビルダー」パターンのアプリケーションの呼び出しを延期します。
この新しい質問を参照してください: プログラマーズSE:「StringBuilder」はビルダーデザインパターンのアプリケーションですか?
ModelDef
コンストラクターでこの作業を行わない最善の理由はすでに与えられています。
ModelDef
については、XMLドキュメントからのみ作成できると言うことは明らかです。クラスには、ModelDef.FromXmlString(string xmlDocument)
、ModelDef.FromXmlDoc(XmlDoc parsedNodeTree)
、etc。などのさまざまな静的メソッドが必要なようです
その「ルール」は以前聞いたことがあります。私の経験では、それは真と偽の両方です。
より「古典的な」オブジェクト指向では、状態と動作をカプセル化するオブジェクトについて説明します。したがって、オブジェクトコンストラクターは、オブジェクトが有効な状態に初期化されていることを確認する必要があります(指定された引数によってオブジェクトが有効にならない場合は、エラーを通知します)。オブジェクトが有効な状態に初期化されていることを確認することは、私にとって実際の作業のように聞こえます。そして、このアイデアにはメリットがあります。コンストラクターを介して有効な状態への初期化のみを許可するオブジェクトがある場合andオブジェクトは状態を適切にカプセル化するため、状態を変更する各メソッドもそれをチェックしますt状態を何か悪いものに変更します...そのオブジェクトは本質的に「常に有効」であることを保証します。それは本当に素晴らしいホテルです!
そのため、問題をテストしてモックを作成するためにすべてを細かく分割しようとすると、問題が発生します。オブジェクトが本当に適切にカプセル化されている場合、実際にそこに入り込んでFooBarServiceをモック化されたFooBarServiceに置き換えることができず、(おそらく)テストに合わせて値を変更することはできません(または、単純な割り当てよりもはるかに多くのコード)。
したがって、もう1つの「思考の学校」であるSOLIDが得られます。そして、この考えの集まりでは、コンストラクターで実際の作業を行うべきではないということは、おそらくもっと真実です。 SOLIDコードは、多くの場合(常にではありませんが)テストが簡単ですが、理由を説明するのが難しい場合もあります。コードを1つの責任で小さなオブジェクトに分割します。オブジェクトはその状態をカプセル化しなくなりました(通常は状態または動作のいずれかを含みます)。検証コードは通常、バリデータークラスに抽出され、状態とは別に保持されます。しかし、まとまりがなくなったため、オブジェクトがそれらを取得したときに有効完全にであることを確認するオブジェクトについて何かを試みる前に、オブジェクトについて持っていると考える前提条件が真であることを常に検証する必要があります(もちろん、一般的には、1つのレイヤーで検証を行ってから、オブジェクトが下位レイヤーで有効であると想定します。)しかし、テストは簡単です!
それで、誰が正しいのですか?
本当に誰もいない。どちらの学派にもメリットがあります。現在SOLIDはすべての大流行であり、誰もがSRPとOpen/Closedとそのすべてのジャズについて話している。しかし、何かが人気があるからといって、それがすべての人にとって正しいデザインの選択であるとは限らない単一のアプリケーションです。そのため、状況によって異なります。SOLIDの原則に厳密に従っているコードベースで作業している場合、はい、コンストラクタで実際に作業することはおそらく悪い考えです。それ以外の場合は、状況で、判断を使用してみてください。オブジェクトのプロパティgainはコンストラクタで機能することから、どのようなプロパティloseですか?アプリケーションの全体的なアーキテクチャは?
コンストラクタでの実際の作業はアンチパターンではありません。正しい場所で使用すると、まったく逆になる可能性があります。しかし、それは明確に(例外が発生する場合はスローされる可能性があるとともに)文書化され、設計上の決定と同様に、現在のコードベースで使用されている一般的なスタイルに適合する必要があります。
このルールには根本的な問題があり、これは「実際の作業」を構成するものですか?
質問に投稿された 元の記事 から、作成者が「実際の作業」とは何かを定義しようとしていることがわかりますが、これには重大な欠陥があります。実践が上手になるためには、明確に定義された原則である必要があります。つまり、ソフトウェアエンジニアリングに関しては、移植性があり(あらゆる言語にとらわれず)、テストし、実証する必要があります。その記事で議論されていることのほとんどは、その最初の基準に適合しません。以下は、「実際の仕事」を構成するものと、それらが悪い定義ではない理由について、その記事で著者が言及しているいくつかの指標です。
new
キーワードの使用。その定義はドメイン固有であるため、根本的に欠陥があります。一部の言語ではnew
キーワードを使用していません。最終的に彼が示唆しているのは、それが他のオブジェクトを構築するべきではないということです。ただし、多くの言語では、最も基本的な値でさえ、それ自体がオブジェクトです。そのため、コンストラクタで割り当てられた値も新しいオブジェクトを構築しています。そのため、このアイデアは特定の言語に限定され、「実際の仕事」を構成するものについての悪い指標となります。
コンストラクターの完了後にオブジェクトが完全に初期化されていません。これは良いルールですが、その記事で言及されている他のいくつかのルールとも矛盾しています。それが他の人とどのように矛盾する可能性があるかについての良い例は、ここに私をもたらした question に言及されています。その質問では、誰かがこの原則のためにJavaScriptのように見えるもののコンストラクターでsort
メソッドを使用することを懸念しています。この例では、人は他のオブジェクトのソートされたリストを表すオブジェクトを作成していました。議論のために、ソートされていないオブジェクトのリストがあり、ソートされたリストを表す新しいオブジェクトが必要だったと想像してください。ソフトウェアの一部がソートされたリストを想定しているため、この新しいオブジェクトが必要です。このオブジェクトをSortedList
と呼びます。この新しいオブジェクトはソートされていないリストを受け入れ、結果のオブジェクトは、現在ソートされているオブジェクトのリストを表す必要があります。そのドキュメントで言及されている他のルール、つまり静的メソッド呼び出し、制御フロー構造、割り当て以外のルールに従う場合、結果のオブジェクトは有効な状態で構築されず、完全に初期化されているという他のルールに違反しますコンストラクタが終了した後。これを修正するには、コンストラクターでソートされていないリストをソートするための基本的な作業を行う必要があります。これを行うと、他の3つのルールに違反し、他のルールは無関係になります。
結局のところ、コンストラクターで「実際の作業」を行わないというこのルールは、正しく定義されておらず、欠陥があります。どの「実際の仕事」が無益な運動であるかを定義しようとすること。その記事での最良のルールは、コンストラクターが完了したときに完全に初期化する必要があるということです。コンストラクターで実行する作業量を制限する他の多くのベストプラクティスがあります。これらのほとんどは、SOLIDの原則でまとめることができます。これらの同じ原則は、コンストラクターでの作業を妨げるものではありません。
PS。私はここで、コンストラクターでいくつかの作業を行うことには何の問題もないと断言しますが、それは一連の作業を行う場所でもないと言います。 SRPは、コンストラクターがそれを有効にするために十分な作業を行うべきであることを示唆しています。コンストラクターのコードが多すぎる場合(非常に主観的に私が知っている)は、おそらくこの原則に違反しており、おそらくより適切に定義された小さなメソッドとオブジェクトに分解される可能性があります。