web-dev-qa-db-ja.com

DRY原則により、コードが複雑になり、理解が難しくなることがよくあります

これは私のプロジェクトでよく起こります。時々私はこの他の部分に非常に似ているコードのこの部分を持っていますが、それでも数行はコードをクリーンで重複なしに保つことを複雑にします。ここに最近のプロジェクトの例があります。

私は2つのことを実行できるプログラムに取り組んでいます。Aは何らかの方法で投影された画像を生成し、Bは他の投影でいくつかの画像を生成します。

選択肢A:

void generateImage(width, height)
{
    vector<Pixel> pixels;

    for(x = 0; x < width, x++)
    {
        for(y = 0; y < height; y++)
        {
            Position pos = projectPixel(x, y);
            Pixel p = someCrazyFunc(pos);
            pixels.add(p);
        }
    }
}

選択肢B:

void generateImage(width, height, images)
{
    vector< Pixel > pixels;

    foreach(image in images)
    {
        for(x = 0; x < width, x++)
        {
            for(y = 0; y < height; y++)
            {
                Position pos = projectPixel2(x, y, image);
                Pixel p = someCrazyFunc(pos);
                pixels.add(p);
            }
        }
    }
}

ご覧のとおり、ほとんど同じです。 Aとの唯一の違いはposを同じ方法で計算しないことと、複数の画像を生成する必要があることです。

実際には、より多くのコードがあるため、これは少し異なります。基本的に、someCrazyFuncを呼び出す前にループのすべてのposを計算する必要があるため、およびスライス(最初の1000番目の行、次に1000など)によって画像を計算するため、より多くのループがあります。これは、重複する4行だけではなく、ループと関数呼び出しであるとだけ言っています。

私はそれをこのように修正しました:

void generateImage(width, height, images, projectPixelFunction)
{
    vector< Pixel > pixels;

    foreach(image in images)
    {
        for(x = 0; x < width, x++)
        {
            for(y = 0; y < height; y++)
            {
                Position pos = projectPixelFunction(x, y, image);
                Pixel p = someCrazyFunc(pos);
                pixels.add(p);
            }
        }
    }
}

したがって、基本的にAの場合、numImagesは1に等しくなり、古いprojectPixel関数はダミーパラメータを取ります。

これは問題の許容できる解決策ではないと思います。私は正直に言って、2つのほとんど同じ機能を持つことを好みます。しかし、複雑さが増すにつれ、重複したコードがたくさんあるか、奇妙な解決策を伴うコードがたくさんあるでしょう。

どうすればこれを克服できますか?私は何を考慮していませんか?

編集:明確にするために、私はパラメーターとしての関数の使用に反対していません。私が好きではないのは、ダミーパラメーターの使用です。選択Aのコンテキストでは目的はありません。

6
Aulaulz

DRYは、「可能な限り最小限の行数を使用する」、または「他のコードのように見えるコードを記述しない」という意味では絶対にありません。

DRYとは、2つの異なる場所で同じことを行うコードを持つことを指します。ただし、「コードが同じに見える」という意味ではなく、「同じ概念的なタスクを実行する」という意味です。コードがどのように見えるかは重要ではありませんが、それが何をするかが重要です。同じことを別の場所で行わないでください。

「誤って」同じものとして始まったコードに出くわすこともあります。この場合、2つの異なる概念の基本的な実装は同じであり、プログラマはその違いを処理するための条件を追加しました。これは大きな問題ではありません。要件がドリフトし始め、コードブロックがますます多くの条件を1つのタスクと別のタスクの間で混同するようになるまでは。

反対の方向では、与えられた概念を表現するために常に正確に10億の異なる方法が存在します。異なるコードを別の概念であることを意味することを誤解させないでください。ラムダ述語がある場所に実装されていて、forループと条件分岐が別の場所に実装されているからといって、繰り返していないわけではありません。

19
jmoreno

@JacquesBの答えは問題ありませんが、C#では、一般化されたgenerateImage内のすべての画像に対してループを維持したい場合は、同等のアプローチがあります。コンテキストAでは、ラムダ式を渡すだけです

 (x,y,image) => projectPixel(x,y)

generateImageへの関数パラメーターとして、projectPixelにダミーパラメーターを追加する必要はありません。この方法では、クロージャーも必要ありません。

5
Doc Brown

Bでクロージャを使用すれば、醜いダミーパラメータを回避できます。次のようなもの(C#では、選択した言語に翻訳できます):

Vector<Pixel> generateImage(int width, int height, Func<Position, int, int> projectPixelFunction)
{
    Vector<Pixel> pixels;
    for(x = 0; x < width, x++)
    {
        for(y = 0; y < height; y++)
        {
            Position pos = projectPixelFunction(x, y);
            Pixel p = someCrazyFunc(pos);
            pixels.add(p);
        }
    }
    return pixels;
}

A:

void generateImage(width, height)
{
    Vector<Pixel> pixels = generateImage(width, height, projectPixel);    
}

B:

void generateImage(width, height, numImages)
{
    Vector<Pixel> pixels;

    for(image = 0; image < images; image++)
    {
        pixels += generateImage(width, height, (x,y)=>projectPixel2(x, y, image));
    }
}

これがコードを理解しやすくする必要がないことに同意します。あなたが本当に望んでいるのは:

  • x、yではなくPositionタイプを使用する
  • 個々のピクセルの処理からループを分離します。
4
JacquesB

何かが足りないかもしれませんが、projectPixelとprojectPixel2は別々に存在しているように感じます。それらのパラメーターが異なるためです。パラメータをオブジェクトに抽象化し、少なくともコードの大部分を抽象化しないのはなぜですか(for(x ...から)一般的でしょう。

そう:

MyObj obj = new MyObj();
obj.X = x;
obj.Y = y;
obj.Image = image; // Optional

次に:

Position pos = projectPixel(obj);
3
Robbie Dee

この問題に別の角度から取り組み、メンテナンスの問題が発生しないときに、自分自身を繰り返すこと、自分自身を繰り返すこと、自分自身を繰り返すことへの愛を表現します。熱心な開発者が心を正しい場所に置くことは、知覚された悪と戦うために、彼らが同じように悪いまたはより悪いものを作成するという程度まで、繰り返される自分への憎しみを生み出すことは簡単です。

冗長性を計算する必要がない場合、冗長性を無駄で最大かつ最も直接的な形と考えるのは人間にとって最も直感的ですそれが物事を簡単にするならば、いくつかの単純な定型文をあちこちに複製することは必ずしも無駄ではありません。

そしてもちろん、冗長性は本当に複雑で、エラーが発生しやすい複雑なコードを複製するコードベースのように、最初は正しく修正されず、適切にテストされていたため、中央で修正するのではなく、バグを1か所で修正して見つけるだけでした。問題、あなたはまだ修正していない他の5つの場所でバグを複製していて、まったく同じロジックを適用しています。同様に、多くのメモリのチャンクを必要以上に10回割り当てたり解放したりするアプリケーションがあり、そのタイプの計算の冗長性は簡単なものではありません。しかし、これらは単純さではなく、複雑さが重複している極端なケースです。

さらにjmorenoが指摘したように、コードが同じに見えるからといって、コードが反復的であるとは限りません。 DRYは、基本的な構文レベルで適用しようとする場合に最も悪用されやすく、ツールや使用している言語と戦ってしまう可能性があり、最も書きたくなります。あなたが数年後に嫌うようになるエキゾチックなコードです。それでも、高レベルの論理的冗長性を排除するためにDRYを適用する場合でも、すべての論理的冗長性が私が見るほど必ずしも悪いわけではありません。

独立した巨大な数学ライブラリに依存する代わりに、ベクトルや行列演算などのいくつかの数学関数を複製するいくつかの画像演算を提供するサードパーティライブラリのどちらかを選択した場合、ベクトル/行列乗算などのコードの複製を優先しますそして、マトリックスの転置は、外部依存関係の数が最小限であるこのニースの独立した自己完結型の画像ライブラリを支持することを支持します。その場合、コードの複製は分離メカニズムのようになり、孤立したミニマリズムを実現する方法になります。これは、十分にテストされて機能し、バグのあるタイプや非効率なコード、または常に変更が必要な狭く適用できるコードではない場合に提供されます。

安定していてうまく機能している場合は、コードベースの他のセクションのどこかで提供されている何百行ものコードが重複しているかどうかは気にしません。 10年以上更新していない、または更新する必要のない、並列化されたベクトル化された畳み込みフィルター用の画像ルーチンのような、時間のないコードがあります。ライブラリをより独立させるために少量のコードを複製する代わりに、中央の数学ライブラリ、中央の画像ライブラリなどに依存している場合は、そうではなかったかもしれません。この小さくて独立したライブラリがそうでなかった方法で時代遅れになったかもしれません。複雑でありながら時間の試練に耐える孤立したコードをあちこちに持つのはいいことです。周囲のコードの老朽化、変更の必要性、陳腐化への取り組みにもかかわらず、それ以上の変更を必要とせずに広く適用し続けることができます。そのため、そのようなコードは、さらなる変更を必要とする不安定なコードから分離する必要があり、その分離された品質は、コードの独立性を完全に実現するために、コードの適度な複製を意味する傾向があります。

とにかく、私はいくつかのコードの重複を支持して訴訟を起こすことによって、ここで少し悪魔の擁護者を演じていますが、それは、すべての単一タイプの冗長性を熱心に打ち消そうとしない誘惑に駆られないものに対する考え方をバランスさせるのに役立つかもしれませんシステムで、あちこちで無害な冗長性を受け入れるよりもさらに悪い可能性があることがわかります。私は「オーバーエンジニアリング」という用語を気にしませんでしたが、SEには、徹底的にテストして、できるだけ多くのコードを再利用するように変更する理由を最小限に抑えるよう徹底的にテストすることなど、より大きな優先順位があります(実際には、それらが変化する理由-不安定性の最大化)。

さらに、画像処理を扱っている場合、通常はいくつかの特徴があります。

  1. 通常、何百万ものピクセルをループしていることを考えると、効率は大きな要因になる傾向があります。効率が十分に懸念される場合、よりフラットなコードは、たとえそれが最も構文的にエレガントでなくても、最適化とプロファイラーとの連携が容易になる傾向があります。同様に、最適化では、まれなケースのパスの効率を下げる代わりに、一般的なケースの実行パスをより効率的にするトレードオフが必要になることがよくあります。変更がプロファイリングしている特定の操作または関連する操作にのみ適用可能であり、再利用コードの中央ライブラリに適用できない場合は、コードを歪め、トレードオフを行う方が簡単です。さまざまなニーズを両立させようとするこのような中央ライブラリには、最適化しているイメージ操作の一般的なケースの要件と同じ一般的なユースケースがない場合があります。
  2. 一般に、画像処理は、非常に困難で不可解なバグを引き起こすタイプのものではありません。画像の操作は複雑で、最初の書き込みが難しい場合がありますが、自己完結型であり、ある程度のテストを行うと、巧妙なEdgeのケースや問題がなくても問題なく動作する傾向があります。その結果、通常、イメージフィルター用に最も保守可能なコードを作成することを心配する必要があるフィールドのタイプではありません。たとえば、一度イメージフィルターが機能すると、変更する理由がほとんどなく、または正確さのために変更する理由はありません。通常、何度も書いたコードに戻ることはありません。テストが適切である場合は、チェックリストの考え方を適用して、ある画像操作の記述から次の画像操作に移動することができます。通常、あちこち行き来する必要はありません。

これら2つの特性を考えると、DRYに夢中にならない理由はまだまだあります。画像の操作を完了してテストし、十分に効率的であるかどうかを確認して、1日と呼びます。彼らが概念的に必要なものよりも少し多くのコードを書く必要があり、可能な限り最もエレガントな方法で物事を実装しないが、それでも高速でテストでうまく機能する場合、それは世界の終わりではありません。以前の画像処理コードを再検討する最も一般的な2つの理由は、通常は効率です(そのため、ベンチマークとプロファイルを作成して、安定性を高めるために事前に調整する必要があります)、または廃止されたライブラリの一部の外部型と関数に依存していたためです(一部の適度なコードの複製と単純なPODに依存することで、実際に戦うことができ、変更の理由が少なく、より安定したソリューションを実現できます。

2
user204677

これをあまり繰り返さずに書く別の方法は、次のように選択肢Aを書くことです。

void generateImage(image, width, height, projectionFunc)
{
    vector<Pixel> pixels = image.getPixels;

    for(x = 0; x < width, x++)
    {
        for(y = 0; y < height; y++)
        {
            Position pos = projectionFunc(x, y);
            Pixel p = someCrazyFunc(pos);
            pixels.add(p);
        }
    }
}

次に、選択肢Bを次のように記述します。

void generateImages(width, height, numImages)
{
    vector<Pixel> pixels;

    for(image = 0; image < images; image++)
    {
        generateImage(image, width, height, projectPixel2);
    }
}

現在、共通のものは単一の関数にあり、Choice Aのケースでは単一の画像をループすることはありません。

0
user1118321