複雑さを追加して重複コードを削除する


24

私はすべてが一般的な基本クラスから継承するいくつかのクラスを持っています。基本クラスには、typeのいくつかのオブジェクトのコレクションが含まれますT

各子クラスは、オブジェクトのコレクションから補間値を計算できる必要がありますが、子クラスは異なるタイプを使用するため、計算はクラスごとにわずかに異なります。

これまで、コードをクラスからクラスにコピー/ペーストし、それぞれに小さな変更を加えました。しかし、今は重複したコードを削除し、基本クラスの1つの汎用補間メソッドに置き換えようとしています。しかし、それは非常に困難であることが証明されており、私が考えているすべての解決策は非常に複雑に思えます。

DRYの原則はこのような状況にはあまり当てはまらないと思い始めていますが、それは冒aspのように聞こえます。コードの重複を削除しようとすると、どれだけ複雑になりますか?

編集:

私が思いつく最良の解決策は次のようなものです:

基本クラス:

protected T GetInterpolated(int frame)
{
    var index = SortedFrames.BinarySearch(frame);
    if (index >= 0)
        return Data[index];

    index = ~index;

    if (index == 0)
        return Data[index];
    if (index >= Data.Count)
        return Data[Data.Count - 1];

    return GetInterpolatedItem(frame, Data[index - 1], Data[index]);
}

protected abstract T GetInterpolatedItem(int frame, T lower, T upper);

子クラスA:

public IGpsCoordinate GetInterpolatedCoord(int frame)
{
    ReadData();
    return GetInterpolated(frame);
}

protected override IGpsCoordinate GetInterpolatedItem(int frame, IGpsCoordinate lower, IGpsCoordinate upper)
{
    double ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);

    var x = GetInterpolatedValue(lower.X, upper.X, ratio);
    var y = GetInterpolatedValue(lower.Y, upper.Y, ratio);
    var z = GetInterpolatedValue(lower.Z, upper.Z, ratio);

    return new GpsCoordinate(frame, x, y, z);
}

子クラスB:

public double GetMph(int frame)
{
    ReadData();
    return GetInterpolated(frame).MilesPerHour;
}

protected override ISpeed GetInterpolatedItem(int frame, ISpeed lower, ISpeed upper)
{
    var ratio = GetInterpolationRatio(frame, lower.Frame, upper.Frame);
    var mph = GetInterpolatedValue(lower.MilesPerHour, upper.MilesPerHour, ratio);
    return new Speed(frame, mph);
}

9
DRYやCode Reuseなどの概念を過度にミクロに適用すると、はるかに大きな罪につながります。
Affe

1
あなたはいくつかの良い一般的な答えを得ています。サンプル関数を含むように編集すると、この特定のインスタンスであなたがそれを取りすぎているかどうかを判断するのに役立ちます。
カールビーレフェルト

これは実際には答えではなく、より多くの観察結果です。因子分解された基本クラス何をするのかを簡単に説明できない場合、それを持たない方が良いかもしれません。別の見方をすれば(SOLIDに精通していると思いますか?)「この機能を使用する可能性のある消費者はLiskovの代替が必要ですか?」補間機能の一般消費者にビジネスケースが存在しない場合、基本クラスには価値がありません。
トムW

1
最初に、トリプレットX、Y、ZをPosition型に収集し、その型にメンバーまたは静的メソッドとして補間を追加します:Position interpolate(Position other、ratio)。
ケビンクライン14

回答:


30

ある意味で、あなたはあなた自身の質問に最後の段落でその発言で答えました:

DRYの原則はこの種の状況にはあまり当てはまらないと考え始めていますが、それは冒blのように聞こえます。

問題を解決するのに実際的でない実践を見つけた場合は、その実践を宗教的に使用しようとしないでください(冒とくという言葉は一種の警告です)。ほとんどのプラクティスは、彼らの持っているwhens疑問の解消と、彼らはすべての可能なケースの99%をカバーしていても、あなたは異なるアプローチが必要になる場合があり、1%ということはまだあります。

具体的には、DRYに関しては、見たときに気分が悪くなる1つの巨大な怪物よりも実際にはいくつかの複製されたシンプルなコードを持っている方が良いこともあります。

そうは言っても、これらのエッジケースの存在は、粗雑なコピーアンドペーストコーディングや再利用可能なモジュールの完全な欠如の言い訳として使用すべきではありません。簡単に言えば、ある言語の問題に対して一般的なコードと読み取り可能なコードの両方を書く方法がわからない場合は、おそらく冗長性があればそれほど悪くはありませ。コードを保守する必要がある人を考えてください。彼らは冗長性または難読化でより簡単に生きますか?

特定の例に関するより具体的なアドバイス。あなたはこれらの計算はているがわずかに異なると言った。計算式をより小さなサブ式に分割してから、わずかに異なるすべての計算でこれらのヘルパー関数を呼び出して、サブ計算を実行することができます。すべての計算が過度に一般化されたコードに依存する状況を避け、それでもある程度の再利用が必要になるでしょう。


10
類似しているがわずかに異なるもう1つの点は、コードが似ているように見えても、「ビジネス」が似ている必要があるという意味ではないということです。もちろんそれが何であるかに依存しますが、同じように見えても異なるビジネスの決定/要件に基づいている可能性があるため、物事を分離しておくことをお勧めします。そのため、コード的には似ているように見えるかもしれませんが、それらを非常に異なる計算として見たいかもしれません。(未規則か何かが、物事を組み合わせるか、リファクタリングすべきかを決定する際に留意すべきだけで何か:)
Svish

@Svish興味深い点。そのように考えたことはありません。
フィル

17

子クラスは異なるタイプを使用し、計算はクラスごとにわずかに異なります。

まず第一に、クラス間で変化している部分と変化していない部分を明確に特定します。それを特定したら、問題は解決します。

リファクタリングを開始する前に、最初の演習としてそれを実行します。その後、他のすべてが自動的に所定の位置に配置されます。


2
よく置きます。これは、繰り返し機能が大きすぎるという問題である可能性があります。
カールビーレフェルト

8

数行以上のコードのほとんどすべての繰り返しは、何らかの方法でファクタリングされる可能性があり、ほぼ常にそうであると信じています。

ただし、このリファクタリングは、一部の言語では他の言語よりも簡単です。LISP、Ruby、Python、Groovy、Javascript、Luaなどの言語では非常に簡単です。通常、テンプレートを使用したC ++ではそれほど難しくありません。Cでより苦痛を感じるのは、唯一のツールがプリプロセッサマクロである場合です。多くの場合、Javaで苦痛を伴い、時には単に不可能です。たとえば、複数の組み込み数値型を処理する汎用コードを記述しようとしています。

より表現力のある言語では、問題はありません。数行のコードを超えるリファクタリングを行います。表現力の低い言語では、リファクタリングの苦痛と、繰り返されるコードの長さと安定性のバランスを取る必要があります。繰り返されるコードが長いか、頻繁に変更される可能性がある場合、結果のコードが多少読みにくい場合でも、リファクタリングする傾向があります。

コードが短く、安定しており、リファクタリングがあまりにもい場合にのみ、繰り返しコードを受け入れます。基本的に、Javaを記述していない限り、ほとんどすべての重複を除外します。

コードを投稿していないか、使用している言語を示していないため、ケースに特定の推奨事項を提示することはできません。


Luaは頭字語ではありません。
DeadMG

@DeadMG:注意; 編集してください。だからこそ、私たちはあなたにすべての評判を与えました。
ケビンクライン

5

基本クラスはアルゴリズムを実行する必要があるが、アルゴリズムはサブクラスごとに異なると言うと、これはテンプレートパターンの完璧な候補のように聞こえます。

これにより、基本クラスはアルゴリズムを実行し、各サブクラスのバリエーションになると、実装するサブクラスの責任である抽象メソッドに従います。たとえば、Page_Loadを実装する方法については、ASP.NETページでコードを変更してください。


4

各クラスの「1つの一般的な補間方法」が多すぎるため、より小さな方法にする必要があります。

計算の複雑さに応じて、計算の各「ピース」をこのような仮想メソッドにできないのはなぜですか

Public Class Fraction
{
     public virtual Decimal GetNumerator(params?)
     public virtual Decimal GetDenominator(params?)
     //Some concrete method to actually compute GetNumerator / GetDenominator
}

また、計算のロジックでその「わずかな変動」を行う必要がある場合は、個々のピースをオーバーライドします。

(これは、小さなメソッドをオーバーライドしながら、多くの機能を追加する方法の非常に小さな役に立たない例です)


3

私の意見では、あなたはある意味で、DRYをあまりにも遠ざけることができるというのは正しいと思います。似たような2つのコードが非常に異なる方向に進化する可能性がある場合、最初は自分自身を繰り返さないことで問題を引き起こす可能性があります。

しかし、あなたはそのような冒thought的な考えに警戒することも全く正しいです。あなたがそれを放っておくことに決める前にあなたのオプションを熟考するために非常に一生懸命に努力してください。

たとえば、その繰り返しコードを基本クラスではなくユーティリティクラス/メソッドに配置する方が良いでしょうか?継承よりも構成を優先するを参照してください。


2

DRYは従うべきガイドラインであり、壊れない規則ではありません。ある時点で、使用しているすべてのクラスでXレベルの継承とYテンプレートを使用する価値がないと判断する必要があります。いくつかの良い質問は、これらの同様のメソッドを抽出して1つとして実装するのに時間がかかり、変更が必要な場合、または変更が発生する可能性がある場合はすべてを検索することですそもそもこれらのメソッドを抽出する作業を元に戻しますか?追加の抽象化がこのコードがどこで何をしているのかが理解し始めた時点で私は挑戦ですか?

これらの質問のいずれかに「はい」と答えることができれば、重複する可能性のあるコードを残す強力なケースがあります。


0

「なぜリファクタリングする必要があるのか​​」という質問を自問する必要がありますか?あるアルゴリズムに変更を加えた場合、「類似するが異なる」コードがある場合、他のスポットにもその変更を反映することを確認する必要があります。これは通常、災害のレシピであり、常に誰かが1つのスポットを見逃して別のバグを導入することになります。

この場合、アルゴリズムを1つの巨大なものにリファクタリングすると、アルゴリズムが複雑になりすぎ、将来の保守が難しくなります。したがって、一般的なものを賢明に除外できない場合は、次のようにします。

// this code is similar to class x function b

コメントで十分です。問題が解決しました。


0

重複する機能を持つ1つの大きなメソッドまたは2つの小さなメソッドのどちらを使用するかを決定する際の最初の5万ドルの問題は、動作の重複する部分が変更される可能性があるかどうか、および変更を小さなメソッドに等しく適用する必要があるかどうかです。最初の質問への回答がyesであるが、2番目の質問への回答がnoである場合、メソッドは別々のままにしてください。両方の質問に対する答えが「はい」である場合、コードのすべてのバージョンが同期したままになるように、何かを行う必要があります。多くの場合、これを行う最も簡単な方法は、1つのバージョンのみにすることです。

MicrosoftがDRYの原則に反していると思われる場所がいくつかあります。たとえば、Microsoftは、障害が例外をスローするかどうかを示すパラメーターをメソッドが受け入れることを明示的に推奨していません。メソッドの「一般的な使用」APIで「失敗スロー例外」パラメーターがいのは事実ですが、そのようなパラメーターは、Try / Doメソッドを他のTry / Doメソッドで構成する必要がある場合に非常に役立ちます。失敗が発生したときに外部メソッドが例外をスローすることになっている場合、失敗した内部メソッド呼び出しは、外部メソッドが伝播できる例外をスローする必要があります。外部メソッドが例外をスローすることになっていない場合、内部メソッドも例外ではありません。パラメータを使用してtry / doを区別する場合、その後、外部メソッドはそれを内部メソッドに渡すことができます。それ以外の場合、外側のメソッドは、「try」として動作するはずのときに「try」メソッドを呼び出し、「do」として動作するはずのときに「do」メソッドを呼び出す必要があります。

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