軟件開發(fā)為啥強調(diào)“單向數(shù)據(jù)流”
Bug的出現(xiàn)
? ? ? ??先來看1個筆者在實際開發(fā)中遇到的Bug。
? ? ? ??因業(yè)務(wù)需要,需要在某個頁面創(chuàng)建很多輸入框(UITextField),來接受眾多不同類型的參數(shù),如下圖:

? ? ? ??等用戶填寫完整所有數(shù)據(jù)后,點擊“完成保存”,程序會在此時將輸入框中的數(shù)據(jù)逐個寫到一個data model上,隨后再做數(shù)據(jù)持久化,如下圖:

? ? ? ??需要提到的是 dataModel 以及 眾多輸入框控件都被同一個視圖控制器持有。為什么不在輸入框完成編輯的時候通過delegate立即將數(shù)據(jù)變化同步到dataModel?因為頁面上的輸入框?qū)嵲谔嗔?,一個個的都通過delegate來傳遞數(shù)據(jù)給視圖控制器,意味著在delegate里要出現(xiàn)大量的if-else分支來判斷數(shù)據(jù)變化是來自哪個輸入框,這將是異常笨拙凌亂的代碼,所以作罷。
? ? ? ??索性編輯的時候就讓用戶愛怎么弄就怎么弄,我們等最后完成保存的時候再統(tǒng)一將數(shù)據(jù)從UI控件中更新到dataModel,之后愛干嘛干嘛好了。怎么樣?這種做法看起來是不是非常nice,省時省力不折騰???
? ? ? ??萬萬沒想到,bug出現(xiàn)了。
? ? ? ??它的癥狀是這樣,無論用戶修改多少個UI輸入框中的信息,點擊保存后,最多只有1個變動能夠被成功保存,有時甚至連1個變動也沒法保存!
原因分析
? ? ? ??在花費了幾個小時的盤查分析后,終于發(fā)現(xiàn)了問題的原因所在,如下圖:

? ? ? ??圖中的refreshUI()函數(shù),作用是將dataModel的各項屬性數(shù)據(jù)應(yīng)用到UI上,包括那些眾多的輸入框。而這個refreshUI()函數(shù)本身是沒有任何問題的,之所以出問題,是因為千不該萬不該地在dataModel的 didSet() 方法里去調(diào)用它。
? ? ? ??這個didSet方法是Swift語言提供的一項便利語法糖,作用是某個屬性被修改時會觸發(fā)調(diào)用,然后開發(fā)者可以在里面做些想要的事情,如保存數(shù)據(jù)之類的,類似OC中手寫的Setter方法。
? ? ? ??筆者在構(gòu)建該頁面的時候心想,既然頁面要呈現(xiàn)傳入的dataModel,那就索性在dataModel發(fā)生變化的時候去調(diào)用refreshUI刷新各種UI控件,這樣多好,關(guān)鍵是有現(xiàn)成的didSet方法可以用!
? ? ? ??于是就出現(xiàn)了上面提到的問題,dataModel本質(zhì)上是Struct,里面的屬性propA, propB, propC ....對應(yīng)著UI上的輸入框A、B、C....。設(shè)想用戶修改了眾多輸入框信息后,點擊保存,觸發(fā)統(tǒng)一的寫數(shù)據(jù)邏輯writeInfoToData(),在該方法運行到第一行代碼時
? ? ?data.propA = textFieldA.text
? ? ? ??
該dataModel的didSet方法被觸發(fā),進(jìn)而refreshUI()被觸發(fā),其中的代碼被悄無聲息地執(zhí)行 ? ??
? ?
? ? ?textFieldA.text = data.propA
? ? ?textFieldB.text = data.propB
? ? ?...
? ?
? ? ? ??老天!data.propB可是修改前的舊數(shù)據(jù)~ ?
? ? ? ??這樣執(zhí)行下來的結(jié)果,就是輸入框B,C,D....中的內(nèi)容都被強制更新成了用戶修改之前的舊值,等于用戶的修改除了輸入框A,其他的全部被強制撤銷了!
怎么補救?
? ? ? ??可以看到,造成問題的原因在于,筆者嘗試在dataModel數(shù)據(jù)變動的回調(diào)方法(didSet)里去刷新UI控件,之后企圖在用戶點擊保存的時候從UI控件中將數(shù)據(jù)寫回到dataModel,而這個寫回操作又會觸發(fā)dataModel的數(shù)據(jù)變動回調(diào)方法,形成了相互調(diào)用。進(jìn)一步講,數(shù)據(jù)一會兒從dataModel流向UI控件,一會兒又從UI控件流向dataModel,最終釀成了Bug。 ?
? ? ? ??加上執(zhí)行完writeInfoToData()后,頁面就立即銷毀了,看不到輸入框中的修改值被強制重置為舊值,這又增加了發(fā)現(xiàn)Bug的難度。
? ? ? ??最后的解決辦法是將refreshUI從dataModel 的didSet方法里摘除,放到ViewDidLoad中執(zhí)行,隨后問題消失了。
感想
? ? ? ??之前做RN項目的時候,官方文檔一直在強調(diào)一個術(shù)語“單向數(shù)據(jù)流”(貌似搞Web前端的朋友對這個概念更熟悉,奇怪App端開發(fā)知道這個的并不多)。但當(dāng)時也只是按照規(guī)定做事,具體為什么要保證“單向數(shù)據(jù)流”卻沒有弄清楚。這次算是真的碰到了實際Bug,才知道單向數(shù)據(jù)流之于程序穩(wěn)定性的重要,因為不這么弄容易出Bug。 ? ?
? ? ? ??再有就是,新的高級語言會提供很多語法糖之類的東西,使得相對于之前的語言,會有很多非常省事方便的操作來使用鍵值觀察、信號傳遞之類的特性。這就導(dǎo)致很多開發(fā)者濫用這些語法糖、操作,因為它們可以讓代碼寫的更少,相對于開發(fā)者老老實實地用代理,自定義函數(shù)方法來實現(xiàn)。 ??
? ? ? ??后者雖然繁瑣,但調(diào)用邏輯清晰,利于代碼維護和Bug定位。而這又得扯到開發(fā)效率和代碼健壯性之間的平衡,是另外一個話題了。
P.s: ?用作Bug分析的源碼可以從下面的地址獲取:
https://gitee.com/BeiTianSoftware/bugdemo.git