コンストラクターでの正当な「実際の作業」


23

私はデザインに取り組んでいますが、引き続き障害にぶつかっています。私は特定のクラス(ModelDef)を持っています。これは、本質的にXMLスキーマを解析することで構築された複雑なノードツリーの所有者です(DOMを考えてください)。優れた設計原則(SOLID)に従い、結果のシステムを簡単にテストできるようにします。DIを使用してModelDefのコンストラクターに依存関係を渡すつもりです(テスト中に必要に応じてこれらを簡単に交換できます)。

しかし、私が苦労しているのは、ノードツリーの作成です。このツリーは、個別にテストする必要のない単純な「値」オブジェクトで完全に構成されます。(ただし、これらのオブジェクトの作成を支援するために、Abstract FactoryをModelDefに渡すことができます。)

しかし、私は、コンストラクターが実際の作業を行うべきではないことを読み続けています(例:Flaw:Constructor does Real Work)。「実際の作業」とは、後でテストのためにスタブアウトする可能性のある重い依存オブジェクトを構築することを意味する場合、これは私にとって完全に理にかなっています。(これらはDI経由で渡される必要があります。)

しかし、このノードツリーなどの軽量の値オブジェクトはどうでしょうか。ツリーはどこかに作成する必要がありますよね?ModelDefのコンストラクター(buildNodeTree()メソッドなどを使用)を使用しないのはなぜですか?

ModelDefの外でノードツリーを作成してから(コンストラクターDIを介して)渡したいとは思いません。スキーマを解析してノードツリーを作成するには、かなりの量の複雑なコードが必要です。 。私はそれを「グルー」コードに委ねたくありません(これは比較的簡単なはずで、おそらく直接テストされません)。

ノードツリーを作成するためのコードを別の「ビルダー」オブジェクトに入れることを考えましたが、実際にはビルダーパターン(テレスコープの排除に関心があると思われる)と一致しないため、「ビルダー」と呼ぶことをためらいます。コンストラクター)。ただし、別の名前(NodeTreeConstructorなど)を呼び出したとしても、ModelDefコンストラクターがノードツリーを構築するのを避けるために、ちょっとしたハックのように感じます。どこかに構築する必要があります。なぜそれを所有しようとしているオブジェクトではないのですか?


7
ただし、そのようなブランケットステートメントには常に注意する必要があります。一般的な経験則は、明確で、機能的で、テスト、再利用、および保守が容易な方法のコードであり、どのような方法であっても、状況によって異なります。そのような「ルール」に従うことを試みるコードの複雑さと混乱以外に何も得られない場合、それはあなたの状況に適したルールではありません。これらの「パターン」と言語機能はすべてツールです。特定の仕事に最適なものを使用してください。
ジェイソンC

回答:


26

そして、ロス・パターソンが示唆したものに加えて、正反対のこの位置を考えてください:

  1. 「なたのコンストラクタで実際の仕事をしてはならない」などの格言を一粒の塩で取ります。

  2. コンストラクターは、実際には静的メソッドにすぎません。したがって、構造的には、実際にはそれほど違いはありません。

    a)単純なコンストラクターと一連の複雑な静的ファクトリーメソッド

    b)単純なコンストラクターと、より複雑なコンストラクターの束。

コンストラクターで実際の作業を行うことに対する否定的な感情のかなりの部分は、コンストラクター内で例外がスローされた場合にオブジェクトがどの状態のままになるかについての議論があったC ++の歴史の一定の期間に由来します。そのようなイベントではデストラクタを呼び出す必要があります。C ++の歴史のその部分は終わり、問題は解決されましたが、Javaのような言語では、この種の問題は最初からありませんでした。

私の意見ではnew、コンストラクターでの使用を単純に回避する場合は(依存性注入を使用する意図が示すように)、問題ないはずです。「コンストラクターの条件付きロジックまたはループロジックは欠陥の警告サインです」などのステートメントを笑います。

それに加えて、個人的には、コンストラクターに複雑なロジックがあるのは悪いことではなく、「関心の分離」の原則に従うのが良いため、XML解析ロジックをコンストラクターから取り出します。したがって、XML解析ロジックを、ModelDefクラスに属するいくつかの静的メソッドではなく、いくつかの個別のクラスに移動します。

修正

外部にXML ModelDefを作成するメソッドがある場合ModelDef、いくつかの動的な一時ツリーデータ構造をインスタンス化し、XMLを解析してデータModelDefを設定し、その構造をコンストラクタパラメーターとして新しく渡す必要があります。したがって、おそらく「ビルダー」パターンのアプリケーションと考えることができます。やりたいこととStringStringBuilderペアの間には非常に近い類似点があります。しかし、私にははっきりしない理由でStackoverflow-StringBuilderとBuilder PatternというこのQ&Aが一致していないようです。したがって、ここでStringBuilder「ビルダー」パターンを実装するかどうかについての長い議論を避けるために、どのようにインスピレーションを受けられるのかお気軽にStrungBuilder ニーズに合ったソリューションを考案し、その詳細が確定するまで「Builder」パターンのアプリケーションと呼ぶことを延期します。

この新しい質問をご覧ください。プログラマーズSE:「StringBuilder」はBuilderデザインパターンのアプリケーションですか?


3
@RichardLevasseur 90年代前半から中期にかけて、C ++プログラマーの間で懸念と議論のトピックになっていたことを覚えています。この投稿:gotw.ca/gotw/066.htmを見ると、かなり複雑であり、かなり複雑なものは物議を醸す傾向があることがわかります。確かなことはわかりませんが、90年代前半にはその部分はまだ標準化されていなかったと思います。しかし、申し訳ありませんが、良い参考資料を提供することはできません。
マイクナキス

1
@Gurtz私はそのようなクラスをアプリケーション固有の「xmlユーティリティ」と考えます。なぜなら、xmlファイルの形式(またはドキュメントの構造)は、可能性に関係なく、おそらく開発中の特定のアプリケーションに関連しているからです。 「ModelDef」を再利用します。
マイクナキス

1
@Gurtzですから、Ross Pattersonが提案した方法と非常によく似た方法で、メインの「アプリケーション」クラスのインスタンスメソッドにするか、面倒すぎる場合はヘルパークラスの静的メソッドにします。
マイクナキス

1
@Gurtzは、以前に「ビルダー」アプローチに特に取り組んでいないことをおologiesびします。答えを修正しました。
マイクナキス

3
@Gurtzそれは可能ですが、学問的な好奇心以外では問題ではありません。「パターンアンチパターン」に夢中にならないでください。パターンは実際には、一般的/有用なコーディング手法を他の人に便利に説明するための単なる名前です。必要なことを行い、後で説明する必要がある場合はラベルを平手打ちします。コードが意味をなす限り、「ある意味、ビルダーパターンのようなもの」を実装してもまったく問題ありません。新しいテクニックを学ぶときはパターンに集中するのが合理的です。あなたがすることはすべて名前付きのパターンでなければならないという考えのtrapに陥らないでください。
ジェイソンC

9

ModelDefコンストラクターでこの作業を行わない最良の理由をすでに示しています。

  1. XMLドキュメントをノードツリーに解析することに関して「軽量」というものはありません。
  2. ModelDefXMLドキュメントからしか作成できないというaについては、明らかなことは何もありません。

あなたのクラスのような静的な様々な方法を持っていなければならないようですねModelDef.FromXmlString(string xmlDocument)ModelDef.FromXmlDoc(XmlDoc parsedNodeTree)など


返信いただきありがとうございます!静的メソッドの提案について。これらは、(さまざまなxmlソースから)ModelDefのインスタンスを作成する静的ファクトリーでしょうか?または、すでに作成されたModelDefオブジェクトの読み込みを担当しますか?後者の場合、オブジェクトを部分的にのみ初期化する必要があります(ModelDefにはノードツリーを完全に初期化する必要があるため)。考え?
ガーツ

3
突入してすみませんが、はい、Rossが意味するのは、完全に構築されたインスタンスを返す静的ファクトリメソッドです。完全なプロトタイプは次のようになりpublic static ModelDef createFromXmlString( string xmlDocument )ます。これはかなり一般的な方法です。時々私もやります。あなたができることを私の提案ちょうどコンストラクタを行うには、私は別のアプローチは、正当な理由なしに「コーシャない」として却下されている疑いがある状況で、私の応答の標準タイプです。
マイクナキス

1
@ Mike-Nakis、明確化してくれてありがとう。したがって、この場合、静的ファクトリメソッドはノードツリーを構築し、それをModelDefのコンストラクタ(おそらくプライベート)に渡します。理にかなっています。ありがとう。
ガーツ

@Gurtzその通り。
ロスパターソン

5

その「ルール」を聞いたことがあります。私の経験では、それは真実と偽の両方です。

より「古典的な」オブジェクト指向では、状態と動作をカプセル化するオブジェクトについて説明します。したがって、オブジェクトコンストラクターは、オブジェクトが有効な状態に初期化されていることを確認する必要があります(また、指定された引数がオブジェクトを有効にしない場合にエラーを通知します)。オブジェクトが有効な状態に初期化されていることを確認することは、私にとって本当の仕事のように聞こえます。そして、このアイデアにはメリットがあります。コンストラクタ介して有効な状態への初期化のみを許可するオブジェクトがあり、オブジェクトがその状態を適切にカプセル化するため、状態を変更する各メソッドは、状態を悪い状態に変更しないこともチェックします...そのオブジェクトは本質的に、「常に有効」であることを保証します。それは本当に素晴らしいプロパティです!

そのため、問題をテストして模擬するためにすべてを小さな断片に分解しようとすると、一般に問題が発生します。オブジェクトが実際に適切にカプセル化されている場合、実際にそこに行き、FooBarServiceをモックされたFooBarServiceに置き換えることはできず、テストに合わせて値を自由に変更することはできません(または、単純な割り当てよりも多くのコード)。

このようにして、もう1つの「思考の学校」、つまりソリッドが得られます。そして、この一連の考え方では、コンストラクタで実際の作業を行うべきではないことは、おそらくもっと真実です。SOLIDコードは多くの場合(常にではありませんが)テストが簡単です。しかし、推論することも難しくなります。コードを単一の責任で小さなオブジェクトに分割するため、ほとんどのオブジェクトは状態をカプセル化しなくなります(そして一般に状態または動作のいずれかを含みます)。通常、検証コードは検証クラスに抽出され、状態とは別に保持されます。しかし、今では凝集力が失われているため、オブジェクトを取得したときにオブジェクトが有効であるかどうかを確認できなくなりました。オブジェクトについて何かをしようとする前に、オブジェクトについて考えている前提条件が正しいことを常に検証する必要があります。(もちろん、一般に1つのレイヤーで検証を行い、オブジェクトが下位のレイヤーで有効であると仮定します。)しかし、テストは簡単です!

だから誰が正しいの?

本当に誰も。どちらの考え方にもメリットがあります。現在、SOLIDが大流行しており、誰もがSRPとOpen / Closedとそのジャズのすべてについて話している。しかし、何かが普及しているからといって、それがすべてのアプリケーションに適切な設計選択であることを意味するわけではありません。依存します。SOLID原則に大きく準拠しているコードベースで作業している場合は、はい、コンストラクターでの実際の作業はおそらく悪い考えです。しかし、そうでなければ、状況を見て、あなたの判断を使用してみてください。プロパティは、オブジェクトの何ゲインはコンストラクタで仕事をしてから、それがどのような性質をん失いますか?アプリケーションの全体的なアーキテクチャにどの程度適合しますか?

コンストラクターでの実際の作業はアンチパターンではありません。正しい場所で使用すると、まったく逆になります。しかし、それは明確に(例外があればスローされる可能性がある)文書化され、設計上の決定と同様に-現在のコードベースで使用される一般的なスタイルに収まる必要があります。


これは素晴らしい答えです。
ジャラハリ

0

このルールには根本的な問題があり、これが「本当の仕事」を構成するものですか?

質問で投稿された元の記事から、著者が「実際の作業」とは何かを定義しようとしていることがわかりますが、それは深刻な欠陥です。実践するためには、明確に定義された原則である必要があります。つまり、ソフトウェアエンジニアリングに関しては、アイデアは移植可能(あらゆる言語にとらわれない)、テストされ、実証されている必要があります。その記事で議論されているもののほとんどは、その最初の基準に適合していません。その記事で著者が「本当の仕事」を構成するものと、それらが悪い定義ではない理由について言及しているいくつかの指標があります。

newキーワードの使用。この定義は、ドメイン固有であるため根本的に欠陥があります。一部の言語では、newキーワードを使用しません。最終的に彼が示唆しているのは、他のオブジェクトを構築するべきではないということです。ただし、多くの言語では、最も基本的な値自体がオブジェクトです。そのため、コンストラクターで割り当てられた値も新しいオブジェクトを構築しています。これにより、このアイデアは特定の言語に限定され、「実際の作業」を構成するものに関する悪い指標となります。

コンストラクターの終了後にオブジェクトが完全に初期化されていません。これは良いルールですが、その記事で言及されている他のルールのいくつかとも矛盾しています。それが他の人とどのように矛盾するかの良い例は、ここで私をもたらした質問に言及されています。その質問では、誰かがsortこの原則のためにJavaScriptのように見えるものでコンストラクタでメソッドを使用することを心配しています。この例では、人は他のオブジェクトのソートされたリストを表すオブジェクトを作成していました。議論の目的で、ソートされていないオブジェクトのリストがあり、ソートされたリストを表す新しいオブジェクトが必要だと想像してください。ソフトウェアの一部がソートされたリストを期待しているため、この新しいオブジェクトが必要です。このオブジェクトを呼び出すことができますSortedList。この新しいオブジェクトはソートされていないリストを受け入れ、結果のオブジェクトはオブジェクトのソートされたリストを表す必要があります。そのドキュメントに記載されている他のルール、つまり静的メソッド呼び出し、制御フロー構造、割り当て以外のルールに従う場合、結果のオブジェクトは有効な状態で構築されず、完全に初期化されるという他のルールに違反しますコンストラクターが終了した後。これを修正するには、コンストラクタでソートされていないリストをソートするための基本的な作業を行う必要があります。これを行うと、他の3つのルールが破られ、他のルールは無関係になります。

最終的に、コンストラクターで「実際の作業」を行わないというこのルールは、不明確であり、欠陥があります。どのような「本物の仕事」を無益な運動と定義しようとする。その記事の最良のルールは、コンストラクターが終了したら、完全に初期化する必要があるということです。コンストラクターで実行される作業の量を制限する他の多くのベストプラクティスがあります。これらのほとんどは、SOLID原則で要約することができ、それらの原則は、コンストラクターでの作業を妨げるものではありません。

PS。ここで、コンストラクターで何らかの作業を行うことに何の問題もないと断言する一方で、それは多くの作業を行う場所でもない、と言うことを義務付けられていると感じます。SRPは、コンストラクターがそれを有効にするのに十分な作業を行うことを提案します。コンストラクタのコード行が多すぎる場合(非常に主観的です)、おそらくこの原則に違反している可能性があり、より適切に定義されたより小さなメソッドとオブジェクトに分割される可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.