単一の責任原則を破ることなく、クラスに複数のメソッドを持たせる方法


64

単一責任の原則は、ウィキペディアで次のように定義されています

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

クラスの責任が1つだけの場合、どうすれば複数のメソッドを持つことができますか?各メソッドに異なる責任はありません。つまり、クラスには複数の責任があることになります。

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


11
なぜ下票なのか?SE.SEにとって理想的な質問のようです。その人はトピックを調査し、質問を非常に明確にする努力をしました。代わりに賛成に値します。
Arseni Mourzenko

19
おそらく、これはすでに何度か質問され、回答された質問であるためです。たとえば、softwareengineering.stackexchange.com / questions / 345018 /…を参照してください。私の意見では、それは実質的な新しい側面を追加しません。
ハンスマーティンモスナー


9
これは単なる不条理です。すべてのクラスが文字通り1つのメソッドのみを許可されている場合、プログラムが文字どおり複数のことを実行できる方法はありません。
ダレルホフマン

6
@DarrelHoffmanそれは本当ではありません。すべてのクラスが「call()」メソッドのみを持つファンクターである場合、基本的にはオブジェクト指向プログラミングで単純な手続き型プログラミングをエミュレートしているだけです。クラスの「call()」メソッドは他の多くのクラスの「call()」メソッドを呼び出すことができるため、他の方法でできることは何でもできます。
Vaelus

回答:


29

単一の責任は、単一の機能が果たすことができるものではないかもしれません。

 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は、カーリーの法則よりもコンウェイの法則についてのことです。ボブおじさんは、クラスではなく関数にカーリーの法則を適用することを提唱しています。SRPは、一緒に変更する理由を混在させることに注意します。コンウェイの法則によると、システムは組織の情報がどのように流れるかに従っています。あなたが聞いたことがないことを気にしないので、それはSRPに従うことにつながります。

「モジュールは、ただ一人のアクターに責任があるべきです」

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

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

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

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

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

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

たぶん、あなたはそれを気にしません。いいよ 「1つのことをする」ことで、設計上のすべての問題を解決できると考えると、「1つのこと」が何であるかについての想像力が失われます。範囲を制限するもう1つの理由は組織です。ジャンクドロワーにすべてがいっぱいになるまで、多くの「1つのモノ」を他の「1つのモノ」の中にネストできます。私はにそれについて話しました

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

これを見るための機能的な方法は、a.f(x)それa.g(x)は単にf a(x)とg a(x)です。2つの関数ではなく、一緒に変化する一連の関数のペア。ザ・はaもそれにデータを持っている必要はありません。単純に、どの方法fg実装を使用するかを知ることができます。一緒に変化する機能は一緒に属します。それは古き良きポリモーフィズムです。

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


25
この答えは、SRPを理解しようとする人を混乱させると思います。社長と夫人の間の戦いは、技術的な手段によって解決されるものではなく、エンジニアリングの決定を正当化するためにそれを使用することは無意味です。行動中のコンウェイの法則。
whatsisname

8
@whatsisnameそれどころか。SRPは、利害関係者に適用することを明確に意図していました。技術設計とは関係ありません。あなたはそのアプローチに反対するかもしれませんが、それがSRPが元々ボブおじさんによって定義された方法であり、何らかの理由で、人々はこの単純な概念を理解できないようです(心、実際に役立つのは、完全に直交する質問です)。
ルアーン

ティム・オッティンガーが述べたカーリーの法則は、変数は常に一つのことを意味するべきだと強調しています。私にとって、SRPはそれよりも少し強力です。クラスは概念的に「1つのこと」を表すことができますが、2つの外部の変化ドライバーがその「1つのこと」のある側面を異なる方法で扱う場合、または2つの異なる側面を懸念する場合、SRPに違反します。問題はモデリングの1つです。何かを単一のクラスとしてモデル化することを選択しましたが、その選択を問題にしているドメインについて何かがあります(コードベースの進化に伴い、物事が邪魔になり始めます)。
フィリップミロヴァーノヴィッチ

2
@FilipMilovanovićボブおじさんが彼のClean Architecture本でSRPを説明したコンウェイの法則とSRPの類似性は、組織にきれいな非周期的な組織図があるという仮定から来ています。これは古い考えです。聖書でさえ、「だれも二人の主人に仕えることはできない」という引用があります。
candied_orange

1
@TKKは、それを(カーリーの法則ではなく)コンウェイズの法則に関連付けています(同等ではありません)。ボブおじさんが彼のClean Architectureの本でそう言っていたから、私はSRPがカーリーの法則であるという考えに反論しています。
candied_orange

48

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

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

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

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

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


パブロHはコメントしました:

素敵な例ですが、SRPがクラスに複数のパブリックメソッドを許可する理由にまだ答えていないように感じます。

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

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

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

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


1
素敵な例ですが、SRPがクラスに複数のパブリックメソッドを許可する理由にまだ答えていないように感じます。
パブロH

1
@PabloH:公平。クラスに2つのメソッドがある別の例を追加しました。
Arseni Mourzenko

30

関数は関数です。

責任は責任です。

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

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

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


2
同意します。@Aulis Ronkainen-2つの答えに結び付けます。入れ子になった責任については、整備士の例えを使用すると、ガレージが車両のメンテナンスを担当します。ガレージ内のさまざまなメカニックが車のさまざまな部分に対して責任を負いますが、これらのメカニックはそれぞれ一体となって動作します
wolfsshield

2
@wolfsshield、同意しました。1つのことだけを行うメカニックは役に立たないが、単一の責任を負うメカニックは(少なくとも必ずしも)役に立たない。現実の類推は、抽象的なOOPの概念を記述するのに常に最適とは限りませんが、これらの違いを区別することが重要です。違いを理解していないことが、そもそも混乱を引き起こすものだと思います。
オーリスロンカイネン

3
@AulisRonkainen見た目、匂い、そしてアナロジーのように感じますが、このメカニズムを使用して、SRPの責任という用語の特定の意味を強調するつもりでした。私はあなたの答えに完全に同意します。
ピーター

20

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

たとえば、ユーザーサービスクラスを考えます。

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に感謝します。


3
実際には、インターフェイス分離の原則(SOLIDの「I」)を説明しています。SRPはまったく別の獣です。
ルアーン

余談ですが、ここではどのコーディング規約を使用していますか?私が期待するオブジェクト UserServiceUserUpperCamelCaseするが、方法を CreateExistsUpdate私はlowerCamelCaseを作っただろう。
KlaymenDK

1
@KlaymenDKそうですね、大文字はGoを使用する習慣です(大文字=エクスポート/パブリック、小文字=プライベート)
Jesse

@Luaanそれを指摘してくれてありがとう、私の答えを明確にします
ジェシー

1
@KlaymenDK多くの言語は、クラスだけでなくメソッドにもPascalCaseを使用しています。たとえば、C#。
オメガスティック

15

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

シェフに給与計算も依頼する場合、SRPに違反します。


4

反例:可変状態の保存。

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

public class State {
    private int i;


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

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

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

したがって、この単一の責任では、このクラスに少なくとも2つのメソッドが必要です。QED。


4

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

単一の責任は単一の方法とは異なります。それらは異なることを意味します。ソフトウェア開発では、凝集性について説明します。高い凝集度を持ち、単一の責任を果たすとカウントできる関数(メソッド)。

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


2

物事を見て、それらを関数ではなくデータの観点から整理することは、(どの言語でも、特にOO言語では)しばしば役立ちます。

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

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

ここでのレイヤー化は完全に受け入れられることに注意してください。あなたは持っているかもしれないPoint個々の点とを扱うためのクラスPolygonの集合を扱うためのクラスPoint秒。クラスに(ポイントがan とvalueの両方を持っていることを保証するなど)Polygonだけを行うために何かを処理するすべての責任を委任するため、これらはまだ別の責任をPoint持ちます。xyPoint

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