『碼』出高質量

語言: CN / TW / HK

本文從易理解、可維護、可擴充套件三個維度簡要介紹了對高質量程式碼的理解。

同時,提出了一種新的 GUI 模式:MVVS。

本文同時發表於我的個人部落格

Overview


個人認為,高質量的程式碼首先應該是『簡單的』。

『簡單』又可以從以下三個維度去度量:

  • 易理解
  • 可維護
  • 可擴充套件

本文將從上述三點展開討論,談談我個人的理解。

易理解


高質量的程式碼一定是易於理解的程式碼,讀起來應該像言情小說,而不是詰詘的文言文。

高質量的程式碼可以是優雅的,但一定不是炫技的。

高質量的程式碼應該是深入淺出的,儘量用簡單、樸實的方式去表達。

易理解面向的是整個團隊,而不是寫程式碼的那個人。

易理解是團隊高效、無縫協作的基礎。

影響程式碼可理解性的因素有很多,比如:

『良好命名』

  • 好的命名即是成功的一半,也是最具挑戰性的事情之一;
  • 總的原則就是體現『做什麼』,而不是『怎麼做』;
  • 在閱讀優秀開原始碼以及系統 API 時多留意、多思考。

『清晰結構』

( 這裡指的是程式碼的組織結構 )

  • 相關程式碼組織在一起,如:initdeinit/dealloc、UIViewController 生命週期方法viewDidLoadviewWillAppearviewDidAppear等;
  • 不同組織間用空行隔開;
  • 或者通過 Category、Extension 的方式來組織。

『合理抽象』

  • 抽象的關鍵是隱藏細節(『細節是魔鬼』);
  • 在我們不關心具體細節時能很容易地忽略細節、抓住重點、抓住主幹;
  • 抽象存在於每個層級:變數、方法、類、模組。

『線性邏輯』

  • 每個實體 (變數、方法、類、模組) 在不同的層級上都只圍繞一件事展開 --『高內聚』;
  • 模組內部的類、類內部的方法、方法內部的語句儘量都能通過線性的方式串連起來;
  • 而如果它們間形成的是一張網或離散的點都是壞程式碼的味道 --『低內聚』;
  • 如,類內部存在大量不相關的方法,方法內部存在過多的ifswitch語句。一個類動輒幾千行、一個方法動輒幾百行,此時就值得我們高度警惕;

很明顯,線性的邏輯比網狀的邏輯更易於理解。

『最小依賴』

( 依賴可以從兩個角度來度量:依賴的多少以及強弱 )

  • 依賴肯定是越少越好 --『低耦合』;
  • 過多的依賴無疑會增加程式碼的複雜性,往往也是程式碼設計出問題的一個訊號;
  • 提高內聚、面向介面程式設計等都是降低依賴的有效方式;
  • 同時,依賴關係越弱越好,繼承無疑是最先考慮到的程式碼複用方式,但是繼承也是最強的一種依賴、耦合關係。
  • 程式碼複用,應優先考慮組合。

『精簡體積』

簡單講,就是無用程式碼及時刪除;

在閱讀程式碼時經常會遇到一些莫名其妙的邏輯,經過一番調查,發現已是廢棄的程式碼;

無形中增加了理解成本,並且隨著時間推移,後浪們也不敢去刪這些程式碼,最終越積越多。

『樸實表述』

正如名言:程式碼首先是寫給人看的,其次才是讓機器執行的;

因此,儘量用平易、樸實的方式去描述、去表達,讓大家都能『看得懂』;

總之,好的程式碼一定是簡單的、清晰的。

易理解是高質量程式碼最基礎的要求,上述只是影響程式碼易理解性的幾個小點,更多的需要我們在實際編碼過程中不斷思考總結。

『 淺談高質量移動開發 』一文中對類的設計、方法的設計等有更具體的討論,感興趣的同學可以看看。

可維護性


移動端開發最主要的場景就是基於 GUI 的業務開發。

因此,本文談到的可維護性也主要是圍繞 GUI 流程展開。

關於 GUI 的設計模式 (MV*) 在近幾年也是老生常談的話題之一。

無論 GUI 模式如何演化,個人認為其核心原則未曾改變:

  • 單向資料流
  • 資料完整性
  • 資料驅動 UI

『單向資料流』

無論哪種 GUI 模式,從大的方向上都可以分為兩層:

  • Domain Layer:資料層(業務邏輯)
  • Presentation Layer:表現層(UI)

單向資料流是指『業務資料』一定是從『資料層』流向『表現層』,絕不允許反過來。

『表現層』可以響應 UI 事件並傳遞給『資料層』, 至於『資料層』如何處理這些事件純屬其『內政』,『表現層』無權干涉。

簡單概括,『 單向資料流 』背後有兩個『 流 』:

  • 資料流:從『 資料層 』流向『 表現層 』;
  • 事件流:從『 表現層 』流向『 資料層 』。

關於事件,很多響應式框架會將其定義為獨立的資料結構,如:Redux中的ActionBLoC中的Event

我個人認為這種設計有一個比較嚴重的問題:不能通過『 command + 單擊 』的方式『 鏈式 』地閱讀程式碼,需要通過全域性搜尋。

這嚴重影響了開發效率。

個人覺得事件可以直接是『 資料層 』暴露給『 表現層 』的介面,即有事件需要處理時,直接呼叫相應的介面即可。

為什麼?

我相信任何一位移動開發者對於『單向資料流』都耳熟能詳,但其背後深層次的原因不見得都能說清楚。

首先,資料流不能從『表現層』流向『資料層』,說的到底是什麼?

其真正的含義是『表現層』不能直接修改『資料層』的資料。為什麼?

從設計的角度講,資料管理是『資料層』的職責,『表現層』不應越俎代庖。 否則,『表現層』就違反了『單一職責』原則,也違反了『高內聚』的設計理念。

從現實的角度講,『表現層』直接修改資料對於程式碼維護性來說是一個『災難』。

首先,直接後果就是可能造成『不同步』:

  • UI 重新整理與資料修改不同步,資料修改後 UI 沒有及時重新整理;
  • 多個 UI 場景間不同步,有的資料可能被多個業務模組所使用,如果由其中某一處直接修改,其他模組很可能無法感知到這一修改,造成不同步。

如下圖,Scenes1 直接修改了底層資料,但並未通知 Scenes2、Scenes3 (Scenes1 可能根本不知道 Scenes2、Scenes3 的存在),造成資料不同步:

『不同步』常見的後果就是在使用 TableView 時陣列越界。

對於此類問題,我們經常是對陣列訪問加個保護,而很少也很難從根源上解決問題。

尤其是多業務場景共享資料時。因為你根本不知道是『 誰 』在『 什麼時候 』修改了資料來源。

其次,還可能引起多執行緒問題:

  • 『資料層』可能有專職的執行緒去管理資料,如果『表現層』擅自修改資料,很可能引發多執行緒問題。

如何解

『資料層』一定不能向『表現層』直接暴露可修改 (mutable) 的屬性。

對於引用型別來說,情況可能更復雜一些。理想情況下,『資料層』返回給『表現層』的資料應該是final的或是深拷貝的。

總之,要做到『表現層』毫無直接修改底層資料的可能性。

此時,大家可能有疑問了,即使是『表現層』通過事件觸發『資料層』內部去修改資料,還是可能會引起不同步的問題。

對,這就需要通過『資料完整性』、『資料驅動 UI』來解決了。

『資料完整性』

資料完整性指的是資料不應該存在中間臨時狀態。 如上圖左則所示,可能會導致意想不到的結果。

在需要修改時,應該對資料作整體替換,這也是我們常說的『資料不可變性』。

在函數語言程式設計中,所有資料都是不可變的,所有操作的結果都是生成新的資料,而不是在原有資料上作修改。

嚴格遵守『資料完整性』、『資料不可變性』原則,能很好地避免中間狀態問題。

『資料驅動 UI』

底層資料發生變化後,上層 UI 如何感知並重新整理?

總的原則就是『資料驅動 UI』

直白點,就是『資料層』有渠道、有方法在資料變化時能主動通知到所有關注該資料的『表現層』物件。

看似很簡單?實則很多專案都沒能做到這一點。

有沒有聞到『響應式程式設計』的味道。 有同學一聽到『響應式程式設計』就覺得很複雜,難於接受。

其實,我個人認為『響應式程式設計』並非一定要使用諸如Rx*ReactiveCocoa或 iOS 原生的KVO這樣的框架或技術。(它們只是一種實現手段而以) 其核心在於:

  • 『資料層』主動通知,『表現層』被動響應;
  • 資料的所有使用方都能及時感知到資料的變化。

因此,像 Delegate、Callback 甚至 Notification 都可以用來實現『響應式程式設計』。

『響應式程式設計』其實就是『觀察者』設計模式的一種應用場景。

好了,我們來回顧總結一下:

  • 『單向資料流』:保證資料的修改僅發生在『資料層』內部,這也是『資料完整性』、『資料驅動 UI』得以實現的前提;
  • 『資料完整性』:任何資料的修改都是整體替換,實現『資料不可變』的語義,避免出現數據的中間狀態;
  • 『資料驅動 UI』:保證資料修改後能及時通知 UI,避免狀態不同步。

MVVS

基於『單向資料流』、『資料完整性』以及『資料驅動 UI』的原則,我們提出一種新的 GUI 模式:MVVS

  • M:Manager,處理業務邏輯,管理業務資料,將資料轉換為 ViewState 並通知上層 UI;
  • V:View,UIViewController/UIView,面向 ViewState 程式設計;
  • VS:ViewState,當前的 UI 狀態,將『資料驅動 UI』進一步拆分:資料驅動狀態、狀態驅動 UI。

MVVS vs. MVVM,相當於將 MVVM 中的 VIewModel 拆分為 MVVS 中的 Manager 和 ViewState,使得各自的職責更加清晰。

MVVS 中的 ViewState 受 Flutter 響應式框架 flutter_bloc 中 state 啟發。

Manager、View、ViewState 間的關係如下圖所示:

ViewState

ViewState 代表當前的 UI 狀態,為 UI 提供展示用的資料。

原則上,ViewState 與 View 一一對應。 對於過於簡單的 UI 也可以沒有與之對應的 ViewState。

如上圖,與 ViewController 對應的是 Root ViewState,代表當前該模組的整體狀態。

根據不同的狀態,ViewState 可以是不同的子型別,如:LoadingViewState、ErrorViewState、EmptyViewState、LoadedViewState 等。ViewController 再根據不同的狀態作出不同的響應。

其他

上面我們主要講述了 GUI 架構上會影響程式碼可維護性的幾個關鍵點 影響程式碼可維護性的因素還有很多,如:

  • 最少狀態: 在程式碼中經常會看到很多的狀態變數,如:is***(isFirstLoadisNewUser)、has***(hasLoaded)、***Count(userCount)等等。

    這些狀態本身的維護以及其對程式碼整體的維護都是非常容易出問題的。

    是否在所有需要修改的地方都正確的修改了?

    修改後使用到這些狀態的地方都及時通知到了?

    因此,狀態要越少越好,能不加就不加。

    如,***Count是否可以從資料集上動態計算得到

  • 最小許可權: 無論是模組還是類暴露的介面都應遵守最小許可權原則

    在具體開發過程中可以通過『 依賴倒置 』原則,讓介面需求方提出介面需求,避免由實現方直接提供介面而無意中暴露過多細節。

    具體可以參看『 面向物件設計原則『SOLID』在開發中的應用 』

  • 寫純函式: 純函式幾乎沒有外界依賴,其在可維護性、易理解上有天然優勢。

    一般類方法都有純函式特性,因此,能寫類方法的時候就不要寫例項方法。

    『 函式式思維 』一文中對函數語言程式設計有過簡單討論

可擴充套件性


『唯一不變的就是變化』

面對變化,我們唯以積極心態對之。

因此,程式碼的可擴充套件性也是我們需要重點思考的問題之一。

『 SOLID 』中的『 O — OCP,開放-封閉原則 』,就是用於指導可擴充套件性的原則之一。

OCP:『 對擴充套件開放,對修改封閉 』。

強調『可擴充套件性』就是要求我們寫『活程式碼』。 在平時 Code Review 時,我經常開玩笑地說『你這個程式碼寫的太死』。

23 種設計模式中的『 策略模式 』、『 模板方法 』都是用於指導提升可擴充套件性的方法。

『 策略模式 』

提高程式碼可擴充套件性最有效的方式之一就是面向介面程式設計。

『 論面向介面程式設計 』一文中對此有詳細介紹,在此不再贅述。

在具體開發時如何寫出擴充套件性高的程式碼呢?

『擴充套件性』面向的是未來,

因此,首先要思考的是『 什麼是可能會變的 』

再將『 可變部分 』隔離出來,並以介面的形式去抽象它。

熟悉設計模式的同學可能已經看出來了,這其實就是『策略模式,Strategy 』。

在之前的文章中,我們也提到過,如:登入模組、多 Tab 頁面都是典型的可通過『 策略模式 』來提高擴充套件性的場景。

登入模組的例子在『 面向物件設計原則『SOLID』在開發中的應用 』一文中有詳細介紹

多 Tab 頁面的例子在『 論面向介面程式設計 』一文中有詳細介紹

『 模板方法 』

『 策略模式 』的基礎是面向介面程式設計。

而『 模板方法,Template Method 』的基礎是繼承。

同樣,在『 面向物件設計原則『SOLID』在開發中的應用 』一文中對『 模板方法 』模式有過簡單的介紹。

『 iOS 高效開發解決方案 』一文中介紹過通過『 模板方法 』模式提高 UI 元件的擴充套件性。

提升程式碼可擴充套件性的方法絕不僅上述 2 種模式,但其背後的思想非常重要,在平時開發時可靈活應用。

其他


除了上述提到的『 易理解 』、『 可維護 』、『 可擴充套件 』之外,還有很多點是值得我們去思考和關注的,如:

  • 錯誤處理: 如何優雅地處理錯誤其實非常重要,但往往被忽略。在設計介面時需要關注出錯的情況;

  • 關鍵路徑打 log: 錯誤是無法避免的,除了在出錯時給使用者一個較好地提示外,我們也需要去了解出錯的原因,此時 log 就顯得尤為重要,要養成在關鍵路徑打 log 的習慣。否則,使用者反饋問題後,兩眼一抹黑,無從下手;

  • 適時重構: 重構不一定是要對程式碼做出『 翻天覆地 』的改變,小到對變數重新命名、抽取一個方法等『 微小 』的優化都算是重構。總之,在當前程式碼結構已不再適應新業務需要時,就需要及時重構,切不可在原有基礎上打補丁,程式碼的惡化往往就是從此開始的。

    對待程式碼我們同樣要有敬畏之心:『 勿以善小而不為,勿以惡小而為之 』。

小結


寫出高質量的程式碼可謂『 功在當時,利在未來 』

每一位開發者都應為開發出高質量的程式碼也努力

程式碼設計能力的提升非一日之功,需要我們長期不斷地學習、實踐、思考、總結

優秀的書籍、優秀的開原始碼我們要學習

糟糕的程式碼我們也要去反思,去總結

在程式碼上同樣要做到『 勿以善小而不為,勿以惡小而為之 』

諸君共勉!