単一責任の原則を明確にする


64

単一責任の原則では、クラスはただ1つのことを行うべきであると述べています。かなり明確な場合もあります。ただし、特定の抽象化レベルで表示したときに「1つのもの」のように見えるものは、下位レベルで表示したときに複数のものになる可能性があるため、他のものは困難です。また、単一の責任原則が低レベルで尊重されている場合、過度に分離された冗長なラビオリコードが発生し、すべての小さなクラスを作成し、実際の問題を解決するよりも情報を配管することに多くの行が費やされる可能性があります。

「一つのこと」の意味をどのように説明しますか?クラスが実際に「1つのこと」以上のことを行う具体的な兆候は何ですか?


6
最上位の「ラビオリコード」に対して+1。私のキャリアの早い段階で、私はそれをやりすぎた人々の一人でした。クラスだけでなく、メソッドのモジュール化も使用します。私のコードには、スクロールせずに画面に収まるように問題を小さなチャンクに分割するために、簡単なことをする小さなメソッドがたくさんありました。明らかに、これはしばしば行き過ぎでした。
ボビーテーブル

回答:


50

本当に道ロバートC.マーティン(おじさんボブ)のような私はシングル責任原則を(PDFにリンク)修正再表示します

クラスが変更される理由は複数あるべきではありません

これは、従来の「たった1つのことをすべき」という定義とは微妙に異なります。クラスの考え方を変えることを余儀なくされるので、これが気に入っています。「これは一つのことをしているのでしょうか?」と考える代わりに、何が変わるのか、そしてそれらの変化があなたのクラスにどのように影響するのかを考えます。たとえば、データベースが変更された場合、クラスを変更する必要がありますか?出力デバイス(スクリーン、モバイルデバイス、プリンターなど)が変更された場合はどうですか?他の多くの方向からの変更のためにクラスを変更する必要がある場合、それはクラスにあまりにも多くの責任があることを示しています。

リンクされた記事で、ボブおじさんは次のように結論付けています。

SRPは最も単純な原則の1つであり、最も適切なものの1つです。責任の結合は、私たちが自然に行うことです。これらの責任を見つけ、互いに分離することは、ソフトウェア設計の真の目的です。


2
彼の発言方法も気に入っています。抽象的な「責任」よりも、変化に関連する場合に確認する方が簡単だと思われます。
マチューM.

それは実際にそれを置くための優れた方法です。私はそれが好きです。注として、私は通常、SRPをメソッドにより強く適用するものと考える傾向があります。クラスは2つのことだけを行う必要がある場合があります(クラスが2つのドメインを橋渡しする場合があります)が、メソッドはタイプシグネチャで簡潔に説明できる以上のことを行うことはほとんどありません。
CodexArcanum

1
ちょうどこれを私の卒業生に見せてくれました-素晴らしい読み物であり、私自身への素晴らしい思い出になります。
Martijn Verburg

1
わかりました。これは、オーバーエンジニアリングされていないコードは、すべての可能な変更ではなく、予見可能な将来の変更のみを計画するという考えと組み合わせると、非常に理にかなっています。私はこれを、「予見可能な将来に発生する可能性が高いクラスを変更する理由が1つだけであるべきだ」と少し述べます。これにより、変更される可能性が低い設計の部分を単純化し、変更される可能性のある部分を分離することが推奨されます。
dsimcha

18

SRPはどのような問題を解決しようとしているのでしょうか?SRPが役立つのはいつですか?ここに私が思いついたものがあります:

次の場合、責任/機能をクラス外にリファクタリングする必要があります。

1)機能を複製しました(DRY)

2)コードを理解するには、コードに別のレベルの抽象化が必要であることがわかります(KISS)

3)ドメインの専門家は、機能の一部が異なるコンポーネント(ユビキタス言語)の一部であると理解していることに気付く

次の場合、クラスから責任をリファクタリングすることはできません。

1)重複する機能はありません。

2)この機能は、クラスのコンテキスト以外では意味がありません。別の言い方をすれば、クラスは機能を理解しやすいコンテキストを提供します。

3)ドメインの専門家には、その責任の概念がありません。

SRPが広く適用されると、ある種の複雑さ(クラスの頭や尾を内部であまりにも大きくしようとする)を別の種類(すべての協力者/レベルを維持しようとする)これらのクラスが実際に何をしているのかを理解するための抽象化)。

疑わしい場合は、そのままにしてください!明確なケースがある場合は、後でいつでもリファクタリングできます。

どう思いますか?


これらのガイドラインは役立つ場合とそうでない場合がありますが、SOLIDで定義されているSRPとはまったく関係ありませんか?
サラ

ありがとうございました。いわゆるSOLID原則に伴う狂気のいくつかは信じられません。非常に単純なコードは、正当な理由もなく 100倍複雑になります。指定するポイントは、SRPを実装する実際の理由を説明しています。上記の原則は独自の頭字語になり、「SOLID」を捨てるべきだと思います。「建築宇宙飛行士」、確かに、あなたがここに私を導くスレッドで指摘したように。
ニコラス・ピーターセン

4

客観的なスケールがあるかどうかはわかりませんが、これをもたらすのはメソッドです。メソッドの数ではなく、その機能の多様性です。私はあなたが分解を取りすぎることができることに同意しますが、私はそれについての厳格な規則に従うことはありません。


4

答えは定義にあります

あなたが責任を定義するものは、最終的にあなたに境界を与えます。

例:

私は請求書を表示する責任があるコンポーネントを持っています->この場合、さらに何かを追加し始めたら、原則を破っています。

一方、請求書の処理の責任->複数の小さな機能の追加(たとえば、Invoiceの印刷、Invoiceの更新)がすべてその境界内にある場合。

そのモジュールを処理するために開始した場合は任意の外部機能の請求書を、それはその境界の外になります。


ここでの主な問題は「処理」という言葉だと思います。単一の責任を定義するには一般的すぎます。単一のコンポーネントハンドル{print、update、--なぜですか?ではなく、請求書印刷するコンポーネントと請求書更新するコンポーネントを用意する方が良いと思います。-何でも表示}請求書
マチャド

1
OPは基本的に「責任をどのように定義しますか?」そのため、責任はあなたが定義したものであると言うとき、それは質問を繰り返しているように思えます。
デスペルター

2

私は常に2つのレベルでそれを表示します:

  • 私のメソッドは1つのことだけを行い、それをうまく行うようにします
  • クラスは、1つの事柄をうまく表現するこれらのメソッドの論理(OO)グループとして表示されます

したがって、Dogというドメインオブジェクトのようなもの:

Dog私のクラスですが、犬は多くのことができます!walk()run()およびbite(DotNetDeveloper spawnOfBill)(申し訳ありませんが抵抗できませんでした; p)などのメソッドがあります。

Dog扱いにくい場合は、これらのメソッドのグループをMovement、my walk()run()メソッドを含む可能性があるクラスなどの別のクラスで一緒にモデル化する方法について考えます。

厳格なルールはありません。オブジェクト指向デザインは時間とともに進化します。私は明確なインターフェイス/パブリックAPIだけでなく、1つのことと1つのことをうまく行う簡単なメソッドを探しています。


バイトは実際にオブジェクトのインスタンスを取得する必要があり、DotNetDeveloperはPersonのサブクラスである必要があります(通常、とにかく!)
Alan Pearce

@アラン-そこ-あなたのためにそれを修正しました:
Martijn Verburg

1

私はクラスの線に沿ってそれをもっと見て、ただ一つのことを表すべきです。Kariannaの例@充当するために、私は私の持っているDogためのメソッドを持つクラスを、walk()run()bark()。私はのためのメソッドを追加するつもりはないmeaow()squeak()slither()またはfly()それらは、犬が行うものではありませんので。それらは他の動物が行うことであり、それらの他の動物はそれらを表現する独自のクラスを持っています。

(ところで、あなたの犬飛ぶ場合、おそらくあなたは窓から彼を放り出すのをやめるべきです)。


「あなたの犬が飛ぶ場合、おそらくあなたは窓の外に彼を投げることを停止する必要があります」の+1。:)
ボビーテーブル

クラスが何を表すべきかという問題を超えて、インスタンスは何を表していますか?1よろしく場合SeesFoodの特徴としてDogEyesBark何かのようにはで行わDogVoiceれ、Eatによって行わものとしてDogMouth、そのロジックは次のようif (dog.SeesFood) dog.Eat(); else dog.Bark();になりますif (eyes.SeesFood) mouth.Eat(); else voice.Bark();目は、口、との声がすべて単一entitityに接続されているアイデンティティのいずれかの感覚を失って、。
supercat

@supercatは公平なポイントですが、コンテキストは重要です。言及するコードがDogクラス内にある場合、おそらくDog関連しています。そうでない場合は、おそらくmyDog.Eyes.SeesFood単にのようなものではなく、何かのようになるでしょうeyes.SeesFood。別の可能性は、プロパティとメソッドを要求するインターフェイスをDog公開することです。ISeeDog.EyesSeesFood
JohnL 14

@JohnL:猫やシマウマと本質的に同じように、実際の見る力学が犬の目で処理される場合、力学をEyeクラスで処理するのは理にかなっているかもしれませんが、犬は「見る」べきです目が見えるだけでなく、目を使って。犬は目ではありませんが、単なる目を引くものでもありません。それは「見ることができる」ものであり、インターフェースを介してそのように記述されるべきです。盲犬であっても、食べ物を見るかどうかを尋ねられます。犬は常に「いいえ」と言うので、あまり有用ではありませんが、質問しても害はありません。
supercat

次に、コメントで説明したように、ISeeインターフェイスを使用します。
JohnL 14

1

クラスは、独自の抽象化レベルで表示するときに1つのことを行う必要があります。それほど抽象的でないレベルで多くのことを行うことは間違いありません。これは、プログラムがより保守しやすいようにするためにクラスがどのように機能するかです。実装を詳細に調べる必要がない場合、実装の詳細を隠します。

このテストとしてクラス名を使用します。クラスにわかりやすい短い名前を付けられない場合、またはその名前に「And」などの単語が含まれている場合、おそらく単一責任原則に違反していることになります。

私の経験では、物事がより具体的な下位レベルでこの原則を維持する方が簡単です。


0

それは、1つのユニークなロールを持つことです。

各クラスはロール名で再開する必要があります。ロールは、実際には、コンテキストに関連付けられた動詞(のセット)です。

例えば ​​:

ファイルはファイルへのアクセスを提供します。FileManagerはFileオブジェクトを管理します。

リソースは、ファイルから1つのリソースのデータを保持します。ResourceManagerは、すべてのリソースを保持および提供します。

ここでは、「管理」などの一部の動詞が他の動詞のセットを意味することがわかります。ほとんどの場合、クラスだけでなく動詞だけが関数として考えられます。動詞が独自の共通コンテキストを持つアクションを多すぎることを意味する場合、それはそれ自体がクラスでなければなりません。

そのため、いくつかのサブロール(メンバーオブジェクトまたは他のオブジェクトによって実行される)の集合体である可能性のある一意のロールを定義することにより、クラスが何を行うかを簡単に理解できるようにします。

多くの場合、いくつかの異なる他のクラスを含むManagerクラスを作成します。ファクトリー、レジストリーなど。グループチーフ、オーケストラチーフのようなマネージャークラスを見て、他の人々が協力して高レベルのアイデアを実現するように導いてください。彼には1つの役割がありますが、内部で他のユニークな役割と連携することを意味します。また、会社がどのように組織されているかのように見ることができます。CEOは純粋な生産性レベルでは生産的なものではありませんが、彼がいなければ、何も正しく機能しません。それが彼の役割です。

設計時に、一意のロールを識別します。そして、各役割について、他のいくつかの役割でカットできないかどうかをもう一度確認します。そうすれば、Managerがオブジェクトを作成する方法を簡単に変更する必要がある場合は、単にFactoryを変更して、安心してください。


-1

SRPは、クラスを分割するだけでなく、機能を委任することも目的としています。

上記で使用したDogの例では、DogBarker、DogWalkerなど(凝集度が低い)などの3つの別個のクラスを持つための正当化としてSRPを使用しないでください。代わりに、クラスのメソッドの実装を見て、それらが「知りすぎている」かどうかを判断します。dog.walk()は引き続き使用できますが、おそらくwalk()メソッドは別のクラスに歩行の詳細を委任する必要があります。

実際、Dogクラスに変更する理由が1つあります。それは、Dogが変更されるためです。もちろん、これを他のSOLID原則と組み合わせて、Dogを変更する(開く/閉じる)のではなく、新しい機能のためにDogを拡張します。そして、IMoveやIEatなどの依存関係を注入します。そしてもちろん、これらの個別のインターフェースを作成します(インターフェースの分離)。犬は、バグが見つかった場合、または犬が根本的に変わった場合にのみ変更されます(Liskov Sub、拡張せずに動作を削除します)。

SOLIDの最終的な効果は、既存のコードを変更するよりも頻繁に新しいコードを作成できることであり、これは大きなメリットです。


-1

それはすべて、責任の定義と、その定義がコードの保守にどのように影響するかによって異なります。すべてが1つの事に要約されます。それが、コードを維持する上で設計がどのように役立つかということです。

そして、誰かが言ったように、「水の上を歩いて、指定された仕様からソフトウェアを設計するのは簡単です。両方とも凍結していれば」

したがって、責任をより具体的な方法で定義する場合、変更する必要はありません。

責任は明白な場合もありますが、微妙な場合もあり、慎重に決定する必要があります。

catchThief()という名前の別の責任をDogクラスに追加するとします。今では、さらに別の責任に向かっている可能性があります。明日、犬が泥棒を捕まえる方法を警察部が変更する必要がある場合、犬のクラスを変更する必要があります。この場合、別のサブクラスを作成して、ThiefCathcerDogという名前を付ける方が良いでしょう。しかし、別の見方をすると、どのような状況でも変化しないことが確実な場合、またはcatchThiefの実装方法が外部パラメーターに依存している場合、この責任を負うことは完全に問題ありません。責任が著しく変わっていない場合は、ユースケースに基づいて慎重に決定する必要があります。


-1

「変更する1つの理由」は、システムを使用しているユーザーによって異なります。各アクターのユースケースがあることを確認し、最も可能性の高い変更のリストを作成します。ユースケースの可能な変更ごとに、その変更が1つのクラスのみに影響することを確認します。まったく新しいユースケースを追加する場合は、クラスを拡張するだけでよいことを確認してください。


1
変更する理由の1つは、ユースケース、アクターの数、または変更の可能性とは関係ありません。起こりそうな変更のリストを作成するべきではありません。1つの変更が1つのクラスにのみ影響を与えるのは正しいことです。クラスを拡張してその変更に対応できるのは良いことですが、それはSRPではなくオープンクローズド原則です。
candied_orange

最も可能性の高い変更のリストを作成しないのはなぜですか?投機的デザイン?
kiwicomb123 16

それは役に立たず、完全ではなく、変化を予測するよりも効果的に変化に対処する方法があるためです。期待してください。決定を分離すると、影響は最小限になります。
candied_orange 16

わかりました、それは「あなたの必要はない」という原則に違反しています。
kiwicomb123 16

実際にyagniも遠くまでプッシュできます。それは、投機的なユースケースを実装しないようにするためのものです。実装されたユースケースを隔離しないようにしてください。
candied_orange 16
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.