単一責任の原則-コードの断片化を回避する方法


57

私は、チームリーダーがSOLID開発原則の熱心な支持者であるチームに取り組んでいます。しかし、彼は複雑なソフトウェアをドアから取り出すのに多くの経験を欠いています。

すでに非常に複雑なコードベースであったものにSRPを適用した状況があり、現在では非常に断片化されており、理解とデバッグが困難になっています。

現在、コードの断片化だけでなく、プライベートまたは保護されている可能性のあるクラス内のメソッドが「変更の理由」を表すと判断され、パブリックまたは内部クラスおよびインターフェースに抽出されているため、カプセル化にも問題がありますアプリケーションのカプセル化の目標に沿っていない。

20を超えるインターフェイスパラメーターを受け取るクラスコンストラクターがいくつかあるため、IoCの登録と解決はそれ自体が怪物になりつつあります。

これらの問題のいくつかを修正するために使用できる「SRPからのリファクタリング」アプローチがあるかどうかを知りたいです。私は、いくつかの密接に関連するクラスを「ラップ」してそれらの機能の合計への単一のアクセスポイントを提供する多数の空の粗いクラスを作成する場合、SOLIDに違反しないことを読んだ過度にSRPされたクラスの実装)。

それとは別に、誰もが幸せなまま、開発努力を実践的に続けることを可能にする解決策は考えられません。

助言がありますか ?


18
これは私の意見ですが、もう1つのルールがあると思います。これは、さまざまな頭字語の山の下で非常に簡単に忘れられてしまいます-「常識の原則」。「解決策」が実際に解決するより多くの問題を作成する場合、何かが間違っています。私の考えは、問題が複雑であるが、その複雑さを処理するクラスに囲まれていて、まだ比較的簡単にデバッグできる場合です-私はそれを放っておきます。一般的に、あなたの「ラッパー」のアイデアは私には聞こえるかもしれませんが、より知識のある誰かに答えを残します。
はPatrykĆwiek

6
「変更する理由」に関しては、早急にすべての理由を推測する必要はありません。実際にそれを変更する必要があるまで待ってから、そのような種類の変更を将来簡単にするために何ができるかを見てください。

62
20のコンストラクタパラメーターを持つクラスは、私にとってあまりSRPに聞こえません!
MattDavey

1
「... IoC登録および解決...」と記述します。これは、あなた(またはチームリーダー)が「IoC」と「依存性注入」(DI)を同じものと考えているように聞こえますが、これは真実ではありません。DIはIoCを達成するための手段ですが、それだけではありません。IoCを行う理由を慎重に分析する必要があります。単体テストを作成するためである場合は、サービスロケーターパターンまたは単にインターフェイスクラスを使用することもできます(ISomething)。私見、これらのアプローチは依存性注入よりもはるかに扱いやすく、より読みやすいコードになります。

2
ここで与えられる答えはすべて空虚でしょう。特定の応答を行うには、コードを確認する必要があります。コンストラクターの20個のパラメーター?さて、あなたはオブジェクトを失っているかもしれません...またはそれらはすべて有効かもしれません。または、構成ファイルに属しているか、DIクラスに属しているか、...症状は確かに疑わしいように聞こえますが、CSのほとんどの場合と同様に、「それは依存します」...
Steven A. Lowe

回答:


85

クラスのコンストラクタに20個のパラメーターがある場合、SRPが何であるかをチームが十分に把握しているとは思えません。1つのことだけを行うクラスがある場合、20の依存関係がどのようにありますか?それは釣り旅行に行って、釣り竿、タックルボックス、キルティング用品、ボウリングボール、ヌンチャク、火炎放射器などを持って行くようなものです。

とはいえ、SRPは他のほとんどの原則と同様に、過剰に適用される可能性があります。整数をインクリメントするための新しいクラスを作成する場合、そうです、それは単一の責任かもしれませんが、続けてください。それはばかげている。固い原則のようなものが目的のためにあることを忘れがちです。SOLIDは目的の手段であり、それ自体の目的ではありません。最後は保守性です。Single Responsibility Principleを使用して詳細を取得する場合、SOLIDに対する熱意がSOLIDの目標に対するチームの目をくらませていることを示しています。

だから、私が言っていることは... SRPはあなたの問題ではないと思います。SRPの誤解か、信じられないほどきめ細かなアプリケーションです。あなたのチームが主要なものを主要なものに保つようにしてください。そして、主なものは保守性です。

編集

ユーザーに使いやすさを促す方法でモジュールを設計してもらいます。各クラスをミニAPIと考えてください。最初に「このクラスをどのように使用したいか」を考えてから、それを実装します。「このクラスは何をする必要があるのか​​」と考えないでください。SRPは、使用することを困難なクラスを作るための素晴らしい傾向持っている場合は、あなたがユーザビリティに多くの考えを入れていないし。

編集2

リファクタリングのヒントを探しているなら、提案したことを始めることができます-より粗いクラスを作成して、他のいくつかをラップします。粗いクラスがまだSRP準拠していることを確認しますが、より高いレベルにあります。次に、2つの選択肢があります。

  1. 粒度の細かいクラスがシステム内の他の場所で使用されなくなった場合、それらの実装を粒度の粗いクラスに徐々に取り込み、削除することができます。
  2. より詳細なクラスはそのままにします。おそらくそれらはうまく設計されていて、使いやすくするためにラッパーが必要でした。私はこれがあなたのプロジェクトの多くに当てはまると思います。

リファクタリングが完了したら(ただし、リポジトリにコミットする前に)、作業内容を確認し、リファクタリングが実際に保守性と使いやすさの改善であったかどうかを自問してください。


2
人々にクラスの設計について考えさせる代替方法:CRCカード(クラス名、責任、共同作業者)を書いてもらいます。クラスに協力者や責任者が多すぎる場合、SRPに十分ではない可能性が高くなります。言い換えれば、すべてのテキストがインデックスカードに収まらないと、あまりにも多くの処理が行われます。
スポイケ

18
私は火炎放射器の目的を知っていますが、ポールでどのように釣りますか?
R.マルティーニ・フェルナンデス

13
+1 SOLIDは、それ自体が目的ではなく、目的を達成するための手段です。
B七つの

1
+1:「デメテルの法則」などの名前が間違っていると主張したことがありますが、それは「デメテルのガイドライン」であるべきです。これらのことはあなたのために働くべきであり、あなたはそれらのために働くべきではありません。
バイナリウォーリアー

2
@EmmadKareem:DAOオブジェクトにはいくつかのプロパティがあるはずです。しかし、ここでもまた、Customerクラスのような単純なものにグループ化し、より保守しやすいコードを作成できるものがいくつかあります。ここでは例を参照してください:codemonkeyism.com/...
Spoike

33

Martin FowlerのRefactoringで、SRPの反則を読んで、行き過ぎを定義していると思います。2つ目の質問があります。「すべてのクラスには、変更する理由が1つしかありませんか?」そして、それは「すべての変更は1つのクラスにのみ影響しますか?」

最初の質問に対する答えがすべての場合で「はい」であり、2番目の質問が「近くさえない」の場合、SRPの実装方法をもう一度確認する必要があります。

たとえば、1つのフィールドをテーブルに追加すると、DTO、バリデータクラス、永続性クラス、ビューモデルオブジェクトなどを変更する必要がある場合、問題が発生します。たぶん、SRPの実装方法を再考する必要があります。

おそらく、フィールドを追加することがCustomerオブジェクトを変更する理由であると言いましたが、永続化レイヤー(XMLファイルからデータベースへ)を変更することがCustomerオブジェクトを変更するもう1つの理由です。したがって、CustomerPersistenceオブジェクトも作成することにします。しかし、フィールドを追加するためにCustomerPersisitenceオブジェクトの変更が必要になるようにした場合、何がポイントでしたか?変更する2つの理由を持つオブジェクトがまだあります。それはもはや顧客ではありません。

ただし、ORMを導入すると、フィールドをDTOに追加すると、そのデータの読み取りに使用されるSQLが自動的に変更されるように、クラスを機能させることができます。次に、2つの懸念を分離する正当な理由があります。

要約すると、ここに私がする傾向があります:「いいえ、このオブジェクトを変更する理由は複数あります」と言う回数と「いいえ、この変更はする」と言う回数の間に大まかなバランスがある場合複数のオブジェクトに影響を与えます。」SRPとフラグメンテーションのバランスが取れていると思います。しかし、両方がまだ高い場合、懸念を分離できる別の方法があるかどうか疑問に思い始めます。


+1「すべての変更は1つのクラスにのみ影響しますか?」
dj18

私が見ていない関連する問題は、1つの論理エンティティに関連付けられているタスクが異なるクラス間で断片化される場合、コードがすべて同じエンティティに関連付けられている複数の異なるオブジェクトへの参照を保持する必要がある場合があることです たとえば、関数「SetHeaterOutput」と「MeasureTemperature」を備えたキルンを考えてみましょう。キルンが独立したHeaterControlおよびTemperatureSensorオブジェクトで表される場合、TemperatureFeedbackSystemオブジェクトが1つのキルンのヒーターと別のキルンの温度センサーへの参照を保持することを妨げるものは何もありません。
supercat

1
代わりに、これらの関数がKilnオブジェクトによって実装されたIKilnインターフェイスに結合された場合、TemperatureFeedbackSystemは単一のIKiln参照を保持するだけで済みます。独立したアフターマーケット温度センサーを備えたキルンを使用する必要がある場合、IHeaterControlおよびITemperatureSensorを受け入れ、IKilnの実装に使用するコンストラクターを持つCompositeKilnオブジェクトを使用できますが、そのような意図的な緩やかな構成はコードで簡単に認識できます。
supercat 14年

24

システムが複雑だからといって、それを複雑にしなければならないという意味ではありません。このような依存関係(または共同作業者)が多すぎるクラスがある場合:

public class MyAwesomeClass {
    public class MyAwesomeClass(IDependency1 _d1, IDependency2 _d2, ... , IDependency20 _d20) {
      // Assign it all
    }
}

...それでは複雑になりすぎて、あなたは本当にSRPをフォローしていないのですか?CRCカードで何MyAwesomeClassがインデックスカードに収まらないのかを書き留めるか、本当に小さく判読できない文字で書かなければならないと思います。

あなたがここで持っているのは、あなたの仲間が代わりにインターフェイス分離の原則に従っているだけであり、極端にそれを取ったかもしれないということですが、それはまったく別の話です。依存関係はドメインオブジェクトであると主張することができます(これは起こります)が、同時に20個のドメインオブジェクトを処理するクラスがあると、それが少し広がりすぎます。

TDDは、クラスがどれだけの能力があるかを示す良い指標となります。率直に言って; テストメソッドに、テストをリファクタリングしたとしても、書き込みに永遠にかかるセットアップコードがある場合、MyAwesomeClassおそらく実行することが多すぎます。

では、この難問をどのように解決しますか?責任を他のクラスに移動します。この問題が発生したクラスに対して実行できる手順がいくつかあります。

  1. クラスが依存関係で行うすべてのアクション(または責任)を特定します。
  2. 密接に関連する依存関係に従ってアクションをグループ化します。
  3. 再委任!つまり、識別された各アクションを、新しいクラスまたは(より重要なことですが)他のクラスにリファクタリングします。

リファクタリングの責任に関する抽象的な例

してみましょうCいくつかの依存関係を持っているクラスでD1D2D3D4あなたはあまり使用するリファクタリングする必要があります。C依存関係を呼び出すメソッドを特定したら、簡単なリストを作成できます。

  • D1- performA(D2)performB()
  • D2 - performD(D1)
  • D3 - performE()
  • D4 - performF(D3)

リストを見て、私たちはそれを見ることができますD1し、D2クラスは何とか一緒にそれらを必要と互いに関連しています。また、そのD4ニーズを見ることができますD3。したがって、2つのグループがあります。

  • Group 1- D1<->D2
  • Group 2-- D4>D3

グループ化は、クラスに2つの責任があることを示す指標です。

  1. Group 1-互いに必要な2つのオブジェクトの呼び出しを処理するための1つ。クラスでC両方の依存関係を処理する必要をなくし、どちらか一方を代わりにこれらの呼び出しを処理するようにすることができます。このグループ化でD1は、への参照を持つ可能性があることは明らかですD2
  2. Group 2-他の責任は、別のオブジェクトを呼び出すために1つのオブジェクトを必要とします。クラスの代わりにD4処理できませんD3か?そうすれば、代わりに呼び出しを実行させることで、おそらくD3クラスから削除できます。CD4

この例は非常に抽象的であり、多くの仮定を立てているので、石のように私の答えを受け取らないでください。これをリファクタリングする方法は他にもあると確信していますが、少なくとも手順を実行することで、クラスを分割する代わりに何らかのプロセスを実行して責任を移動させることができます。


編集:

@Emmad Karem氏のコメント:

「クラスのコンストラクタに20個のパラメーターがある場合、チームはSRPをよく知っているようには聞こえません。1つのことだけを行うクラスがある場合、20個の依存関係があるのはどうですか?」 Customerクラスがあり、コンストラクターに20個のパラメーターがあるのは奇妙なことではありません。

確かに、DAOオブジェクトには多くのパラメーターがあり、コンストラクターで設定する必要があり、パラメーターは通常、文字列などの単純なタイプです。ただし、Customerクラスの例では、他のクラス内でそのプロパティをグループ化して、物事を簡単にすることができます。Address通りのあるZipcodeクラスと、郵便番号を含むクラスを持ち、データ検証などのビジネスロジックも処理するなど:

public class Address {
    private String street1;
    //...

    private Zipcode zipcode;

    // easy to extend
    public bool isValid() {
        return zipcode.isValid();
    }
}

public class Zipcode {
    private string zipcode;
    public bool isValid() {
        // return regex match that zipcode contains numbers
    }
}

このことについては、ブログ投稿「JavaでStringを使用しない(決して使用しない)(または少なくとも頻繁に使用する)」で詳しく説明しています。コンストラクターまたは静的メソッドを使用してサブオブジェクトを作成しやすくする代わりに、流体ビルダーパターンを使用できます。


+1:すばらしい答えです!グループ化は再帰的にグループ化を適用できるため、IMOは非常に強力なメカニズムです。大まかに言えば、n個の抽象化レイヤーを使用して、2 ^ n個のアイテムを整理できます。
ジョルジオ

+1:最初の数段落で、私のチームが直面していることを正確に要約します。実際にはサービスオブジェクトである「ビジネスオブジェクト」、および書くのが面倒な単体テストセットアップコード。サービスレイヤーの呼び出しに1行のコードが含まれる場合、問題が発生することはわかっていました。ビジネスレイヤーメソッドの呼び出し。

3

私は、SRPに関するすべての回答と、それをどのように取りすぎることができるかに同意します。投稿の中で、SRPに準拠するための「過剰なリファクタリング」のために、カプセル化が壊れているか変更されていることがわかりました。私のために働いた一つのことは、常に基本に固執し、目的を満たすために必要なものを正確に実行することです。

レガシーシステムを使用する場合、チームリード、特にその役割に初めて参加するチームリードでは、すべてを改善するためにすべてを修正する「熱意」が通常非常に高くなります。ソリッド、SRPはありません-これは単なるSです。ソリッドをフォローしている場合は、OLIDも忘れないでください。

私は今、レガシーシステムに取り組んでおり、最初から同様の道を歩み始めました。私たちのために働いたのは、SOLIDとKISS(Keep It Simple Stupid)の両方の世界を最大限に活用するという集団のチームの決定でした。コード構造の主要な変更点をまとめて議論し、さまざまな開発原則を適用する際に常識を適用しました。「S / W開発の法則」ではなく、ガイドラインとして素晴らしいです。チームはチームリーダーだけでなく、チームのすべての開発者についてです。私のためにいつも働いていたのは、全員を部屋に入れて、チーム全体が従うことに同意したガイドラインの共有セットを考え出すことです。

現在の状況を修正する方法については、VCSを使用し、アプリケーションにあまり多くの新機能を追加していない場合は、チーム全体が理解可能で、読みやすく、保守可能であると考えるコードのバージョンにいつでも戻ることができます。はい!私はあなたに仕事を捨ててゼロから始めるように頼んでいます。これは、壊れたものを「修正」して、すでに存在するものに戻すことよりも優れています。


3

答えは、何よりも保守性とコードの明快さです。私にとって、それはより少ないコードを書くことを意味します。抽象化、インターフェース、オプション、パラメーターが少なくなります。

コードの再構築を評価したり、新しい機能を追加したりするたびに、実際のロジックと比較して必要なボイラープレートの量を考えます。答えが50%を超える場合、おそらく私はそれを考えすぎていることを意味します。

SRPの上に、他の多くの開発スタイルがあります。あなたの場合、YAGNIのような音は間違いなく欠けています。


3

ここでの回答の多くは本当に良いものですが、この問題の技術的な側面に焦点を当てています。開発者がSRPに実際に違反しているようにSRPの音を追いかけようとしているように聞こえるだけです。

この状況に関するボブのブログはこちらで見ることができますが、複数のクラスに責任が塗りつぶされた場合、それらのクラスが並行して変化するため責任SRPに違反すると主張します。あなたの開発者はボブのブログのトップにあるデザインを本当に気に入っており、それがばらばらになっているのを見ると少しがっかりするかもしれません。特に、「共通の閉鎖原則」に違反しているため、一緒に変化するものは一緒にとどまります。

SRPは「1つのことをする」のではなく「変更の理由」を指し、実際に変更が発生するまで、その変更の理由を気にする必要はないことに注意してください。2番目の男は抽象化の費用を支払います。

2番目の問題があります-「SOLID開発の強力な擁護者」。あなたがこの開発者と素晴らしい関係を持っているとは思えないので、コードベースの問題を彼/彼女に納得させようとする試みは困惑します。問題を実際に議論できるように、関係を修復する必要があります。私がお勧めするのはビールです。

まじめな話-コーヒーショップに頭を飲まなければ。オフィスを出て、どこかリラックスして、非公式にこのことについて話すことができます。ミーティングで議論を勝ち取ろうとするのではなく、議論をどこかで楽しみましょう。あなたを夢中にさせているこの開発者は、ソフトウェアを「ドアの外に」出そうとし、くだらないものを出荷したくない実際に機能している人間であることを認識してください。その共通点を共有している可能性が高いため、SRPに準拠しながら設計を改善する方法について話し合うことができます。

SRPが良いことであり、アスペクトを異なるように解釈するだけであると認めることができれば、おそらく生産的な会話を始めることができます。


-1

チームリードの決定[update = 2012.05.31]に同意します。SRPは一般的に良い考えです。しかし、@ Spoike -sのコメントに完全に同意します。20個のインターフェイス引数を持つコンストラクターは非常に遠いということです。[/更新]:

IoCを使用したSRPの導入により、1つの「マルチレスポンシブルクラス」から多くの srpクラスへの複雑さが移行し、以下の利点のためにはるかに複雑な初期化が行われます。

  • 簡単な単体テスト性/ tdd(一度に1つのsrpクラスを単独でテストする)
  • しかし、犠牲にして
    • はるかに困難なコードの初期化と統合
    • より困難なデバッグ
    • フラグメンテーション(=複数のファイル/ディレクトリにわたるコードの分散)

srpを犠牲にせずにcodefragmentationを減らすことはできないと思います。

しかし、コンストラクターで初期化の複雑さを隠す構文シュガークラスを実装することにより、コード初期化の「苦痛を軽減する」ことができます。

   class MySrpClass {
      MySrpClass(Interface1 parm1, Interface2 param2, .... Interface20 param2) {
      }
   } 

   class MySyntaxSugarClass : MySrpClass {
      MySyntaxSugarClass() {
         super(new MyInterface1Implementation(), new MyImpl2(), ....)
      }
   }

2
20個のインターフェースは、クラスがやることが多すぎることを示す指標だと思います。つまり、変更する理由は20ありますが、これはほとんどSRP違反です。システムが複雑だからといって、複雑にならなければならないというわけではありません。
スポイケ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.