With(out) アプリのクラッシュをお願いします!

公開: 2020-02-12

プログラマーは、コードのクラッシュを回避しようとします。 誰かが自分のアプリケーションを使用しても、予期せず中断したり終了したりしてはなりません。 これは、品質の最も単純な測定の 1 つです。アプリが頻繁にクラッシュする場合は、おそらく適切に行われていません。

アプリのクラッシュは、値をゼロで除算したり、マシン上の制限されたリソースにアクセスしたりするなど、プログラムが未定義または悪いことをしようとすると発生します。 アプリケーションを作成したプログラマーが明示的に行う場合もあります。 「そんなことは絶対に起こらないからスキップする」 – これはよくあることであり、完全に不合理な考え方ではありません。 起こるまで、絶対に、絶対に起こらないケースがいくつかあります。

破られた約束

何かが起こらないことがわかっている最も一般的なケースの 1 つは API です。 バックエンドとフロントエンドの間で合意しました。これが、このリクエストに対して取得できる唯一のサーバー応答です。 このライブラリの管理者は、この関数の動作を文書化しています。 関数は他に何もできません。 どちらの考え方も正しいですが、どちらも問題を引き起こす可能性があります。

ライブラリを使用しているときは、考えられるすべてのケースを処理するのに役立つ言語ツールに依存できます。 使用する言語に型チェックや静的解析が欠けている場合は、自分で処理する必要があります。 それでも、本番環境に出荷する前に確認できるので、大したことではありません。 それは難しいかもしれませんが、依存関係を更新する前に変更ログを読み、単体テストを作成しますよね? ライブラリを使用するか作成するかのどちらかで、より厳密な型付けを提供できるほど、コードや他のプログラマーにとってより良いものになります。

バックエンドとフロントエンドの通信は少し難しくなります。 多くの場合、疎結合であるため、一方の変更が他方にどのように影響するかを意識せずに簡単に変更できます。 多くの場合、バックエンドでの変更はフロントエンドでの想定を破る可能性があり、両方が別々に配布されることがよくあります。 それはひどく終わらせなければなりません。 私たちはただの人間であり、相手のことを理解していなかったり、その小さな変化を伝えるのを忘れたりすることがあります。 繰り返しますが、これは適切なネットワーク処理では大したことではありません。応答のデコードは失敗し、それを処理する方法はわかっています。 最高のデコード コードでさえ、悪い設計の影響を受ける可能性がありますが…

部分的な機能。 悪いデザイン。

「ここには 2 つのブール変数があります。'isActive' と 'canTransfer' です。もちろん、アクティブでない場合は転送できませんが、それは単なる詳細です。」 ここから始まります。私たちの悪い設計は、大打撃を受ける可能性があります。 誰かがこれらの 2 つの引数を使用して関数を作成し、それに基づいてデータを処理します。 最も簡単な解決策は…無効な状態でクラッシュするだけです。それは決して起こらないはずなので、気にする必要はありません。 時々気にして、後で修正したり、どうしたらよいかを尋ねたりするためにコメントを残しますが、そのタスクを完了せずに最終的に出荷される可能性があります.

 // 疑似コード
function doTransfer(Bool isActive, Bool canTransfer) {
  If ( isActive and canTransfer ) {
    // 利用可能な転送のために何かをする
  } それ以外の場合 ( isActive ではなく、canTransfer ではありません) {
    // 利用できない転送のために何かをする
  } それ以外の場合 (isActive であり、canTransfer ではありません) {
    // 利用できない転送のために何かをする
  } else { // 別名 ( isActive および canTransfer ではない)
    // 4 つの可能な状態があります
    // これは発生すべきではありません。アクティブでないときは転送を使用できません。
    クラッシュ()
  }
}

この例はばかげているように見えるかもしれませんが、これよりも見つけて解決するのが少し難しいようなトラップに陥ることがあります。 部分関数と呼ばれるものになります。 これは、他の入力を無視またはクラッシュする可能性のある入力の一部に対してのみ定義されている関数です。 部分関数は常に避ける必要があります (動的に型付けされた言語では、ほとんどの関数が部分関数として扱われることに注意してください)。 言語が型チェックと静的解析で適切な動作を保証できない場合、しばらくすると予期しない方法でクラッシュする可能性があります。 コードは常に進化しており、昨日の仮定が今日有効ではない場合があります。

早く失敗しましょう。 頻繁に失敗します。

どうすれば身を守ることができますか? 最高の防御は攻撃です! この素晴らしいことわざがあります。 よく失敗する。」 しかし、アプリのクラッシュ、部分的な機能、および悪い設計を回避する必要があることに同意しただけではありませんか? Erlang OTP はプログラマーに、予期しない状態の後に自己修復し、実行中に更新するという神話上の利点を提供します。 彼らはそれを買う余裕がありますが、誰もがこの種の贅沢を持っているわけではありません. では、なぜ迅速かつ頻繁に失敗する必要があるのでしょうか。

まず、それらの予期しない状態と動作を見つけること。 アプリの状態が正しいかどうかを確認しないと、クラッシュよりもさらに悪い結果になる可能性があります。

2 つ目は、他のプログラマーが同じコード ベースで共同作業できるようにすることです。 あなたが今プロジェクトに一人でいる場合、あなたの後に他の誰かがいるかもしれません. いくつかの仮定と要件を忘れているかもしれません。 すべてが機能するまで提供されたドキュメントを読まなかったり、内部メソッドや型をまったくドキュメント化しなかったりするのはかなり一般的です。 その状態で、誰かが予期しない有効な値を使用して、使用可能な関数の 1 つを呼び出します。 たとえば、任意の整数値を取り、その秒数だけ待機する「待機」関数があるとします。 誰かが「-17」を渡すとどうなりますか? それを行った直後にクラッシュしない場合、重大なエラーや無効な状態が発生する可能性があります。 それは永遠に待ちますか、それともまったく待ちませんか?

意図的なクラッシュの最も重要な部分は、適切に行うことです。 アプリケーションがクラッシュした場合、診断できるようにいくつかの情報を提供する必要があります。 デバッガーを使用している場合は非常に簡単ですが、デバッガーを使用せずにアプリのクラッシュを報告する方法が必要です。 ロギング システムを使用して、アプリケーションの起動間でその情報を保持したり、外部から参照したりできます。

意図的なクラッシュの 2 番目に重要な部分は、本番環境でそれを回避することです…

失敗しないでください。 これまで。

最終的にコードを出荷します。 完璧にすることはできません。多くの場合、正確性を保証することさえ考えるにはコストがかかりすぎます。 ただし、誤動作やクラッシュが発生しないようにする必要があります。 高速かつ頻繁にクラッシュすることをすでに決めているのに、どうすればそれを達成できるでしょうか?

意図的なクラッシュの重要な部分は、非運用環境でのみ行うことです。 アプリケーションの本番ビルドでは取り除かれたアサーションを使用する必要があります。 これは開発中に役立ち、エンド ユーザーに影響を与えずに問題を発見することができます。 ただし、無効なアプリケーション状態を回避するために、時々クラッシュすることをお勧めします。 部分関数を既に作成している場合、どうすればそれを達成できますか?

未定義の無効な状態を表現できないようにし、それ以外の場合は有効な状態にフォールバックします。 それは簡単に聞こえるかもしれませんが、多くの思考と作業が必要です。 それがいくらであっても、バグを探したり、一時的な修正を行ったり、ユーザーを悩ませたりするよりは常に少なくなります。 一部の部分的な機能が自動的に発生しにくくなります。

 // 疑似コード
関数 doTransfer(State state) {
  スイッチ (状態) {
    case State.canTransfer {
      // 利用可能な転送のために何かをする
    }
    case State.cannotTransfer {
      // 利用できない転送のために何かをする
    }
    ケースState.notActive {
      // 利用できない転送のために何かをする
    }
    // アクティブでないと転送可能であることを表すことはできません
    // 可能な状態は 3 つだけです
  }
}

どうすれば無効な状態を不可能にできますか? 前の例から 2 つ選んでみましょう。 2 つのブール変数「isActive」と「canTransfer」の場合、これら 2 つを単一の列挙に変更できます。 考えられるすべての有効な状態を網羅的に表します。 それでも誰かが未定義の変数を送ることができますが、その方がはるかに扱いやすいです。 内部で渡される無効な状態の代わりに、プログラムにインポートされない無効な値になり、すべてが難しくなります。

待機関数は、強く型付けされた言語でもうまく改善できます。 入力で符号なし整数のみを使用するようにすることができます。 無効な引数はコンパイラによって削除されるため、これだけですべての問題が解決されます。 しかし、言語に型がない場合はどうなるでしょうか? 考えられる解決策がいくつかあります。 まず、クラッシュしてください。この関数は負の数に対して未定義であり、無効または未定義のことは行いません。 テスト中にそれの無効な使用を見つける必要があります。 ここでは、単体テスト (とにかく行う必要があります) が非常に重要になります。 第二に、これは危険かもしれませんが、コンテキストによっては役立つ場合があります。 非実稼働ビルドでアサーションを維持する有効な値にフォールバックして、可能な場合は無効な状態を修正できます。 このような関数には適切な解決策ではないかもしれませんが、代わりに整数の絶対値を作成すると、アプリのクラッシュを回避できます。 具体的な言語によっては、代わりにエラー/例外をスロー/発生させることも良い考えかもしれません。 ユーザーにエラーが表示された場合でも、クラッシュするよりもはるかに優れたエクスペリエンスであるため、可能であればフォールバックする価値があるかもしれません。

ここでもう 1 つ例を挙げましょう。 フロントエンド アプリケーションのユーザー データの状態が無効になりそうな場合は、クラッシュするのではなく、強制的にログアウトしてサーバーから有効なデータを再度取得する方がよい場合があります。 ユーザーは強制的にそうしなければならないか、無限のクラッシュ ループに陥る可能性があります。 繰り返しになりますが、非実稼働環境ではこのような状況でアサートしてクラッシュする必要がありますが、ユーザーを外部テスターに​​しないでください。

概要

クラッシュしたり不安定なアプリケーションが好きな人はいません。 私たちはそれらを作ったり使ったりするのが好きではありません。 開発およびテスト中に有用な診断を提供するアサーションで迅速に失敗すると、多くの問題を早期に発見できます。 本番環境で有効な状態にフォールバックすると、アプリがより安定します。 無効な状態を表現できないようにすると、問題のクラス全体が取り除かれます。 開発の前に、無効な状態を削除してフォールバックする方法について考える時間をもう少し取ってください。また、いくつかのアサーションを含めるために執筆中にもう少し時間をかけてください。 今日からアプリケーションの改善を開始できます!

続きを読む:

  • 受託設計
  • 代数データ型