在軟體業工作的人,應該都聽過敏捷(Agile)
然而,我們認識敏捷的契機,可能都是來自 Scrum, Kanban 等等實務操作
而不曾細究過根本
敏捷究竟是什麼?敏捷又想解決什麼樣的問題?
我想在讀了 Clean Agile 這本書之後
談談我對「敏捷的本質」所做的一些思考。
另外,我也將我的讀書筆記,整理成了一張 Heptabase 白板
有興趣的朋友們也歡迎參考。
軟體開發的兩大難題
談敏捷之前,我想先談談敏捷的背景。
敏捷是在軟體業中發展出來的
而談到軟體業,就不得不談軟體業中經常碰到的兩個大問題:
「客戶要什麼」與「技術債」。
客戶要什麼
跟客戶溝通討論,問出他們的需求,這會有什麼問題呢?
問題在於:客戶也不曉得自己真正要什麼!
客戶一開始會給我們一些場景和範例,作為開發軟體的參考
於是我們開始分析需求、設計軟體架構。
到目前為止沒什麼問題。
然而當我們將程式實作出來後,問題就來了:
「這邊可以改成這樣嗎?應該很容易吧!」
「這邊可以加個功能嗎?」
「這個功能想一想好像不需要,可不可以拿掉?」
我們費了好大的工夫寫出來的程式
客戶這邊要改、那邊也要改,沒完沒了
於是開發團隊只能疲於奔命
一邊應付客戶的要求,一邊追趕著時限
最後交出一個看似符合客戶需求,但是暗藏很多問題的半成品。
技術債
既然可以符合客戶需求,那會有什麼問題呢?
問題可大了。
因為開發過程中做了大幅度的修改,加了許多意料之外的功能
導致原本程式的架構設計不敷使用
於是新功能只能拼拼湊湊、疊床架屋
以類似違章建築的形式加在上面
就這樣程式碼漸漸變得越來越複雜
就像是一團義大利麵,剪不斷理還亂
而這團混亂的程式碼
我們就給他取了一個好聽的名字「技術債」。
更慘的是,這亂成一團的程式碼
和改需求的問題會開始產生一個惡性循環:
為了趕時間加需求呢,只好拼命蓋違建
而違建蓋上去之後呢,新需求又更難塞進去了
於是有一天,我們會發現
程式碼完全改不動了
我們只好祭出殺手鐧:砍掉重練!
於是,我們又開始了一個新的迴圈。
直覺害死了我們
究竟是什麼讓我們落到這步田地,我想我會這麽說:
是直覺害死了我們。
在我詳細說明之前,大家可以先回答看看這兩個問題:
- 當你想解決一個簡單的小問題時,你會怎麼開始?
- 當你面對一個複雜又麻煩的大問題時,你又會怎麼開始?
我想大多數人的回答應該都會是:
簡單的問題直接動手,臨機應變
複雜的問題則是先做分析和規劃,然後一步一步執行
對吧?
然而,我想說的是:正是這樣的直覺將我們推入深淵。
完整計畫的盲點
分析問題、設計解法、制定計畫、完美執行。
這樣的方法論確實可以幫助我們解決許多問題
但這個方法其實有一個很大的前提,就是:
這要是一個固定而不會改變的問題
例如我們準備考試,考試範圍是固定的
所以我們可以透過周全的準備來處理它
但如果問題本身改變,這套方法就失效了
(請想像一下清朝末年科舉廢除時,十年寒窗中傳來書生們的哀號)
這也就是「計畫趕不上變化」
而當問題的變動幅度與頻率越高,這個方法的失敗率也就跟著越高
變動正是軟體的本質
而軟體開發,恰恰好就屬於這種變動的問題
甚至我們可以說,變動本身正好就是軟體的使命
我們就是不希望每次修改功能都要「重新製造硬體」
所以才發明了「軟體」
「軟體」這個名字本身就代表著「容易修改」的意義
「容易修改」本來就是軟體的使命
「產出容易修改的軟體」也正是軟體工程師的價值所在
所以,到頭來,原來我們沒有資格抱怨客戶改需求!
晴天霹靂!
我們之所以陷在技術債和改需求的泥沼裡
罪魁禍首原來不是客戶和老闆
而是「做出完整計畫」的直覺害死了我們。
敏捷 — 解決動態問題的方法論
瀑布 vs. 敏捷
使用完整的設計與計畫來進行軟體開發
這種方式被稱為「瀑布式開發」
因為這種方式的流程是先分析、再設計、最後實作
一步接著一步,就像是水從河川的上游流到下游一樣
於是我們給它取了一個「瀑布式開發」這麼澎湃的名字。
但現在,我們已經可以看出這種方式的問題所在:
軟體的本質就是會變動
所以我們一開始做再多、再完整的分析與設計
其實就只是做白工而已。
那麼,敏捷是怎麼處理這樣的問題的呢?
我會用八個字來總結敏捷的精神:快速回饋,快速反應。
因為問題本身不斷變動,所以我們必須不斷從問題身上取得回饋
得到了回饋,就有辦法修正我們的方向,緊緊追著問題跑
而為了能夠追上問題改變的速度,我們的計畫也不能太過龐大笨重
於是我們縮小計畫的規模,把做事的顆粒度變小
這樣就能在得到回饋時即時轉向
也就是說,敏捷放棄了一口氣解決大問題
而是透過把大問題切小,每次都只處理最重要的小問題
透過頻繁的追著重要的小問題跑
我們就可以跟上整個大問題改變的腳步。
敏捷的業務實踐:持續為客戶提供價值
以上的策略,實際應用到「客戶要什麼」的問題上
就成為了「敏捷的業務實踐」
也就是我們所熟知的 Scrum 或是 Kanban
以 Scrum 為例:
通常,我們會設定一個兩週到一個月的開發週期
每個週期開始前,團隊都會跟客戶討論當下最重要的需求
在這個開發週期內就只做這些事
而在週期結束時,團隊則會向客戶展示這些完成的功能
再開始下一個週期
於是,我們就有了一個需求與開發的迴圈
每個迴圈中團隊的所做,都是基於客戶當時的回饋
而在每個迴圈中,客戶只能決定需求的優先順序
能夠完成多少事則是由團隊決定
這樣一來,就能確保客戶永遠得到最重要的產出
而團隊也不需為了一些可能根本不需要的需求
趕工端出粗製濫造的產品
因為每個周期做的都是最重要的需求,我們也最小化了作白工的機率
而其他次要的需求,則因為團隊根本尚未開始設計與實作
客戶也可以輕易地改變心意,不會對開發的進度造成影響
於是,我們可以看到,敏捷以一種非常優雅的方式
一步一步地解決客戶當下最迫切的小問題
同時解決了「完成之後才發現不是客戶要的」這個大問題
敏捷的技術實踐:維持高品質的程式碼
現在我們知道了,敏捷是如何持續為客戶提供價值
接著,讓我們談談技術債的問題。
技術債的本質其實就是程式碼的品質問題
而我們可以將程式碼的品質分成兩個層面:功能面和設計面
在功能面,我們希望軟體能夠正確提供所需的功能
在設計面,我們則希望軟體可以擁有良好的架構,高效能、易修改
而當我們犧牲設計面來滿足功能面時,所得到的就是技術債
但設計面所累積的問題,到頭來還是會變成功能面的問題
因為功能面與設計面,其實是一體兩面。
面對技術債,直覺上我們仍然會這樣想像:
如果有一段時間可以把技術債一次解決
從此之後,就能過著幸福快樂的日子。
然而事與願違
還技術債通常還是免不了與新功能的開發同步進行
於是,我們就又碰到了相同的狀況:變動的問題。
我們費盡心思梳理糾結的程式碼,想出更好的設計,改好了程式
結果程式碼想要合併時
卻發現同事最近新加的功能,又產生了一坨亂七八糟的程式碼
而這些程式碼和整理過後的設計並不相容
於是,負責處理技術債的工程師只好和負責新需求的工程師賽跑
拼命趕在新程式碼產出之前把舊的程式碼改好。
然而,就算成功趕上了,還有一個難關要過:
「你怎麼知道舊的功能沒有被改壞?」
於是為了避免改壞程式,只好做測試
但是做了測試,又被新加的程式碼超車了。
於是,重構的程式碼永遠無法真正上線使用
最後還是只能回到那團「可以動的程式碼」,將就著用。
那麼敏捷又是怎麼處理技術債問題的呢?
還是相同的精神:快速回饋,快速反應
首先,敏捷建議我們使用測試驅動開發(TDD)
也就是說,在實作一個功能之前,永遠都先實作這個功能的「測試」
所以當我們的實作有問題時,可以馬上得到回饋
而因為這時改動的程式碼還非常少,所以 debug 的速度也會快上許多
而且,當我們遵循 TDD 的紀律
程式中所有的功能都將會有對應的測試
這代表著:我們可以得到整個程式「功能面的完整回饋」
也就是說,只要所有的測試都亮綠燈
我們就可以很有信心的說:程式沒問題。
而有了這些測試作為基礎,我們就可以安心的進行結對編程 (Pair Programing)、程式碼審查等等敏捷實踐,讓團隊成員之間可以頻繁地互相給出「設計面」的回饋。
因為在完整的功能面測試的支持下
我們可以隨時隨地進行設計面的修改,而不再害怕把程式弄壞。
認真來說,技術債的問題其實比「變動的問題」更加複雜
因為它是「會持續累積的變動問題」
而 TDD 的解法
就像是在每個小問題可以發生的地方,都預先裝上了護欄
它不只解決了當下的小問題
在不斷累積之下,這些小護欄最後會形成一個很大的防護網
這個防護網確保了程式在功能面上的完整性
同時也給了我們對設計面動刀的勇氣
讓我們不再害怕會把功能面改壞。
TDD 表面上看起來,像是繞了個遠路去解決功能面上的小問題
而實際上,卻因為它堅實地守護住了功能面
而為程式的整體帶來長遠的複利效應。
能夠想出這個方法的人,我只能說佩服佩服!五體投地!
結論
軟體開發所面對的客戶需求與技術債都是頻繁變動的問題
客戶需求,是大問題當中的小問題不停變動
技術債,則是小問題不斷疊加變成大問題
而敏捷給我們的解方則是:
藉由快速得到小問題的回饋,來解決小問題
只要緊緊跟上問題的變動
就能一步一步將整個大問題解決。
然而,這樣的做法其實非常地違反直覺:
明明我們所面對的問題
是比大問題還要困難的「變動的大問題」
而敏捷卻不要我們「解決大問題」
甚至要我們從可以解決大問題的「完整計畫」向後退步
變成一種「有計劃地臨機應變」
正是這樣的矛盾與不協調,形成一股隱形的力量,阻礙我們推行敏捷。
但只要當我們看清楚敏捷的本質在於「處理問題的變動性」時
我們就能夠真正接受敏捷、享受敏捷
並且開始欣賞敏捷的巧妙之處
下次,當我們碰到新的變動問題時
也許我們也可以思考看看敏捷的精神:快速回饋,快速反應
相信敏捷能夠再次派上用場!