有(出)應用程序崩潰,拜託!
已發表: 2020-02-12程序員試圖避免他們的代碼崩潰。 如果有人使用他們的應用程序,它不應該意外中斷或退出。 這是最簡單的質量衡量標準之一——如果一個應用程序經常崩潰,它可能做得不好。
當程序即將執行未定義或錯誤的操作(例如將值除以零或訪問機器上的受限資源)時,就會發生應用程序崩潰。 它也可能由編寫應用程序的程序員顯式完成。 “那永遠不會發生,所以我會跳過它”——這是很普遍的想法,也不是完全不合理的想法。 有些情況是不可能發生的,永遠不會發生,直到……它確實發生了。
失信的諾言
我們知道某事不可能發生的最常見情況之一是 API。 我們在後端和前端之間達成了一致——這是您可以針對此請求獲得的唯一服務器響應。 這個庫的維護者已經記錄了這個函數的行為。 該函數不能做任何其他事情。 這兩種思維方式都是正確的,但都可能導致問題。
當您使用庫時,您可以依靠語言工具來幫助您處理所有可能的情況。 如果您使用的語言缺少任何形式的類型檢查或靜態分析,您必須自己處理。 不過,您可以在運送到生產環境之前檢查一下,所以這沒什麼大不了的。 這可能很困難,但是您在更新依賴項和編寫單元測試之前閱讀了變更日誌,對嗎? 無論是使用庫還是創建庫,您都可以為您的代碼和其他程序員提供更好的類型。
後端 - 前端通信有點困難。 它通常是鬆散耦合的,因此可以輕鬆地在一側進行更改,而無需意識到它將如何影響另一側。 後端的更改通常會打破您對前端的假設,並且兩者通常是分開分佈的。 它必須以糟糕的方式結束。 我們只是人類,有時我們不了解對方或忘記告訴他們那個小小的變化。 同樣,這對於正確的網絡處理來說並不是什麼大問題——解碼響應將失敗,我們知道如何處理它。 即使是最好的解碼代碼也會受到不良設計的影響……
部分函數。 糟糕的設計。
“我們這裡會有兩個布爾變量:‘isActive’和‘canTransfer’,當然你不能在它不活躍的時候進行轉賬,但這只是一個細節。” 從這裡開始,我們的糟糕設計可能會受到重創。 現在有人會用這兩個參數創建一個函數並根據它處理一些數據。 最簡單的解決方案是……只是在無效狀態下崩潰,它不應該發生,所以我們不應該關心。 我們有時甚至會關心並留下一些評論以供稍後修復或詢問應該發生什麼,但最終可以在不完成該任務的情況下發貨。
// 偽代碼 函數 doTransfer(Bool isActive, Bool canTransfer) { If ( isActive 和 canTransfer ) { // 為可用的傳輸做一些事情 } else if ( 不是 isActive 也不是 canTransfer ) { // 做一些轉移不可用的事情 } else if ( isActive 而不是 canTransfer ) { // 做一些轉移不可用的事情 } else { // aka (不是 isActive 和 canTransfer ) // 有四種可能的狀態 // 這不應該發生,傳輸不應該在不活動時可用 碰撞() } }
這個例子可能看起來很傻,但有時你可能會陷入那種比這更難發現和解決的陷阱。 你最終會得到一個叫做偏函數的東西。 這是一個僅針對某些可能的輸入定義的函數,忽略或與其他輸入發生衝突。 您應該始終避免使用部分函數(請注意,在動態類型語言中,大多數函數都可以被視為部分函數)。 如果您的語言無法確保類型檢查和靜態分析的正確行為,它可能會在一段時間後以意想不到的方式崩潰。 代碼在不斷發展,昨天的假設今天可能無效。
快速失敗。 經常失敗。
你怎麼能保護自己? 最好的防守就是進攻! 有這樣一句好話:“快速失敗。 經常失敗。” 但是我們不是剛剛同意我們應該避免應用程序崩潰、部分功能和糟糕的設計嗎? Erlang OTP 為程序員提供了一個神話般的優勢,即它會在意外狀態後自我修復並在運行時更新。 他們負擔得起,但並不是每個人都有這種奢侈。 那麼我們為什麼要快速而頻繁地失敗呢?
首先,要找到那些意想不到的狀態和行為。 如果您不檢查您的應用程序狀態是否正確,則可能導致比崩潰更糟糕的結果!
其次,幫助其他程序員在相同的代碼庫上進行協作。 如果你現在一個人在一個項目中,可能會有其他人在你之後。 您可能會忘記一些假設和要求。 在一切正常之前不閱讀提供的文檔或根本不記錄內部方法和類型是很常見的。 在這種狀態下,有人調用了一個具有意外但有效值的可用函數。 例如,假設我們有一個“等待”函數,它接受任何整數值並等待該秒數。 如果有人將'-17'傳遞給它怎麼辦? 如果這樣做後它沒有立即崩潰,則可能會導致一些嚴重的錯誤和無效狀態。 它是永遠等待還是根本不等待?

故意崩潰最重要的部分是優雅地進行。 如果您的應用程序崩潰,您必須提供一些信息以進行診斷。 當你使用調試器時這很容易,但你應該有一些方法來報告沒有它的應用程序崩潰。 您可以使用日誌系統在應用程序啟動之間保留該信息或在外部查看它。
故意崩潰的第二個最重要的部分是避免在生產環境中發生這種情況......
不要失敗。 曾經。
您最終將發布您的代碼。 您無法使其完美,甚至考慮做出正確性保證通常都太昂貴了。 但是,您應該確保它不會出現異常或崩潰。 既然我們已經決定快速且頻繁地崩潰,你怎麼能做到這一點?
故意崩潰的一個重要部分是只在非生產環境中進行。 您應該使用在應用程序的生產版本中被剝離的斷言。 這將在開發過程中有所幫助,並允許在不影響最終用戶的情況下發現問題。 但是,有時最好還是崩潰以避免無效的應用程序狀態。 如果我們已經製作了部分函數,我們如何實現這一點?
使未定義和無效狀態無法表示,否則回退到有效狀態。 這聽起來很容易,但需要大量的思考和工作。 不管它有多少,它總是比尋找錯誤、進行臨時修復和……煩人的用戶少。 它會自動使某些部分功能不太可能發生。
// 偽代碼 函數doTransfer(狀態狀態){ 開關(狀態){ 案例狀態.canTransfer { // 為可用的傳輸做一些事情 } case State.cannotTransfer { // 做一些轉移不可用的事情 } 案例狀態.notActive { // 做一些轉移不可用的事情 } // 如果沒有激活,就不可能表示可用的傳輸 // 只有三種可能的狀態 } }
如何使無效狀態成為不可能? 讓我們選擇前面的兩個例子。 在我們的兩個布爾變量“isActive”和“canTransfer”的情況下,我們可以將這兩個變量更改為單個枚舉。 它將詳盡地表示所有可能和有效的狀態。 即使這樣,也有人可以發送未定義的變量,但這更容易處理。 這將是一個無效值,不會被導入我們的程序,而不是在內部傳遞無效狀態,這會使一切變得更加困難。
我們的等待功能也可以在強類型語言中得到很好的改進。 我們可以讓它在輸入時只使用無符號整數。 僅此一項就可以解決我們所有的問題,因為無效的參數將被編譯器刪除。 但是如果你的語言沒有類型怎麼辦? 我們有一些可能的解決方案。 首先——只是崩潰,這個函數對於負數是未定義的,我們不會做無效或未定義的事情。 我們將不得不在測試期間發現它的無效使用。 單元測試(無論如何我們都應該這樣做)在這裡非常重要。 第二——這可能是有風險的,但取決於上下文可能是有用的。 我們可以回退到有效值,在非生產構建中保持斷言,以盡可能修復無效狀態。 對於這樣的函數,這可能不是一個好的解決方案,但如果我們將整數設為絕對值,我們將避免應用程序崩潰。 根據具體的語言,拋出/引發一些錯誤/異常也可能是一個好主意。 如果可能的話,回退可能是值得的,即使用戶看到錯誤,這也比崩潰要好得多。
讓我們在這裡再舉一個例子。 如果您的前端應用程序中的用戶數據狀態在某些情況下即將失效,最好強制註銷並再次從服務器獲取有效數據,而不是崩潰。 無論如何,用戶可能會被迫這樣做,或者可能會陷入無休止的崩潰循環。 再一次——我們應該在非生產環境中的這種情況下斷言和崩潰,但不要讓你的用戶成為外部測試人員。
概括
沒有人喜歡崩潰和不穩定的應用程序。 我們不喜歡製造或使用它們。 在開發和測試期間提供有用診斷的斷言快速失敗將及早發現很多問題。 生產中有效狀態的回退將使您的應用程序更加穩定。 使無效狀態不可表示將排除一整類問題。 在開發之前給自己多一點時間思考如何去除和回退無效狀態,並在編寫過程中多花一點時間來包含一些斷言。 您可以從今天開始改進您的應用程序!
閱讀更多:
- 合同設計
- 代數數據類型