淺談開發者友好的軟件設計
面向開發者的軟件,相比普通用戶僅在限定的場景下使用外,還可能會被集成、擴展、二次開發等等,因此在代碼或設計層面也應該盡可能考慮如何對開發者更友好。
本文從:
- Least Surprise(最小驚嚇原則)
- Guide, Not Blame(別怪用戶,嘗試引導)
- Keep It Simple, Stupid(盡量保持簡單)
三個不同的角度,結合實際案例,嘗試闡述和討論哪些設計是對開發者友好的。
Least Surprise(最小驚嚇原則)
不要驚嚇用戶!
通常在某個特定的領域,人們會在領域上下文內形成一系列的慣例和常識,比如:
- 走路撞到墻,頭會痛,但墻通常不會塌
- 在網頁上填完表單按下提交按鈕,頁面會跳轉
- 在命令后面追加 --help 通常會返回該命令的使用方法
因此,我們的軟件所表現出的行為,應該盡量滿足在其領域內具有一致性、顯而易見、可預測。
1. 單一控制來源
作為用戶,通常期望軟件能提供來源清晰,行為一致的配置,而如果有很多種不同的方式都能達到類似的配置效果,用戶就會感到困惑,不知道應該用哪一個。
Spring 框架在發展了很多年后,由于其出色的靈活性設計,反過來也導致了一定程度的理解困難。
比如 Spring Security 中想要配置自定義的認證時,可以:
上面這三種方式都可以滿足認證的要求,包括官方文檔在內的諸多資料都會嘗試使用其中的一種或兩種方式來配置認證,如果用戶對其設計原理不甚了解(比如剛剛上手),看到這么多種不同的配置方法,就很容易會產生不解與慌亂。
2. 無二義性
某些情況下,用戶在使用我們的軟件時必須要對某些配置進行設定。從用戶的角度看,對于配置項,用戶期望的是最好能一眼就看出來該配置的內涵是什么,假如配置項存在二義性,就會讓用戶摸不著頭腦。
這里引用一個討論 TiDB 可交互性文章中的例子:
在 TiDB 5.0 版本中引入了一個配置開關:
- tidb_allow_mpp = ON|OFF (default=ON)
這個開關項的本意是如果設置為 OFF,則禁止優化器使用 TiFlash 來執行查詢,而假如設置為 ON,那么優化器會根據實際情況自行選擇是否使用 TiFlash 。
所以雖然配置的是 ON,但其實到底有沒有用 TiFlash,還得看優化器的判斷。“就像是房間里控制燈光的開關,關掉時燈一定不會亮,而打開后燈卻不一定會亮”。這種二義性開關的存在,容易讓用戶誤解、會錯意。
面對上述問題,文中給出的修改建議是,改為:
- tidb_allow_mpp = ON|OFF|AUTO
多了的這個 AUTO 確實能讓用戶一目了然。
3. 遵循慣例
有很多設計上的、語言層面的或是領域內的慣例和規范,通常軟件開發者們都會默認去遵循這些慣例和規范。
這里引用了《重構 2》 中查詢和修改分離的例子:
某些時候方法命名甚至直接省略了后面,變成 getTotalOutstanding()。
通常遇到以getXXX開頭的函數,用戶大都會默認該函數具有冪等性,假如使用后發現調用動作竟然產生了某些副作用(比如這里是每調用一次都會發送一次賬單),就會讓用戶費解。(Rust 很棒的一點就是當發現 get_xxx(&mut self) 這種方法定義時會自動高亮警告 )
另有一例:
通常類似上述的 “移動” 操作,都是 from 在前,to 在后,而如果我們的函數是反的,to 在前,from 在后,那就是在坑用戶了。
不過,考慮到上述操作的兩個參數同屬于 string 類型,我們就沒辦法限制用戶一定會按照先 from 后 to 的形式傳參(也許用戶鐘愛 intel 匯編語法?),那么更好的方式可能是:
Guide, Not Blame(別怪用戶,嘗試引導)
RTFM,是老人對新人的諄諄教誨?還是軟件作者對伸手黨的有聲控訴?
每當我們看到用戶報告的錯誤顯示Http Code 400時是否都一陣竊喜?
“用戶錯誤” 是用戶自己的問題,與開發者無關,是這樣嗎?
1. 報錯了,然后呢?
當用戶執行了誤操作后,我們的軟件理應將詳細的錯誤信息反饋給用戶,但除此之外,能做的還有很多:
上面展示的是 Rust 編譯器的編譯報錯,從上到下分別是:
- 告訴我們錯誤原因是 “缺少生命周期標志”,錯誤碼是 E0106
- 指出是 “linear_probe_hash_table.rs” 文件的第 17:26 個字符出錯
- 又用箭頭指明了代碼錯誤的位置
- “help” 部分告訴我們 “可以考慮使用 'a 符號”,最后用波浪線給出了改正后的效果
有人說寫 Rust 是 “compiler-driven development”,從編譯器這種保姆級的報錯信息來看,確實所言不虛。
2. 幫助用戶識別而非記憶
在一些較復雜、步驟較多的配置操作后,最終執行前用戶心里可能沒底,我們的軟件應該幫用戶檢查并識別問題(即類似 dry-run 的能力),從而降低錯誤發生的概率。
我們知道 Terraform 的工作流是 Write -> Plan -> Apply。
在編寫完成 tf 文件(Write)之后,執行操作(Apply)之前,有一個Plan階段,就是用于告知客戶接下來將要執行操作的執行計劃,以及可能產生的影響。
Plan 會根據當前資源的狀態和用戶期望狀態作對比,給出執行計劃,而不會對系統產生任何實質影響。假如用戶發現執行計劃中與其預期不符,就可以回過頭去重新修正。
3. 交互式文檔
雖然用戶最開始可能只會花 30 秒來瀏覽文檔,但真正到深入使用我們的軟件時,看文檔是必須的。
傳統的文檔看起來不僅枯燥,而且由于缺少反饋,用戶很難記住文檔要傳達的知識。
(來源:https://arthas.aliyun.com/doc/arthas-tutorials.html?language=en&id=arthas-basics)
上圖展示的是 Arthas 提供的交互式文檔(學習課程),通過在線的 ”playground + 引導用戶完成任務” 的形式,加強反饋,按階段給予獎勵,可以很好的提升體驗。
Keep It Simple, Stupid(盡量保持簡單)
用戶想要我們的軟件易用,易懂,易擴展。
開發者就需要從 API、設計、協作等多個方面確保簡單,而簡單很難。
1. 耐心與好奇心成反比
當我們嘗試使用一種新的包、工具等等時,首先面臨的就是如何引用、安裝的問題。
我們會去主頁看 README,但人的耐心通常很有限…
下圖是 Prometheus 的 Get Started 頁面:
(來源:https://prometheus.io/docs/introduction/first_steps/)
它不僅存在大段的文字,甚至還有配置文件,這潛在的給用戶施加了不小的心理負擔。
如果用戶想要嘗試,可能要專門找半小時空閑,鼓起勇氣、正襟危坐,這才開始依照文檔試驗。
再來看看 rustup 的 Home 頁:
(來源:https://rustup.rs/)
相比起來,一眼就能看到深色背景的命令,30 秒就可以在 shell 里面執行,那么任何人都可以近乎零負擔的在本地快速搭建 rust 環境。
README 或者 Home 頁是通常是用戶第一次接觸我們的軟件的地方,怎么樣抓住用戶的好奇心的確需要仔細研究。
2. 簡潔就是美
簡潔之美,體現在如何優雅的解決問題。
Golang 中啟動一個 go-routine 的操作可謂極致簡潔:
不需要 import 任何包,沒有其他與之相關的 key word 要理解和記憶,甚至連對 go-routine 本身的引用都不給返回(怎么管理 go-routine 是另一個故事了)。正是這種簡單易用的設計,使程序員想要啟動一個 go-routine 時毫無負擔。
3. 約定大于配置
將環境、配置,以約定默認的方式自動設置,這樣就減少使用者在最開始需要做出決定的數量,也就降低了上手難度和用戶的心理負擔。
Ruby on Rails 相對較早的實踐了這一概念,并在其框架內應用了大量約定,來降低初學者的使用門檻以及提升專家的生產效率。
Spring Boot 甚至完全就是為了方便用戶使用 Spring 框架而創造的。通過一系列的自動化配置、條件配置等方法,讓用戶只需要非常少量的配置(甚至零配置)就可以 “Just Run”。
而對于不同的使用場景下用戶可能會選擇不同的自定義配置項,這時候如何優雅的讓用戶只關心自己想要的配置呢?
Functional Options
當構建某個實體需要許多必選、可選的參數時,傳統的兩種辦法:
- 全部作為傳入函數,或每種參數寫一個包裝函數
- 傳入一個配置類(或結構)
上述方法都存在一些問題,更好的辦法是以可變參數的形式進行配置。以創建 grpc server 為例:
不同的用戶對配置的關注點可能不同,上述代碼既設置了超時時間、消息大小、攔截器等等,又不用關心其他的配置。
這樣的設計能夠方便使用者靈活的選擇想要的或是自定義的配置項。
4. 不僅好用,還免費
“What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
”以上是 Bjarne Stroustrup 對 C++ 零成本抽象原則的描述。
符合零成本抽象原則的功能和特性,不會產生任何的全局開銷(不使用就沒有開銷),任何比該功能抽象級別更低層的手寫代碼其性能也不會更好。
其實對用戶而言,付出一定的成本來提升使用體驗通常也是值得的,比如用泛型讓代碼實現更優雅,而成本可能是代碼膨脹或運行時開銷。
然而,對于實現了零成本抽象的功能,不僅提升用戶體驗,還不額外引入任何成本。
顯然功能既要好用又要免費,在其設計上就會十分的困難,但帶來的價值也是巨大的。
Rust 通過引入 Ownership 和 Borrowing 的概念讓自動內存管理完全在編譯期完成,免去了手動申請釋放內存的成本,也免去了運行時 GC 的成本。這一特性讓 Rust 迅速受到了用戶的追捧和簇擁。
5. 關注結果,不關注過程
如果允許用戶直接描述 ta 想要的結果,那么用戶就不必指定具體的工作過程了。
以下代碼描述的是用 java 語言來實現 word count:
先將單詞映射為 (word, count) - pair,之后對相同的 word 進行聚合,最后得到結果。
這是過程式的辦法。
而如果用 SQL 這種聲明式的實現,見下圖:
SQL 語言只描述了用戶想要的結果,至于獲取這一結果中所要經歷的過程,用戶無需過問,也不關心。
另外,在 K8S 的聲明式 API 設計中,除了能靈活的描述結果狀態以外,還能保證操作的冪等性,用戶體驗非常好。
顯然,聲明式 API 的抽象層次要比過程式 API 更高,但這也意味著聲明式 API 更難實現。常見的聲明式 API 的實現大都基于解決特定領域的問題,并不具備圖靈完備性。
結語
本文主要討論了構建開發者友好的軟件需要包含的三點要素,并通過一些事例佐證了這些要素本身的必要性。
綜上來看,我們認為對開發者體驗友好的軟件:
- 首先,應該遵循一些常識和領域內的慣例,從而避免在使用中讓用戶產生困惑。
- 其次,應該盡量引導用戶做出正確的操作,同時降低試錯成本改善學習體驗。
- 最后,應該在設計和交互上盡量保持簡單,做到易用、易懂、易擴展。
【本文是51CTO專欄作者“ThoughtWorks”的原創稿件,微信公眾號:思特沃克,轉載請聯系原作者】