Categories
程式開發

一個輸入框你要做一周?


如果PO說這是個很小的改動,你不要信他

一次有爭議的估點

在某次迭代會議上,PO希望交付這樣一個“簡單”功能:在應用中,用戶可以輸入自己的地址,這樣我們可以定期郵寄一些宣傳冊給用戶。

按照PO的描述,這只是一個很簡單的文本輸入框,用戶填寫地址之後,地址信息隨著其他個人信息一起存到數據庫即可。 PO甚至在白板上畫了一個不太規則的長方形作為示意,然後滿懷期望的將目光投向了你— 一個做事情還算靠譜的開發– 友善的問道:“你覺得實現這樣一個輸入框,需要多長時間?如果你覺得太小的話,我們是不是可以在做其他卡的時候順手做了?”。

你定了定神,在腦海里大致驗算了一遍,說:“嗯,我覺得在理想情況下,大概需要五、六天。如果算上開會……”。

“什麼?這樣一個輸入框你要用一周?!”,PO敲著白板上那個不規則的長方形問道。

“呃……,我說的是理想情況,實際上應該會比這個時間更長……”

“……”

如果你有過和非技術出身的PO(或者站得太高而忘記地面是什麼樣子的架構師)一起工作過,大約很大程度上有過類似的經歷。通常來說,預期有這麼大的偏差,很可能是大家說的並不是同一件事兒:要么是PO想的過於簡單,要么是開發想的過於復雜。

遺漏掉的細節

由於專業知識的屏障,以及對細節的過度簡化,使得非專業人士往往會低估完成某項工作所需要的工作量。另一方面,對於專業人士自身,如果忽略了外部環境中客觀存在的阻力,同樣會對實際工作量產生錯誤的判斷。

“簡單”的輸入框

在PO眼中,一個普通的文本輸入框大約長這個樣子:

一個輸入框你要做一周? 1

輸入之後,傳到後端保存一下就完事兒了。當然,可能還需要一些必要的校驗,比如長度不能太短或者太長,地址遵循一定的格式之類。

不那麼簡單的輸入框

不過在一個有經驗的開發眼裡,一個“普通”的文本輸入框是這樣的:

一個輸入框你要做一周? 2

顯然它擁有更多的狀態,也更加複雜:

  • 禁用狀態
  • 內容為空的時候
  • 設置焦點之後的狀態
  • 非法輸入狀態
  • 提示信息(helperText)
  • 可用性(Accessibility)
  • 其他狀態

通常來說,在初始狀態下,輸入框會顯示一個佔位符。當用戶開始輸入時,需要有各種各樣的反饋:拼寫錯誤、太短或者超長等。此外,系統的其他部分的狀態還可能影響輸入框的狀態,比如一個未授權的用戶不能輸入,這時我們需要將輸入框禁用。

通常這些狀態對應的樣式會有差異,比如字體、字號、顏色和間距等等。這些細小的,但是需要和UX頻繁溝通和改進的細節無疑會耗掉很多時間。

除了眾多的狀態之外,另一個會花費很多時間的地方是校驗(以及限制)。

校驗

事實上,校驗作為一個Cross Functional的點在實際中佔用的開發時間(包括測試時間)往往被嚴重低估。除了基本的校驗規則如:長度限制(最長10位,最短3位),格式限制(郵件)等之外,往往會有更為複雜的校驗規則,這些規則有時候對校驗邏輯實現的結構有一定的“破壞性”。

比如,開發可能定義了一系列的validations

const validations = {
    minLength: 1,
    maxLength: 16,
}


經過一些時間的調試和代碼的重構之後,假設這個校驗機制可以良好的運行了。

const builtIns = {
    minLength: (value, criteria) => value && value.length > criteria,
    maxLength: (value, criteria) => value && value.length  (value) => {
    return _.every(validations, (k, v) => builtInsk)
}

const AddressSearch = ({validations, value}) => ();

很快,下一個需求是將這個AddressSearch的合法性和頁面上的另外一個輸入框關聯起來:當另一個選擇國家的Dropdown的值發生變化之後,AddressSearch組件的校驗規則會隨之變化。

這種情況下,之前的很多邏輯被打破,開發需要更多的時間來修改代碼,以及代碼對應的測試,比如上面代碼片段中的builtIns中的規則都需要重寫。

輸入值的限制

另一個與校驗相關的功能是限制某些值的輸入,對於某些字段,需要禁止用戶輸入特定的字符。它可以認為是對校驗的進一步擴展,不過有時候在實現上會將其獨立起來。一些常見的例子如下:

  • 不允許輸入特殊字符
  • 只允許輸入數字
  • 只允許輸入alphabet
  • 只允許輸入1-12的月份或者1-31的年份
  • 允許輸入數字的小數點,其餘則為非法

等等,有時候這些限制是正交的,互不干涉。有時候則不然。比如在允許輸入數字(初衷是允許輸入手機號碼)的場景下,如果使用


作為實現,則當輸入值實際需要0作為前綴的場景就會出現問題:瀏覽器往往會很智能的將前綴0刪掉。

你可以通過:


修復這個問題,不過很有可能你又會遇到跨瀏覽器等其他問題。總之,每一個潛在的問題,以及對應的解決方案都隱藏在表面之下,我們通常很難在開始前就能預料到這些細節。即使對有經驗的開發者也是如此,更不用說遠離這些細節的業務人員了。

通過網絡獲取數據

現在,我相信PO已經能對“一個簡單的輸入框”所需要的工作量有一個初步的了解了。這些還只是對於純前端的工作量。

現在假設有這樣的一個增強:當用戶輸入地址時,我們需要搜索地址並智能地自動補全(auto-suggestion):

一個輸入框你要做一周? 3

當引入網絡數據獲取之後,情況會變得更加複雜。一方面,異步數據本身就比本地數據複雜,它需要額外的庫(如果你想要屏蔽各個瀏覽器對異步請求的差異的話)。另一方面,網絡中很多變量會超出開發者的控制:比如網速,網絡異常(路由配置,防火牆等)。另外,當有了前端和後端的分別之後,協議/契約就會變成另一個障礙 — 如何保證協議雙方對契約的消費的同步。比如,當前端發現需要展現一個額外字段在界面上,而發現後端並沒有提供的時候;或者後端將日期存成了更加方便存儲的long,而前端消費時發現沒有timezone信息等等。

有了前後端雙方的交互之後,校驗規則同樣需要在後端的模型對象和持久化層中都有所體現。一種做法是前後端使用同構的架構(JavaScript全棧),這樣有一部分代碼可以在全後端復用(我們在上一個項目就中採用了React+AWS Lambda+Node.js的模式,整體體驗還算不錯)。如果是異構架構,則需要將類似的代碼用不同的語言寫兩遍,而且另外一個潛在的問題是:如何使得兩者保持同步?

當然,我相信在工程上,這些問題最終都可以被解決,但是每個問題及其解決方案都不是免費的。即使團隊中的開發有足夠的經驗和快速的學習能力,很多問題依然是無法預見的,而你永遠無法解決一個你不知道的問題。

異常情況

大部分情況下,人們傾向於從正常流程去考慮工作量。而事實上在開發過程中,所謂的“正常流程”才是不正常的。現實世界中有太多的不確定的因素可以讓我們的應用崩潰或者停止工作。網絡請求超時,地址服務器宕機,後端版本升級,瀏覽器的不兼容,操作系統的不兼容,不同的硬件環境,特定的瀏覽器版本(我最近工作的項目上,由於客戶使用的kerberos鑑權機制導致只有Firefox的特定版本才可以正常訪問應用)等等。與之對應的正常流程反而如同在走鋼絲。

既然異常無可避免,我們需要為其設計很精巧的保護機制,一方面需要讓系統可以在錯誤中恢復(不至於白屏,或者禁用所有功能),另一方面還需要展示以及記錄可靠且準確的信息以供修復(日誌,前端的Modal,截圖等等)。

用戶體驗

功能之外,還有很多其他因素需要考慮。比如可用性(Accessibility)以及易用性(包括頁面上字詞的選擇和用戶體驗),以及兼容性的考慮。一些常見的會影響開發工作量的因素包括:

  • 跨瀏覽器支持
  • 老舊的瀏覽器兼容
  • 多設備支持

如果涉及響應式設計,則需要和UX進一步合作來確定實際方案。很多時候,多個設備上的交互模式都不盡相同,比如iPad上的hover效果,小尺寸屏幕上的字體等等。

技術之外的因素

除了技術上很難預見的延遲和障礙之外,實際項目中還有很多會消耗掉大量時間的事件,它們無處不在,細微而瑣碎,但是累積起來產生的影響則相當可觀。

混亂才是常態

比如項目上有若干名同事:

const roster = (
    '吴荣华',
    '侯晓成',
    '贾彦军',
    '邱俊涛'
)

從概率上來說,每個人都或多或少有些工作之外的事情要處理,比如偶爾休假,不太舒服在家休息,通勤路上堵車,或者在買咖啡排時迷路等等。

const excuses = (
    '堵车了',
    '迟到了',
    '要接小孩',
    '不舒服,请半天假',
    '休假了'
)

而生活就像是這樣一行代碼:

`${_.shuffle(roster)(0)} ${_.shuffle(excuses)(0)}`

所以,我們眼中的世界就是這個樣子的:

一個輸入框你要做一周? 4

當然這些異常不會每天都發生,但是如果項目足夠長,這些事情則幾乎必然會發生。隨著人員的增加,參與方的增加,不確定性隨之提高,不是一切都正常的概率則會變得非常大。畢竟,混亂才是這個世界的常態。

而這些混亂可以讓我們之前的估算失效,耗時增長。而這些混亂在估算之初則很難被我們看到,從而導致估算往往偏小。

一個小故事

在幾年前的一個項目上,我估計理想情況下大概需要三天來實現一個對某個資源的RESTful API,客戶的技術負責人當時就怒了,說他自己寫半天就可以寫完,不就幾個CURD嘛。

我幫他略為列了一些子任務之後,他陷入了沉思:

  • Database migration腳本
  • 實體類
  • 實體類之間的關聯
  • Service/Controller類
  • 異常處理
  • 單元測試
  • 與下游系統的契約
  • 集成測試

我印像中這個任務花費的時間應該是多於三天的,因為光找其他團隊的接口人要契約就花了一天半。

應對策略

通過上述的例子,相信大家已經看出評估失誤的一些原因了,而要應對這些錯誤,大的原則當然是反其道而行之。在具體的實踐上,除了經驗之外,一個可行的應對策略是足夠程度的細化。通過細化事實上可以在一定程度上降低不確定性,使得很多原先沒有看清楚的點被重新發現

比如輸入框的各種狀態,狀態之間的遷移,每個不同狀態所涉及的樣式等,通過細化(比如通過可視化的方式畫出狀態機),則很多細節自然浮現​​。更進一步,如果涉及到數據的獲取,那麼對應的加载中加载失败无数据等等狀態又會進一步驅動出更多的細節,從而潛在的可以產生更加客觀的評估。

實踐中,通過有限狀態機的方式來描述窮舉一個組件(或者一組互相關聯組件)的狀態是一個比較有效的方式,它可以更好的揭示組件的各種變體的形態和所需邏輯。

一個輸入框你要做一周? 5

來源:https://www.freecodecamp.org/news/designing-ui-states-and-communicate-with-developers-effectively-by-fsm-fb420ca53215/

另一方面,在心理上需要充分認識到現實世界的複雜程度,特別是涉及組織,相關方很多的時候,不確定性會非線性的增加,從而導致評估的失誤。這要求我們一方面要擁抱變化而不是對抗變化,另一方面驅使我們採用更簡單的設計,更堅固的基礎設施,質量更高的構建方法等等,從而更快速的響應變化。

小結

至此,相比你已經猜到,雖然開發在估算中以為自己已經留有足夠buffer,事實上這個功能的實現必然超過一周。

在實際項目中,一方面由於知識壁壘和一些偏見,人們傾向於忽略必要的細節,從而造成對實際所需工作量錯誤的評估。另一方面,由於我們所處於的現實世界是一個高度複雜,不確定性很高的環境,很多因素往往會互相疊加,互相影響,從而導致即使我們從比較客觀的視角去評估,如果忽略了不確定性,同樣可能低估實際所需的工作量。

我們可以通過足夠細化的方式降低不確定性,從而提高估算的可靠性。另一方面,還需要積極的擁抱混亂,通過更簡單可靠的方式來構建軟件,從而提高響應變化的能力。

P.S. 我本來計劃著寫這篇文章大概需要3天,結果畫圖就花了兩個晚上。然後又去檢查了之前的博客,確定相對於之前的類似觀點有了一些提升,才開始寫草稿(耗時兩個晚上),然後又花了兩個晚上來潤色。

作者介紹

邱俊濤,ThoughtWorks 諮詢師,喜歡技術,崇尚輕量級的開發 / 工作方式,痛恨冗長的會議和各種繁瑣的流程。他還是《JavaScript 核心概念及實踐》一書的作者,個人博客是 http://icodeit.org ,博客上經常會有各種技術的分享。

本文轉載自ThoughtWorks洞見

原文鏈接

https://insights.thoughtworks.cn/an-input-box/