『碼』出高質量

本文從易理解、可維護、可擴展三個維度簡要介紹了對高質量代碼的理解。git

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

本文同時發表於個人我的博客編程

Overview


我的認爲,高質量的代碼首先應該是『簡單的』。redux

『簡單』又能夠從如下三個維度去度量:設計模式

  • 易理解
  • 可維護
  • 可擴展

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

易理解


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

高質量的代碼能夠是優雅的,但必定不是炫技的。數據結構

高質量的代碼應該是深刻淺出的,儘可能用簡單、樸實的方式去表達。多線程

易理解面向的是整個團隊,而不是寫代碼的那我的。架構

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

影響代碼可理解性的因素有不少,好比:

『良好命名』

  • 好的命名便是成功的一半,也是最具挑戰性的事情之一;
  • 總的原則就是體現『作什麼』,而不是『怎麼作』;
  • 在閱讀優秀開源代碼以及系統 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 直接修改了底層數據,但並未通知 Scenes二、Scenes3 (Scenes1 可能根本不知道 Scenes二、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 的習慣。不然,用戶反饋問題後,兩眼一抹黑,無從下手;

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

    對待代碼咱們一樣要有敬畏之心:『 勿以善小而不爲,勿以惡小而爲之 』。

小結


寫出高質量的代碼可謂『 功在當時,利在將來 』

每一位開發者都應爲開發出高質量的代碼也努力

代碼設計能力的提高非一日之功,須要咱們長期不斷地學習、實踐、思考、總結

優秀的書籍、優秀的開源代碼咱們要學習

糟糕的代碼咱們也要去反思,去總結

在代碼上一樣要作到『 勿以善小而不爲,勿以惡小而爲之 』

諸君共勉!

相關文章
相關標籤/搜索