SRPは、クラスを変更する理由が1つだけあるべきであると明確に述べていません。
問題の「レポート」クラスを分解すると、3つの方法があります。
printReport
getReportData
formatReport
Report
すべての方法で使用されている冗長性を無視すると、これがSRPに違反する理由が簡単にわかります。
「印刷」という用語は、ある種のUI、または実際のプリンターを意味します。したがって、このクラスにはある程度のUIまたはプレゼンテーションロジックが含まれています。UI要件を変更すると、Report
クラスを変更する必要があります。
「データ」という用語は、何らかのデータ構造を意味しますが、実際には何を指定するものではありません(XML?JSON?CSV?)。いずれにしても、レポートの「内容」が変わると、この方法も変わります。データベースまたはドメインのいずれかに結合されています。
formatReport
は、一般にメソッドのひどい名前ですが、これを見ると、UIと何か関係があると思いprintReport
ます。したがって、変更するもう1つの無関係な理由。
したがって、この1つのクラスは、データベース、スクリーン/プリンターデバイス、およびログやファイル出力などの内部フォーマットロジックます。3つの関数すべてを1つのクラスに含めることにより、依存関係の数を増やし、依存関係または要件の変更によってこのクラス(またはそれに依存する他の何か)が壊れる可能性を3倍にします。
ここでの問題の一部は、あなたが特に厄介な例を選んだことです。おそらくと呼ばれるクラスを持つべきではないReport
、それだけでなくても、一つのことをするので...、何の報告?すべての「レポート」は、さまざまなデータとさまざまな要件に基づいて、完全に異なる獣ではありませんか?また、レポートは、画面用または印刷用に既にフォーマットされているものではありませんか?
しかし、それを過ぎて、架空の具体的な名前を作成します-それを呼び出しましょうIncomeStatement
(1つの非常に一般的なレポート)-適切な「SRPed」アーキテクチャーには、3つのタイプがあります。
IncomeStatement
- 書式設定されたレポートに表示される情報を含むか計算するドメインおよび/またはモデルクラス。
IncomeStatementPrinter
これはおそらくのようないくつかの標準インターフェースを実装しますIPrintable<T>
。Print(IncomeStatement)
印刷固有の設定を構成するための1つの重要なメソッド、およびおそらく他のいくつかのメソッドまたはプロパティがあります。
IncomeStatementRenderer
、画面のレンダリングを処理し、printerクラスとよく似ています。
最終的にはIncomeStatementExporter
/のような機能固有のクラスを追加することもできIExportable<TReport, TFormat>
ます。
これは、ジェネリックとIoCコンテナーの導入により、現代の言語で非常に簡単になっています。ほとんどのアプリケーションコードは特定のIncomeStatementPrinter
クラスに依存する必要がなくIPrintable<T>
、あらゆる種類の印刷可能なレポートを使用および操作できます。これによりReport
、print
メソッドを持つ基本クラスのすべての認識された利点が得られ、通常のSRP違反はありません。 。実際の実装は、IoCコンテナー登録で一度だけ宣言する必要があります。
一部の人々は、上記の設計に直面すると、「しかし、これは手続き型コードのように見え、OOPの全体のポイントは、データと動作の分離から私たちを遠ざけることでした!」私が言うには:間違っています。
IncomeStatement
ではないだけで、「データ」、そして前述の間違いは、彼らがに無関係な機能のすべての種類を妨害開始し、その後、このような「透明」クラスを作成することによって、何か間違っているとしている感じにOOPの人々の多くを引き起こすものであるIncomeStatement
こと、(まあそして一般的な怠惰)。このクラスは単なるデータとして開始される場合がありますが、時間が経つと、保証されて、最終的にはモデルのようになります。
たとえば、実際の損益計算書には、総収益、総費用、純利益の行があります。これらはトランザクションデータではないため、適切に設計された金融システムはこれらを保存しない可能性が高く、実際、新しいトランザクションデータの追加に基づいて変化します。ただし、これらの線の計算は、レポートを印刷、レンダリング、エクスポートするかどうかに関係なく、常にまったく同じになります。したがって、IncomeStatement
クラスには、、、、およびメソッドの形式で、そしておそらく他のいくつかの形式でgetTotalRevenues()
、かなりの量の動作があります。これは、実際にはあまり機能していないように見えても、独自の動作を備えた本物のOOPスタイルのオブジェクトです。getTotalExpenses()
getNetIncome()
ただし、format
およびprint
メソッドは、情報自体とは関係ありません。実際、これらのメソッドのいくつかの実装が必要になる可能性はそれほど高くありません。たとえば、経営者向けの詳細なステートメントや株主向けのそれほど詳細ではないステートメントなどです。これらの独立した関数をさまざまなクラスに分離することで、すべてのprint(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
メソッドを1つのサイズに収める負担なしに、実行時にさまざまな実装を選択できます。おい!
うまくいけば、上記の大規模なパラメーター化されたメソッドがどこで失敗し、個別の実装が適切であるかを確認できます。単一オブジェクトの場合、印刷ロジックに新しいしわを追加するたびに、ドメインモデルを変更する必要があります(FinanceのTimはページ番号が必要ですが、内部レポートでのみ追加できますか?)代わりに、1つまたは2つのサテライトクラスに構成プロパティを追加するだけです。
SRPを適切に実装するには、依存関係を管理します。簡単に言えば、クラスがすでに何か有用なことをしていて、新しい依存関係を導入する別のメソッド(UI、プリンター、ネットワーク、ファイルなど)を追加することを検討している場合は、しないでください。代わりに、この機能を新しいクラスに追加する方法と、この新しいクラスをアーキテクチャ全体に適合させる方法を考えてください(依存性注入を中心に設計するのは非常に簡単です)。それが一般的な原則/プロセスです。
補足:Robertと同様に、SRP準拠のクラスには1つまたは2つの状態変数のみが含まれるべきだという考えを、私は特許で拒否しました。このような薄いラッパーが本当に役立つことはほとんどありません。だから、これを使いすぎないでください。