web-dev-qa-db-ja.com

将来必要になる場合に備えて、冗長コードを追加する必要がありますか?

正しいか間違っているかにかかわらず、私は現在、冗長なコードを追加することを意味する場合でも、常にコードを可能な限り堅牢にするように努めるべきであると考えています/確認します知っている現時点では役に立たないでしょうが、将来的にはx年になる可能性があります。

たとえば、私は現在、次のコードを含むモバイルアプリケーションに取り組んでいます。

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
    }

    //2. Is rows empty? - This will be the case if this is the first appointment / some other unknown reason.
    if(rows.Count == 0)
    {
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

特にセクション2を見ると、セクション1が当てはまる場合、セクション2も当てはまることがわかります。セクション1がfalseでセクション2がtrueと評価される理由については、2番目のifステートメントが冗長になる理由は考えられません。

ただし、既知の理由により、この2番目のifステートメントが実際に必要になるケースが将来発生する可能性があります。

一部の人々はこれを最初に見て、私が将来を念頭に置いてプログラミングしていると思うかもしれません。これは明らかに良いことです。しかし、私はこの種のコードが私からバグを「隠した」いくつかの例を知っています。つまり、実際にxyzを実行する必要があるのに、関数abcdefを実行している理由を理解するのにさらに時間がかかります。

一方、この種類のコードによって、新しい動作でコードを拡張するのがはるかに簡単になった例も数多くあります。前に戻って、関連するすべてのチェックが行われていることを確認する必要がないためです。

この種のコードに関する一般的な経験則ガイドラインはありますか? (これが良い練習と悪い練習のどちらと見なされるかについても聞きたいと思いますか?)

N.B:これは this question、 に似ていると考えることができますが、その質問とは異なり、締め切りがないと仮定して回答をお願いします。

TLDR:将来的に堅牢にするために、冗長なコードを追加する必要がありますか?

116
KidCode

演習として、最初にロジックを検証しましょう。後で見るように、論理的な問題よりも大きな問題があります。

最初の条件Aと2番目の条件Bを呼び出します。

あなたは最初に言う:

特にセクション2を見ると、セクション1が当てはまる場合、セクション2も当てはまることがわかります。

つまり、AはBを意味し、より基本的には_(NOT A) OR B_を意味します。

その後:

セクション1がfalseでセクション2がtrueと評価される理由については、2番目のifステートメントが冗長になる理由は考えられません。

つまり、NOT((NOT A) AND B)です。デモルガンの法則を適用して_(NOT B) OR A_を取得します。これはBであり、Aを意味します。

したがって、両方のステートメントが真の場合、AはBを意味し、BはAを意味します。つまり、これらは等しい必要があります。

したがって、はい、チェックは冗長です。プログラムを通る4つのコードパスがあるように見えますが、実際には2つしかありません。

だから今問題は:コードの書き方?本当の質問は次のとおりです:メソッドの規定された契約は何ですか?条件が冗長であるかどうかの問題は、赤いニシンです。本当の質問は、「私は賢明な契約を設計しましたか、そして私の方法はその契約を明確に実装していますか?」です。

宣言を見てみましょう:

_public static CalendarRow AssignAppointmentToRow(
    Appointment app,    
    List<CalendarRow> rows)
_

公開されているため、任意の呼び出し元からの不良データに対して堅牢でなければなりません。

値を返すため、副作用ではなく、戻り値に役立ちます。

しかし、メソッドの名前は動詞であり、その副作用に役立つことを示唆しています。

リストパラメータの規約は次のとおりです。

  • 空のリストでも問題ありません
  • 1つ以上の要素を含むリストは問題ありません
  • 要素のないリストは誤りであり、不可能であるべきです。

この契約はinsaneです。このドキュメントを書いてみよう!テストケースを書いてみよう!

私のアドバイス:最初からやり直し。このAPIには、キャンディマシンインターフェースがすべて記述されています。 (表現は、Microsoftのキャンディマシンに関する昔の話からのものです。価格と選択の両方が2桁の数字であり、アイテム75の価格である「85」を入力するのは非常に簡単です。間違った項目です。おもしろい事実:はい、Microsoftの自動販売機からガムを取り出そうとしたときに、実際に誤ってそれを実行しました!)

賢明な契約を設計する方法は次のとおりです。

メソッドをその副作用または戻り値のいずれかではなく、どちらかに対して有用なものにします。

リストのように、可変タイプを入力として受け入れないでください。一連の情報が必要な場合は、IEnumerableを使用してください。シーケンスのみを読み取ります。これがメソッドの規約であることをvery明確にしない限り、渡されたコレクションに書き込まないでください。 IEnumerableを取得することで、コレクションを変更しないというメッセージを呼び出し元に送信します。

Nullを受け入れないでください。 nullシーケンスは嫌悪です。それが理にかなっている場合は、呼び出し側が空のシーケンスを渡すことを要求します。決してnullになることはありません。

発信者が契約に違反した場合はすぐにクラッシュし、ビジネスを意味することを伝え、本番環境ではなくテストでバグをキャッチします。

最初に可能な限り賢明な契約を設計し、次に契約を明確に実装します。 Thatは、設計の将来性を保証する方法です。

今、私はあなたの特定のケースについてのみ話しました、そしてあなたは一般的な質問をしました。だからここにいくつかの追加の一般的なアドバイスがあります:

  • あなたが開発者として推論できるがコンパイラーが推論できないという事実がある場合は、アサーションを使用してその事実を文書化します。将来のあなたやあなたの同僚のような他の開発者がその仮定に違反した場合、アサーションはそれを教えてくれます。

  • テストカバレッジツールを入手します。テストがコードのすべての行をカバーしていることを確認してください。カバーされていないコードがある場合は、テストが欠落しているか、デッドコードがあります。通常、死んだコードは死んだものではないため、死んだコードは驚くほど危険です。数年前の信じられないほどひどいApple "goto fail"セキュリティ欠陥がすぐに思い浮かびます。

  • 静的分析ツールを入手してください。ええと、いくつかを取得します。すべてのツールには独自の特別な機能があり、他のツールのスーパーセットはありません。到達できないコードや冗長なコードがあることを通知しているときに注意してください。繰り返しになりますが、それらはおそらくバグです。

私が言っているように聞こえる場合は、最初にコードを適切に設計し、次にコードをテストして、それが今日正しいことを確認します。それが私が言っていることです。これらのことを行うと、将来への対応がはるかに簡単になります。未来についての最も難しい部分は、人々が過去に書いたすべてのバギーなキャンディマシンコードを処理することです。今日それを正しく取得し、コストは将来的に低くなります。

176
Eric Lippert

上に示したコードで何をしているのかというと、将来の証明ではなくディフェンシブコーディングです。

両方のifステートメントは、さまざまなことをテストします。どちらもニーズに応じて適切なテストです。

セクション1では、nullオブジェクトをテストして修正します。 補足事項:リストを作成しても子アイテムは作成されません(例:CalendarRow)。

セクション2では、ユーザーエラーや実装エラーをテストして修正します。 List<CalendarRow>は、リストに項目があることを意味するものではありません。ユーザーや実装者は、理にかなっているかどうかにかかわらず、許可されているからといって、想像もできないようなことをします。

89
Adam Zuckerman

私の推測では、この質問は基本的に好みに基づいています。はい、堅牢なコードを作成することをお勧めしますが、この例のコードはKISSの原則にわずかに違反しています(そのような「将来の証拠」となるコードの多くがそうなるため))。

私は個人的には将来のためにコードを完全に防護することを気にしないでしょう。私は未来を知らないので、そのような「将来の防弾」コードは、未来がとにかく到着したときに無残に失敗する運命にあります。

代わりに、私は別のアプローチを好みます。assert()マクロまたは同様の機能を使用して明示的に行うことを前提とします。そのようにして、未来がドアに近づくと、それはあなたの仮定がもはや成り立たない場所を正確に教えてくれます。

あなたが考えたいかもしれないもう一つの原則は、fasting failingの考え方です。プログラムで問題が発生した場合、少なくともプログラムをリリースする前に開発している間は、プログラムを完全に停止したいという考えです。この原則の下では、仮定が確実に満たされるように多くのチェックを記述したいが、仮定に違反した場合は常に、プログラムを完全に停止させることを検討してください。

大胆に言えば、プログラムに小さなエラーがあったとしても、視聴中に完全にクラッシュさせたいのです。

これは直感に反するように思えるかもしれませんが、日常の開発中にできるだけ早くバグを発見することができます。コードを書いていて、それが終了したと思っているが、テストするとクラッシュする場合は、まだ終了していないことは間違いありません。さらに、ほとんどのプログラミング言語は、プログラムが完全にクラッシュしたときに、エラー後に最善を尽くそうとするのではなく、最も使いやすい優れたデバッグツールを提供します。最大の最も一般的な例は、未処理の例外をスローしてプログラムをクラッシュさせた場合、例外メッセージには、失敗したコードの行や、プログラムが実行したコードのパスなど、バグに関する信じられないほどの情報が表示されますそのコード行への道(スタックトレース)。

より多くの考えについては、この短いエッセイを読んでください: プログラムを直立させないでください


何か問題が発生した後でもプログラムを実行し続けたいために、作成中のチェックが存在する可能性があるため、これはあなたにとって重要です。たとえば、フィボナッチ数列のこの簡単な実装を考えてみます。

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

これは機能しますが、誰かが関数に負の数を渡した場合はどうなりますか?その場合は機能しません。したがって、関数が負でない入力で呼び出されることを確認するチェックを追加する必要があります。

このような関数を書くのは魅力的かもしれません:

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        n = 1; // Replace the negative input with an input that will work
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

ただし、これを行うと、後で誤って負の入力を使用してフィボナッチ関数を呼び出しても、気付くことはありません!さらに悪いことに、プログラムはおそらく実行を続けますが、問題が発生した場所についての手がかりを与えずに無意味な結果を生成し始めます。これらは修正するのが最も難しいタイプのバグです。

代わりに、次のようなチェックを書くことをお勧めします。

// Calculates the nth Fibonacci number
int fibonacci(int n) {
    int a = 0;
    int b = 1;

    // Make sure the input is nonnegative
    if(n < 0) {
        throw new ArgumentException("Can't have negative inputs to Fibonacci");
    }

    for(int i = 0; i < n; i++) {
        int temp = b;
        b = a + b;
        a = temp;
    }

    return b;
}

これで、負の入力で誤ってフィボナッチ関数を呼び出した場合、プログラムはすぐに停止し、何かが間違っていることを知らせます。さらに、プログラムはスタックトレースを提供することにより、プログラムのどの部分がフィボナッチ関数を誤って実行しようとしたかを通知し、何が問題なのかをデバッグするための優れた出発点を提供します。

23
Kevin

冗長なコードを追加しますか?いいえ

しかし、あなたが説明するのは冗長なコードではありません

あなたが説明するのは、関数の前提条件に違反するコードの呼び出しに対して防御的にプログラミングすることです。これを行うか、単にドキュメントを読んでユーザー自身がそれらの違反を回避するかは、主観的なものです。

個人的には、私はこの方法論を大いに信じていますが、すべての場合と同様に、注意が必要です。たとえば、C++の_std::vector::operator[]_を考えてみましょう。 VSのデバッグモード実装を一時的に脇に置くと、この関数は境界チェックを実行しません。存在しない要素をリクエストすると、結果は未定義になります。有効なベクトルインデックスを提供するのはユーザーの責任です。これは非常に慎重です。コールサイトで追加することで境界チェックに「オプトイン」できますが、_operator[]_実装がそれを実行すると、「オプトアウト」できなくなります。かなり低レベルの関数として、これは理にかなっています。

しかし、もしあなたがより高いレベルのインターフェースのためにAddEmployee(string name)関数を書いていたら、空のnameを提供した場合、この関数は少なくとも前提条件は、関数宣言のすぐ上に記載されています。現在、この機能に無害なユーザー入力を提供していない可能性がありますが、この方法で「安全」にすることで、将来発生するあらゆる前提条件違反を簡単に診断でき、困難なドミノのチェーンをトリガーするのではなく、バグを検出します。これは冗長ではありません。勤勉です。

一般的な規則を考えなければならない場合(ただし、一般的な規則としては、それらを避けようとします)、次のいずれかを満たす関数と言います。

  • 超高級言語(たとえば、CではなくJavaScript)で生活している
  • 境界面に位置する
  • パフォーマンスは重要ではありません
  • ユーザー入力を直接受け入れる

…防御的プログラミングから利益を得ることができます。他の場合では、テスト中に起動するがリリースビルドでは無効になるassertionsを引き続き記述して、バグを見つける能力をさらに高めることができます。

このトピックはWikipedia( https://en.wikipedia.org/wiki/Defensive_programming )でさらに探索されます。

ここでは、10のプログラミングの戒めのうち2つが関連しています。

  • あなたは入力が正しいと思い込んではならない

  • あなたは将来の使用のためにコードを作ってはならない

ここで、nullのチェックは「将来使用するためのコードの作成」ではありません。将来的に使用するためのコードの作成は、「いつか」役立つと思われるため、インターフェースを追加するようなものです。言い換えれば、今必要とされない限り、抽象化のレイヤーを追加しないことが戒めです。

Nullのチェックは、将来の使用とは関係ありません。それは戒め#1と関係があります:入力が正しいと仮定しないでください。関数が入力のサブセットを受け取ると想定しないでください。関数は、入力がいかに偽造されていても、論理的な方法で応答する必要があります。

9
Tyler Durden

「冗長コード」と「YAGNI」の定義は、多くの場合、あなたがどこまで先を見ているかに依存します。

問題が発生した場合、その問題を回避するような方法で将来のコードを作成する傾向があります。その特定の問題を経験していなかった別のプログラマーは、コードを冗長に過剰と見なす可能性があります。

私の提案は、ロードやピアが機能よりも速く機能を停止した場合に、「まだエラーが発生していないもの」に費やした時間を追跡し、それを減らすことです。

ただし、私のような場合は、「デフォルト」ですべて入力しただけで、もう実際には入力していません。

7
Ewan

パラメータに関する仮定を文書化することをお勧めします。また、クライアントコードがこれらの前提に違反していないことを確認することをお勧めします。私はこれを行います:

/** ...
*   Precondition: rows is null or nonempty
*/
public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    Assert( rows==null || rows.Count > 0 )
    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

[これがC#であると仮定すると、Assertはリリースされたコードでコンパイルされていないため、ジョブに最適なツールではない可能性があります。しかし、それは別の日の議論です。]

なぜこれがあなたが書いたものよりも優れているのですか?クライアントが変更された後で、クライアントが空のリストを渡したときに、最初の行を追加し、その予定にアプリを追加するのが正しい場合は、コードは意味があります。しかし、そうであることをどうやって知っていますか?将来について今より少ない仮定をする方が良いです。

6

そのコードを追加するコストを見積もります。それはあなたの心の中ですべて新鮮なので、それは比較的安いでしょう、それであなたはこれを素早く行うことができるでしょう。単体テストを追加する必要があります-1年後の方法を使用するよりも悪いことは何もありません。それは機能せず、最初から壊れていて実際には機能しなかったことがわかります。

必要なときにそのコードを追加するコストを見積もります。コードに戻ってすべてを覚えておく必要があるため、コストが高くなります。

追加のコードが実際に必要になる確率を見積もります。次に、計算を行います。

一方、「Xは絶対に発生しない」という前提でコードを完全にデバッグすると、デバッグはひどくなります。何かが意図したとおりに機能しない場合、それは愚かな間違い、または誤った仮定を意味します。あなたの「Xは決して起こらない」は仮定であり、バグが存在する場合は疑わしいものです。これにより、次の開発者はそれに時間を浪費することになります。通常、このような想定に依存しない方が良いでしょう。

5
gnasher729

ここでの主な質問は、「しなかった場合どうなるか」です。

他の人が指摘したように、この種の防御的プログラミングは良いですが、時には危険なこともあります。

たとえば、デフォルト値を指定した場合、プログラムは稼働し続けます。しかし、プログラムは今あなたがしたいことをしていないかもしれません。たとえば、その空の配列をファイルに書き込む場合、バグを「誤ってnullを指定したためにクラッシュする」から「誤ってnullを指定したためにカレンダー行をクリアする」に変わった可能性があります。 (たとえば、開始した場合removingその部分のリストに表示されていない「// blah」と表示されているもの)

私にとっての鍵はデータを破損しないです。繰り返しますが、 NEVER。CORRUPT。DATA。プログラムで例外が発生した場合は、パッチを適用できるバグレポートが表示されます。後で不良データがファイルに書き込まれる場合は、塩を地面にまき散らす必要があります。

すべての「不要な」決定は、その前提を念頭に置いて行う必要があります。

3
deworde

ここで扱っているのは基本的にインターフェースです。 「入力がnullの場合、入力を初期化する」という動作を追加することで、メソッドインターフェイスが効果的に拡張されました。これで、常に有効なリストを操作するのではなく、入力を「修正」しました。これがインターフェースの公式な部分であろうと非公式な部分であろうと、誰か(おそらくあなたを含む)がこの振る舞いを使用することになるでしょう。

インターフェイスはシンプルに保つ必要があり、特にpublic staticメソッドなどでは比較的安定している必要があります。プライベートメソッド、特にプライベートインスタンスメソッドには、多少の余裕があります。暗黙的にインターフェースを拡張することにより、実際にはコードがより複雑になります。ここで、実際にそのコードパスを使用したくないwantと想像してください-そうしないでください。これで、メソッドの動作の一部のように、pretendingのテストされていないコードが少しあります。そして、私は今それがおそらくバグを持っていることをあなたに言うことができます:あなたがリストを渡すとき、そのリストはメソッドによって変更されます。ただし、作成しない場合は、localリストを作成し、後でそれを破棄します。これは、あいまいなバグを追跡しようとするときに、半年で泣くような一貫性のない動作です。

一般的に、防御的プログラミングはかなり便利なものです。ただし、ディフェンシブチェックのコードパスは、他のコードと同様にテストする必要があります。このようなケースでは、理由もなくコードが複雑になっているため、代わりに次のような代替手段を選択します。

if (rows == null) throw new ArgumentNullException(nameof(rows));

rowsがnullである入力wantを行わず、すべての呼び出し元にエラーを明確にしたいできるだけ早く

ソフトウェアを開発する際には、さまざまな価値観を調整する必要があります。ロバストネス自体も非常に複雑な品質です。たとえば、私はあなたの防御チェックを例外をスローすることよりもロバストとは見なしません。例外は、安全な場所から再試行する安全な場所を提供するのに非常に便利です。通常、データ破損の問題は、問題を早期に認識して安全に処理するよりも追跡するのがはるかに困難です。結局、それらはあなたに頑強さの幻想を与える傾向があるだけで、それから1か月後に、別のリストが更新されたことに気づかなかったので、予定の10分の1がなくなっていることに気づきます。痛い。

2つを必ず区別してください。 防御的プログラミングは、エラーが最も関連する場所でエラーを検出するための便利な手法であり、デバッグ作業を大幅に支援し、優れた例外処理により「不正な破損」を防ぎます。早く失敗し、速く失敗します。一方、あなたがやっていることは、「エラー隠蔽」のようなものです-あなたは入力をジャグリングし、呼び出し側が何を意味しているかについて仮定をしています。これは非常に重要ですユーザー向けのコード(スペルチェックなど)にとって重要ですが、開発者向けのコードでこれを確認する場合は注意が必要です。

主な問題は、どのような抽象化を行ってもリークすることです(「前もって入力するのではなく、入力したかったのです!愚かなスペルチェッカーです!」)、すべての特殊なケースと修正を処理するコードは、コード化されたままです。維持し、理解し、テストする必要があるコード。 null以外のリストが渡されることを確認する作業と、1年後に本番環境で発生するバグを修正する作業を比較してください。これは適切なトレードオフではありません。理想的な世界では、すべてのメソッドが独自の入力で排他的に機能し、結果を返し、グローバル状態を変更しないようにする必要があります。もちろん、現実の世界では、それがではなく最も単純で明確な解決策(たとえば、ファイルを保存する場合)である場合がたくさんありますが、グローバルな状態を読み取ったり操作したりする理由がないときにメソッドを「純粋」に保つと、コードの推論がはるかに容易になることがわかりました。また、メソッドを分割するためのより自然なポイントを与える傾向があります:)

これは、予想外のすべてがアプリケーションをクラッシュさせるという意味ではありません。例外をうまく使用すると、安全なエラー処理ポイントが自然に形成され、安定したアプリケーション状態を復元して、ユーザーが実行していることを続行できるようにします(ユーザーのデータ損失を回避しながら)。これらの処理ポイントでは、問題を修正する機会(「注文番号2212が見つかりません。2212bですか?」)またはユーザーに制御権を与える機会(「データベースへの接続エラー。再試行しますか?」)が表示されます。そのようなオプションが利用できない場合でも、少なくとも何も破損していない可能性があります-usingおよびtry...finallytry...catchよりもはるかに多く使用するコードを評価し始めました。例外的な状況下でも不変条件を維持する多くの機会。

ユーザーは自分のデータを失って作業するべきではありません。これはまだ開発コストなどとバランスを取る必要がありますが、これはかなり一般的なガイドラインです(ユーザーがソフトウェアを購入するかどうかを決定する場合-通常、内部ソフトウェアにはその贅沢はありません)。アプリケーション全体がクラッシュしても、ユーザーが再起動して元の状態に戻ることができれば、それほど問題にはなりません。これは本当の堅牢性です-Wordは作業を常に保存しますディスク上のドキュメントを破損することなく optionは、クラッシュ後にWordを再起動した後にこれらの変更を復元します。そもそもバグがないよりは良いですか?おそらくそうではありません-まれなバグをキャッチするために費やされた作業は、どこでも費やされる可能性が高いことを忘れないでください。ただし、他の方法よりもはるかに優れています。ディスク上の破損したドキュメント、最後の保存以降のすべての作業が失われ、ドキュメントがクラッシュ前にCtrl + AとDeleteで変更されて自動置換されました。

2
Luaan

将来必要になる可能性がある場合に備えて、冗長コードを追加する必要がありますか?

冗長コードはいつでも追加しないでください。

将来必要になるだけのコードは追加しないでください。

何が起こってもコードが正しく動作することを確認する必要があります。

「うまく動作する」の定義はあなたのニーズ次第です。私が使用したい1つのテクニックは、「パラノイア」の例外です。特定のケースが絶対に起こらないと100%確信している場合でも、例外をプログラムしますが、これは、a)これが起こるとは決して予想しないことをすべての人に明確に伝え、b)明確に表示およびログ記録し、したがって、後から忍び寄る腐敗につながることはありません。

擬似コードの例:

file = File.open(">", "bla")  or raise "Paranoia: cannot open file 'bla'"

file.write("xytz") or raise "Paranoia: disk full?"

file.close()  or raise "Paranoia: huh?!?!?"

これは、ファイルを常に開いたり、書き込んだり、閉じたりできることを100%確信していることを明確に伝えています。つまり、複雑なエラー処理を作成することはありません。しかし、(いいえ:いつ)ファイルを開けない場合でも、プログラムは制御された方法で失敗します。

もちろん、ユーザーインターフェイスはそのようなメッセージをユーザーに表示せず、スタックトレースと共に内部的にログに記録されます。繰り返しますが、これらは内部の「Paranoia」例外であり、予期しないことが起こったときにコードが「停止」することを確認します。この例は少し工夫されています。実際には、ファイルを開くときのエラーに対する実際のエラー処理を実装します。これは定期的に発生するためです(ファイル名が間違っている、USBスティックが読み取り専用にマウントされているなど)。

他の回答で述べられているように、非常に重要な関連検索語は「失敗が速い」であり、堅牢なソフトウェアを作成するのに非常に役立ちます。

1
AnoE

Noすべきではありません。そして、このコーディング方法がバグを隠すであると述べるとき、あなたは実際にあなた自身の質問に答えています。これによってコードがより堅牢になるわけではありません。むしろ、バグが発生しやすくなり、デバッグが困難になります。

rows引数に関する現在の期待を述べます。それがnullであるか、それ以外の場合は少なくとも1つの項目を持っています。だから問題は:rowsにゼロのアイテムがあるという予期しない3番目のケースを追加で処理するコードを書くことは良い考えですか?

答えはノーだ。予期しない入力があった場合は、常に例外をスローする必要があります。これを考慮してください:コードの他の部分がメソッドの期待(つまり、コントラクト)を破った場合バグがあることを意味します。バグがある場合は、できるだけ早くそれを知りたいので、修正できます。例外はそれを行うのに役立ちます。

コードが現在行っていることは、推測コードに存在する場合と存在しない場合があるバグからどのように回復するかです。しかし、バグがあったとしても、完全に回復する方法を知ることはできません。定義上、バグには未知の影響があります。おそらく、一部の初期化コードが期待どおりに実行されなかったため、行が欠落しているだけでなく、他の多くの結果が生じる場合があります。

したがって、コードは次のようになります。

public static CalendarRow AssignAppointmentToRow(Appointment app, List<CalendarRow> rows)
{
    if (rows != null && rows.Count == 0) throw new ArgumentException("Rows is empty."); 

    //1. Is rows equal to null? - This will be the case if this is the first appointment.
    if (rows == null) {
        rows = new List<CalendarRow> ();
        rows.Add (new CalendarRow (0));
        rows [0].Appointments.Add (app);
    }

    //blah...
}

注:例外をスローするだけでなく、無効な入力を処理する方法を「推測」することが理にかなっている固有のケースがいくつかあります。たとえば、外部入力を処理する場合は制御できません。 Webブラウザーは、あらゆる種類の不正な無効な入力を適切に処理しようとするため、悪名高い例です。しかし、これは外部入力でのみ意味があり、プログラムの他の部分からの呼び出しでは意味がありません。


編集:他のいくつかの回答は、あなたが防御的プログラミングをしていると述べています。同意しません。防御的プログラミングとは、入力が有効であると自動的に信頼しないことを意味します。したがって、パラメーターの検証(上記のとおり)は防御的なプログラミング手法ですが、推測によって予期しないまたは無効な入力を変更する必要があるという意味ではありません。堅牢で防御的なアプローチは、入力およびを検証し、予期しないまたは無効な入力の場合に例外をスローすることです。

1
JacquesB

堅牢なコードが今から「数年」の間あなたに利益をもたらすというあなたの仮定に基づいてこれに答えます。長期的なメリットが目標である場合は、堅牢性よりも設計と保守性を優先します。

設計と堅牢性の間のトレードオフは、時間と焦点です。ほとんどの開発者は、問題のある場所を通過して、追加の条件やエラー処理を行うことを意味する場合でも、適切に設計された一連のコードを用意することを望んでいます。数年の使用の後、本当に必要な場所はおそらくユーザーによって識別されています。

設計がほぼ同じ品質であると仮定すると、コードが少ないほど保守が容易になります。これは、既知の問題を数年間行ったほうがよいという意味ではありませんが、不要であることがわかっているものを追加することは困難です。私たちは皆、レガシーコードを見て、不要な部分を見つけました。あなたは何年も働いてきた高水準の自信を変えるコードを持たなければなりません。

そのため、アプリが適切に設計されていて、保守が簡単で、バグがないと感じた場合は、不要なコードを追加するよりも、何か良いことを見つけてください。それは、無意味な機能に長時間を費やしている他のすべての開発者に対して敬意を払って行うことができる最低限のものです。

1
JeffO