web-dev-qa-db-ja.com

DRYとKISSの原則に互換性がない場合、何を考慮すべきですか?

DRY原則 を使用すると、プログラマは複雑でメンテナンスが難しい関数/クラスを作成しなければならない場合があります。このようなコードは、時間の経過とともにより複雑になり、保守が難しくなる傾向があります。 KISS原則 に違反しています。

たとえば、複数の関数が同様のことを行う必要がある場合。通常のDRY解決策は、使用法のわずかな変化を可能にするために、異なるパラメーターを取る関数を作成することです。

利点は明白です、DRY =変更を加える1つの場所など。

欠点とそれに違反する理由KISSは、これらのような関数は、時間の経過とともにますます多くのパラメーターでますます複雑になる傾向があるためです。結局、プログラマーは非常に恐れます。このような関数に変更を加えると、関数の他の使用例でバグが発生します。

個人的には、DRY原則に違反することは、KISS原則に従うことを意味します。

私は、1つの超複雑な関数を持つよりも、10の超単純な関数が欲しいです。

1つの場所で非常に怖い/困難な変更を行うよりも、退屈ですが簡単(10か所で同じ変更または同様の変更を行う)をしたいです。

明らかに、理想的な方法は、DRYに違反することなくKISS=とすることです。しかし、場合によっては不可能と思われることもあります。

出てくる質問の1つは、「このコードはどのくらいの頻度で変更されるのか」というものです。頻繁に変更される場合は、DRYにする方が適切です。この1つの複雑なDRY関数を変更すると、多くの場合、複雑になり、時間とともにさらに悪化するため、同意しません。

つまり、基本的には、一般的にKISS> DRY。

どう思いますか? DRYは常にKISSに勝つべきであり、その逆の場合もあると思いますか?決定を下す際に何を考慮しますか?どのようにして状況を回避しますか?

71
user158443

KISSは主観的です。 DRYは過剰に適用するのは簡単です。どちらにも良いアイデアがありますが、どちらも悪用しやすいです。重要なのはバランスです。

KISSは本当にあなたのチームの目の前にあります。あなたはKISSが何であるかを知りません。あなたのチームはそうします。彼らにあなたの仕事を見せて、彼らがそれが簡単だと思うかどうか確かめてください。あなたはそれがどのように機能するかをすでに知っているので、これの悪い判断者です。他の人がコードを読むのがいかに難しいかを調べてください。

DRYは、コードの外観についてではありません。同一のコードを検索しても実際のDRY問題を見つけることはできません。実際のDRY問題は、まったく異なる外観で同じ問題を解決している可能性があります別の場所にあるコード。同じコードを使用して別の場所にある別の問題を解決する場合、DRYに違反しないでください。異なる理由は、別の問題が個別に変わる可能性があるためです。ここで、もう1つはしません。

1か所で設計の決定を行います。決定を広めないでください。しかし、たまたま同じように見えるすべての決定を同じ場所に折り畳まないでください。 xとyの両方が1に設定されている場合でも、両方とも使用できます。

この観点では、KISSやDRYを他の上に置くことはありません。それらの間の緊張はほとんどわかりません。私は虐待から守りますこれらはどちらも重要な原則ですが、どちらも特効薬ではありません。

144
candied_orange

私はこれについてすでに コメント から 別の回答 でcandied_orangeによって 類似の質問 に書き、そして 別の答え 、しかしそれは繰り返しを負担します:

DRYは、ニーモニック「Do n't Repeat Yourself」のかわいい3文字の頭字語です。これは、本の中で造られた The Pragmatic Programmer です 8.5ページのセクション全体wikiの複数ページの説明とディスカッション もあります。

本の定義は次のとおりです。

すべての知識は、システム内で単一の明確で信頼できる表現を持つ必要があります。

重複の削除については、強調してnotとしていることに注意してください。それは約identifying重複のどれが標準的なものであるかについてです。たとえば、キャッシュがある場合、キャッシュには他のものの複製である値が含まれます。 ただし、キャッシュがnot正規ソースであることを明確にする必要があります。

原則はnot 3文字のDRYです。それは本とウィキのそれらの20かそこらのページです。

この原則は、OAOOとも密接に関連しています。OAOOは、「Once And Only Once」のそれほどかわいい4文字の頭字語ではなく、 複数ページの説明とディスカッションを持つeXtreme Programmingの原則です)ウィキ

OAOO wikiページには、Ron Jeffriesからの非常に興味深い引用があります。

ベックがほぼ完全に異なるコードの2つのパッチを「複製」であると宣言し、それらが複製になるように変更してから、新しく挿入された複製を削除して、より優れたものを思いつくことがありました。

彼はこれについて詳しく述べています:

かなり似ていない2つのループをBeckが見たときのことを思い出します。それらは構造と内容が異なり、内容が異なるため、「for」という単語を除いてほとんど何も重複していません。同じようにループしているという事実コレクション。

彼は2番目のループを最初のループと同じようにループを変更しました。以前のバージョンではコレクションの前部しか実行していなかったため、コレクションの最後に向かって項目をスキップするようにループの本体を変更する必要がありました。これでforステートメントは同じになりました。 「まあ、その重複を取り除かなければならない、と彼は言った、そして2番目のボディを最初のループに動かして、2番目のループを完全に削除しました。

これで、1つのループで2種類の同様の処理が行われました。彼はそこで何らかの重複を発見し、メソッドを抽出し、他のいくつかのことを行い、そして出来上がりました!コードははるかに優れていました。

その最初のステップ-複製の作成-は驚くべきものでした。

これは、コードを複製せずに複製できることを示しています。

そして本はコインの裏側を示しています:

オンラインのワイン注文アプリケーションの一部として、ユーザーの注文量と一緒にユーザーの年齢を記録して検証します。サイトの所有者によると、どちらも数字で、どちらもゼロより大きい必要があります。したがって、検証をコード化します。

def validate_age(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

def validate_quantity(value):
 validate_type(value, :integer)
 validate_min_integer(value, 0)

コードのレビュー中、常駐のノウハウはこのコードをバウンスし、DRY違反であると主張します。両方の関数本体は同じです。

彼らは間違ってる。コードは同じですが、それらが表す知識は異なります。 2つの関数は、たまたま同じルールを持つ2つの別個のものを検証します。それは偶然であり、重複ではありません。

これは、知識の複製ではない複製されたコードです。

プログラミング言語の性質についての深い洞察につながる複製についての素晴らしい逸話があります。多くのプログラマーはプログラミング言語Schemeを知っており、それはファーストクラスのLISPファミリーの手続き型言語であり、高次の手続き、字句スコープ、字句クロージャ、そして純粋に機能的で参照透過的なコードとデータ構造に焦点を当てています。しかし、多くの人が知らないことは、オブジェクト指向プログラミングとアクターシステム(Schemeの作者が同じではないとしても密接に関連していると見なしたもの)を研究するために作成されたことです。

Schemeの基本的なプロシージャには、プロシージャを作成するlambdaと、プロシージャを実行するapplyの2つがあります。 Schemeの作成者はさらに2つ追加しました:actor(またはオブジェクト)を作成するalphasend 、アクター(またはオブジェクト)にメッセージを送信します。

applysendの両方を使用することの厄介な結果は、プロシージャコールのエレガントな構文が機能しなくなったことです。今日私たちが知っているScheme(およびほとんどすべてのLISP)では、単純なリストは通常​​、「リストの最初の要素をプロシージャとして解釈し、applyそれをリストの残りの部分に解釈し、解釈されます引数として」。だから、あなたは書くことができます

(+ 2 3)

そしてそれは

(apply '+ '(2 3))

(または何か近い、私のスキームはかなり錆びています。)

ただし、applysendのどちらであるかがわからないため、これは機能しなくなりました(Schemeの作成者が行った2つのうちの1つを優先したくない場合)そうではない、彼らは両方のパラダイムが等しくなることを望んだ。 …または、あなたは? Schemeの作成者は、実際には、シンボルによって参照されるオブジェクトのタイプを確認するだけでよいことに気づきました。+がプロシージャの場合、apply itの場合、+は俳優であり、あなたはsendへのメッセージです。実際にはapplysendを個別に使用する必要はありません。apply-or-sendのようなものを使用できます。

そして、それは彼らがしたことです:彼らは2つの手続きapplysendのコードを取り、条件の2つの分岐として同じ手続きに入れました。

その直後、彼らはまた、Schemeインタープリターを書き直しました。その時点まで、レジスターマシン用の非常に低レベルのレジスタートランスファーアセンブリ言語で、高レベルのSchemeで書かれていました。そして、彼らは驚くべきことに気づきました:条件付きの2つのブランチのコード同一になった。彼らはこれに気づいていませんでした:2つの手順は異なる時点で作成され(「最小限のLISP」で開始され、次にOO)が追加されました)、冗長性と低レベル議会の彼らは実際にはかなり異なって書かれたことを意味しましたが、高級言語で書き直した後、彼らが同じことをしたことが明らかになりました.

これは、アクターとオブジェクト指向の深い理解につながります。オブジェクト指向プログラムの実行と、語彙的閉包と適切な末尾呼び出しを伴う手続き型言語でのプログラムの実行同じものです。唯一の違いは、言語のプリミティブがオブジェクト/アクターまたはプロシージャであるかどうかです。しかし、操作上、それは同じです。

これはまた、残念ながら今日でもよく理解されていない別の重要な実現につながります。適切な末尾呼び出しなしではオブジェクト指向の抽象化を維持できない、またはより積極的に置くことはできません。オブジェクト指向であると主張するが適切な末尾呼び出しがない言語、is n'tオブジェクト指向。 (残念ながら、これは私のお気に入りのすべての言語に当てはまり、学術的ではありません。私はhaveこの問題に遭遇し、スタックオーバーフローを回避するためにカプセル化を解除する必要がありました。)

これは、非常によく隠された重複が実際にobscured重要な知識の断片であり、discoveringこの重複も知識を明らかにした例です。

39
Jörg W Mittag

疑問がある場合は、常に問題を解決する最も簡単な解決策を選択してください。

単純なソリューションが単純すぎることが判明した場合は、簡単に変更できます。一方、過度に複雑なソリューションは、変更がより困難でリスクも伴います。

KISSは本当にすべての設計原則の中で最も重要ですが、私たちの開発者の文化は巧妙であり、手の込んだ技術を使用することに多くの価値を置いているため、見落とされがちです。しかし、時々if戦略的パターン よりも実際に優れています。

DRY原則により、プログラマは関数/クラスを維持するのが困難で複雑なものを書くように強いられることがあります。

すぐにやめて! DRY=原則の目的は、より保守しやすいコードを取得することです。特定の場合に原則を適用すると、 less保守可能なコードに変換する場合は、原則を適用しないでください。

これらの原則はそれ自体が目標ではないことに注意してください。 目標は、その目的を達成し、必要に応じて修正および拡張できるソフトウェアを作成することです。 KISS、DRY、SOLID、およびその他すべての原則は、この目標を達成するための手段です。しかし、すべてに制限があり、それらが機能し、保守可能なソフトウェアを作成するという最終目標に反する方法で適用できます。

8
JacquesB

私見:コードがKISS/DRYであることに集中するのをやめて、コードを駆動する要件に集中し始めると、探しているより良い答えが見つかります。

私は信じている:

  1. 私たちはお互いに実践的であることを奨励する必要があります(あなたがしているように)

  2. テストの重要性を宣伝することを止めてはなりません

  3. 要件に焦点を合わせると、問題が解決します。

TLDR

パーツを個別に変更する必要がある場合は、ヘルパー関数を持たないことで、関数を独立させます。要件(および将来の変更)がすべての関数で同じである場合は、そのロジックをヘルパー関数に移動します。

これまでのすべての回答はベン図を作成すると思います。私たちは皆同じことを言っていますが、異なる部分に詳細を与えています。

また、他に誰もテストについて言及していませんでした。そのため、この回答を書きました。誰かがプログラマーが変更を加えることを恐れていると言っているとしたら、テストについてしないことは賢明ではないと思います!問題がコードに関するものだと「考え」ても、テストの欠如が本当の問題である可能性があります。客観的に優れた決定は、人々が最初に自動テストに投資したときに、より現実的になります。

まず、恐れを避けることは知恵です-よくできました!

ここにあなたが言った文があります:プログラマーはそのような[ヘルパー]関数に変更を加えることを非常に恐れるか、または関数の他の使用例でバグを引き起こすでしょう

この恐怖が敵であることに同意します。それらが連鎖するバグ/作業/変更の恐怖を引き起こしているだけの場合は、原則に固執する絶対にしない必要があります。複数の関数間のコピー/貼り付けがこの恐怖を取り除くonlyの方法である場合(私はそうではないと思います-以下を参照)、それはあなたがすべきことです。

変更を加えることへのこの恐怖を感じ、それについて何かしようとしているという事実は、コードの改善について十分に気にしていない他の多くの人よりも優れた専門家になります-彼らは言われたことをするだけですチケットをクローズするための最小限の変更を行います。

また(私はあなたがすでに知っていることを繰り返していると言うことができます):人々のスキル切り札のデザインスキル。あなたの会社の実生活の人々が全くひどいのであれば、あなたの「理論」がより良いかどうかは問題ではありません。あなたは客観的に悪い決定をしなければならないかもしれませんが、それを維持する人々は理解し、共に働くことができることを知っています。また、私たちの多くは、管理者(IMO)が私たちを細かく管理していることを理解し、必要なリファクタリングを常に拒否する方法を見つけています。

顧客のためにコードを書くベンダーである私は、常にこれを考えなければなりません。客観的により良いという議論があるので、カリー化とメタプログラミングを使用したいかもしれませんが、実際には、視覚的に明白ではないため、そのコードに人々が混乱しているのがわかります何が起こっているのか。

第二に、より良いテストは複数の問題を一度に解決します

効果的で安定した、実績のある自動テスト(ユニットおよび/または統合)がある場合(かつその場合のみ)、恐怖が消えていくのがわかります。自動テストの初心者にとって、自動テストを信頼するのは非常に怖いかもしれません。新規参入者は、これらすべての緑のドットを目にする可能性があり、それらの緑のドットが実際の生産作業を反映しているという確信はほとんどありません。ただし、個人的に自動テストに自信がある場合は、感情的/関係的に他の人にも信頼を促すことができます。

あなたのために、(もしあなたがまだなら)最初のステップは、もしそうでなければテストの実践を研究することです。正直なところ、あなたはすでにこのことを知っていると思いますが、私はこれが元の投稿で言及されていなかったので、それについて話さなければなりません。自動テストこれは重要であり、あなたが提起した状況に関連しています。

ここでは、単一の投稿ですべてのテストプラクティスを独力で煮詰めようとするつもりはありませんが、「リファクタリングに耐える」テストのアイデアに集中するようにチャレンジします。ユニット/統合テストをコードにコミットする前に、書いたばかりのテストを中断させるCUT(テスト中のコード)をリファクタリングする有効な方法があるかどうかを自問してください。それが本当なら、(IMO)そのテストを削除します。テストのカバレッジが高い(量より質が高い)ことを通知するよりも、リファクタリング時に不必要に壊れない自動テストを少なくする方が良いでしょう。結局のところ、リファクタリングをeasierにすること(IMO)は自動テストの主な目的です。

私はこの「リファクタリングに耐える」哲学を長い間採用してきたので、次の結論に達しました。

  1. 自動統合テストは単体テストより優れています
  2. 統合テストの場合、必要に応じて、「シミュレータ/偽物」を「契約テスト」と記述します
  3. プライベートAPIは絶対にテストしないでください。プライベートクラスのメソッドであったり、ファイルからエクスポートされていない関数であったりします。

参照:

テストプラクティスを調査している間、それらのテストを自分で書くために余分な時間をとる必要がある場合があります。時々、唯一の最良のアプローチは、あなたがそうしていることを誰にも言わないことです。 明らかにテストの必要性の量は、仕事/生活のバランスの必要性よりも大きい可能性があるため、これが常に可能であるとは限りません。しかし、必要なテストやコードを書くためだけに、1日か2日、タスクをこっそり遅らせるのに十分なほど小さいことがある場合があります。これは、物議を醸す声明かもしれませんが、現実だと思います。

さらに、他の人がテスト自体を理解/作成するためのステップを踏ませるのを助けるために、可能な限り政治的に賢明であることは明らかです。あるいは、コードレビューに新しいルールを課すことができる技術リーダーかもしれません。

同僚とテストについて話すとき、うまくいけば、上の1番(実用的)のポイントが、最初に耳を傾け続けて強引にならないように私たち全員に思い出させます。

第三に、コードではなく要件に焦点を合わせる

多くの場合、コードに集中し、コードが解決するはずの全体像を深く理解していません。コードがクリーンであるかどうかについての議論をやめ、コードを駆動することになっているはずの要件を十分に理解していることを確認する必要がある場合があります。

rightを行うことは、KISS/DRYなどのアイデアに従ってコードが「きれい」であると感じるよりも重要です。 (実際には)要件という事実を考慮せずに誤ってコードに集中してしまうため、私はそれらのキャッチフレーズについて気にするのをためらっています。 -)は、優れたコード品質の適切な判断を提供するものです。


2つの関数の要件が相互依存/同じである場合は、その要件の実装ロジックをヘルパー関数に入れます。そのヘルパー関数への入力は、その要件のビジネスロジックへの入力になります。

機能の要件が異なる場合は、機能間でコピー/貼り付けします。この瞬間に両方が同じコードを持っているが、could正しく独立して変更される場合、ヘルパー関数はになります。 badこれは、requirementが独立して変更される別の関数に影響を与えるためです。

例1:「getReportForCustomerX」と「getReportForCustomerY」という関数があり、どちらも同じ方法でデータベースにクエリを実行します。また、各顧客がレポートを文字通り好きなようにカスタマイズできるビジネス要件があるとします。この場合、設計により、顧客はレポートに異なる数値を要求します。したがって、レポートが必要な新しい顧客Zがいる場合は、別の顧客からクエリをコピーして貼り付け、コードをコミットして移動するのが最善の方法です。クエリがexactlyと同じであっても、これらの関数の定義上のポイントはseparate別の顧客に影響を与えるある顧客からの変更。すべての顧客がレポートで必要とする新しい機能を提供する場合は、はい:すべての機能間で同じ変更を入力する可能性があります。

ただし、先に進んでqueryDataというヘルパー関数を作成するとします。悪いのは、ヘルパー関数を導入することによりmoreのカスケード変更があるためです。クエリにすべての顧客に対して同じ "where"句がある場合、1人の顧客がフィールドを異なるようにしたい場合は、1)関数X内のクエリを変更する代わりに、1にする必要があります。 )顧客Xが望むことを行うようにクエリを変更します2)他の人に対してそれを行わないように、クエリにconditionalsを追加します。クエリに条件を追加することは、論理的に異なります。クエリにサブ句を追加する方法を知っているかもしれませんが、それを使用していないユーザーのパフォーマンスに影響を与えずに、そのサブ句を条件付きにする方法を知っているわけではありません。

したがって、ヘルパー関数を使用するには、1つではなく2つの変更が必要であることがわかります。私はこれが不自然な例であることを知っていますが、私の経験では、維持するためのブールの複雑さは線形よりも大きくなります。したがって、条件を追加する行為は、人々が気にかけなければならない「もう1つのこと」と、毎回更新する「もう1つのこと」としてカウントされます。

この例は、私には聞こえますが、あなたが遭遇している状況のようなものかもしれません。一部の人々は、これらの機能の間のコピー/貼り付けのアイデアに感情的にうんざりしており、そのような感情的な反応は問題ありません。しかし、「カスケード変更を最小限に抑える」という原則は、コピー/貼り付けが適切な場合の例外を客観的に識別します。

例2:3人の異なる顧客がいますが、それらのレポート間で異なることができる唯一のものは、列のタイトルです。この状況は大きく異なることに注意してください。私たちのビジネス要件は、もはや「レポートで柔軟性のある競争を可能にすることによって顧客に価値を提供する」ことではありません。代わりに、ビジネス要件は、「顧客がレポートをあまりカスタマイズできないようにすることで、過剰な作業を回避する」ことです。この状況で、クエリロジックを変更するのは、他のすべての顧客が同じ変更を確実に取得する必要があるときだけです。この場合、1つの配列を入力としてヘルパー関数を作成する必要があります。列の「タイトル」は何ですか。

将来、製品の所有者が、顧客がクエリについて何かをカスタマイズできるようにすることを決定した場合、ヘルパー関数にさらにフラグを追加します。

結論

コードではなく要件に重点を置くほど、コードはリテラル要件に同型になります。あなたは自然により良いコードを書きます。

4
Alexander Bird

妥当な中間点を見つけるようにしてください。多数のパラメーターと複雑な条件が散在する1つの関数ではなく、いくつかの単純な関数に分割します。呼び出し元にはいくつかの繰り返しがありますが、共通のコードを関数に移動しなかった場合ほどではありません。

私は最近、GoogleやiTunesのアプリストアとのインターフェースをとるために取り組んでいるいくつかのコードでこれに遭遇しました。一般的なフローのほとんどは同じですが、すべてをカプセル化する1つの関数を簡単に作成できないほどの違いがあります。

したがって、コードは次のように構成されています。

Google::validate_receipt(...)
    f1(...)
    f2(...)
    some google-specific code
    f3(...)

iTunes::validate_receipt(...)
    some iTunes-specific code
    f1(...)
    f2(...)
    more iTunes-specific code
    f3(...)

両方の検証関数でf1()とf2()を呼び出すことは、DRYの原則に違反することをあまり心配していません。これらを組み合わせると、より複雑になり、単一の明確な定義が実行されなくなるためです。仕事。

3
Barmar

ケントベックは、この質問に関連するシンプルなデザインの4つのルールを採用しました。マーティン・ファウラーが言ったように、それらは:

  • テストに合格する
  • 意図を明らかにする
  • 重複なし
  • 最小限の要素

真ん中の2つの順序については多くの議論があるので、それらをほぼ同じくらい重要であると考える価値があるかもしれません。

DRYはリストの3番目の要素であり、KISSは2番目と4番目の組み合わせ、またはリスト全体を組み合わせたものと考えることができます。

このリストは、DRY/KISS二分法の代替ビューを提供します。あなたのDRYコードは意図を明らかにしますか?あなたのKISSコード?

目標はDRYまたはKISSではありません。それは良いコードです。DRY、KISS、およびこれらのルールは、そこに到達するための単なるツールです。

3
Blaise Pascal