「変更する理由が1つしかないクラス」について知っています。さて、それは正確には何ですか?クラスが単一の責任を負っていないことを示す匂い/兆候はありますか? それとも、本当の答えが YAGNI に隠され、クラスが最初に変更されたときにのみ単一の責任にリファクタリングすることができますか?
13 に答える
単一責任の原則
多くの明らかなケースがありますCoffeeAndSoupFactory
。同じ器具にコーヒーとスープを入れると、非常に不快な結果になる可能性があります。HotWaterGenerator
この例では、アプライアンスはと何らかの種類の に分割されている可能性がありStirrer
ます。次に、それらのコンポーネントから新しいCoffeeFactory
とSoupFactory
を構築することができ、偶発的な混合を避けることができます。
より微妙なケースの中でも、データ アクセス オブジェクト (DAO) とデータ転送オブジェクト (DTO) の間の緊張関係は非常に一般的です。DAO はデータベースと対話し、DTO はプロセスとマシン間の転送のためにシリアライズ可能です。通常、DAO はデータベース フレームワークへの参照を必要とするため、データベース ドライバーがインストールされておらず、DB へのアクセスに必要な権限も持たないリッチ クライアントでは使用できません。
コードの匂い
クラス内のメソッドは、機能の領域ごとにグループ化され始めます (「これらは
Coffee
メソッドであり、これらはSoup
メソッドです」)。多くのインターフェースを実装しています。
クラスが何をするかについて、簡潔かつ正確な説明を書きます。
説明に「and」という単語が含まれている場合は、分割する必要があります。
さて、この原則は、クラスの爆発を避けるために、多少の塩と一緒に使用する必要があります。
単一の責任が単一のメソッド クラスに変換されるわけではありません。それは、オブジェクトがクライアントに提供するサービスという、存在するための単一の理由を意味します。
道にとどまる良い方法... オブジェクトを人の比喩として使用する. オブジェクトが人である場合、誰にこれを依頼しますか? その責任を対応するクラスに割り当てます。ただし、ファイルの管理、給与の計算、給与の発行、財務記録の確認を同じ人に依頼することはありません...なぜこれらすべてを 1 つのオブジェクトで行う必要があるのでしょうか? (クラスが複数の役割を担っていても、それらがすべて関連していて一貫している限り問題ありません。 )
- CRC カードを使用する場合、これは微妙なガイドラインです。CRC カードでそのオブジェクトのすべての責任を取得するのに問題がある場合は、おそらくやりすぎです...最大 7 が良いマーカーとして機能します。
- リファクタリングの本からのもう 1 つのコード臭は、巨大なクラスです。散弾銃の手術は別です...クラスの1つの領域に変更を加えると、同じクラスの無関係な領域でバグが発生します...
- 無関係なバグ修正のために同じクラスに何度も変更を加えていることが判明した場合は、そのクラスがやりすぎていることを示しています。
単一の責任(クラスだけでなくクラスのメソッド)をチェックするための簡単で実用的な方法は、名前の選択です。クラスを設計するときに、クラスが定義する内容を正確に指定するクラスの名前を簡単に見つけることができれば、それは正しいことです。名前を選ぶのが難しいのは、ほとんどの場合、悪い設計の兆候です。
クラス内のメソッドはまとまりがある必要があります...それらは連携して動作し、内部で同じデータ構造を利用する必要があります。完全に関連性がないように見えるメソッドや、別のものに作用しているように見えるメソッドが多すぎることに気付いた場合は、適切な単一の責任を持っていない可能性が非常に高くなります。
多くの場合、最初に責任を見つけるのは困難です。時には、いくつかの異なるコンテキストでクラスを使用し、違いが見え始めたらクラスを 2 つのクラスにリファクタリングする必要があります。抽象的な概念と具体的な概念が混ざり合っていることが原因であることに気付くことがあります。それらは見にくい傾向があり、繰り返しになりますが、さまざまなコンテキストで使用すると明確になります。
明らかな兆候は、クラスが大きな泥の玉のように見えることです。これは、SRP (単一責任の原則) とは正反対です。
基本的に、すべてのオブジェクトのサービスは、単一の責任を実行することに集中する必要があります。つまり、クラスが変更され、それを尊重しないサービスが追加されるたびに、「正しい」パスから「逸脱」していることがわかります;)
原因は通常、いくつかの欠陥を修復するために急いでクラスに追加されたいくつかの迅速な修正によるものです。したがって、クラスを変更する理由は、通常、SRP を破ろうとしているかどうかを検出するための最良の基準です。
Martin のAgile Principles, Patterns, and Practices in C#は、SRP を理解するのに大いに役立ちました。彼は SRP を次のように定義しています。
クラスを変更する理由は 1 つだけである必要があります。
では、何が変化を促進しているのでしょうか。
マーティンの答えは次のとおりです。
[...] それぞれの責任は変化の軸です。(p. 116 )
そしてさらに:
SRP のコンテキストでは、変更の理由として責任を定義します。クラスを変更する理由が複数考えられる場合、そのクラスには複数の責任があります (p. 117 )
実際、SRP は変化をカプセル化しています。変更が発生した場合、それはローカルに影響を与えるだけです。
ヤグニはどこ?
YAGNI は SRP とうまく組み合わせることができます。YAGNI を適用すると、何らかの変化が実際に起こるまで待ちます。これが発生した場合、変更の理由から推測される責任を明確に確認できるはずです。
これは、責任が新しい要件や変更ごとに進化する可能性があることも意味します。さらに考えて SRP と YAGNI は、柔軟な設計とアーキテクチャについて考える手段を提供します。
また、OOD の SOLID 原則、特に SRP とも呼ばれる単一責任原則についても理解しようとしています(ちなみに、Jeff Atwood、Joel Spolsky、および「Uncle Bob」によるポッドキャストは一見の価値があります)。私にとって大きな疑問は、SOLID が取り組もうとしている問題は何かということです。
OOP はすべてモデリングに関するものです。モデリングの主な目的は、問題を理解して解決できるように問題を提示することです。モデリングでは、重要な詳細に集中する必要があります。同時に、カプセル化を使用して「重要でない」詳細を隠し、絶対に必要な場合にのみ処理する必要があるようにすることができます。
自問する必要があると思います: あなたのクラスが解決しようとしている問題は何ですか? この問題を解決するために必要な重要な情報が表面化していますか? 重要でない詳細は、絶対に必要な場合にのみ考えればよいように、隠していますか?
これらのことを考えると、理解しやすく、保守しやすく、拡張しやすいプログラムが得られます。これが、OOD と SRP を含む SOLID 原則の核心にあると思います。
MethodA
その使用MemberA
とMethodB
その使用になってしまい、それが同時実行またはバージョン管理スキームの一部MemberB
ではない場合、SRP に違反している可能性があります。
呼び出しを他の多くのクラスに委任するだけのクラスがあることに気付いた場合は、プロキシ クラスの地獄に陥っている可能性があります。これは、特定のクラスを直接使用できる場合に、どこでもプロキシ クラスをインスタンス化することになる場合に特に当てはまります。私はこれをたくさん見てきました。リポジトリ パターンを使用する代わりにクラスをProgramNameBL
考えてください。ProgramNameDAL
おそらく、他の匂いよりも少し専門的です。
- いくつかの「フレンド」クラスまたは関数が必要であることがわかった場合、それは通常、SRP の悪臭を放っています。必要な機能が実際にはクラスによって公開されていないためです。
- 過度に「深い」階層 (リーフ クラスに到達するまでの派生クラスの長いリスト) または「広い」階層 (単一の親クラスから浅く派生した非常に多くのクラス) になる場合。これは通常、親クラスの処理が多すぎるか少なすぎることを示しています。何もしないことがその限界です。はい、実際には、「空の」親クラス定義を使用して、関連のない一連のクラスを単一の階層にグループ化するだけであることがわかりました。
また、単一の責任にリファクタリングするのは難しいこともわかりました。最終的にそれに到達するまでに、クラスのさまざまな責任がクライアント コードに絡み合っており、他のものを破壊せずに 1 つのものを分解することが難しくなっています。私は「多すぎる」よりも「少なすぎる」側で間違いを犯したいと思っています。
私が導入したいもう 1 つの経験則は次のとおりです。
テストケースで何らかのデカルト積を書く必要があると感じた場合、またはクラスの特定のプライベートメソッドをモックしたい場合は、単一の責任に違反しています。
私は最近これを次のようにしました。後でCに生成されるコルーチンの特定の抽象構文ツリーがありました。ここでは、ノードを Sequence、Iteration、および Action と考えてください。Sequence は 2 つのコルーチンを連鎖させます。Iteration はユーザー定義の条件が true になるまでコルーチンを繰り返し、Action は特定のユーザー定義のアクションを実行します。さらに、コルーチンが先に進むときに評価するアクションと条件を定義するコードブロックでアクションと反復に注釈を付けることができます。
これらすべてのコード ブロックに特定の変換を適用する必要がありました (興味のある方へ: 変数の衝突を防ぐために、概念的なユーザー変数を実際の実装変数に置き換える必要がありました。Lisp マクロを知っている人は、gensym の動作を考えることができます。 ) ) )。したがって、動作する最も単純なことは、操作を内部的に認識し、アクセス時にアクションとイテレーションの注釈付きコード ブロックでそれらを呼び出すだけで、すべての構文ツリー ノードをトラバースするビジターでした。ただし、この場合、visitAction-Method と visitIteration-Method のテストコードで、「変換が適用されました」というアサーションを複製する必要がありました。言い換えれば、責任 Traversion (== {traverse iteration, traverse action, traverse sequence}) x Transformation (まあ、コードブロックが変換され、反復が変換され、アクションが変換されました)。したがって、powermock を使用して変換メソッドを削除し、それを「return "I was transform!";」-Stub に置き換えることにしました。
ただし、経験則に従って、このクラスを NodeModifier インスタンスを含むクラス TreeModifier に分割します。このクラスは、modifyIteration、modifySequence、modifyCodeblock などのメソッドを提供します。したがって、トラバース、NodeModifier の呼び出し、ツリーの再構築の責任を簡単にテストし、コード ブロックの実際の変更を個別にテストすることができました。これにより、責任が (トラバースと再構築に) 分離されたため、製品テストの必要がなくなりました。具体的な修正)。
後で、他のさまざまな変換で TreeModifier を再利用できることにも注目してください。:)
他の何かを壊してしまうのではないかと恐れずにクラスの機能を拡張する際に問題を見つけている場合、またはクラスの動作を変更する多数のオプションを変更せずにクラスを使用できない場合は、クラスがやりすぎているようなにおいがします。
メソッド「ZipAndClean」を持つレガシークラスで作業していたら、明らかに指定されたフォルダーを圧縮およびクリーニングしていました...