原則は変更する理由の1つを持つモジュールとして定義されます。私の質問は、確かにこれらの変更の理由は、コードが実際に変更され始めるまでわかりません。ほぼすべてのコードには、多くの理由がありますcould変更される可能性がありますが、これらすべてを予測し、これを念頭に置いてコードを設計しようとすると、コードが非常に貧弱になります。コードを変更する要求が出始めたときにのみ、実際にSRPの適用を開始する方が良いでしょうか?より具体的には、1つのコードが複数の理由で複数回変更されたため、変更する理由が複数あることを証明します。変更の理由を推測しようとすることは、非常に反アジャイルに聞こえます。
例としては、ドキュメントを印刷するコードがあります。 PDFに印刷するように変更するように要求が出され、次にそれを変更して文書にいくつかの異なる書式を適用するように2番目の要求が行われます。この時点で、複数の変更する理由(およびSRPの違反)。適切なリファクタリングを行う必要があります。
もちろん [〜#〜] yagni [〜#〜] の原則は、本当に必要になる前ではなく、SRPを適用するように指示します。しかし、あなた自身に尋ねるべき質問は次のとおりです実際にコードを変更する必要がある場合にのみ、最初にSRPを適用する必要がありますか?
私の経験では、SRPを適用するとはるかに早い段階でメリットが得られます。whereとhowを適用する必要がある場合コードの特定の変更。このタスクでは、既存の関数とクラスを読んで理解する必要があります。すべての関数とクラスに特定の責任がある場合、これは非常に簡単になります。したがって、私見では、コードが読みやすくなり、関数が小さくなり、自己記述的になる場合は、SRPを適用する必要があります。したがって、答えはyesです。新しいコードにもSRPを適用することは理にかなっています。
たとえば、印刷コードがドキュメントを読み取り、ドキュメントをフォーマットし、結果を特定のデバイスに印刷する場合、これらは3つの明確な分離可能な責任です。それらから少なくとも3つの関数を作成し、それらに名前を付けてください。例えば:
void RunPrintWorkflow()
{
var document = ReadDocument();
var formattedDocument = FormatDocument(document);
PrintDocumentToScreen(formattedDocument);
}
これで、ドキュメントのフォーマットを変更するための新しい要件またはPDFに印刷するための別の要件を取得すると、変更を適用する必要があるこれらの関数またはコードの場所が正確にわかります。さらに重要なことですが、そうではありません。
したがって、関数にアクセスすると、関数が「多すぎる」ために理解できず、変更を適用するかどうかと場所がわからない場合は、、次にを検討します。関数を個別の小さな関数にリファクタリングします。何かを変更する必要があるまで待つ必要はありません。コードは変更よりも10倍の頻度で読み取られ、小さい関数は非常に読みやすくなります。私の経験では、関数が特定の複雑さを持っている場合、常に関数を異なる責任に分割できます。将来どの変更が行われるかを知る必要はありません。ボブ・マーティンは通常、さらに一歩進んでいます。以下のコメントで私が挙げたリンクを参照してください。
編集:あなたのコメントへ:上記の例の外部関数の主な責任は、特定のデバイスに印刷したり、ドキュメントをフォーマットしたりすることではありません印刷ワークフローを統合するです。したがって、外部関数の抽象化レベルでは、「ドキュメントをフォーマットしない」または「ドキュメントを印刷する代わりにメールで送信する」などの新しい要件は「同じ理由」、つまり「印刷ワークフローが変更された」だけです。そのようなことについて話す場合、正しいレベルの抽象化に固執することが重要です。
あなたはSRPを誤解していると思います。
変更の唯一の理由は、コードを変更することではなく、コードが何をするかについてです。
「変更する理由が1つある」というSRPの定義は、まさにこの理由で誤解を招くと思います。それを額面通りに正確に受け止めてください。単一の責任の原則では、クラスまたは関数は正確に1つの責任を持つべきであると述べています。変更する理由が1つしかないことは、最初は1つのことだけを行うことの副作用です。将来的にどのように変化するかについて何も知らずに、コード内で単一の責任に向けて努力することができない理由はありません。
この種の最良の手がかりの1つは、クラス名または関数名を選択するときです。クラスの名前がすぐにわからない場合、または名前が特に長く複雑な場合、または名前に「マネージャー」や「ユーティリティ」などの一般的な用語が使用されている場合は、SRPに違反している可能性があります。同様に、APIを文書化するとき、説明している機能に基づいてSRPに違反しているかどうかがすぐに明らかになるはずです。
もちろん、プロジェクトの後半まで知ることができないSRPのニュアンスがあります-単一の責任のように見えたものは、2つまたは3つであることが判明しました。これらは、SRPを実装するためにリファクタリングする必要がある場合です。ただし、変更要求を受け取るまでSRPを無視する必要があるという意味ではありません。これはSRPの目的を無効にします。
例を直接説明するには、印刷方法を文書化することを検討してください。 「この方法でデータを印刷用にフォーマットし、それをプリンターに送信する」と言う場合、そのandがあなたを手に入れます:それは単一の責任ではなく、それは2つの責任です:フォーマットとプリンター。これを認識して2つの関数/クラスに分割すると、変更要求が発生したときに、各セクションが変更される理由は1つしかありません。
例としては、ドキュメントを印刷するコードがあります。 PDFに印刷するように変更するように要求が出され、次にそれを変更して文書にいくつかの異なる書式を適用するように2番目の要求が行われます。この時点で、複数の変更する理由(およびSRPの違反)。適切なリファクタリングを行う必要があります。
これらの変更に対応するためにコードを適応させるために多くの時間を費やすことによって、私は何度も自分の足を撃ちました。とんでもない愚かなPDFを印刷する代わりに。
使い捨てのパターンでは、コードが膨張する可能性があります。個別に意味をなさないコードのガベージパイルを作成する小さな特定のクラスでパッケージが汚染されている場合。印刷部分に到達する方法を理解するためだけに、何十ものソースファイルを開く必要があります。その上、実際の印刷を行う10行のコードを実行するためだけに配置されている数千行とは言わないまでも数百行のコードが存在する可能性があります。
シングルユースパターンは、ソースコードを削減し、コードの再利用を改善することを目的としています。これは、特殊化と特定の実装を作成するためのものでした。あなたがgo to specific tasks
するためのソースコードの一種のbullseye
。印刷に問題があったときは、どこで修正するかを正確に知っていました。
はい、すでにドキュメントを印刷するコードがあります。はい、PDFも印刷するようにコードを変更する必要があります。はい、ドキュメントのフォーマットを変更する必要があります。
usage
が大幅に変更されましたか?
リファクタリングによってソースコードのセクションが過度に一般化される場合。 printing stuff
の本来の目的が明示的ではなくなった時点で、ソースコードにあいまいなフラクチャリングが作成されました。
新しい男はこれをすぐに理解できるでしょうか?
組織を理解しやすいように、常にソースコードを維持してください。
私が開発者が接眼レンズを装着するのを見たことはあまりにも多く、細かい部分に焦点を合わせて、バラバラになったときに他の人が再びそれらを元に戻すことができないようにしています。
変更の理由は、最終的には、アプリケーションが実行される環境に関する仕様または情報の変更です。したがって、単一の責任の原則は、各コンポーネント(クラス、関数、モジュール、サービスなど)を記述して、仕様と実行環境をできる限り考慮しないようにすることです。
コンポーネントを作成するときに仕様と環境がわかっているので、原則を適用できます。
ドキュメントを印刷するコードの例を考えてみましょう。ドキュメントが最終的にPDFになることを考慮せずにレイアウトテンプレートを定義できるかどうかを検討する必要があります。可能ですので、SRPが指示するべきです。
もちろん、YAGNIはすべきでないと言っています。設計原理のバランスを見つける必要があります。
Flupは正しい方向に向かっています。もともと手順に適用されていた「単一責任原則」。たとえば、デニスリッチーは、関数は1つの処理を実行し、それを適切に実行する必要があると言います。次に、C++では、Bjarne Stroustrupはクラスは1つのことを実行し、それを適切に実行する必要があると言います。
経験則を除いて、これら2つは正式にはほとんどまたはまったく関係がないことに注意してください。彼らは、プログラミング言語で表現するのに便利なものだけに応えます。まあ、それは何かです。しかし、それはflupがドライブしているものとはかなり異なる話です。
最新の(つまり、アジャイルとDDD)実装は、プログラミング言語が表現できるものよりも、ビジネスにとって何が重要であるかに重点を置いています。驚くべきことは、プログラミング言語がまだ追いついていないことです。古いFORTRANのような言語は、当時の主要な概念モデルに適合する責任を獲得しました。カードリーダーを通過するときに各カードに適用されるプロセス、または(Cのように)各割り込みに伴う処理。その後、ADT言語が登場しました。これは、DDDの人々が後に重要なものとして再発明するものを捉えるまで成熟しました(ジムネイバーズがこれのほとんどを理解し、公開し、1968年までに使用していました)。 。 (これらはモジュールではありません。)
このステップは、振り子のスイングよりも進化ではありませんでした。振り子がデータに揺れるにつれて、FORTRANに固有のユースケースモデリングが失われました。主な焦点がデータまたは画面上の形状に関係している場合は問題ありません。これは、PowerPointのようなプログラム、または少なくともその単純な操作にとって優れたモデルです。
失われたのはシステムの責任です。 DDDの要素は販売していません。また、メソッドを適切にクラス化していません。システム責任を販売しています。あるレベルでは、単一責任の原則に基づいてシステムを設計する必要があります。
したがって、クラスメソッドについて語っていたRebecca Wirfs-Brockや私などの人を見ると、ユースケースの観点から話していることになります。それは私たちが販売するものです。以上がシステム運用です。ユースケースには単一の責任があるべきです。ユースケースがアーキテクチャーユニットになることはめったにありません。しかし、誰もがそうしたふりをしようとしていました。 SOA人などを目撃します。
これが、私がTrygve ReenskaugのDCIアーキテクチャに興奮している理由です。これは、上記のLean Architectureブックで説明されているものです。最後に、これは「単一の責任」に対する恣意的で神秘的な従順であったものにいくつかの本当の意味を与えます—上記の議論のほとんどで見られるように。その身長は人間のメンタルモデルに関連しています。最初にエンドユーザー、次にプログラマーです。それはビジネス上の懸念に関係しています。そして、ほとんど偶然にも、それはflupが私たちに挑戦する変化をカプセル化します。
私たちが知っている単一責任の原則は、起源の日から残った恐竜か、理解の代わりに使用する趣味の馬のどちらかです。優れたソフトウェアを実行するには、これらの趣味の馬を何匹か残しておく必要があります。そして、それは箱から出して考えることを必要とします。物事をシンプルで理解しやすい状態に保つことは、問題がシンプルで理解しやすい場合にのみ有効です。私はそれらの解決策にそれほど興味がありません。それらは典型的ではなく、課題が存在する場所ではありません。
新しいシステムを設計するときは、その存続期間中に行う必要のある変更の種類と、導入するアーキテクチャに与える変更の費用を考慮することをお勧めします。システムをモジュールに分割することは、誤りを犯すための高価な決定です。
優れた情報源は、ビジネスのドメインエキスパートが率いるメンタルモデルです。ドキュメント、フォーマット、PDFの例を見てみましょう。ドメインエキスパートは、ドキュメントテンプレートを使用してレターをフォーマットすることを教えてくれるでしょう。文房具でも、Wordでも、何でも。コーディングを開始する前にこの情報を取得して、デザインで使用できます。
これらについての素晴らしい読み物:Coplienによる無駄のないアーキテクチャ
はい、単一責任原則を新しいコードに適用する必要があります。
だが!責任とは何ですか?
「レポートの印刷は責任がありますか?」答えは「たぶん」だと思います。
SRPの定義を「変更する理由が1つだけある」として使用してみましょう。
レポートを印刷する関数があるとします。 2つの変更がある場合:
次に、最初の変更は「レポートスタイルの変更」で、もう1つは「レポート出力形式の変更」です。これらは異なるものであるため、これらを2つの異なる関数に配置する必要があります。
しかし、2番目の変更が次のようなものだったとしたら、
2b。レポートに別のフォントが必要なため、その機能を変更する
どちらの変更も「レポートスタイルの変更」であり、1つの機能にとどまることができます。
それでは、どこに行くのでしょうか?いつものように、物事をシンプルで理解しやすいものにしておくべきです。背景色の変更が20行のコードを意味し、フォントの変更が20行のコードを意味する場合は、2つの関数を再度作成します。それぞれが1行の場合は、1行にしてください。
「印刷」は、MVCの「ビュー」によく似ています。オブジェクトの基本を理解している人なら誰でもそれを理解するでしょう。
システムの責任です。これは、メカニズム(MVC)として実装されます。これには、プリンター(ビュー)、印刷されるもの(モジュール)、およびプリンターの要求とオプション(コントローラーから)が含まれます。
これをクラスまたはモジュールの責任としてローカライズしようとするのは至難の業であり、30年前の考え方を反映しています。それ以来多くのことを学び、それは文献や成熟したプログラマーのコードで十分に証明されています。
コードを変更する要求が出始めたときにのみ、実際にSRPの適用を開始する方が良いでしょうか?
理想的には、コードのさまざまな部分の責任について、すでに十分理解していることが理想的です。最初の本能に従って責任を分割します。おそらく、使用しているライブラリが実行したいことを考慮に入れます(タスク、責任、ライブラリへの委任は、ライブラリが実際にタスクを実行できることを条件として、通常、実行するのが素晴らしいことです) )。次に、変化する要件に応じて、責任の理解を深めます。システムを最初に理解すればするほど、責任の割り当てを根本的に変更する必要性が少なくなります(ただし、責任がサブ責任に分割されるのが最善であることに気付く場合もあります)。
それについて心配するのに長い時間を費やすべきではありません。コードの重要な特徴は、後で変更できることです。最初から完全に正しくする必要はありません。将来どのようなミスを犯さないように、時間の経過とともにどのような形の責任があるかを理解するようにしてください。
例としては、ドキュメントを印刷するコードがあります。 PDFに印刷するように変更するように要求が出され、次にそれを変更して文書にいくつかの異なる書式を適用するように2番目の要求が行われます。この時点で、複数の変更する理由(およびSRPの違反)。適切なリファクタリングを行う必要があります。
これは厳密には、全体的な責任(コードの「印刷」)には副次的な責任があり、細かく分割する必要があることを示しています。これはSRP per seの違反ではなく、パーティション化(おそらく「フォーマット」および「レンダリング」サブタスクへの)がおそらく必要であることを示しています。それらの責任を明確に説明して、サブタスクの実装を確認せずに、サブタスク内で何が行われているのかを理解できますか?可能であれば、それらは合理的な分割である可能性があります。
簡単な実際の例を見てみると、もっとわかりやすいかもしれません。 _Java.util.Arrays
_のsort()
ユーティリティメソッドについて考えてみましょう。それは何をするためのものか?配列を並べ替えるだけです。それは要素を出力しません、それは最も道徳的に適合するメンバーを見つけません、それは口笛を吹きませんDixie。 配列を並べ替えるだけです。方法を知る必要はありません。ソートはそのメソッドの唯一の責任です。 (実際、Javaには多くのソート方法があります。プリミティブ型を扱うには技術的にやや醜い理由があります。ただし、それらにはすべて同等の責任があるので、それに注意する必要はありません。 。)
メソッド、クラス、モジュールを作成し、それらが明確に指定された役割を持つようにします。これにより、一度に理解する必要のある量が抑えられ、大規模なシステムの設計と保守を処理できるようになります。