web-dev-qa-db-ja.com

単一の責任の原則を破ることなく、クラスが複数のメソッドを持つことができる方法

単一責任の原則は wikipedia として定義されています

単一責任の原則は、すべてのモジュール、クラス、または機能がソフトウェアによって提供される機能の単一の部分に対して責任を負うべきであり、その責任はクラスによって完全にカプセル化されるべきであると述べるコンピュータプログラミングの原則です。

クラスが単一の責任しか持たない場合、どのようにして複数のメソッドを持つことができますか?各メソッドが異なる責任を負うことはないでしょう。これは、クラスが複数の責任を持つことを意味します。

単一の責任の原則を示す私が見たすべての例は、1つのメソッドのみを持つクラスの例を使用しています。例を見るか、1つの責任を持つと見なすことができる複数のメソッドを持つクラスの説明があると役立つ場合があります。

68
Goose

単一の責任は、単一の機能が果たすことができるものではない可能性があります。

_ class Location { 
     public int getX() { 
         return x;
     } 
     public int getY() { 
         return y; 
     } 
 }
_

このクラスは、単一責任の原則を破る可能性があります。 2つの関数があるからではなく、getX()getY()のコードが、変更を要求する可能性のあるさまざまな利害関係者を満足させる必要がある場合。 X氏がメモを送信してすべての数値を浮動小数点数として表現し、Y氏が彼女の部門のレビューはすべてX氏の考えに関係なく整数のままであると主張する場合、このクラスは物事が混乱しそうになるので、それが誰に責任があるかについての単一の考え。

SRPが実行されていた場合、LocationクラスがX氏とそのグループがさらされていることに貢献しているかどうかは明らかです。クラスが何を担当するかを明確にし、どのディレクティブがこのクラスに影響を与えるかを理解します。両方がこのクラスに影響を与える場合、変更の影響を最小限に抑えるように設計されていません。 「クラスが変更する理由は1つだけである」とは、クラス全体が1つの小さなことしかできないということではありません。つまり、私はクラスを見て、X氏とY氏の両方がこのクラスに関心があるとは言えないはずです。

そのようなもの以外。いいえ、複数の方法で問題ありません。クラスに属しているメソッドと属していないメソッドを明確にする名前を付けてください。

ボブおじさんのSRPは カーリーの法則 よりも コンウェイの法則 についてです。ボブおじさんは、クラスではなく関数にカーリーの法則(1つのことを行う)を適用することを提唱しています。 SRPは、一緒に変更する理由を混合しないように注意します。コンウェイの法則によると、このシステムは組織の情報がどのように流れるかに従います。聞いたことがないことを気にしないので、SRPをフォローすることにつながります。

「モジュールは1人の俳優だけに責任を持つべきです」

ロバートCマーティン-クリーンアーキテクチャ

人々は、SRPがスコープを制限するあらゆる理由になることを望み続けています。 SRPよりもスコープを制限する理由は他にもあります。さらに、クラスが 内部を見ても驚くことはない を保証する名前をとることができる抽象化であると主張することにより、スコープをさらに制限します。

カーリーの法則をクラスに適用できます。あなたはボブおじさんが話していることの外にいますが、あなたはそれをすることができます。あなたが間違っているのは、それが1つの機能を意味すると考え始めたときです。それは、家族には子供が1人だけであるべきだと考えるようなものです。子供が2人以上いるからといって、家族になるのをやめることはありません。

カーリーの法則をクラスに適用する場合、クラス内のすべては単一の統一アイデアについてのものでなければなりません。そのアイデアは幅広いものにすることができます。アイデアはしつこいかもしれません。いくつかのロギングユーティリティ関数がそこにある場合、それらは明らかに場違いです。このコードを気にかけているのがX氏だけかどうかは関係ありません。

ここで適用する古典的な原理は、 懸念の分離 と呼ばれます。すべての懸念を切り離すと、1つの場所に残されたものは1つの懸念であると主張できます。 1991年の映画City SlickersがキャラクターCurlyを紹介する前に、それをこのアイデアと呼んでいました。

これで結構です。ボブおじさんが責任と呼んでいることは問題ではないというだけです。彼への責任はあなたが焦点を当てるものではありません。それはあなたに変化を強いることができるものです。 1つの懸念事項に焦点を当てながら、異なるアジェンダを持つ異なるグループの人々に責任を持つコードを作成できます。

多分あなたはそれを気にしない。いいよ「1つのこと」を行うことですべての設計上の問題が解決されると考えることは、「1つのこと」が何になり得るかについての想像力の欠如を示しています。範囲を制限するもう1つの理由は組織です。すべてがいっぱいのがらくたの引き出しができるまで、多くの「1つのもの」を他の「1つのもの」の中に入れ子にすることができます。私はそれについて話しました before

もちろん、古典的なOOPスコープを制限する理由は、クラスにプライベートフィールドがあり、代わりにゲッターを使用してそのデータを共有することです。そのデータを必要とするすべてのメソッドをクラスに配置します。データをプライベートで使用できます。多くの場合、これは制限が多すぎてスコープリミッターとして使用できません。なぜなら、一緒に属するすべてのメソッドがまったく同じフィールドを使用するわけではないためです。メソッドを一緒に。

これを見る機能的な方法は、a.f(x)およびa.g(x)が単にfであることです。a(x)とga(バツ)。 2つの関数ではなく、一緒に変化する関数のペアの連続体。 aにはデータがなくてもかまいません。使用するfgの実装を簡単に知る方法です。一緒に変化する機能は一緒に属します。それは古き良きポリモーフィズムです。

SRPは、範囲を制限する多くの理由の1つにすぎません。それは良いものです。しかし、それだけではありません。

30
candied_orange

ここでのキーはscope、または必要に応じてgranularityです。クラスによって表される機能の一部は、機能の一部にさらに分離できます。各部分はメソッドです。

ここに例があります。シーケンスからCSVを作成する必要があると想像してください。 RFC 4180に準拠したい場合、アルゴリズムを実装してすべてのEdgeケースを処理するにはかなりの時間がかかります。

単一のメソッドでそれを行うと、コードが特に読みにくくなり、特に、メソッドは一度にいくつかのことを行います。したがって、いくつかの方法に分割します。たとえば、そのうちの1つはヘッダー、つまりCSVの最初の行の生成を担当している場合があります。一方、別の方法では、任意の型の値をCSV形式に適した文字列表現に変換し、別の方法では、値は二重引用符で囲む必要があります。

これらのメソッドには独自の責任があります。二重引用符を追加する必要があるかどうかをチェックするメソッドには独自のメソッドがあり、ヘッダーを生成するメソッドには独自のメソッドがあります。これはメソッドに適用されるSRPです。

さて、これらすべてのメソッドには共通の1つの目標があります。つまり、シーケンスを取り、CSVを生成します。これはクラスの単一の責任です。


Pablo H コメント:

良い例ですが、SRPでクラスが複数のパブリックメソッドを持つことを許可している理由はまだ解っていません。

確かに。私が提供したCSVの例には、理想的には1つのパブリックメソッドがあり、他のすべてのメソッドはプライベートです。より良い例は、Queueクラスによって実装されるキューです。このクラスには基本的に2つのメソッドが含まれます:Pushenqueueとも呼ばれます)、およびpopdequeueとも呼ばれます)。

  • Queue.Pushの役割は、オブジェクトをキューの末尾に追加することです。

  • Queue.popの役割は、キューの先頭からオブジェクトを削除し、キューが空の場合を処理することです。

  • Queueクラスの責任は、キューロジックを提供することです。

51

関数は関数です。

責任は責任です。

整備士は自動車を修理する責任があり、診断、いくつかの簡単なメンテナンス作業、実際の修理作業、他の人への作業の委任などが含まれます。

コンテナクラス(リスト、配列、ディクショナリ、マップなど)は、オブジェクトを格納する責任があります。これには、オブジェクトの格納、挿入の許可、アクセスの提供、ある種の順序付けなどが含まれます。

単一の責任は、コード/機能が非常に少ないことを意味するのではなく、同じ責任の下で「一緒に属している」あらゆる機能を意味します。

31
Peter

単一の責任は、必ずしも1つのことだけを実行することを意味するわけではありません。

たとえば、Userサービスクラスを見てみましょう。

class UserService {
    public User Get(int id) { /* ... */ }
    public User[] List() { /* ... */ }

    public bool Create(User u) { /* ... */ }
    public bool Exists(int id) { /* ... */ }
    public bool Update(User u) { /* ... */ }
}

このクラスには複数のメソッドがありますが、その責任は明らかです。データストア内のユーザーレコードへのアクセスを提供します。その唯一の依存関係は、ユーザーモデルとデータストアです。それは疎結合であり、非常にまとまりがあり、SRPが実際に考えさせようとしていることです。

SRPを「インターフェース分離の原則」と混同しないでください( [〜#〜] solid [〜#〜] を参照)。インターフェース分離の原則(ISP)によると、より小さくて軽量なインターフェースは、より大きくより一般化されたインターフェースよりも望ましいとされています。 Goは、標準ライブラリ全体でISPを多用しています。

// Interface to read bytes from a stream
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Interface to write bytes to a stream
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Interface to convert an object into JSON
type Marshaler interface {
    MarshalJSON() ([]byte, error)
}

SRPとISPは確かに関連していますが、一方が他方を意味するわけではありません。 ISPはインターフェイスレベルにあり、SRPはクラスレベルにあります。クラスが複数の単純なインターフェースを実装する場合、そのクラスはもはや1つの責任だけを持たない可能性があります。

ISPとSRPの違いを指摘してくれたLuaanに感謝します。

20
Jesse

レストランにはシェフがいます。彼の唯一の責任は料理することです。それでも彼はステーキ、ジャガイモ、ブロッコリー、その他何百もの料理を作ることができます。メニューでは、料理ごとに1人のシェフを雇いますか?または、各料理のコンポーネントごとに1人のシェフがいますか?または、彼の単一の責任を果たすことができる1人のシェフ:料理すること?

そのシェフに給与計算も依頼する場合、それはSRPに違反するときです。

15
gnasher729

あなたは単一責任の原則を誤って解釈しています。

単一の責任は単一の方法と同じではありません。彼らは別のことを意味します。ソフトウェア開発では cohesion について話します。高い凝集度を持つ関数(メソッド)は一緒に「所属」し、単一の責任を実行するものとして数えることができます。

単一責任の原則が満たされるようにシステムを設計するのは開発者の責任です。これは抽象化手法と見なすことができるため、意見の相違がある場合があります。単一責任の原則を実装すると、コードの主なテストが容易になり、アーキテクチャと設計を理解しやすくなります。

4
Aulis Ronkainen

反例:変更可能な状態の保存。

今までで最も単純なクラスがあり、その唯一の仕事はintを格納することであるとします。

_public class State {
    private int i;


    public State(int i) { this.i = i; }
}
_

メソッドが1つだけに制限されている場合は、カプセル化を解除してiを公開しない限り、setState()またはgetState()のいずれかを使用できます。

  • セッターはゲッターなしでは役に立ちません(情報を読み取ることはできません)
  • ゲッターはセッターがないと役に立ちません(情報を変更することはできません)。

明らかに、この単一の責任必須このクラスには少なくとも2つのメソッドが必要です。 QED。

関数ではなくdataの観点から物事を見て整理することは、(どの言語でも、特にOO言語)で)役立つことがよくあります。

したがって、クラスの責任は、クラスの整合性を維持し、所有するデータを正しく使用するための支援を提供することであると考えてください。明らかに、これは、すべてのコードが1つのクラスにある場合、複数のクラスに分散するよりも簡単です。 PointクラスのPoint add(Point p)メソッドを使用すると、他の場所に追加するよりも、2つのポイントを追加するほうが確実に実行され、コードのメンテナンスがより簡単になります。

特に、クラスは、データの不整合や不正確をもたらす可能性のあるものを公開するべきではありません。たとえば、Pointが(0,0)から(127,127)までの平面内になければならない場合、コンストラクターと、新しいPointを変更または生成するメソッドには、値をチェックする責任があります。それらは与えられ、この要件に違反する変更を拒否します。 (しばしばPointのようなものは不変であり、構築後にPointを変更する方法がないことを保証することもクラスの責任になります)

ここでのレイヤー化は完全に許容できることに注意してください。個々のポイントを処理するためのPointクラスと、Polygonsのセットを処理するためのPointクラスがあるかもしれません。 PolygonPointとのみ関係するものを処理するすべての責任を委任するため(たとえば、ポイントにxy value)をPointクラスに追加します。

2
cjs