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