私はできるだけ単一責任原則(SRP)を遵守するよう努めており、デリゲートに大きく依存している特定のパターン(メソッドのSRPの場合)に慣れています。このアプローチが適切かどうか、または深刻な問題があるかどうかを知りたいのですが。
たとえば、コンストラクタへの入力を確認するために、次のメソッドを導入できます(Stream
入力はランダムで、何でもかまいません)。
private void CheckInput(Stream stream)
{
if(stream == null)
{
throw new ArgumentNullException();
}
if(!stream.CanWrite)
{
throw new ArgumentException();
}
}
このメソッドは(おそらく)複数のことを行います
したがって、SRPに準拠するために、ロジックを
private void CheckInput(Stream stream,
params (Predicate<Stream> predicate, Action action)[] inputCheckers)
{
foreach(var inputChecker in inputCheckers)
{
if(inputChecker.predicate(stream))
{
inputChecker.action();
}
}
}
おそらくたった1つのことしかしません(そうしますか?):入力を確認します。入力の実際のチェックと例外のスローのために、私は次のようなメソッドを導入しました
bool StreamIsNull(Stream s)
{
return s == null;
}
bool StreamIsReadonly(Stream s)
{
return !s.CanWrite;
}
void Throw<TException>() where TException : Exception, new()
{
throw new TException();
}
CheckInput
を次のように呼び出すことができます
CheckInput(stream,
(this.StreamIsNull, this.Throw<ArgumentNullException>),
(this.StreamIsReadonly, this.Throw<ArgumentException>))
これは最初のオプションよりも優れていますか、それとも不要な複雑さを導入しますか?このパターンが実行可能な場合でも、このパターンを改善する方法はありますか?
SRPはおそらく最も誤解されているソフトウェア原則です。
ソフトウェアアプリケーションは、モジュールから構築されたモジュールから構築されます。
一番下では、CheckInput
などの単一の関数に含まれるロジックはごくわずかですが、上に行くほど、連続する各モジュールはますます多くのロジックをカプセル化しますこれは正常です。
SRPは、単一のatomicアクションを実行することではありません。その責任が複数のアクションを必要とする場合でも、それは単一の責任を持つことです...そして最終的にそれはmaintenanceとtestabilityについてです:
CheckInput
が2つのチェックで実装され、2つの異なる例外が発生するという事実は、ある程度無関係です。
CheckInput
には狭い責任があります。つまり、入力が要件に準拠していることを確認します。はい、複数の要件がありますが、これは複数の責任があることを意味しません。はい、小切手を分割できますが、それはどのように役立ちますか?ある時点で、チェックは何らかの方法でリストされなければなりません。
比較してみましょう:
Constructor(Stream stream) {
CheckInput(stream);
// ...
}
対:
Constructor(Stream stream) {
CheckInput(stream,
(this.StreamIsNull, this.Throw<ArgumentNullException>),
(this.StreamIsReadonly, this.Throw<ArgumentException>));
// ...
}
現在、CheckInput
はより少ない処理を実行しますが、呼び出し元はより多くの処理を実行します!
要件のリストを、カプセル化されているCheckInput
から、要件が表示されているConstructor
にシフトしました。
良い変化ですか?場合によります:
CheckInput
がそこでのみ呼び出された場合:議論の余地があります。一方では要件が表示され、もう一方ではコードが雑然としています。CheckInput
が複数回呼び出される場合同じ要件での場合、DRY)に違反し、カプセル化の問題が発生します。単一の責任はlotの作業を意味する可能性があることを認識することが重要です。自動運転車の「頭脳」には単一の責任があります。
目的地まで車を運転します。
それは単一の責任ですが、大量のセンサーとアクターを調整し、多くの決定を下す必要があり、場合によっては競合する要件もあります1...
...しかし、それはすべてカプセル化されています。したがって、クライアントは気にしません。
1 乗客の安全、他者の安全、規制の尊重、...
SRPについてボブおじさんを引用する( https://8thlight.com/blog/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html ):
単一責任原則(SRP)は、各ソフトウェアモジュールには変更する理由が1つだけあるべきであると述べています。
...この原則は人に関するものです。
...ソフトウェアモジュールを作成するとき、変更が要求されたときに、それらの変更が1人から、または狭く定義された1つのビジネス機能を表す1つの密結合グループからのみ発生できることを確認する必要があります。
...これが、JSPにSQLを配置しない理由です。これが、結果を計算するモジュールでHTMLを生成しない理由です。これが、ビジネスルールがデータベーススキーマを認識してはならない理由です。これが、懸念を分離する理由です。
彼は、ソフトウェアモジュールは特定の利害関係者の懸念に対処する必要があると説明しています。したがって、あなたの質問に答える:
これは最初のオプションよりも優れていますか、それとも不要な複雑さを導入しますか?このパターンが実行可能な場合でも、このパターンを改善する方法はありますか?
IMO、より高いレベル(この場合はクラスレベル)を調べる必要があるときに、1つのメソッドのみを調べています。おそらく、私たちはあなたのクラスが現在何をしているのかを見る必要があります(そして、これにはあなたのシナリオについてより多くの説明が必要です)。今のところ、クラスはまだ同じことをしています。たとえば、明日いくつかの検証に関する変更要求がある場合(たとえば、「現在、ストリームはnullにできる」)、このクラスに移動して、その中のものを変更する必要があります。
いいえ、この変更はSRPから通知されません。
"渡されたオブジェクトはストリームです"のチェッカーにチェックがない理由を自問してください。答えは明白です。言語は、呼び出し側が非ストリームで渡されるプログラムのコンパイルを阻止します。
C#の型システムは、ニーズを満たすには不十分です。チェックは今日の型システムでは表現できない不変条件の強制の実装です。メソッドがnullにできない書き込み可能なストリームをとるという言い方ができたなら、それを書いたはずですが、そうではないので、次の最善策を実行しました。実行時に型制限を強制したことです。うまくいけば、あなたもそれを文書化して、あなたの方法を使う開発者がそれに違反する必要がなく、彼らのテストケースを失敗させ、そして問題を修正することができるようにします。
メソッドに型を置くことは単一責任原則の違反ではありません。前提条件を強制する方法も、事後条件をアサートする方法もありません。
すべての責任が平等になるわけではありません。
ここに2つの引き出しがあります。彼らは両方とも1つの責任を負います。それぞれに、何に属しているかを知らせる名前があります。 1つは銀器の引き出しです。もう1つは、ジャンクドロワーです。
違いは何ですか?銀器の引き出しは、それに何が属していないかを明らかにします。ただし、ジャンクドロワーは、適合するものであれば何でも受け入れます。銀器の引き出しからスプーンを取り出すのは非常に間違っているようです。しかし、私がジャンクドロワーから取り除かれた場合に見逃してしまうことを考えるのは難しいです。真実は、何でも単一の責任があると主張することができますが、どちらがより集中した単一の責任があると思いますか?
単一の責任を持つオブジェクトは、ここで1つだけ発生する可能性があることを意味しません。責任は入れ子にすることができます。しかし、それらの入れ子の責任は理にかなっているはずです。ここでそれらを見つけたとき、彼らはあなたを驚かせるべきではありません。
だからあなたが提供するとき
CheckInput(Stream stream);
入力のチェックと例外のスローの両方が原因であることに、私は心配していません。入力のチェックと入力の保存の両方であるかどうか心配になります。それは厄介な驚きです。なくなってしまえば見逃せない一枚。
重要なソフトウェア原則に準拠するために結び目を作り、奇妙なコードを書く場合、通常は原則を誤解しています(ただし、原則が間違っている場合もあります)。 Matthieuの優れた回答が指摘するように、SRPの全体的な意味は「責任」の定義に依存します。
経験豊富なプログラマーはこれらの原則を見て、私たちが台無しにしたコードの記憶にそれらを関連付けます。経験の浅いプログラマはそれらを見ており、それらをまったく関連付ける必要がないかもしれません。それは宇宙に浮かぶ抽象概念であり、すべてにやにや笑い猫ではありません。だから彼らは推測し、それは通常うまくいかない。馬の感覚をプログラミングする前に、奇妙な複雑すぎるコードと通常のコードの違いはまったく明らかではありません。
これは、個人的な結果に関係なく従わなければならない宗教的な戒めではありません。これは、プログラミングの馬の感覚の1つの要素を形式化するための経験則であり、コードをできるだけ単純かつ明確に保つのに役立ちます。逆の効果がある場合は、外部入力を探すのが正しいでしょう。
プログラミングでは、それをじっと見つめるだけで第一の原則から識別子の意味を推測しようとするよりもずっと悪いことはできません。それは、about実際のコードの識別子と同じくらいプログラミングします。
まず、さまざまな側面をチェックしている場合でも、CheckInput
isが1つのことを行うことを明らかにします。最終的には入力をチェックです。 DoSomething
と呼ばれるメソッドを処理している場合、それは1つのことではないと主張することができますが、入力のチェックはそれほど曖昧ではないと想定しても安全だと思います。
このパターンを述語に追加すると、入力をチェックするロジックをクラスに配置したくない場合に役立ちますが、このパターンは、達成しようとしていることに対してかなり冗長に見えます。取得したい場合は、単一のメソッドisValid(Stream)
でインターフェースIStreamValidator
を渡すだけの方がはるかに直接的です。 IStreamValidator
を実装するクラスは、必要に応じてStreamIsNull
やStreamIsReadonly
のような述語を使用できますが、中心的な点に戻ると、これは、単一責任の原則を維持する。
少なくともnullでなく、書き込み可能なStreamを処理していることを確認するための「健全性チェック」がすべて許可されていると私は考えています。この基本的なチェックでは、クラスを何らかの方法でバリデータストリームの。ちなみに、より洗練されたチェックはクラスの外に残しておくのが最善ですが、そこに線が引かれます。ストリームの読み取りまたは検証に向けたリソースの割り当てによってストリームの状態の変更を開始する必要がある場合、ストリームの正式なvalidationの実行を開始しており、thisは独自のクラスに引き込まれる。
私の考えでは、パターンを適用してクラスの側面をより適切に整理している場合、パターンを独自のクラスに含めるメリットがあります。パターンは適合しないので、そもそもそれが本当に独自のクラスに属しているかどうかについても質問する必要があります。私の考えでは、ストリームの検証が将来変更される可能性が高いと思わない限り、特にこの検証が本質的に動的である可能性が高いと思われる場合は、説明したパターンは良い考えです。最初は些細なこと。それ以外の場合は、プログラムを任意に複雑にする必要はありません。スペードをスペードと呼びましょう。検証は1つですが、null入力のチェックは検証ではないため、単一の責任の原則に違反することなく、クラスにそれを保持しても安全だと思います。
原則は、コードの一部が「1つのことだけを行う」べきであることを強調していません。
SRPの「責任」は、要件レベルで理解する必要があります。コードの責任は、ビジネス要件を満たすことです。オブジェクトが複数の独立したビジネス要件を満たす場合、SRPに違反します。独立とは、1つの要件が変更されても、他の要件が維持されることを意味します。
新しいビジネス要件が導入された可能性があります。つまり、この特定のオブジェクトは読み取り可能かどうかをチェックする必要はありません一方で、別のビジネス要件では、オブジェクトをチェックする必要があります。読みやすい?いいえ、ビジネス要件はそのレベルでの実装の詳細を指定していないためです。
SRP違反の実際の例は、次のようなコードです。
var message = "Your package will arrive before " + DateTime.Now.AddDays(14);
このコードは非常に単純ですが、テキストはビジネスのさまざまな部分によって決定されるため、テキストは予定配達日とは無関係に変更される可能性があります。
あなたのアプローチは現在手続き型です。 Stream
オブジェクトを分解して、外部から検証しています。それをしないでください-それはカプセル化を壊します。 Stream
が独自の検証を担当するようにします。適用するクラスがいくつかあるまで、SRPを適用しようとすることはできません。
検証に合格した場合にのみアクションを実行するStream
は次のとおりです。
class Stream
{
public void someAction()
{
if(!stream.canWrite)
{
throw new ArgumentException();
}
System.out.println("My action");
}
}
しかしnow私たちはSRPに違反しています! 「クラスには、変更する理由が1つだけあるべきです。」 1)検証と2)実際のロジックが混在しています。変更が必要になる理由は2つあります。
検証デコレータ でこれを解決できます。まず、Stream
をインターフェイスに変換し、それを具象クラスとして実装する必要があります。
interface Stream
{
void someAction();
}
class DefaultStream implements Stream
{
@Override
public void someAction()
{
System.out.println("My action");
}
}
これで、Stream
をラップし、検証を実行して、アクションの実際のロジックのために指定されたStream
に委ねるデコレーターを作成できます。
class WritableStream implements Stream
{
private final Stream stream;
public WritableStream(final Stream stream)
{
this.stream = stream;
}
@Override
public void someAction()
{
if(!stream.canWrite)
{
throw new ArgumentException();
}
stream.someAction();
}
}
これで、これらを好きなように作成できます。
final Stream myStream = new WritableStream(
new DefaultStream()
);
追加の検証が必要ですか?別のデコレータを追加します。
私は @ EricLippertの答え からのポイントが好きです:
渡されたオブジェクトがストリームのチェッカーにチェックがない理由を自問してください。答えは明白です。言語は、呼び出し側が非ストリームで渡されるプログラムのコンパイルを阻止します。
C#の型システムは、ニーズを満たすには不十分です。チェックは今日の型システムでは表現できない不変条件の強制の実装です。メソッドがnullにできない書き込み可能なストリームをとるという言い方ができたなら、それを書いたはずですが、そうではないので、次の最善策を実行しました。実行時に型制限を強制したことです。うまくいけば、あなたもそれを文書化して、あなたの方法を使う開発者がそれに違反する必要がなく、彼らのテストケースを失敗させて、そして問題を修正できるようにします。
EricLippertは、これが型システムの問題であることを認めています。また、単一責任の原則(SRP)を使用したいので、基本的には、この仕事を担当する型システムが必要です。
これを実際にC#で実行することは実際に可能です。コンパイル時にリテラルnull
をキャッチし、実行時に非リテラルnull
をキャッチできます。これは、完全なコンパイル時のチェックほど良くはありませんが、コンパイル時にまったく検出されないことを大幅に改善しています。
だから、あなたはC#がどのように _Nullable<T>
_ を持っているか知っていますか?それを逆にして_NonNullable<T>
_を作りましょう:
_public struct NonNullable<T> where T : class
{
public T Value { get; private set; }
public NonNullable(T value)
{
if (value == null) { throw new NullArgumentException(); }
this.Value = value;
}
// Ease-of-use:
public static implicit operator T(NonNullable<T> value) { return value.Value; }
public static implicit operator NonNullable<T>(T value) { return new NonNullable<T>(value); }
// Hack-ish overloads that prevent null-literals from being implicitly converted into NonNullable<T>'s.
public static implicit operator NonNullable<T>(Tuple<T> value) { return new NonNullable<T>(value.Item1); }
public static implicit operator NonNullable<T>(Tuple<T, T> value) { return new NonNullable<T>(value.Item1); }
}
_
今、書く代わりに
_public void Foo(Stream stream)
{
if (stream == null) { throw new NullArgumentException(); }
// ...method code...
}
_
、 書くだけ:
_public void Foo(NonNullable<Stream> stream)
{
// ...method code...
}
_
次に、3つのユースケースがあります。
ユーザーがnull以外のStream
を使用してFoo()
を呼び出します:
_Stream stream = new Stream();
Foo(stream);
_
これは望ましいユースケースであり、_NonNullable<>
_の有無にかかわらず動作します。
ユーザーがFoo()
をnullで呼び出しますStream
:
_Stream stream = null;
Foo(stream);
_
これは呼び出しエラーです。ここで_NonNullable<>
_は、これを行うべきではないことをユーザーに通知するのに役立ちますが、実際には停止しません。どちらにしても、これはランタイムNullArgumentException
になります。
ユーザーがnull
を使用してFoo()
を呼び出します:
_Foo(null);
_
null
は暗黙的に_NonNullable<>
_に変換されないため、ユーザーはIDEでエラーbeforeランタイムを受け取ります。これは、SRPがアドバイスするように、nullチェックを型システムに委任しています。
このメソッドを拡張して、引数について他のこともアサートできます。たとえば、書き込み可能なストリームが必要なので、コンストラクターでnull
と_struct WriteableStream<T> where T:Stream
_の両方をチェックする_stream.CanWrite
_を定義できます。これは実行時の型チェックのままですが、次のようになります。
型をWriteableStream
修飾子で修飾し、呼び出し元に必要を通知します。
コード内の1か所でチェックを行うため、毎回チェックと_throw InvalidArgumentException
_を繰り返す必要はありません。
型チェックの義務を型システムにプッシュすることで、SRPへの適合性が向上します(ジェネリックデコレーターによって拡張されます)。
クラスの仕事は契約を満たすのサービスを提供することです。クラスには常にコントラクトがあります。これは、クラスを使用するための一連の要件であり、要件が満たされていれば、クラスの状態と出力について約束します。この契約は、ドキュメントやアサーションを通じて明示的または暗黙的である場合がありますが、常に存在します。
クラスの規約の一部は、呼び出し側がコンストラクターにnullであってはならない引数を与えることです。コントラクトの実装isクラスの責任なので、呼び出し元がコントラクトの一部を満たしていることを確認することは、クラスの責任の範囲内にあると容易に見なすことができます。
クラスがコントラクトを実装するという考えは、エッフェルプログラミング言語のデザイナーである Bertrand Meyer と コントラクトによるデザイン のアイデアによるものです。エッフェル言語は、言語の契約部分の指定とチェックを行います。
他の回答で指摘されているように、SRPはしばしば誤解されています。 1つの機能だけを実行するアトミックコードを持つことではありません。それは、オブジェクトとメソッドが1つのことだけを行い、1つのことが1つの場所でのみ行われるようにすることです。
疑似コードの悪い例を見てみましょう。
class Math
private int a;
private int b;
def constructor(int x, int y)
if(x != null)
a = x
else if(x < 0)
a = abs(x)
else if (x == -1)
throw "Some Silly Error"
else
a = 0
end
if(y != null)
b = y
else if(y < 0)
b = abs(y)
else if(y == -1)
throw "Some Silly Error"
else
b = 0
end
end
def add()
return a + b
end
def sub()
return b - a
end
end
不合理な例では、Math#constructorの「責任」は、数学オブジェクトを使用可能にすることです。最初に入力をサニタイズし、次に値が-1でないことを確認することにより、これを行います。
これは有効なSRPです。これは、コンストラクターが実行していることが1つだけであるためです。 Mathオブジェクトを準備しています。しかし、それは非常に保守可能ではありません。 DRYに違反しています。
だからそれで別のパスを取ることができます
class Math
private int a;
private int b;
def constructor(int x, int y)
cleanX(x)
cleanY(y)
end
def cleanX(int x)
if(x != null)
a = x
else if(x < 0)
a = abs(x)
else if (x == -1)
throw "Some Silly Error"
else
a = 0
end
end
def cleanY(int y)
if(y != null)
b = y
else if(y < 0)
b = abs(y)
else if(y == -1)
throw "Some Silly Error"
else
b = 0
end
end
def add()
return a + b
end
def sub()
return b - a
end
end
このパスでは、DRYについて少し良くなりましたが、DRYを使う方法はまだあります。一方、SRPは少しずれているようです。これで、同じ仕事を持つ2つの関数ができました。 cleanXとcleanYはどちらも入力をサニタイズします。
もう一度やってみましょう
class Math
private int a;
private int b;
def constructor(int x, int y)
a = clean(x)
b = clean(y)
end
def clean(int i)
if(i != null)
return i
else if(i < 0)
return abs(i)
else if (i == -1)
throw "Some Silly Error"
else
return 0
end
end
def add()
return a + b
end
def sub()
return b - a
end
end
ようやくDRYの方が良くなり、SRPも同意したようです。 「消毒」作業を行う場所は1つだけです。
理論的にはコードはより保守可能でより優れていますが、バグを修正してコードを強化する場合、1か所で行うだけで済みます。
class Math
private int a;
private int b;
def constructor(int x, int y)
a = clean(x)
b = clean(y)
end
def clean(int i)
if(i == null)
return 0
else if (i == -1)
throw "Some Silly Error"
else
return abs(i)
end
end
def add()
return a + b
end
def sub()
return b - a
end
end
ほとんどの現実世界の場合、オブジェクトはより複雑になり、SRPは一連のオブジェクトに適用されます。たとえば、年齢は父、母、息子、娘に属している可能性があるため、生年月日から年齢を計算する4つのクラスを持つ代わりに、それを行うPersonクラスがあり、4つのクラスはそれから継承します。しかし、この例が説明に役立つことを願っています。 SRPはアトミックアクションではなく、「ジョブ」の実行についてです。