SRPを実装する実際的な方法は何ですか?


11

クラスが単一責任の原則に違反しているかどうかを確認するために人々が使用する実用的なテクニックは何ですか?

クラスには変更する理由が1つだけあるべきだと知っていますが、その文は実際にそれを実装する実用的な方法に少し欠けています。

私が見つけた唯一の方法は、「それは…………それ自体……」という文を使うことです。ここで、最初のスペースはクラス名で、後はメソッド(責任)名です。

ただし、責任が本当にSRPに違反しているかどうかを判断するのが難しい場合があります。

SRPを確認する方法は他にありますか?

注意:

問題は、SRPの意味ではなく、SRPを確認して実装するための実際的な方法論または一連の手順です。

更新

レポートクラス

SRPに明らかに違反するサンプルクラスを追加しました。単一の責任の原則にどのように取り組むかを説明するための例として、人々がそれを使用できれば素晴らしいと思います。

例はここからです


これは興味深いルールですが、「Personクラスはそれ自体をレンダリングできます」と書くこともできます。ビジネスルールとデータの永続性を含む同じクラスにGUIを含めることはOKではないため、これはSRPの違反と見なされる場合があります。私はあなたが建築のドメイン(ティアと層)の概念を追加し、この文は唯一(例えばGUI、データアクセスなど)、これらのドメインの1に有効であることを確認する必要があると思うので
NoChance

@EmmadKareemこのルールは、Head First Object-Oriented Analysis and Designで言及されており、まさにこれについて考えていました。それを実装する実用的な方法がいくらか欠けています。彼らは時々、責任がデザイナーにとってそれほど明白ではないかもしれないと彼は言った、そして彼はメソッドが本当にこのクラスにあるべきかどうかを判断するためにかなりの常識を使わなければならない。
ソンゴ2012

SRPを本当に理解したい場合は、ボブマーティンおじさんの著作を読んでください。彼のコードは私が見た中で最も美しいものの1つであり、彼がSRPについて言っていることはすべて、健全なアドバイスであるだけでなく、ただ手を振る以上のものであると信じています。
ロバートハーヴェイ

そして、反対票を投じて、投稿を改善する理由を説明してください。
Songo

回答:


7

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>あらゆる種類の印刷可能なレポートを使用および操作できます。これによりReportprintメソッドを持つ基本クラスのすべての認識された利点が得られ、通常の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つの状態変数のみが含まれるべきだという考えを、私は特許で拒否しました。このような薄いラッパーが本当に役立つことはほとんどありません。だから、これを使いすぎないでください。


確かに+1の素晴らしい答え。しかし、私はクラスについて混乱していIncomeStatementます。あなたの提案された設計であること意味しているIncomeStatementのインスタンスを持つことになりますIncomeStatementPrinterIncomeStatementRendererので、私は呼び出すときことprint()IncomeStatement、それへの呼び出し委譲しますIncomeStatementPrinter代わりに?
Songo

@Songo:絶対にありません!SOLIDをフォローしている場合は、循環依存関係があってはなりません。どうやら私の答えは、それことが明らかに十分しなかったIncomeStatementクラスが持っていないprint方法、またはformat方法、または直接レポートデータ自体を検査または操作を扱っていない他の方法を。それが他のクラスの目的です。印刷したい場合IPrintable<IncomeStatement>は、コンテナに登録されているインターフェースへの依存関係を引き受けます。
アーロノート

ああ、あなたの言うとおりです。ただし、クラスにインスタンスを挿入した場合、循環依存はどこにありPrinterますIncomeStatementか?私がIncomeStatement.print()それを呼び出すとき、私が想像する方法は、それを委任しIncomeStatementPrinter.print(this, format)ます。このアプローチの何が問題になっていますか?...別の質問ですIncomeStatementが、データベースまたはXMLファイルからデータを読み込む場合、フォーマットされたレポートに表示される情報を含める必要があります。データをロードするメソッドを抽出する必要があります。別のクラスに入れて、その呼び出しをIncomeStatement
Songo 2012

@Songo:あなたがしているIncomeStatementPrinterに依存IncomeStatementしてIncomeStatementに応じてIncomeStatementPrinter。それは循環依存です。そしてそれは単に悪いデザインです。またはIncomeStatementについて何かを知る理由はまったくありません。それはドメインモデルであり、印刷には関係ありません。他のクラスはを作成または取得できるため、委譲は無意味です。ドメインモデルで印刷の概念を使用する正当な理由はありません。PrinterIncomeStatementPrinterIncomeStatementPrinter
アーロンノート

IncomeStatementデータベース(またはXMLファイル)からをロードする方法については、通常、ドメインではなく、リポジトリまたはマッパー、あるいはその両方によって処理されます。ここでも、ドメインはこれ委任しません。他のクラスがこれらのモデルのいずれかを読み取る必要がある場合、そのリポジトリを明示的に要求します。Active Recordパターンを実装していない限り、私は推測しますが、私は本当にファンではありません。
アーロンノート

2

SRPを確認する方法は、クラスのすべてのメソッド(責任)を確認し、次の質問をすることです。

「この機能の実装方法を変更する必要はありますか?」

(ある種の構成または条件に応じて)さまざまな方法で実装する必要がある関数を見つけた場合、この責任を処理するために追加のクラスが必要であることは確かです。


1

Object Calisthenicsのルール8からの引用は次のとおりです。

ほとんどのクラスは、単一の状態変数の処理を担当するだけですが、2つ必要なクラスもあります。クラスに新しいインスタンス変数を追加すると、そのクラスのまとまりがすぐに減少します。一般に、これらのルールの下でプログラミングしていると、2種類のクラスがあることがわかります。1つのインスタンス変数の状態を維持するクラスと、2つの別々の変数を調整するクラスです。一般に、2種類の責任を混同しないでください。

この(やや理想主義的な)ビューを考えると、1つまたは2つの状態変数のみを含むクラスはSRPに違反する可能性は低いと言えます。3つ以上の状態変数を含むクラス、SRPに違反する可能性があるとも言えます。


2
この見方は、どうしようもなく単純化している。アインシュタインの有名ですが、簡単な方程式は2つの変数を必要とします。
ロバートハーベイ

OPの質問は、「SRPを確認する方法は他にもありますか?」でした。-これは考えられる指標の1つです。はい、それは単純化されており、すべての場合に耐えられるわけではありませんが、SRPが違反されていることを確認する1つの可能な方法です。
MattDavey

1
可変と不変の状態も重要な考慮事項であると思い
ます

ルール8は、何千ものクラスを持つデザインを作成するための完璧なプロセスを説明し、システムを絶望的に複雑にし、理解しにくく、保守しにくくします。しかし、プラス面は、SRPをフォローできることです。
ダンク

@ダンク私はあなたに反対しませんが、その議論は質問のトピックから完全に外れています。
MattDavey 2012

1

1つの可能な実装(Java)。私は戻り値の型を自由に選択しましたが、全体として、それが質問に答えると思います。TBH私はReportクラスへのインターフェースはそれほど悪いとは思いませんが、もっと良い名前が適切かもしれません。簡潔にするため、ガードステートメントとアサーションは省略しました。

編集:クラスが不変であることにも注意してください。したがって、いったん作成すると、何も変更できません。setFormatter()とsetPrinter()を追加すれば、それほどトラブルに巻き込まれることはありません。重要なのは、インスタンス化後に生データを変更しないことです。

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());
    }
}

実装いただきありがとうございます。私は2つの事柄を持っていif (reportData == null)ますが、data代わりにあなたが意味していると思います。第二に、私はどのようにしてこの実装に到達したかを知りたいと思っていました。なぜあなたは代わりに他のオブジェクトへのすべての呼び出しを委任することにしたのですか?私がいつも疑問に思っていたもう1つのことは、レポート自体を印刷することは本当にレポートの責任なのか、ということです。コンストラクターでprinterを受け取る別のクラスを作成しなかったのはなぜreportですか?
Songo

はい、reportData = data、申し訳ありません。委任により、依存関係を細かく制御できます。実行時に、各コンポーネントの代替実装を提供できます。これで、HtmlPrinter、PdfPrinter、JsonPrinterなどを使用できるようになります。委任されたコンポーネントを個別にテストしたり、上記のオブジェクトに統合したりできるため、これもテストに便利です。プリンターとレポートの関係を逆転させることもできますが、提供されたクラスインターフェイスでソリューションを提供できることを示したかっただけです。これは、レガシーシステムでの作業の習慣です。:)
Heath Lilley

hmmmm ...では、システムを最初から構築する場合、どのオプションを選択しますか?PrinterレポートまたはかかるクラスReportのプリンタを取るクラスを?以前に同様の問題が発生し、レポートを解析する必要がありました。レポートを取得するパーサーを作成する必要があるかどうか、またはレポートにパーサーを含める必要があり、parse()呼び出しがそれに委任されているかどうかについて、TLで議論しました。
Songo

開始するには、printer.print(report)を実行し、後で必要に応じてreport.print()を実行します。printer.print(report)アプローチの優れた点は、再利用性が高いことです。それは責任を分離し、必要な場所で便利なメソッドを持つことができます。システム内の他のオブジェクトがReportPrinterについて知る必要がないようにする必要がない場合があるため、クラスにprint()メソッドを含めることにより、レポートの印刷ロジックを外部から隔離するレベルの抽象化を実現しています。これはまだ変化の範囲が狭く、使いやすいです。
ヒースリリー

0

あなたの例では、SRPが違反されていることは明らかではありません。レポートが比較的単純な場合は、レポート自体をフォーマットして印刷できるはずです。

class Report {
  void format() {
     text = text.trim();
  }

  void print() {
     new Printer().write(text);
  }
}

メソッドは非常に単純なので、ReportFormatterReportPrinterクラスを使用しても意味がありません。インターフェースでの唯一の明白な問題はgetReportData、それが値のないオブジェクトに対して教えてはいけないという要求に違反しているためです。

一方、メソッドが非常に複雑であるか、aをフォーマットまたは印刷する多くの方法があるReport場合、責任を委任することは理にかなっています(これもよりテスト可能です)。

class Report {
  void format(ReportFormatter formatter) {
     text = formatter.format(text);
  }

  void print(ReportPrinter printer) {
     printer.write(text);
  }
}

SRPは哲学的概念ではなく設計原則であるため、実際に使用しているコードに基づいています。意味的には、クラスを必要な数の責任に分割またはグループ化できます。ただし、実用的な原則として、SRPは変更が必要なコードを見つけるのに役立ちます。SRPに違反している兆候は次のとおりです。

  • クラスが大きすぎるため、スクロールや適切なメソッドの検索に時間を浪費しています。
  • クラスは非常に小さく、数が多いため、クラス間をジャンプしたり、正しいクラスを見つけたりするのに時間を浪費します。
  • 変更を加える必要がある場合、それは非常に多くのクラスに影響を与え、追跡するのが困難です。
  • 変更が必要な場合、どのクラスを変更する必要があるかは不明です。

名前を改善し、類似のコードをグループ化し、重複を排除し、階層化された設計を使用し、必要に応じてクラスを分割/結合することにより、リファクタリングを通じてこれらを修正できます。SRPを学ぶための最良の方法は、コードベースに飛び込んで、痛みをリファクタリングすることです。


投稿に添付した例を確認して、それに基づいて回答を詳しく説明できます。
ソンゴ

更新しました。SRPはコンテキストに依存します。クラス全体を(別の質問で)投稿すると、説明しやすくなります。
ギャレットホール

更新していただきありがとうございます。質問ですが、それ自体を印刷するのは本当にレポートの責任ですか?!コンストラクタでレポートを受け取る別のプリンタクラスを作成しなかったのはなぜですか。
Songo

SRPはコード自体に依存しているので、独断的に適用しないでください。
ギャレットホール

ええ、私はあなたの要点を理解します。しかし、システムを最初から構築する場合、どのオプションを選択しますか?PrinterレポートまたはかかるクラスReportのプリンタを取るクラスを?多くの場合、コードが複雑であるかどうかを判断する前に、このような設計上の問題に直面します。
Songo

0

単一責任の原則は、結束の概念と強く結びついています。非常にまとまりのあるクラスを作成するには、クラスのインスタンス変数とそのメソッドの間に相互依存関係が必要です。つまり、各メソッドは、できるだけ多くのインスタンス変数を操作する必要があります。メソッドが使用する変数が多ければ多いほど、そのクラスはよりまとまりがあります。通常、最大の凝集力は達成できません。

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