web-dev-qa-db-ja.com

再帰をオプションにする:悪い習慣?

私がこのような関数を持っているとしましょう:

/*
Mode is a bool that determines whether the function uses 
it's recursive mode or not
*/
public static int func(int n, boolean mode) {
    if (!mode) someCalculation();
    else func(anotherCalculation(), mode);
}

つまり、modeが何であるかに応じて、同じ関数が再帰的計算または非再帰的計算のいずれかを決定します。この場合、2つの計算は関連していませんが、同じデータに対して実行する必要があります。どんな状況でも、これを行う方が、1つの関数で2つのジョブを実行する2つの異なる関数を呼び出すよりも良い方法でしょうか?

4

残りの回答は、一般的にスポットオンです。この関数を必要とするコンテキスト、またはあなたの視点で「二分」である関数に応じて、それだけを追加します(つまり、再帰的であるかどうかは、現実世界の大部分の計算ケースでは必ずしも問題ではありません。しかし、それはあなたにとって重要なようです)、実際に実装を抽象化の背後に隠したい場合があります:(ここではC#を使用)

public interface IMathCalculator
{
    //....
    int Factorial(int n);
    //....
}

次に、2つの実装を使用できます。

public class MathCalculator : IMathCalculator
{
    //....
    public int Factorial(int n)
    {
        //Implement factorial iteratively.
    }
    //....
}

public class RecursionEnabledMathCalculator : IMathCalculator
{
    //....
    public int Factorial(int n)
    {
        //Perform calculation recursively.
    }
    //....
}

このように、モードのチェックをスキップすることもできます。私はクリストフの note に同意しますが:

例外:呼び出し側が計算を行うためのより適切な方法を許可できる何かを知っていて、呼び出し先が自分で見つけることができない場合、戦略パラメーターは完全に受け入れられます。

これは最初にデザインのにおいとして扱うことをお勧めします。コードの一部knowsが必要とする特定の実装の場合は、細部が重要なライブラリの奥深くにあるそれだけのように、非表示にしておくとよいでしょう。実装の詳細は、他の実装の詳細同じコンテキストの(せいぜい)にのみ関係するべきです。この種の実装の詳細(プログラミング/数学的最適化)はデバッグが難しい、または理由を説明するであり、他のメンテナが引き継ぐ場合は非常に低い保守性があります。

ちなみに、KISSの原則に基づいて解釈すると、投稿した関数が期待どおりに機能しません。正しく解釈している場合、func(2, false)は3を返し、func(2, true)を返します。は0を返します(func(-3, true)は単にスタックオーバーフロー例外が発生します)。trueで渡された正の引数が質問のこの部分に基づいて0を返すという事実をバイパスします。

2つの計算は関連していませんが、同じデータに対して実行する必要がある場合

同じint n入力でtrueまたはfalseを渡すと異なる結果が返され、これが意図的なである場合、より良いrethinkあなたのデザインほとんどすべての人が関数のシグネチャを読んだだけでは、ソースコードへのアクセス権を持っている他の人以外は何も理解できませんそしてチェックアウトすることを忘れないでください

また、質問に直接回答するには:

どのような状況でも、1つの関数で2つのジョブを実行する2つの異なる関数を呼び出すよりも、これを行う方が良いでしょうか?

いいえ、そのような状況はほとんどありません。あなたがどちらが良いかを意味する場合、あなたの投稿された例またはこれ:

public static int func(int n, boolean mode) {
    if (!mode) return funcNonRecursive(n);
    else return funcRecursive(n);
}

private static int funcNonRecursive(int n) {
    return n + 1;
}

private static int funcRecursive(int n) {
    return n != 0 ? func(n - 1, mode) : 0;
}

その後、追加のメソッド呼び出しのオーバーヘッドを気にしない限り、これは投稿された例よりもほとんど常に優れています。

  • 非常にタイムクリティカルなコードを書かなければならない非常に少数派に属しています。
  • 最近のコンパイラーは通常、このタイプのコードを(インライン化などによって)最適化するため、おそらく心配する必要はありません。

いずれにせよ、可能性は非常に低いので、readabilityが主な関心事である場合、この方法はより人間に優しく、保守可能です。

4
Vector Zita

ここでの指針となる原則は 懸念の分離 です。

  • 呼び出し元は、計算の実行方法に関係なく、結果に関連するパラメーターのみを使用して関数を呼び出す必要があります。
  • 呼び出された関数は、反復的、再帰的、キャッシュ、またはこれらの組み合わせのいずれであっても、ニーズに最も適した結果を提供する必要があります。

例外:呼び出し側が計算を行うためのより適切な方法を許可できる何かを知っていて、呼び出し先がそれ自体を見つけることができない場合、戦略パラメーターは完全に受け入れられます。

関数も 1つのことだけを行い、うまく実行する にする必要があります。 modeの唯一の目的が2つのうちから選択することである場合、1つの関数で2つの無関係なものを結合することは避けてください。

19
Christophe

関数は抽象化のためのメカニズムの1つであり、抽象化の品質の1つの側面は、使用するプログラマである呼び出し元にとっての有用性です。

呼び出し側を想像してみましょう。2つの異なる計算が行われているため、呼び出し側が常に定数を渡すのではなく、ブール値に変数を使用する可能性があります(trueとfalse)。

常に真である場合、呼び出しコードはブール定数を渡すなど、必要な2つの計算のどちらかを知っています。このアプローチは、2つの別の概念を融合するだけで、値がない場合の複雑さが増します。

ただし、1つの計算から別の計算に動的に切り替える何らかの理由がある場合は、2つのアプローチを一緒に混合することにメリットがあるように思われます。 ただし、その場合、3つのバージョンを提供します。2つは個別のジョブを実行するブール値なし、3つ目は他の2つのうちの1つを呼び出すブール値付きです。どちらが欲しいかを知っている発信者は直接選択します。

いずれの場合も、1つの例として、IDEの方が役立ちます。特定のパラメーターを渡すすべてのメソッドを見つけるのは、より専門的なメソッドを見つけるよりも困難です。一般に、メンテナンスと一緒にする必要のない概念を融合すると、他のすべてはより困難になります。

関数が再帰的かどうかの問題は、この分析とは無関係であることがわかります。私にとっての問題は、別の方法で分離しておくことができる2つのことを融合することには真のメリットがないということです。これらの別の方法で別々の操作は、両方がintを受け取って返す(同じデータで操作する)ためにまとめる必要があるという考えにはメリットがありません。

5
Erik Eidt

これについては、ロバートC.マーティンのクリーンコードからの言葉でお答えします。

関数の引数

関数の引数の理想的な数はゼロ(ニラディック)です。次に1つ(モナディック)、2つ(ダイアディック)がそれに続きます。 3つの引数(3項)は可能な限り避けてください。 3つ以上(ポリアディック)は、非常に特別な正当化を必要とします。したがって、とにかく使用しないでください。

[...]

フラグ引数

フラグの引数は醜いです。ブール値を関数に渡すことは本当にひどい習慣です。メソッドのシグネチャをすぐに複雑にし、この関数が複数のことを行うことを大声で宣言します。フラグが真の場合は1つの処理を行い、フラグが偽の場合は別の処理を実行します。

リスト3-7では、呼び出し元が既にそのフラグを渡していたため、選択肢がなく、リファクタリングのスコープを関数以下に制限したいと考えました。それでも、メソッド呼び出しrender(true)は、貧しい読者を混乱させるだけです。呼び出しの上にマウスを置いてrender(boolean isSuite)を表示すると、少しは役立ちますが、それほど役立ちません。関数を2つに分割する必要があります:renderForSuite()renderForSingleTest()

このアドバイスに従って、関数を2つに分割する必要があります。

public static int func(int n) {
    return n + 1;
}

public static int funcRecursive(int n) {
    return n != 0 ? funcRecursive(n - 1) : 0;
}
1
CJ Dennis

確かに

あなたの特定の例は私には意味がありませんが、再帰的に実行するか、そうでないたくさんの操作があります。

特に、トラバースにコストがかかる階層構造がある場合があります。この構造を検索したり、統計をフェッチしたりすると、呼び出し元がcertainty(再帰的に)またはを必要とする場合があります)approximation(最上位ノードまたはキャッシュされた結果に基づいて推定)。

また、再帰の正確なレベルを構成できる必要がある場合もあります。たとえば、dutreeなどのUnixコマンドでサポートされている引数の--max-depthタイプについて考えてみます。階層をユーザーにレンダリングする場合、多くの場合、トラバースして表示する階層の量を任意に制限するメカニズムを提供することは理にかなっています。

0
svidgen

あなたの例では、クライアントは2つのことを同時に決定しています。

  1. 何をすべきか(1つ追加)
  2. 1を追加する方法(再帰的かどうか)

私はこのようなコードがそうでない場合を想像することはできません:

  1. 理解と保守が難しく、そのためバグが発生しやすい
  2. ユニットテストが難しい
0
Tanager4