怎樣保證我的程式碼不會被別人破壞?

碼農談IT發表於2023-02-08


怎樣保證我的程式碼不會被別人破壞?

有一類Bug是很讓人頭疼的,就是你的程式碼怎麼看都沒問題,可是執行就出問題。

某程式庫是其他人封裝的,我只是拿來用。為定位問題,還是開啟了這個程式庫原始碼。發現底層實現中,出現全域性變數。

在我的程式碼執行過程中,有別的程式會呼叫另外函式,修改這個全域性變數,導致程式執行失敗。 表面看,我呼叫的這個函式和另外那個函式沒關係,但它們卻透過一個底層全域性變數,相互影響。

有人認為這是全域性變數使用不當,所以在Java設計中,甚至取消了全域性變數,但類似問題並未減少,只是以不同面貌展現,比如,static 變數。

這類問題真正原因還是在於變數可變。

1 變數危害

變數不就應該可變?

怎樣保證我的程式碼不會被別人破壞?

併發環境下有 bug。正確寫法如下:

怎樣保證我的程式碼不會被別人破壞?

區別在於,SimpleDateFormat在哪裡構建:

  • • 被當作一個欄位

  • • 在方法內部

不同做法根本差異在於SimpleDateFormat 物件是否共享

為何物件共享有問題?看原始碼:

怎樣保證我的程式碼不會被別人破壞?

calendar是SimpleDateFormat類的一個欄位:

怎樣保證我的程式碼不會被別人破壞?

執行format過程中修改calendar,導致 bug:

怎樣保證我的程式碼不會被別人破壞?
  1. 1. A執行緒把變數值修改成自己需要的值

  2. 2. 此時執行緒切換,B執行緒開始執行,將變數值修改成它需要的值

  3. 3. 執行緒切換回來,A繼續執行,但此時變數已不是原來A自己所設值,執行出錯

對於SimpleDateFormat,calendar就是共享變數:一個執行緒剛設定的值,可能會被另一個執行緒修改。而Test2中,每次建立一個新SDF物件,避免了執行緒共享。

就愛Test1寫法,SDF如何改寫?

SDF作者水平太次,換我寫,就給它加synchronized或Lock鎖,但你輕易引入多執行緒的複雜性。多執行緒能不用就不用。

推薦將calendar變成區域性變數,從根本解決執行緒共享變數問題。

這類問題在函數語言程式設計幾乎不可能存在,因函數語言程式設計的不變性。

2 不變性

函數語言程式設計的不變性主要體現在:

可理解為一個初始化之後就不再改變的量,即當你使用一個值時,值不會變 很多人開始無界:初始化後不會改變的“值”,這不就是常量嗎?注意,常量一般是預先確定的,而值是在執行過程中生成的。

純函式

對於相同的輸入,給出相同的輸出;沒有副作用。

二者結合:

  • • 值保證不會顯式改變一個量

  • • 純函式保證不會隱式改變一個量

函式就是純函式,一個函式計算後不會產生額外改變,而函式中用到的一個個量就是值,它們是不會隨著計算改變的。所以函數語言程式設計中,計算天然不變。

因為不變性,所以前面問題不復存在:

  • • 若你拿到一個量,這次的值是1,下一次它還是1,無需擔心它會改變

  • • 呼叫一個函式,傳進去同樣的引數,它保證給出同樣的結果,行為是完全可以預期的,不會碰觸到其他部分。即便是在多執行緒下,也無需擔心同步問題

傳統方式的基礎是面向記憶體單元,任性的改來改去已成為大多程式設計師的本能。所以習慣如下程式碼

counter = counter + 1

傳統的程式設計方式佔優的地方是執行效率,而如今,這優點則越來越不明顯,反而因為到處都可變,帶來更多問題。 我們更該在設計中,借鑑函數語言程式設計,把不變性更多應用在業務程式碼中。

3 怎麼應用?

3.1 值

編寫不變類,即物件一旦構造出來就不能改變,最常見不變類就是String類。

如何編寫一個不變類

  • • 所有欄位只在構造器初始化

  • • 所有方法都是純函式

  • • 若需改變,就返回一個新物件,而非修改已有的欄位 String類裡的方法都是這樣。

理解這個,你就更理解 DDD 裡的值物件。

3.2 純函式

純函式關鍵:

  • • 不修改任何欄位

  • • 不呼叫修改欄位內容的方法

Java並非嚴格函數語言程式設計語言,不是所有量都是值。所以在實用性角度,可以實踐:

  • • 若要使用變數,就使用區域性變數

  • • 使用語法中不變的修飾符 多用final。無論是修飾變數、方法,都是讓編譯器提醒你,要多在不變性上設計

擁有不變性程式設計思維後,你會發現很多習慣都讓你的程式碼陷入水深火熱,比如你最愛的setter就是提供了一個介面,專門修改一個物件內部的值。

但要承認現實,純粹函數語言程式設計很難,只能把程式設計原則設定為儘可能編寫不變類和純函式。但你依然能套在大量現存業務程式碼上。

大多數涉及可變或副作用的程式碼,應該都是與外部系統互動。能夠把大多數程式碼寫成不變的,的確能減少許多後期維護成本。

正由於不變性,有些新語言預設不再是變數,而是值。比如,Rust宣告的是個值,一旦初始化,就無法修改:

let result = 1;

而若你想宣告一個變數,必須顯式告訴編譯器:

let mut result = 1;

Java的Valhalla 專案也在嘗試將值型別引入語言。所以,不變性,真的是減少程式問題的發展趨勢。

4 事件溯源

事件源:不要輕易新增「狀態」,取而代之的是透過事件源(透過事件的發生時間,去重建歷史的物件及對應關係),我覺得這本質上是給實體模型賦予不變性,從而消除因為狀態變化而引發的副作用。

不變性,也是諸多程式設計原則背後的原則。例如,基於「不變性」這樣一個目標,DDD的「值物件」 做法(定義一個不變的對物件,用於標識實體之外的其他業務模型),以及馬丁.福勒提出的「無副作用方法」(side-effect-free function,指代方法不會對物件狀態產生任何改變) 等,就都顯得很恰如其分了。

更極端的如 Rust ,直接讓不變性成為語法。

對比一般的CRUD,就是沒有修改,只有不斷的插入值不同的同一條記錄,下次修改時,在最新一條基礎上修改值後再插入一條最新的。有點類似Java String 的處理方式,修改是生成另一個物件。

5 總結

函數語言程式設計,限制使用賦值語句,是對程式中的賦值施加約束。一旦初始化好一個量,就不要隨便給它賦值。

函數語言程式設計相關說法:無副作用、無狀態、引用透明等,都是在說不變性。

從Effect Java學到builder模式,實踐DDD,也應多考慮不變性:如修改使用者資訊,業務邏輯提取入引數據,返回值是透過builder構造一個新物件;builder中有完整性校驗;這樣可保證經過業務邏輯處理後返回的物件一定是一個新的並且是符合業務完整性的領域物件。

變化是需求層面的不得已,不變是程式碼層面的努力控制。儘量編寫不變類和純函式!

參考

  • • 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2934376/,如需轉載,請註明出處,否則將追究法律責任。

相關文章