遊戲服務器中的契約式編程與防禦式編程

背景

事情的來由還要從幾十幾億年前的一次星球大爆炸說起,sorry,背錯臺詞了,是從幾天前討論接口返回數據和幾個月前討論課件本地數據結構說起,簡單的說,就是碰到約定好的內容出現異常,是我們在程序中內部作兼容處理,還是拋出去。

打個比方,我們要解析一段json,約定這個json的格式,只能是正常格式,或者是空,那麼一旦返回json的方法返回了一個『既不是正常格式,又不是空的異常值』,程序該如何處理呢?

小花:一旦碰到約定異常,程序必須兼容處理,一定不能讓程序Crash 
小Fa:一旦碰到約定異常,就必須拋出去,告知約定有誤,找出具體錯誤原因

這個問題,相信只要是程序猿基本都遇到過,舉個最常見的栗子,NullPointerException,假如我們要從json中取一個字段,突然發現發生了NullPointerException,一些開發者認爲是數據問題,那麼把json中的這個字段改正確就行了;還有一些開發者認爲是程序問題,認爲程序需要做非空判斷,再去使用。我相信這兩種程序猿都有自己的理由,第一種程序簡潔明瞭,代碼邏輯乾淨,但一旦出錯,就會崩潰,第二種程序耐操,隨你數據怎麼錯,我都能不Crash,但代碼中到處存在非空判斷,臃腫、重複。

生存還是毀滅,這是一個問題!

防禦式編程

就在我們爲了這個問題而爭論的時候,突然有一個姓康的同事,施法祭出了一塊磚頭(《代碼大全2》,近900頁,相當於3本《Android羣英傳》),我一度以爲他想砸在我的臉上,正當我準備閃避的時候,他翻到了這塊磚頭的第八章,幾個大字赫然印入了我的視線——『防禦式編程』。

這裏寫圖片描述

果然是老司機,居然可以從防禦性駕駛中悟出防禦性編程,說好的編程不開車,開車不編程呢?

這位作者編程厲不厲害我不知道,但我知道,論開車,一定沒有何老師diao!

OK,《代碼大全》給我們提供了一個定義——『防禦式編程』,說白了,就是『人類都是不安全、不值得信任的,所有的人,都會犯錯誤,而你寫的代碼,應該考慮到所有可能發生的錯誤,讓你的程序不會因爲他人的錯誤而發生錯誤』

在書中,作者告訴我們,程序需要對可能的錯誤輸入,做出兼容,例如一個除法的函數,你必須判斷分母可能爲0的情況,從而給調用者返回錯誤提示。另外,一般的高級編程語言,都提供了『斷言』和『異常』兩種方式來進行錯誤處理。

斷言

斷言,是一種在開發階段使用的,讓程序在運行時進行自檢的代碼,斷言爲真,那麼程序運行正常,斷言爲假,那麼程序運行異常退出。等等,防禦式編程不是說好的要兼容異常嗎,爲什麼會退出?實際上,作者的意思是,先斷言、後處理錯誤,而斷言是在開發環境中的,正式上線後是不會有斷言的。

這裏寫圖片描述

但實際上,這是一個悖論,開發階段的錯誤處理代碼在開發階段被斷言給攔截掉了,但錯誤處理代碼也是人寫的,那麼如何去檢測『錯誤處理代碼可能發生的錯誤』呢?

異常

當代碼出現問題時,可以通過拋出異常來進行通知,如果你無法處理,則可以交給外界進行處理。這個不多說,畢竟大部分代碼,如果有異常,最簡單的就是try catch了,我甚至見過把所以代碼直接try catch的,你是有多不相信人類。

所以我覺得防禦式編程用久了,會不會開始懷疑人生,果然,在往後翻幾頁,作者也給出了建議。

這裏寫圖片描述

借用奇異博士的一句臺詞——『你TM居然把警告寫在咒語的下一頁』!

簡而言之,防禦式編程,就是持懷疑態度審視所有的代碼,但這個和我們討論的主題還是略有不同的,我們討論的主題是『已經有了約定,但返回了約定之外的內容』。

契約式編程

就在我們討論的時候,天空突然飄來五個字——那都不是事,哦不對,是『契約式編程』。

這個好像有點像!我們先來簡單的看下什麼是契約式編程,簡單的說,契約作用於兩方,每一方都會完成一些任務,從而促成契約的達成,但同時,每一方也會接受一些義務,作爲制定契約的前提,有任意一方無視了必盡義的義務,則契約失敗。

契約式編程要求我們在『前提條件』、『後繼條件』和『不變量條件』進行契約的檢查。類似的,例如檢查參數,一旦參數不對,當即撕毀契約。這一點,現在很多新的語言都支持了,例如Swift,就支持對參數進行約束檢查,這就是一種類契約式編程。

契約所約束的,是『一個爲了確保程序正常運行的條件』,一旦契約被損毀,只有一個原因,那就是程序出了Bug,例如一個數據字段,在我處理的時候,必須保證是不爲空的,那麼誰來保證這一點呢,一定是我的調用方(或者說是其它模塊),所以,一旦出現問題,應該有調用方來檢查,確保調用的時候,必須是不爲空的。

這讓我想到了剛開始在面向日本人編程時期的一些事,日本人的做事風格是出了名的謹慎和詳細,每一個方法、函數,在詳細設計的時候,就已經把參數、返回值,已經它們的類型和所有可能的值都設計好了,每個方法之間有着明確的界限,如果你的方法因爲傳入的參數不在設計範圍內而導致錯誤,你完全可以去找調用方,要求他按照設計來進行調用。不得不說,這應該是契約編程的最佳實踐。日企普遍使用這種方式其實還有一個原因,那就是可以嚴格區分責任,讓每個人都不必爲了遷就他人的錯誤而進行『艱難的編碼』。每個人按照契約處理好自己的事情,讓損毀契約的人承擔責任。

再引申一下,這和現在的『面向接口編程』也非常類似,兩個模塊之間,定義好調用、處理的接口,而具體的實現,對方都不用關心,只要安裝協議的接口來進行開發就可以了,但光有接口也不夠,還需要契約來做進一步的約束,例如參數、返回值的約束。

無獨有偶,在《代碼大全》中,作者也提出了『進攻式編程』,其實和契約編程,有異曲同工之妙。

這裏寫圖片描述

烏托邦

OK,夢醒了,讓陽光照進現實。以上兩種編程方式,都是非常理想化的編程,但在一般的公司裏面不論是防禦還是契約,實現起來都是比較困難的,例如前端與後端的接口、不同部門同事的交流,按照契約式編程,沒人Care你的契約,按照防禦式編程,代碼慘不忍睹,還容易漏掉防禦。那麼到底該怎麼辦呢,我認爲,如果能在公司層面推廣契約式編程,首先是對開發效率的提升,讓每個人都對自己寫的代碼負責,在開發者之間建立良好的信任關係,同時也能減少不必要的溝通成本和精力。但同時,必要的防禦式編程也是不能少的,這是保證程序健壯、穩定的前提。怎麼說呢,中國人民秉承了千百年的傳統——『中庸之道』,契約還是防禦,視情況而定,這是平衡的藝術。

讀完這篇文章說說個人的心得:

1.防禦式編程把被調用函數遇到的錯誤分爲兩類:

  第一類:絕對不應該發生的錯誤。對於這類錯誤要使用斷言,以讓這類錯誤在開發和測試階段就完全暴露出來,並且修正,然後在線上就徹底避免出現這類絕對不應該發生的錯誤。但是,個人認爲這怎麼可能呢?如果在開發和測試階段這個絕對不應該發生的錯誤沒有暴露出來呢?因爲這極有可能發生啊,比如玩家多的時候這個情況才發生,而開發測試階段哪裏找那麼多的自然玩家呢?

第二類:可預見的可能發生的錯誤。比如玩家傳給服務器的參數爲空的情況就是可預見的可能發生的錯誤,對這類錯誤使用錯誤處理機制,即給客戶端返回一個返回嘛,並return當前函數,這個倒是沒有疑問。

2.契約式編程

這個編程方式直接說明了調用雙方的責任,比如在被調用函數中不進行任何傳入參數的合法性校驗。如果調用函數出現錯誤,那一定是調用者的責任,調用者給了被調用者錯誤的參數。這樣做的好處就是劃清責任誰的錯誤誰改,減少溝通成本,而且,被調用函數內部因爲不用校驗參數變得乾淨利落不累贅冗餘,那麼庫和組件的編寫方就輕鬆多了。


對於遊戲開發中,客戶端通過網絡傳給服務器的錯誤數據個人認爲一定採用錯誤處理機制,而不是斷言,否則別人寫個外掛輕鬆斃掉你的服務器。而服務器內部程序相互調用時可採用防禦式編程 和 契約式編程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章