クラスが単一責任の原則に違反しているかどうかを確認するために人々が使用する実際的なテクニックは何ですか?
クラスには変更する理由が1つだけあるはずですが、その文は実際にそれを実装するための実用的な方法に少し欠けています。
私が見つけた唯一の方法は、文を使用することです。 ここで、最初のスペースはクラス名で、後半はメソッド(責任)名です。
ただし、責任が本当にSRPに違反しているかどうかを判断するのが難しい場合があります。
SRPを確認する方法は他にもありますか?
注:
問題は、SRPの意味ではなく、SRPを確認して実装するための実用的な方法論または一連の手順です。
[〜#〜]更新[〜#〜]
SRPに明らかに違反するサンプルクラスを追加しました。単一の責任の原則にどのように取り組むかを説明するための例として、人々がそれを使用できれば素晴らしいと思います。
例は here からです。
SRPは、クラスを変更する理由が1つだけあるべきであると明確に述べていません。
問題の「レポート」クラスを分解すると、3つの方法があります。
printReport
getReportData
formatReport
すべてのメソッドで使用されている冗長なReport
を無視すると、これがSRPに違反する理由が簡単にわかります。
「印刷」という用語は、ある種のUI、または実際のプリンターを意味します。したがって、このクラスにはある程度のUIまたはプレゼンテーションロジックが含まれています。 UIの要件を変更するには、Report
クラスを変更する必要があります。
「データ」という用語は、何らかのデータ構造を意味しますが、実際には何を指定するものではありません(XML?JSON?CSV?)。いずれにせよ、レポートの「内容」が変更されると、このメソッドも変更されます。データベースまたはドメインのいずれかに結合されています。
formatReport
は、一般にメソッドのひどい名前ですが、これを見て、UIと何らかの関係があると思います。おそらく、UIのprintReport
。したがって、変更する別の無関係な理由。
したがって、この1つのクラスは、データベース、スクリーン/プリンターデバイス、およびログまたはファイル出力などの内部フォーマットロジックと結合されている可能性があります。 3つの関数すべてを1つのクラスに含めることで、依存関係の数を増やし、依存関係または要件の変更によってこのクラス(またはそれに依存する他の何か)が壊れる可能性を3倍にします。
ここでの問題の一部は、あなたが特に厄介な例を選んだことです。 1つのことしか行わない場合でも、Report
というクラスはおそらくないはずです... whatレポートですか?すべての「レポート」は、さまざまなデータとさまざまな要件に基づいて、まったく異なる獣ではありませんか? reportalreadyであるものは、画面用または印刷用にフォーマットされていませんか?
しかし、それを過ぎて、架空の具体的な名前を作成します-それをIncomeStatement
と呼びましょう(1つの非常に一般的なレポート)-適切な「SRPed」アーキテクチャーには3つのタイプがあります。
IncomeStatement
-domainおよび/またはmodelフォーマットされたレポートに表示されるinformationを含むおよび/または計算するクラス。
IncomeStatementPrinter
、これはおそらく_IPrintable<T>
_のようないくつかの標準インターフェースを実装します。 1つの重要なメソッドPrint(IncomeStatement)
があり、印刷固有の設定を構成するための他のいくつかのメソッドまたはプロパティがあります。
IncomeStatementRenderer
。画面のレンダリングを処理し、プリンタークラスとよく似ています。
最終的には、IncomeStatementExporter
/_IExportable<TReport, TFormat>
_などの機能固有のクラスを追加することもできます。
ジェネリックとIoCコンテナーの導入により、これは現代の言語で非常に簡単になります。ほとんどのアプリケーションコードは特定のIncomeStatementPrinter
クラスに依存する必要がなく、_IPrintable<T>
_を使用できるため、anyの種類の印刷可能なレポートを操作できます。 Report
メソッドを使用したprint
基本クラスの認識された利点と、通常のSRP違反はありません。実際の実装は、IoCコンテナー登録で一度だけ宣言する必要があります。
一部の人々は、上記の設計に直面すると、次のような応答をします。"しかし、これは手続き型コードのように見え、OOPの目的は、データと動作! "私の言うとおり:間違った。
IncomeStatement
はnotは単に「データ」であり、前述の間違いはOOPの多くの人々がそのような「トランスペアレント」クラスを使用して、関連のないすべての機能をIncomeStatement
に詰め込み始めます(まあ、それと一般的な遅延)。このクラスはstart outとして単なるデータとして使用できますが、時間が経つと保証されますが、最終的にはmodelになります。
たとえば、実際の損益計算書には総収入、総費用、および純収入の行があります。適切に設計された金融システムは、トランザクションデータではないため、ほとんどの場合notを格納します。実際、これらは新しいトランザクションデータの追加に基づいて変化します。ただし、これらの行のcalculationは、レポートを印刷、レンダリング、またはエクスポートするかどうかに関係なく、常にまったく同じになります。したがって、IncomeStatement
クラスは、getTotalRevenues()
、getTotalExpenses()
、およびgetNetIncome()
メソッドの形式で、かなりの量の動作を実行します。そしておそらく他のいくつか。これは、実際にはあまり動作しないように見えても、独自の動作を備えた本物のOOPスタイルのオブジェクトです。
しかし、format
およびprint
メソッドは、情報自体とは何の関係もありません。実際、これらのメソッドのいくつかの実装が必要になる可能性はそれほど高くありません。経営陣のための詳細な声明と株主のためのそれほど詳細ではない声明。これらの独立した関数を異なるクラスに分離することで、すべてのprint(bool includeDetails, bool includeSubtotals, bool includeTotals, int columnWidth, CompanyLetterhead letterhead, ...)
メソッドに負担をかけずに、実行時に異なる実装を選択できます。おい!
うまくいけば、上記の大規模なパラメーター化されたメソッドがどこで失敗し、個別の実装が適切であるかを確認できます。単一オブジェクトの場合、印刷ロジックに新しいしわを追加するたびに、ドメインモデルを変更する必要があります(財務部門のTimはページ番号を必要としていますが、内部レポートでのみ追加できますか?)代わりに、1つまたは2つのサテライトクラスに構成プロパティを追加するだけではありません。
SRPを適切に実装することは、依存関係を管理することです。簡単に言えば、クラスがすでに何か有用なことをしていて、新しい依存関係を導入する別のメソッド(UI、プリンター、ネットワーク、ファイルなど)を追加することを検討している場合、。代わりに、この機能をnewクラスに追加する方法と、この新しいクラスをアーキテクチャ全体に適合させる方法を考えてください(依存関係の注入を考慮して設計するのは非常に簡単です)。それが一般的な原則/プロセスです。
補足:Robertと同様に、SRP準拠のクラスには1つまたは2つの状態変数のみを含める必要があるという考えを、私は特許で拒否しました。このような薄いラッパーが本当に役立つことはほとんどありません。だから、これを使いすぎないでください。
SRPを確認する方法は、クラスのすべてのメソッド(責任)を確認し、次の質問をすることです。
"この関数の実装方法を変更する必要がありますか?"
(何らかの構成または条件に応じて)さまざまな方法で実装する必要がある関数を見つけた場合、この責任を処理するために追加のクラスが必要であることは確かです。
1つの可能な実装(Java)。私は戻り値の型を自由に選択しましたが、何よりもそれが質問に答えると思います。 TBH Reportクラスへのインターフェースはそれほど悪いとは思いませんが、より適切な名前の方がいいかもしれません。簡潔にするために、ガードステートメントとアサーションを省略しました。
編集:クラスが不変であることにも注意してください。したがって、いったん作成すると、何も変更できません。 setFormatter()とsetPrinter()を追加すれば、それほどトラブルに巻き込まれることはありません。 IMHOの鍵は、インスタンス化後に生データを変更しないことです。
public class Report
{
private ReportData data;
private ReportDataDao dao;
private ReportFormatter formatter;
private ReportPrinter printer;
/*
* Parameterized constructor for depndency injection,
* there are better ways but this is explicit.
*/
public Report(ReportDataDao dao,
ReportFormatter formatter, ReportPrinter printer)
{
super();
this.dao = dao;
this.formatter = formatter;
this.printer = printer;
}
/*
* Delegates to the injected printer.
*/
public void printReport()
{
printer.print(formatReport());
}
/*
* Lazy loading of data, delegates to the dao
* for the meat of the call.
*/
public ReportData getReportData()
{
if (reportData == null)
{
reportData = dao.loadData();
}
return reportData;
}
/*
* Delegate to the formatter for formatting
* (notice a pattern here).
*/
public ReportData formatReport()
{
formatter.format(getReportData());
}
}
Object Calisthenics のルール8からの引用は次のとおりです。
ほとんどのクラスは、単一の状態変数の処理を担当するだけですが、2つ必要なクラスもあります。クラスに新しいインスタンス変数を追加すると、そのクラスのまとまりがすぐに減少します。一般に、これらのルールの下でプログラミングしているときに、2種類のクラスがあることがわかります。1つのインスタンス変数の状態を維持するクラスと、2つの別々の変数を調整するクラスです。一般的に、2種類の責任を混同しないでください
この(やや理想主義的な)ビューを考えると、1つまたは2つの状態変数のみを含むクラスはSRPに違反する可能性は低いと言えます。また、3つ以上の状態変数を含むクラスはSRPに違反しているとも言えます。
あなたの例では、SRPが違反されていることは明らかではありません。おそらく、レポートが比較的単純であれば、レポート自体をフォーマットして印刷できるはずです。
class Report {
void format() {
text = text.trim();
}
void print() {
new Printer().write(text);
}
}
メソッドは非常に単純なので、ReportFormatter
またはReportPrinter
クラスを使用しても意味がありません。インターフェースでの唯一の明白な問題はgetReportData
です。これは、値以外のオブジェクトについては尋ねないでください。
一方、メソッドが非常に複雑である場合、またはReport
をフォーマットまたは印刷する多くの方法がある場合は、責任を委任することも理にかなっています(よりテストしやすい)。
class Report {
void format(ReportFormatter formatter) {
text = formatter.format(text);
}
void print(ReportPrinter printer) {
printer.write(text);
}
}
SRPは哲学的概念ではなく設計原則であるため、実際に使用しているコードに基づいています。意味的には、クラスを必要な数の責任に分割またはグループ化できます。ただし、実用的な原則として、SRPは変更する必要があるコードを見つけるのに役立ちますです。 SRPに違反している兆候は次のとおりです。
これらを修正するには、名前を改善し、同様のコードをグループ化し、重複を排除し、レイヤー化された設計を使用し、必要に応じてクラスを分割/結合して、リファクタリングを行います。 SRPを学ぶ最良の方法は、コードベースに飛び込んで、痛みをリファクタリングすることです。
単一の責任の原則は凝集度の概念と強く結びついています。非常にまとまりのあるクラスを作成するには、クラスのインスタンス変数とそのメソッドの間に相互依存関係が必要です。つまり、各メソッドは、できるだけ多くのインスタンス変数を操作する必要があります。メソッドが使用する変数が多ければ多いほど、そのクラスに対するまとまりが強くなります。通常、最大の凝集力は達成できません。
また、SRPを適切に適用するには、ビジネスロジックドメインをよく理解している必要があります。それぞれの抽象化が何をすべきかを知る。レイヤードアーキテクチャは、各レイヤーに特定の処理を実行させることで、SRPにも関連しています(データソースレイヤーはデータなどを提供する必要があります)。
メソッドがすべての変数を使用していない場合でもまとまりに戻ると、それらは結合されるべきです:
public class MyClass {
private Type1 var1;
private Type2 var2;
private Type3 var3;
public Type3 method1() {
//use var1 and var3
}
public void method2() {
//use var1 and var2
}
public Type1 method3() {
//use var2 and var3
}
}
インスタンス変数の一部がメソッドの一部で使用され、変数の他の部分がメソッドの他の部分で使用される、以下のコードのようなものは必要ありません(ここでは、2つのクラスが必要です)変数の各部分)。
public class MyClass {
private Type1 var1;
private Type2 var2;
private Type3 var3;
private TypeA varA;
private TypeB varB;
public Type3 method1() {
//use var1 and var3
}
public void method2() {
//use var1 and var2
}
public TypeA methodA() {
//use varA and varB
}
public TypeA methodB() {
//use varA
}
}