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


114

正しくも間違って、私は現在、コードを可能な限り堅牢にするように常に努めるべきだと考えています。これは、今は役に立たないことがわかっている冗長コード/チェックを追加することを意味しますが、 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が実行している理由を理解するのにさらに時間abcがかかりましたdef

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

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

NB:これはこの質問と似ていると考えられますが、その質問とは異なり、期限がないと仮定して回答をお願いします。

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



95
ソフトウェア開発では、決して起こらないことが常に発生します。
ローマンライナー

34
if(rows.Count == 0)決して起こらないことがわかっている場合は、例外が発生したときに例外を発生させることができます。そして、仮定が間違った理由を確認します。
knut

9
質問とは無関係ですが、コードにバグがあると思われます。行がnullの場合、新しいリストが作成され、(私は推測しています)捨てられます。ただし、行がnullでない場合、既存のリストは変更されます。より良い設計は、クライアントが空であってもなくてもよいリストを渡すことを主張することです。
セオドアノーベル

9
なぜrowsヌルになるのでしょうか?少なくとも.NETの場合、コレクションがnullになるのに正当な理由はありません。 、確かですが、nullではありません。rowsnullの場合、例外をスローします。これは、呼び出し側によるロジックの失効があることを意味するためです。
キラレッサ

回答:


176

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

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

あなたは最初に言う:

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

つまり、AはBを意味し、より基本的な用語では (NOT A) OR B

その後:

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

それは:NOT((NOT A) AND B)です。Demorganの法則を適用して、(NOT B) OR ABがAを意味するものを取得します。

したがって、両方のステートメントが真である場合、AはBを意味し、BはAを意味します。つまり、これらは等しくなければなりません。

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

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

宣言を見てみましょう:

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

パブリックなので、任意の呼び出し元からの不正なデータに対して堅牢でなければなりません。

値を返すので、副作用ではなく戻り値に役立つはずです。

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

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

  • 空のリストはOKです
  • 1つ以上の要素を含むリストはOKです
  • 要素が含まれていないリストは間違っているため、使用できません。

この契約は非常識です。このためのドキュメントを書くことを想像してください!テストケースを書くことを想像してください!

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

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

メソッドをその副作用または戻り値の両方ではなく、どちらか一方に役立つようにします。

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

ヌルを受け入れないでください。ヌルシーケンスは憎悪です。意味があれば空のシーケンスを渡すように呼び出し側に要求します。決してヌルにしないでください。

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

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

さて、私はあなたの特定のケースについてのみ話しましたが、あなたは一般的な質問をしました。そこで、追加の一般的なアドバイスを次に示します。

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

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

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

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


24
私は以前にこのようなメソッドについて本当に考えたことがありませんでしたが、今考えてみると、実際には私のメソッドを呼び出す人/メソッドが必要なものを私に渡さない場合、すべての不測の事態をカバーしようとするようです彼らの間違いを修正しようとはしません(実際に彼らが何を意図しているかわからないとき)、ただクラッシュするだけです。このおかげで、貴重な教訓が学べました!
キッドコード

4
メソッドが値を返すという事実は、その副作用に対しても有用ではないことを意味するものではありません。並行コードでは、多くの場合、両方を返す関数の機能が不可欠です(CompareExchangeそのような機能がないことを想像してください!)。また、非並行のシナリオでも「レコードが存在しない場合は追加し、渡されたレコードまたは存在するレコード」は、副作用と戻り値の両方を使用しないアプローチよりも便利です。
supercat

5
@KidCodeええ、エリックは物事を明確に説明するのが得意で、本当に複雑なトピックもあります。:)
メイソンウィーラー

3
@supercat確かに、しかしそれについて推論することも難しいです。まず、おそらくグローバルな状態を変更しないものを見たいと思うでしょう。したがって、並行性の問題と状態の破損の両方を回避できます。これが合理的でない場合でも、2つを分離する必要があります。これにより、並行性が問題となる(したがって、余分な危険と見なされる)場所と、処理される場所が明確になります。これは、元のOOP論文の中心的なアイデアの1つであり、アクターの有用性の中核です。宗教的なルールはありません-理にかなっている場合は2つを分離することをお勧めします。通常はそうです。
ルアーン

5
これは、ほぼ全員にとって非常に便利な投稿です!
エイドリアンブゼア

89

あなたが上に示しているコードであなたがしていることは、防御的なコーディングであるのと同じくらい将来の証明ではありません。

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

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

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


1
実際、私は入力を意味する「愚かなユーザートリック」を取っています。はい、入力を決して信用しないでください。クラスのすぐ外からでも。検証!これが将来の懸念事項であると思われる場合は、今日ハッキングさせていただきます。
candied_orange

1
@CandiedOrange、それは意図でしたが、言葉遣いは試みられたユーモアを伝えませんでした。文言を変更しました。
アダムザッカーマン

3
ここで簡単な質問ですが、実装エラー/不良データの場合、エラーを修正しようとするのではなく、単にクラッシュするべきではありませんか?
KidCode

4
@KidCode「エラーを修正する」回復を試みるときはいつでも、2つのことを行い、既知の良好な状態に戻り、貴重な入力を静かに失わないようにする必要があります。この場合、そのルールに従うと、疑問が生じます。ゼロ行リストは貴重な入力ですか?
-candied_orange

7
この答えに強く反対します。関数が無効な入力を受け取った場合、定義によりプログラムにバグがあることを意味します。正しいアプローチは、無効な入力に対して例外をスローすることです。そのため、問題を発見してバグを修正します。あなたが説明するアプローチは、バグを隠すだけで、バグをより潜行的で追跡しにくくします。防御的コーディングとは、入力を自動的に信頼せずに検証することを意味しますが、無効または予期しない入力を「修正」するためにランダムな推測を行う必要があるという意味ではありません。
ジャックB

35

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

私は個人的に、将来のためにコードを防弾にすることを気にしません。私は未来がわからないので、そのような「未来の防弾」コードはいずれにせよ、未来が到来すると悲惨に失敗する運命にあります。

代わりに、別のアプローチをお勧めしますassert()。マクロまたは同様の機能を使用して、明示的に行うという前提を立ててください。そうすれば、未来がドアの周りに来るとき、それはあなたの仮定がもはや保持しない場所を正確に教えてくれます。


4
未来がどうなっているかわからないという点が好きです。今私が本当にしていることは、問題になる可能性のあるものを推測してから、解決策を再度推測することです。
KidCode

3
@KidCode:良い観察。ここでのあなた自身の考えは、実際には、あなたが受け入れたものを含め、ここでの多くの答えよりもずっと賢いです。
ジャックB

1
私はこの答えが好きです。コードを最小限に抑えて、将来の読者がチェックが必要な理由を簡単に理解できるようにします。将来の読者が不要に見えるもののチェックを見た場合、彼らはチェックがそこにある理由を理解しようとして時間を浪費するかもしれません。将来の人間は、このクラスを使用する他の人ではなく、このクラスを変更する可能性があります。また、デバッグできないコードを記述しないください。これは、現在発生しないケースを処理しようとする場合に当てはまります。(メインプログラムが実行しないコードパスを実行する単体テストを作成しない限り)
Peter Cordes

23

あなたが考えたいもう一つの原則は、早く失敗するという考えです。アイデアは、プログラムで何か問題が発生した場合、少なくともリリースする前に開発している間は、すぐにプログラムを完全に停止することです。このプリンシパルでは、仮定を確実に維持するために多くのチェックを記述しますが、仮定に違反するたびにプログラムが停止することを真剣に検討します。

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

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

より多くの考えについては、この短いエッセイを読んでくださいあなたのプログラムを直立姿勢で釘付けにしないでください


何か問題が発生した後でもプログラムを実行し続けたいと思うため、あなたが書いているチェックがそこにある可能性があるため、これはあなたに関連しています。たとえば、フィボナッチ数列のこの簡単な実装を考えてみましょう。

// 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;
}

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


1
c#には、無効な引数または範囲外の引数を示す特定の種類の例外はありませんか?
JDługosz

@JDługoszうん!C#にはArgumentExceptionがあり、JavaにはIllegalArgumentExceptionがあります。
ケビン

質問はc#を使用していました。完全を期すためのC ++(概要へのリンク)を次に示します。
JDługosz

3
「最も近くの安全な状態に迅速に失敗する」のがより合理的です。予期しないことが発生したときにクラッシュするようにアプリケーションを作成すると、ユーザーのデータが失われる危険があります。ユーザーのデータが危険にさらされる場所は、「最後から2番目の」例外処理の絶好のポイントです(常に手を放す以外に何もできない場合があります-究極のクラッシュ)。デバッグでクラッシュするのは、別のワーム缶を開くだけです-とにかくユーザーに展開しているものをテストする必要があり、今ではユーザーが決して見ないバージョンでテスト時間の半分を費やしています。
ルアーン

2
@Luaan:しかし、それはサンプル関数の責任ではありません。
-whatsisname

11

冗長コードを追加する必要がありますか?番号。

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

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

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

しかし、もしあなたがAddEmployee(string name)何らかのより高いレベルのインターフェースのために関数を書いているなら、あなたが空を提供した場合、少なくともこの関数が例外をスローすることを完全に期待しますname。今日、この関数に不衛生なユーザー入力を提供していない場合がありますが、この方法で「安全」にすると、将来発生する前提条件違反を簡単に診断でき、潜在的に困難なドミノのチェーンを引き起こす可能性がありますバグを検出します。これは冗長性ではありません。それは勤勉です。

一般的なルールを考え出さなければならなかった場合(一般的なルールとして、私はそれらを避けようとします)、私は次のいずれかを満たす関数であると言います:

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

…防御的なプログラミングの恩恵を受けることができます。また、assertテスト中に起動するがリリースビルドでは無効になっているイオンを記述して、バグを見つける能力をさらに高めることもできます。

このトピックは、ウィキペディア(https://en.wikipedia.org/wiki/Defensive_programming)でさらに詳しく調べられています


9

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

  • あなたは入力が正しいと仮定してはならない

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

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

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


5
これらのプログラミングの戒めはどこにありますか。リンクはありますか?私は、これらの戒めの第2項に加入するいくつかのプログラムと、そうでないいくつかのプログラムに取り組んできたので、興味があります。命令の直観的な論理にもかかわらず、常に、命令を購読した人々は、より早くThou shall not make code for future use保守性の問題に直面しました。実際のコーディングでは、命令は機能リストと期限を制御するコードでのみ有効であり、それらに到達するために将来探しているコードを必要としないことを確認しています...つまり、決してそうではありません。
コートアンモン

1
自明な証明:将来の使用の確率と「将来の使用」コードの予測値を推定でき、これら2つの積が「将来の使用」コードを追加するコストよりも大きい場合、統計的に最適です。コードを追加します。開発者(またはマネージャー)が、推定スキルが望むほど信頼できないことを認めざるを得ない状況では、命令が現れると思います。したがって、防御策として、彼らは将来のタスクをまったく推定しないことを選択します。
コートアンモン

2
@CortAmmonプログラミングには宗教的な戒めの場所はありません。「ベストプラクティス」は文脈でのみ意味があり、推論なしで「ベストプラクティス」を学習すると適応できなくなります。YAGNIは非常に便利であることがわかりましたが、だからといって拡張ポイントを後で追加するのに費用がかかる場所を考えていないわけではありません。単純なケースについて事前に考える必要がないということです。もちろん、これは時間とともに変化します。コードにますます多くの仮定が追加され、コードのインターフェースが効果的に増加します。これはバランスをとる行為です。
ルアーン

2
@CortAmmon「自明な」ケースでは、2つの非常に重要なコストを無視します-推定自体のコストと(おそらく不要な)拡張ポイントのメンテナンスコスト。それは人々が非常に信頼できない見積もりを得る場所です-見積もりを過小評価することによって。非常に単純な機能の場合、数秒間考えるだけで十分かもしれませんが、最初の「単純な」機能から続くワームの缶全体が見つかる可能性が高いでしょう。コミュニケーションが重要です-物事が大きくなるにつれて、リーダー/顧客と話す必要があります。
ルアーン

1
@Luaan私はあなたの主張を主張しようとしていました、プログラミングには宗教的な戒めの場所はありません。拡張ポイントの推定と保守のコストが十分に制限されている1つのビジネスケースが存在する限り、「命令」が疑わしい場合があります。私のコードの経験から、このような拡張ポイントを残すかどうかの問題は、何らかの形で単一行の命令にうまく適合することはありませんでした。
コートアンモン

7

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

問題が発生した場合、その問題を回避するような方法で将来のコードを書く傾向があります。その特定の問題を経験していなかった別のプログラマーは、コードの過剰なやり過ぎを考慮するかもしれません。

私の提案は、その負荷とあなたの仲間があなたよりも早く機能を叩き出している場合、「まだ間違っていないもの」にどれだけの時間を費やしているかを追跡し、それを減らすことです。

しかし、あなたが私のような人なら、「デフォルト」ですべてを入力しただけで、実際にはもうあなたを連れて行っていないでしょう。


6

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

/** ...
*   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はリリースされたコードでコンパイルされていないため、ジョブに最適なツールではない可能性があります。しかし、それは別の日の議論です。]

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


5

今すぐそのコードを追加するコストを見積もります。それはあなたの心の中ですべて新鮮なので、それは比較的安価になりますので、あなたはこれをすぐに行うことができます。ユニットテストを追加する必要があります。1年後に何らかの方法を使用しても何も悪いことはありません。機能しません。

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

追加のコードが実際に必要になる確率を推定します。次に、数学を行います。

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


4
最初の段落で、実際に必要な機能が不必要に追加された機能と相互に排他的であることが判明した場合、そのコードを長期にわたって維持するコストについて言及するのを忘れました。。。
-ruakh

また、無効な入力で失敗しないため、プログラムに忍び込む可能性のあるバグのコストを見積もる必要があります。しかし、バグは定義上予想外であるため、コストを見積もることはできません。したがって、「数学を行う」全体がバラバラになります。
ジャックB

3

ここでの第一の質問は、「あなたがする/しないなら何が起こるか」です。

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

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

私にとっての鍵は、決してデータを破壊しないことです。繰り返しましょう。絶対に。破損。データ。プログラムで例外が発生した場合は、パッチを適用できるバグレポートが表示されます。後でそれが悪いデータをファイルに書き込む場合、塩で地面をまく必要があります。

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


2

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

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

一般的に、防御的なプログラミングは非常に便利です。ただし、他のコードと同様に、防御チェックのコードパスをテストする必要あります。このような場合、彼らはあなたのコードを理由もなく複雑にしているので、代わりにこのような代替手段を選ぶでしょう:

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

nullの入力は必要ありません。できるだけ早くrowsすべての呼び出し元にエラーを明らかにする必要があります。

ソフトウェアを開発する際に調整する必要がある多くの価値があります。堅牢性自体も非常に複雑な品質です。たとえば、例外をスローするよりも堅牢な防御チェックを検討するつもりはありません。例外は、安全な場所から安全な場所から再試行するために非常に便利です。通常、データ破損の問題は、問題を早期に認識して安全に処理するよりも追跡がはるかに困難です。最終的に、彼らはあなたに頑強さの錯覚を与える傾向があり、その後1ヶ月後にあなたは別のリストが更新されたことに気付かなかったため、あなたの予定の10分の1がなくなっていることに気付く。痛い。

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

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

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

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


1

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

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

設計がほぼ同じ品質であると仮定すると、より少ないコードで維持が容易になります。これは、既知の問題を数年間放置しておいた方が良いという意味ではありませんが、不要だとわかっているものを追加するのは難しくなります。私たちは皆、レガシーコードを見て、不要な部分を見つけました。長年にわたって機能してきた信頼性の高い変更コードが必要です。

したがって、アプリができる限り設計されていて、メンテナンスが簡単で、バグがないと感じたら、不要なコードを追加するよりも良い方法を見つけてください。無意味な機能に長時間取り組んでいる他のすべての開発者に対して敬意を払って行うことは、少なくともできることではありません。


1

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

rows引数についての現在の期待を述べます。それはヌルであるか、少なくとも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

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

冗長コードを追加しないでください。

将来のみ必要なコードを追加しないでください。

何が起きても、コードが適切に動作することを確認する必要があります。

「うまく動作する」の定義は、ニーズ次第です。私が使用したいテクニックの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」例外で、予期しない何かが発生したときにコードが「停止」することを確認するだけです。この例は少し工夫されていますが、実際には、ファイルを開く際のエラーに対して実際のエラー処理を実装します。

他の回答に記載されているように、非常に重要な関連検索用語は「フェイルファースト」であり、堅牢なソフトウェアを作成するのに非常に役立ちます。


-1

ここには非常に複雑な答えがたくさんあります。おそらくこの質問をしたのは、そのピースのコードについて正しくないと感じていたが、それを修正する理由または方法がわからなかったからです。私の答えは、問題はコード構造にある可能性が非常に高いということです(いつものように)。

まず、メソッドヘッダー:

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

どの行に予定を割り当てますか?これは、パラメーターリストからすぐに明らかになるはずです。さらに知識がなければ、メソッドparamsは次のようになります(Appointment app, CalendarRow row)

次に、「入力チェック」:

//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);
}

これはでたらめです。

  1. check)メソッド呼び出し元は、メソッド内で初期化されていない値を渡さないようにする必要があります。バカではないのは、(しようとする)プログラマーの責任です。
  2. チェック)rowsメソッドへの引き渡しがおそらく間違っていることを考慮しない場合(上記のコメントを参照)、AssignAppointmentToRow予定をどこかに割り当てる以外の方法で行を操作するために呼び出されるメソッドの責任であってはなりません。

ただし、予定をどこかに割り当てるという概念全体は奇妙です(これがコードのGUI部分でない限り)。あなたのコードが含まれているように思われる(あるいは少なくともそれがにしようとする)カレンダーを表す明示的なデータ構造(つまりList<CalendarRows>、< -のように定義すべきことはCalendar、あなたが渡すことになる、あなたはこの道を行くにしたい場合はどこかにCalendar calendarあなたの方法に)。この方法で行けばcalendar、後で予定を配置(割り当て)するスロットが事前に入力されているcalendar[month][day] = appointmentはずです(たとえば、適切なコードになります)。しかし、その後、メインロジックからカレンダー構造を完全に捨ててList<Appointment>Appointmentオブジェクトに属性が含まれる場所を選択することもできます。date。そして、GUIのどこかにカレンダーをレンダリングする必要がある場合、レンダリングの直前にこの「明示的なカレンダー」構造を作成できます。

私はあなたのアプリの詳細を知らないので、これのいくつかはおそらくあなたには当てはまらないでしょうが、これらのチェックの両方(主に2番目のチェック)は、コード内の懸念の分離で何かが間違っていることを教えてくれます。


-2

単純化するために、最終的にこのコードがN日以内に必要になる(後または前ではない)か、まったく必要ないと仮定します。

擬似コード:

let C_now   = cost of implementing the piece of code now
let C_later = ... later
let C_maint = cost of maintaining the piece of code one day
              (note that it can be negative)
let P_need  = probability that you will need the code after N days

if C_now + C_maint * N < P_need*C_later then implement the code else omit it.

の要因C_maint

  • それは一般的にコードを改善し、より自己文書化し、テストしやすくしますか?はいの場合、負の値がC_maint予想されます
  • コードが大きくなりますか(したがって、読みにくく、コンパイルに長く、テストを実装するなど)?
  • リファクタリング/再設計は保留中ですか?はいの場合、バンプしC_maintます。この場合、より複雑な数式が必要になりますN

コードに重みを付け、2年以内に低い確率で必要になる可能性のある大きなものは除外する必要がありますが、有用なアサーションと3か月で必要になる50%を保護する小さなものは、実装する必要があります。


無効な入力を拒否していないため、プログラムに忍び寄る可能性のあるバグのコストも考慮する必要があります。それでは、見つけにくい潜在的なバグのコストをどのように推定しますか?
ジャックB
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.