「Do One Thing」パラダイムが有害になるのはいつですか?


21

引数のために、指定されたファイルの内容を行ごとに出力するサンプル関数を次に示します。

バージョン1:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  string line;
  while (std::getline(file, line)) {
    cout << line << endl;
  }
}

関数は、抽象化の1つのレベルで1つのことを行うことが推奨されることを知っています。私にとっては、上記のコードはほとんど1つのことを行い、かなりアトミックです。

一部の書籍(Robert C. MartinのClean Codeなど)では、上記のコードを個別の機能に分割することを提案しているようです。

バージョン2:

void printFile(const string & filePath) {
  fstream file(filePath, ios::in);
  printLines(file);
}

void printLines(fstream & file) {
  string line;
  while (std::getline(file, line)) {
    printLine(line);
  }
}

void printLine(const string & line) {
  cout << line << endl;
}

私は彼らが何を達成したいのか理解しています(ファイルを開く/行を読む/行を印刷する)が、ちょっとやり過ぎではないでしょうか?

元のバージョンはシンプルで、ある意味で既に1つのことを行っています-ファイルを印刷します。

2番目のバージョンは、最初のバージョンよりもはるかに読みにくい多数の非常に小さな関数につながります。

この場合、コードを1か所に配置した方が良いと思いませんか?

「Do One Thing」パラダイムはどの時点で有害になりますか?


13
この種のコーディング方法は、常にケースバイケースに基づいています。単一のアプローチはありません。
iammilind

1
@Alex-受け入れられた答えは、文字通り質問とは関係ありません。それは本当に奇妙だと思う。
ChaosPandion

2
リファクタリングされたバージョンは上下逆になっているため、読みにくくなっています。ファイルダウン読書、あなたが見ることを期待するprintFileprintLines最終的には、とprintLine
アンソニーペグラム

1
@Kev、私は再び、特にそのカテゴリー化に関して、意見の相違しかできません。それは物足りない、それがポイントです!特に、2番目のバージョンが読みにくい可能性があると言うのはOPです。クリーンコードを2番目のバージョンのインスピレーションとして具体的に挙げているのはOPです。私のコメントは、基本的にClean Codeは彼にそのような方法でコードを書かせないだろうということです。順序は実際には読みやすさのために重要です。新聞記事を読むようにファイルを読み、基本的に興味がなくなるまで詳細を取得します。
アンソニー

1
詩を逆読みすることを期待していないように、特定のクラス内で最初に最低レベルの詳細を表示することも期待していません。あなたのポイントまで、このコードはすぐにソートするのに少しの時間しかかかりませんが、私はこのコードが彼が書こうとしている唯一のコードではないと仮定するでしょう。私の要点として、彼がClean Codeを引用する場合、彼ができることはそれに従うことです。コードの順序が狂っている場合、他の方法よりも確かに読みにくくなります。
アンソニー

回答:


15

もちろん、これは単に「1つは何ですか?」行を読むことと行を書くことは別ですか?または、1つのストリームから別のストリームに行をコピーして、1つのものと見なしますか?またはファイルをコピーしますか?

それに対する難しい客観的な答えはありません。それはあなた次第です。あなたが決めることができます。あなた決める必要があります。「1つのことを行う」パラダイムの主な目標は、可能な限り理解しやすいコードを作成することです。そのため、それをガイドラインとして使用できます。残念ながら、これは客観的に測定することもできないため、あなたの腸の感覚と「WTF?」に頼らなければなりません。コードレビューでカウントします

IMOの1行のコードのみで構成される関数は、めったに価値がありません。あなたがprintLine()使用してオーバーも利点がありませんstd::cout << line << '\n'1を直接に。が表示される場合printLine()、その名前が示すとおりに動作するものと想定するか、検索して確認する必要があります。見るstd::cout << line << '\n'と、文字列の内容をの行として出力する標準的な方法であるため、それが何をするのかすぐにわかりstd::coutます。

ただし、このパラダイムのもう1つの重要な目標は、コードの再利用を可能にすることであり、これはより客観的な手段です。たとえば、2番目のバージョンでは、あるストリームから別のストリームに行をコピーする、広く有用なアルゴリズムであるように簡単に記述printLines() できます。

void copyLines(std::istream& is, std::ostream& os)
{
  std::string line;
  while( std::getline(is, line) );
    os << line << '\n';
  }
}

このようなアルゴリズムは、他のコンテキストでも再利用できます。

次に、この1つのユースケースに固有のすべてを、この汎用アルゴリズムを呼び出す関数に入れることができます。

void printFile(const std::string& filePath) {
  std::ifstream file(filePath.c_str());
  printLines(file, std::cout);
}

1 ではなくを使用したことに注意してください。改行を出力するためのデフォルトの選択である必要があり奇数の場合です'\n'std::endl'\n'std::endl


2
+1-私はほとんど同意しますが、「直感」以上のものがあると思います。問題は、実装の詳細をカウントすることで「1つのこと」を判断することです。私にとって、この関数は単一の明確な抽象化を実装する(およびその名前が記述する)必要があります。関数に「do_x_and_y」という名前を付けないでください。実装は、いくつかの(単純な)ことを行うことができ、実行する必要があります-そして、それらの単純なものはそれぞれ、いくつかのさらに単純なものなどに分解できます。それは、特別なルールを伴う機能的な分解です-関数(およびその名前)はそれぞれ、単一の明確な概念/タスク/何でも記述する必要があります。
Steve314

@ Steve314:実装の詳細を可能性としてリストしていません。あるストリームから別のストリームに行を明確にコピーすることは、単なる抽象化です。またはそれは?代わりにdo_x_and_y()関数do_everything()に名前を付けることで、簡単に回避できます。はい、それはばかげた例ですが、このルールは悪いデザインの最も極端な例を防ぐことさえできないことを示しています。IMOこれ、慣例によって決定されるのと同じくらい直感的な決定です。そうでなければ、客観的であれば、そのためのメトリックを思い付くことができますが、それはできません。
sbi

1
私は矛盾するつもりはありませんでした-追加を提案するだけです。私が言い忘れたことは、質問から、printLine等への分解が有効であるということだと思います-これらはそれぞれ単一の抽象化です-しかし、それは必要という意味ではありません。printFileすでに「一つのこと」です。これを3つの独立した下位レベルの抽象化に分解できますが、あらゆる抽象化レベルで分解する必要はありません。各関数は「1つのこと」を行う必要がありますが、すべての可能な「1つのこと」が関数である必要はありません。複雑すぎるコールグラフに移動すること自体が問題になる可能性があります。
Steve314

7

機能が「1つのこと」だけを行うことは、2つの望ましい目的を達成するための手段であり、神からの命令ではありません。

  1. 関数が「1つのこと」のみを行う場合、コードの重複やAPIの膨張を回避するのに役立ちます。これは、より複雑で複雑な処理を行うために、より複雑で複雑な機能を組み合わせて、 。

  2. 関数に「1つのこと」だけをさせる があり、コードをより読みやすくします。これは、物事を切り離すことによって、物事を切り離すことを可能にする構成の冗長性、間接性、および概念的なオーバーヘッドを失うよりも、物事を切り離すことによってより明確で推論しやすくなるかどうかに依存します。

したがって、「1つのこと」は不可避的に主観的であり、プログラムに関連する抽象化のレベルに依存します。printLines単一の基本的な操作であり、気にかけている、または気遣うことを予見している行を印刷する唯一の方法であると考えられる場合、あなたの目的のためにprintLines1つのことだけを行います。2番目のバージョンの方が読みやすい(そうではない)場合を除き、最初のバージョンは問題ありません。

抽象化のより低いレベルをより細かく制御する必要があり、微妙な複製と組み合わせの爆発(つまり、printLinesファイル名とオブジェクト、コンソールとファイルの完全な分離)printLinesで終わるfstream場合、そのレベルで複数のことをしていますあなたが気にする抽象化の。printLinesprintLinesprintLines


3つ目を追加すると、小さな関数がより簡単にテストされます。関数が1つのことだけを行う場合、必要な入力はおそらく少ないため、独立してテストするのが容易になります。
PersonalNexus

@PersonalNexus:テストの問題には多少同意しますが、実装の詳細をテストするのは馬鹿げています。私にとって、単体テストは、私の答えで定義されている「1つのこと」をテストする必要があります。より細かいことは、テストを脆弱にし(実装の詳細を変更するにはテストを変更する必要があるため)、コードは煩わしく冗長で間接的なものになります(テストをサポートするためだけにインダイレクションを追加するため)。
dsimcha

6

この規模では、問題ではありません。単一機能の実装は完全に明白で理解しやすいものです。ただし、もう少し複雑さを追加すると、反復をアクションから分割することが非常に魅力的になります。たとえば、「*。txt」などのパターンで指定された一連のファイルから行を印刷する必要があるとします。次に、アクションから反復を分離します。

printLines(FileSet files) {
   files.each({ 
       file -> file.eachLine({ 
           line -> printLine(line); 
       })
   })
}

これで、ファイルの繰り返しを個別にテストできます。

テストを簡素化するか、読みやすさを向上させるために、関数を分割しました。データの各行で実行されるアクションがコメントを正当化するほど複雑な場合、私はそれを確かに別の機能に分割します。


4
釘付けしたと思う。行を説明するコメントが必要な場合は、常にメソッドを抽出します。
ロジャーCSワーナーソン

5

物事を説明するコメントが必要だと感じたら、メソッドを抽出します。

名前の言うことだけを明白な方法で行うメソッドを作成するか、巧妙な名前のメソッドを呼び出してストーリーを伝えます。


3

単純な場合でも、単一責任原則が管理の改善に役立つという詳細が欠落しています。たとえば、ファイルを開くときに問題が発生するとどうなりますか。例外処理を追加してファイルアクセスのエッジケースを強化すると、関数に7〜10行のコードが追加されます。

ファイルを開いた後は、まだ安全ではありません。それはあなたから引っ張られる可能性があります(特にネットワーク上のファイルの場合)、メモリ不足になる可能性があります

ワンライナーのprintlineは十分無害なようです。ただし、ファイルプリンターに新しい機能(テキストの解析と書式設定、さまざまな種類のディスプレイへのレンダリングなど)が追加されると、機能が大きくなり、後で感謝します。

SRPの目標は、一度に1つのタスクについて考えることです。大きなブロックのテキストを複数の段落に分割して、読者が理解しようとしているポイントを理解できるようにします。これらの原則に従ったコードを書くにはもう少し時間がかかります。しかし、そうすることで、そのコードを読みやすくします。コード内のバグを追跡し、きちんと分割されていることに気付くと、将来の自分がどれだけ幸せになるかを考えてください。


2
私はこの意見に賛成しませんでしたが、私は論理に賛成していませんが、論理が好きだからです!将来何が起こるかについての複雑な思考に基づいて構造を提供することは非生産的です。必要なときにコードを分解します。必要になるまで物事を抽象化しないでください。現代のコードは、うまく動作してしぶしぶ適応するコードを書くだけではなく、規則に従順に従おうとする人々に悩まされています。優秀なプログラマーは怠け者です。
イットリル

コメントありがとう。注:早すぎる抽象化を主張するのではなく、後で簡単に行えるように論理演算を分割するだけです。
マイケルブラウン

2

私は個人的に後者のアプローチを好む。なぜならそれはあなたが将来の仕事を節約し、「一般的な方法でそれをする方法」の考え方を強制するからだ。それにもかかわらず、あなたの場合、バージョン1よりもバージョン1の方が優れています-バージョン2で解決された問題があまりにも些細でfstream固有であるからです。次の方法で行うべきだと思います(Nawazが提案したバグ修正を含む):

汎用ユーティリティ関数:

void printLine(ostream& output, const string & line) { 
    output << line << endl; 
} 

void printLines(istream& input, ostream& output) { 
    string line; 
    while (getline(input, line)) {
        printLine(output, line); 
    } 
} 

ドメイン固有の機能:

void printFile(const string & filePath, ostream& output = std::cout) { 
    fstream file(filePath, ios::in); 
    printLines(file, output); 
} 

これでprintLinesprintLineだけでfstreamなく、どのストリームでも機能します。


2
同意しません。そのprintLine()関数には値がありません。私の答えをご覧ください。
sbi

1
さて、printLine()を保持する場合、行番号または構文の色付けを追加するデコレータを追加できます。そうは言っても、理由を見つけるまでこれらのメソッドを抽出しません。
ロジャーCSワーナーソン

2

すべてのパラダイム従う(必ずしも引用したものだけでなく)にはある程度の規律が必要です。したがって、「言論の自由」を減らすために、最初のオーバーヘッドが発生します(少なくともそれを学ばなければならないからです!)。この意味で、すべてのパラダイムは、そのオーバーヘッドのコストが、パラダイムがそれ自体を維持するように設計されている利点によって過剰に補償されない場合、有害になる可能性があります。

したがって、質問に対する真の答えには、次のような将来を「予測」する優れた能力が必要です。

  • 私はやるために必要AB
  • 確率、何が近い将来、私もやるために必要になるA-B+(つまり、何かAとBのように見えますが、少しだけ異なるもの)は?
  • A +がA*またはになる将来の確率はどのくらいA*-ですか?

その可能性が比較的高い場合、AとBについて考えながら、それらの可能なバリアントについても考えて、共通部分を分離して再利用できるようにすることは良いチャンスです。

その可能性が非常に低い場合(周囲のバリアントAが本質的にAそれ自体にすぎない場合)、Aをさらに分解する方法を検討すると、無駄な時間が生じる可能性が高くなります。

例として、この本当の話を話しましょう。

教師としての私の過去の生活の中で、ほとんどの学生のプロジェクトで、事実上すべてがCストリングの長さ計算する独自の機能を提供していることを発見しました。

いくつかの調査の後、私は、頻繁な問題であるため、すべての学生がそのために関数を使用するというアイデアに気付くことを発見しました。そのためのライブラリ関数があると彼らに言った後(strlen)、彼らの多くは、問題はとても単純で些細なものであるため、Cライブラリマニュアルを探すよりも独自の関数(2行のコード)を書く方が効果的であると答えました(それは1984年で、WEBとgoogleを忘れていました!)そのための準備ができている機能があるかどうかを確認するために、厳密なアルファベット順で。

これは、「ホイールを再発明しない」というパラダイムが、効果的なホイールカタログなしで有害になる可能性がある例です。


2

あなたの例は、特定のタスクを実行するために昨日必要であったスローアウェイツールで使用しても問題ありません。または、管理者が直接制御する管理ツールとして。次に、顧客に適した堅牢なものにします。

適切なエラー/例外処理に意味のあるメッセージを追加します。たぶん、あなたは、例えば、存在しないファイルをどのように扱うかなどの決定を含め、パラメータの検証が必要です。情報やデバッグなどのさまざまなレベルのログ機能を追加します。チームの同僚がそこで何が起こっているのかを把握できるように、コメントを追加します。簡潔にするために通常は省略され、コード例を示すときに読者の演習として残されるすべての部分を追加します。ユニットテストを忘れないでください。

あなたの素晴らしく、非常に線形の小さな関数は、別々の関数に分割されるように頼む複雑な混乱で突然終了します。


2

IMOは、機能が他の機能に委任すること以外はほとんど何も行わないことが有害になる。悪いことをしています...

元の投稿から

void printLine(const string & line) {
  cout << line << endl;
}

十分な知識があれば、printLineはまだ2つのことを実行していることに気付くかもしれません:coutへの行の書き込みと「行終了」文字の追加。一部の人々は、新しい関数を作成することでそれを処理したいと思うかもしれません:

void printLine(const string & line) {
  reallyPrintLine(line);
  addEndLine();
}

void reallyPrintLine(const string & line) {
  cout << line;
}

void addEndLine() {
  cout << endl;
}

ああ、今、私たちは問題をさらに悪化させました!今では、printLineが2つのことを行うのは明らかです!!! 1!行を印刷すると行自体を印刷し、行末文字を追加するという避けられない問題を取り除くために想像できる最もばかげた「回避策」を作成することはあまり愚かではありません。

void printLine(const string & line) {
  for (int i=0; i<2; i++)
    reallyPrintLine(line, i);
}

void reallyPrintLine(const string & line, int action) {
  cout << (action==0?line:endl);
}

1

短い答え...それは状況によります。

これについて考えてみてください。将来、標準出力だけでなくファイルに印刷したい場合はどうでしょう。

私はYAGNIが何であるかを知っていますが、いくつかの実装が必要であると知られているが、延期される場合があるかもしれないと言っています。そのため、アーキテクトまたはその機能を知っている人なら誰でもファイルに出力できる必要がありますが、すぐには実装を行いたくないでしょう。そのため、彼はこの追加の関数を作成するため、将来、出力を1か所で変更するだけで済みます。理にかなっていますか?

ただし、コンソールでの出力のみが必要な場合は、あまり意味がありません。「ラッパー」を上書きすることcout <<は役に立たないようです。


1
しかし、厳密に言えば、printLine関数は行の繰り返しとは異なる抽象化レベルではありませんか?

@Petrそう思うので、機能を分離することをお勧めします。概念は正しいと思いますが、ケースバイケースで適用する必要があります。

1

「1つのことをする」という長所に章を捧げる本がある理由は、4ページの長さで6レベルの条件をネストする開発者がまだいるからです。コードがシンプルで明確であれば、正しく実行できています。


0

他のポスターがコメントしているように、一つのことをすることは規模の問題です。

また、One Thingのアイデアは、副作用によってコーディングを停止することです。これは、「正しい」結果を得るために特定の順序でメソッドを呼び出す必要があるシーケンシャルカップリングによって例示されます。

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