about 2 years ago

Translated from "Appreciating Limited Choice in Languages" by Andrew Binstock, Java Magazine September/October 2016, page 4-5. Copyright Oracle Corporation.

欣賞程式語言有限的選擇

在細節上語言規定越多,越容易有效率地開發

本期我們涵蓋一些 JDK 改善提案 (JDK Enhancement Proposals, JEPs),探討一個近期提出的提案,將 JDK 內建工具的命令列參數標準化,作為支持該提案核心觀點:現有太多種方式從命令列取得協助,因此在使用一個工具時,猜錯了,您必須不斷地嘗試各種可能性,可能是 –help--help,也可能是 -?,還有一個未提到的最後手段,不給任何參數,執行程式,看能從錯誤訊息得到什麼有用的資訊。

我完全支持這標準化,但我認為應該更進一步,在我的觀點,這命令列開關的語法應該被納入語言的風格指南 (style guides) 中,如果 Java 團隊在當初釋出語言時 (當時有建議類別名稱以大寫開頭,常數應全部大寫),對開關的語法有規範標準的公約,這小小的煩惱就不會存在,一個語言在小細節上能越正規,能讓事情更容易完成1

但在理想世界,即使這方案是不充足的,我仍強烈相信:缺少 Java 風格指南2,這件事本身是一種侷限,我期望有一套一致的建議,並被廣泛地遵循,例如,針對左大括弧的位置、縮排的寬度、使用 tab 還是空白、是否使用交錯的 if/else、Javadoc 的多種格式化選項等正式寫出指南,顯然,這同樣能套用更高層次的事物上:全展開的 import 還是使用萬用字元、import 與變數宣告的順序等,當有一份固定的指南,每個 Java 程式都會是一致的,不需要去再次檢查,然後調整成個人或是特定的風格。

說也奇怪,Java 最初出現時,想以某種方式處理當時鬆散語言讓某些工作變成極度冗長乏味的情境,在 Java 之前的主流語言是 C,它是刻意以完全無規範的觀點去設計,即使是現在,在幾回的標準化後,C 在許多地方的行為是沒有定義,或是留給實作者定義。在 90 年代中期3,Java 初次亮相,C 變成輸家。一個整數可大可小,由編譯器定義,如果我沒記錯,最小寬度是 16 位元,16、32 及 64 位元都是合法的整數實作,結果導致,將 C 從一個平台移植到另一個平台是非常瑣碎的一件事,Java 解決了這些問題,資料格式在不同平台有固定的大小,程式可以在不經過修改就能在多種平台上執行。

C 缺乏標準化造成許多無意義的行為,讓原本 AT&T 貝爾實驗室的團隊開發了一個新語言:Go,選擇嚴謹規範的實作,Go 有一個正式的程式風格,所有的程式都使用該風格,有一個格式化工具包含在 Go 的發行版本中,在語言本身,還有額外的限制,例如,任何在 if 之後可執行的程式碼,即便只有一行,都必須以大括弧包覆,許多其他慣例以 “idiomatic Go” 的方式被定義,這令人開心的結果是所有 Go 的程式看起來都一樣,閱讀和編寫都容易。

不只命令列的語法,近幾年 Java 在細節缺少標準化已是一個議題,雖然小但擾人,例如有三種方式可以加註一個欄位不該是空值:@NonNull@Nonnull@NotNull,Checkstyle 及 FindBugs 使用第一個,Java EE 6 和 IntelliJ IDE 則分別使用剩下的兩個,這導致當您從某個環境轉移到另一個開發環境時,您需要改變你的程式與工具才能得到預期的空值檢查,當然,這是站在 Java 自吹自擂的可攜性的對立面:切換 IDE 會造成程式行為不一樣。幸運地,Java 8 所使用 Checker framework 已經形成統一的慣例:@NonNull4

這些確保語法一致性的非難所帶來的便利與好處已被廣為認可的,可以從多數開發組織看到,它們選擇規範專屬的『內部風格』,並強制納入在程式碼檢閱 (code review) 中,因此所有的開發者使用相同的慣例,但這些風格彼此衝突,缺乏一個一致的慣例集合。

多數開發組織選擇規範專屬的『內部風格』,並強制納入在程式碼檢閱 (code review) 中,因此所有的開發者使用相同的慣例,但這些風格彼此衝突,缺乏一個一致的慣例集合。

如果整個世界都是 Java,我想對於這些不一致的容忍不會感到特別繁重,雖然時間會因此無意義的耗費。5

但在這日益增加的多語言世界中,其他語言 (JavaScript、HTML 等) 扮演顯著的角色,缺乏強制的程式碼標準,不只是在 Java,尤其是在那些語言,結合在一起對生產力是一個持續且無意義的累贅。

譯註
1. 有點像 Python 哲學。
2. Java 風格指南是有的,只是很久沒更新了。
3. Java 正式發表於 1995 年,同年的語言還有 Delphi、JavaScript 及 PHP,而最近熱門的 Python 和 Ruby 則在更早的 1991 與 1993。
4. 到目前為止,文中所提到 JSR 308 的 Type Annotations 並不包含在 JDK 8 Update 102 中,需要另外加入 Checker Framework 並使用其中的 compiler 替換 JDK 既有的 compiler,這是 Java 另一個莫名其妙的地方,一個 Java 標準竟然沒有放在 JDK 中,類似的還有 JavaMail 及 Servlet API 都需要引入額外的 JAR 檔才能使用,所以我個人目前暫時不用這些 annotation。
5. 這句翻譯要感謝友人 Linda Hu 的協助。

譯者的告白
標題的翻譯似乎還是沒有很達意,有更好的建議嗎?這是我第一次翻譯 from the editor,算是編輯的話吧?平常沒太認真看,但這次看完,心中的 OS:跟《閒談軟體架構:API Naming Style》有些論點可以相呼應,Java 確實是一個好的語言,至今已 21 個年頭,也略顯老態與凌亂,相較於 Go、Rust 和 Swift 等新語言,Java 無法提供開發者更高的語意層次,真的是很可惜,而且作為一個『靜態強型別』的語言,在 compiler 層級能做的事情還有很多,例如這篇提到的 @NonNull,由 compiler 進行檢查,就可以避免很多的 NullPointerException,而不是放在 container 的執行期檢查層級,只是 Java 後續的方向會是如何呢?我也不知道。

 
about 2 years ago

過去寫閒談軟體架構文章時,並沒有特別注意文章的長度,能寫多少就寫多少 (謎之音:實話是能掰多少是多少?) 在友人的提醒下,有些文章有點太長,不過,我只能說盡量,能找到合適的切割點且還要有適合的標題,有時候蠻難的。接下來想聊聊 library 和 framework 等 reusable components,目前想想可能會有點長,所以預計會拆成幾篇文章,這是第一篇。

在學習任何新程式語言,大多都用熟悉的語言習慣去看新語言有沒有類似的事物,自己也是如此,第一次擔任視窗程式設計的助教,也是我第一次認真學 C# 的時候,網路上有不少人說 C# 就是 Java,所以當時我也是以最熟悉的 Java 習慣去學 C#,在好的 model 設計基礎下,寫出來的程式其實並不會太差,但就是有那麼一點點格格不入的感覺,原因主要是沒有使用像是 eventdelegate 等 C# 的語言特性,及遵循 .Net 既有的 《Coding Conventions》。

大多數的語言社群或官方都會有一套該語言的 coding convention,例如 Java 官方提供 《Code Conventions for the Java Programming Language》,雖然這份 convention 已經沒有繼續維護了,但既有的 Java SE/EE framework 大都是以這份 convention 為主,所以自己在寫 Java 程式也是遵循這份 convention。公司或組織內部的 convention 大多也是以既有的基礎再做些調整,例如 Google 公開的 《Google Java Style Guide》。

Coding convention 會因為語言背後的哲學在嚴謹度上不大相同 (有的語言會管很多),相較 Java 與 .Net Framework 的《Framework Design Guidelines》 會發現 Java 的 convention 大多著重在格式上,像是空白、大括弧、換行等規則,對於 API 的命名則只有 method 以動詞開始、class 需是一個名詞盡量避免縮寫幾個簡單的規則,.Net 則有較清楚的 naming 建議,但整體來看,不論是官方或第三方函式庫,Java 和 Net 的 API 還算是相當一致的。

為什麼要提到 API naming style 呢?前幾天在 Facebook 上看到一段針對 Swift 2.2 轉換到 3.0 引起大量 API 變更的討論,因此我也抽空看了一下 WWDC 2016 的《Swift API Design Guidelines》,session 中出現了一個字 swifty,代表 API 的命名是合乎 Swift 風格與背後的哲學 (整段影片其實很有意思,有興趣的人可以看一下),看影片時腦中浮現了前陣子在天瓏翻閱《Fluent Python》的一段話:

One of the best qualities of Python is its consistency. After working with Python for a while, you are able to start making informed, correct guesses about features that are new to you. However, if you learned another object oriented language before Python, you may have found it strange to spell len(collection) instead of collection.len(). This apparent oddity is the tip of an iceberg which, when properly understood, is the key to everything we call Pythonic.

試想你正在替公司開發一個 Python 的函式庫,但卻用 JavaScript 或 Java 的 naming style 設計 API 而忽視 Python 背後哲學的慣例,這恐怕不會受到 Python 社群的青睞,因為 API 不夠 Pythonic!

設計 API 時考慮些什麼呢?對我來說穩定性是很重要的,Swift 當初推出時,既有的 Objective C 和 C API 採取不變動的原則,這個決定能讓大量 Objective C 的開發者快速上手 Swift,但老實說,混合的程式碼讀起來有種不協調感,久而久之會變成新語言的負擔,所以這次 Swift 2.2 到 3.0 以別名的方式,為大量既有的 API 加上了適合 Swift 的 API 命名,並提供 migration 工具幫助開發者轉換既有的程式碼,將開發者的負擔降到最小,不過還是引起不小的抱怨。

如果沒有像 Apple 有這麼大量的開發人力,改變 API 之外還能夠開發 migration 工具,那就盡量避免 API 的改變,如果真要改變,應該要是同時提供新舊 API,並將舊 API 宣告為捨棄,讓開發者能夠有充足的時間轉換到新 API,在多個版本後才將舊 API 移除,這在很多語言都有對應的標示方式,以 Java 來說就是在舊的 API 加上 @Deprecated,並以 JavaDoc 告知該換成哪個 API,話雖如此,API 一旦發布出去就收不回來了,這也是為什麼向下相容很辛苦,但微軟一直在做的事,辛苦了。

除了穩定性,一致性亦很重要,就如同剛剛引用《Fluent Python》的那段話中,一致性能讓開發者在使用一個新的 libaray 或新功能時,能用過去的習慣去猜測新 API 該如何使用,但這真的不是件容易的事情,畢竟網路上每位貢獻者,並不一定使用相同的 convention,甚至像剛剛提到 Java 和 .Net 有 coding convention,但在我過去的經驗還是會遇到一些不一致的地方,舉例來說,Java 在 AWT/Swing 的時期,蠻多 listener 的 method 是以過去分詞結尾:

public interface ActionListener extends EventListener {
    void actionPerformed(ActionEvent e);
}

public interface FocusListener extends EventListener {
    void focusGained(FocusEvent e);
    void focusLost(FocusEvent e);
}

但也有例外,像是 CaretListener 的 method 不是 caretUpdated,但對習慣這慣例的開發者來說還算能接受,因為 IDE 應該能在前面幾個字打完後,就能找到對的 method

public interface CaretListener extends EventListener {
    void caretUpdate(CaretEvent e);
}

比較讓我稍微花點時間適應的是 JavaFX,引入了 property binding 機制,造成一些使用上的習慣和 Swing 稍有不同。這些不大的變動,也許不算是太嚴重,或是因為新機制的導入造成大變動,都會需要開發者花時間適應,所以在設計 API 前,也許可以先想想想要的一致性是什麼?這期望的一致性最好能成為一份 API design guideline,讓組織的成員能遵循,這次 Swift 3.0 就有詳列一份《API Design Guidelines》,希望開發者在設計給 Swift 使用的 API 時能遵守。

整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:

  • 語意清楚,雖然為了讓語意清楚,可能會讓名稱變長,但在 IDE 的協助下,即使名稱稍長,實際上不需要打這麼多字,所以語意比簡潔重要。
  • 相近的顆粒度,API 的抽象程度不應該差太多,或是說,該隱藏不必要的細節。
  • 簡要的文件,即便語意夠清楚,但還是有個文件說明用途、輸入、輸出和可能的例外。
  • 讓程式能像文章般閱讀,這要跟語言配合,像是 Objective C 需要替參數取兩個名字,一開始覺得很瑣碎,但習慣後,在讀 Objective C 程式時真的像在閱讀文章,可讀性高很多,若有合適的參數名稱,查詢 API 文件的頻率會比其他語言少很多;但不是每個語言都是如此,因此只能靠抽象,若抽象做得好,API 就能組成 domain specific language,讓程式,也能讓程式的可讀性變高。

因此,要設計好的 API 真的不容易。最後,引用 Martin Fowler 也引用的一段話做結束,因為命名真的是一件很難的事!

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

系列索引
上一篇《閒談軟體架構:Client Server
下一篇《閒談軟體架構:內部函式庫

 
about 2 years ago

本來本周是想寫跟最近工作有關的 library vs. framework,不過前幾日中午休息時間時,在 FB 上看到一個開發者在某個社團分享他開發的 library,並想了解大家對於用這來設計 Restful API 的想法,我並沒有直接留言 (不想節外生枝去筆戰,不過,基本上那已經不是 Restful API 了,詳見《閒談軟體架構:休息時間》),只是在我的 FB 簡述自己的想法,沒想到引起一些討論,真的是蠻有趣的,所以今天的主題就來聊聊 client server 吧!

什麼!?client server 有什麼好聊的?首先,先想想看為什麼要用 client server? ㄟ... 作者傻了嗎?現在不都是這樣嗎?因為程式要跑在雲端 (要說成雲端才夠潮) 上啊,這樣不就是 client server 了。這裡要來講古一下了,在大型主機的年代 (這裡要澄清一下,本人沒有歷經那個年代,不然都把自己講老了),就已經有 clent server 的概念:由 client (計算能力較差的終端機) 向 server (計算能力較好的大型主機) 請求服務,幫助理解與管理分散式系統程式的複雜度

當時終端機與大型主機之間確實是不同的機器以區網連接,例如:X Window System 便是 client server 的架構,但由於個人電腦的運算能力越來越好,client 與 server 不見得一定得在不同的機器,一般來說 client 和 server 在抽象上會是獨立的執行實體 (process 或 thread),但之間不一定是透過網路或 HTTP 溝通,可以用像是 IPC 的方式溝通。若真要說 client server,其實可視為 multitiered 架構的一個只有 2-tier 的特例,所以使用 client server 和使用 multitiered 的用意,可以說是一樣的:separation of concerns

既然是 separation of concerns,整個應用程式的程式碼,哪些該放在 server 上,哪些又該放在 client 上,在網頁應用程式變熱門之前,其實就有這方面的討論,在《Distributed Systems: Principles and Paradigms》一書中有個有趣的圖,討論在一個分層 (layered) 的系統中,哪些層放在 client 哪些層放在 server 所帶來的優點與缺點:

圖中的 (a),所有的商業邏輯與資料庫則在 server,甚至 presentation model 都是由 server 提供,終端機只須根據 presentation model 顯示使用者介面,這樣的優點是 client 的運算能力不需要很好,但缺點是 client 與 server 之間的溝通會很頻繁 (連 presentation model 都在 server 上),若溝通成本 (網路速度) 太高,使用者的體驗會很差,如果要以現在的網頁應用程式類比,早期 server-rendered page 技術就屬於這個類型,所有動作都需要以換頁的方式進行

隨著終端機的能力越好,漸漸就將完整的使用者介面程式都放到 client 上,即上圖的 (b),像是很多早期的桌面應用程式都是屬於這個類型,大多數與使用者介面有關的邏輯都能在 client 上處理,在體驗上比什麼都要詢問 server 要好,當時的表單系統都是送往 server 才能處理,但若有錯還是要整個流程再來一次,因此像圖中的 (c),漸漸有一些商業邏輯的程式搬到 client 上,像是直接在 client 檢查使用者輸入的內容,以提升使用者體驗。在大量 JavaScript 框架的協助下,目前大多數網頁應用程式都屬於 (c) 類型,同樣,大多數網路遊戲也是屬於這個類型

由於後來個人電腦的運算能力實在太好了,所以 server 只剩下資料庫或檔案伺服器的責任,像圖中的 (d),其他商業邏輯都搬到 client 上,後期的桌面應用程式大多是這類,像是企業用的倉儲系統等。甚至,為了加快儲存速度,如圖中的 (e) 一樣 client 自己本身有儲存能力,在離線時或是用非同步的機制與 server 同步。一般來說,會把 (a) 到 (c) 稱之為 thin client,而 (d) 和 (e) 則稱為 flat client,就過去部署程式需要到每台機器安裝來說,確實 thin client 會是較好管理的,因此 Sun 曾提出過 thin client 的企業解決方案,但在個人桌面環境,還是操作體驗較好的 flat client 較受歡迎。

不過,現在部署程式已經不像過去那樣需要安裝,只要打開瀏覽器,點選某個網站,大量的 JavaScript 程式就部署到 client 端,也因此能提供更好的體驗,但和過去桌面應用程式不同的,網頁應用程式較少是屬於 (d) 或 (e) 類型 (可能是我孤陋寡聞,若有例子歡迎提供),我覺得主要的原因是安全性,在專屬的應用程式協定的保護下,server 比較能放心 client 要儲存到資料庫的資料完整性與正確性,但 stateless 的 HTTP 協定,則比較難確保這件事,因此,大多數的商業邏輯還是放在後台,避免 client 直接操作資料庫竄改 (自己或別人的) 資料

另外,server 只是一個概念上提供服務的實體,可能是由多個執行中的個體組成,其組成可能是水平的擴展,以 load balancer 提升 server 的負載能力;也可能是垂直的擴展,變成更多 tier 的組成,每個 tier 提供特定的功能;或是混合的微服務組成。因此在設計 server 端的 API 時,個人還是以提供什麼服務為主,而不是提供什麼資料,當然這些服務的結果是以資料的形式送到 client 端,但不會是設計時主要的考慮目標,就如同《建構微服務》一書提到的:

這些功能可能需要資訊交換 --- 共用模型 --- 但我見過太多以資料的角度思考而導致貧血的 CRUD 服務,因此,請先詢問『這個上下文是做什麼的?』然後才問,『他需要什麼資料?』

簡單說,還是從商業邏輯的角度出發去設計 API。除此之外,設計 API 時,還需要考慮什麼嗎?其實還蠻多的,像是怎樣的資料格式傳輸是比較有效率的,或是可以減少 request 次數,又或者是安全性控管等等,不過大體上,不管是不是設計微服務的 API 或單體是服務的 API,我都蠻贊同《建構微服務》書中提到的原則:

尋找理想的整合技術

  • 避免破壞性變更
  • 讓你的 API 保持技術無關
  • 讓服務消費者覺得你的服務很簡單
  • 隱藏內部實作細節

基本上,讓 client 直接操作資料庫 (上圖中的 (e) 類型) 違反上述四個原則,因為若調整資料庫的 schema 就可能導致 client 的程式需要修改,這構成破壞性變化,當然即使是將商業邏輯擺在 server 端也是要修改,程式的修改是無法避免的,但 API 可以扮演隔離改變的角色,由於 client 直接操作資料庫,變成 API 與特定技術綁死 (SQL),此外,使用上也不直覺,使用 API 的人需要知道資料庫設計的細節。個人覺得比貧血的 CRUD API 還要糟糕。

由於使用 client server 主要目的是 separation of concerns,那在寫 API 程式的時候,是否也要考慮 separation of concerns 呢?其實也是需要的,過去擔任 OOAD 助教時,曾經在某次全班的 design review 時,老師對著學生畫的 sequence diagram 問:這個 xxx controller (其實是 MVC 中的 controller) 是 main controller 嗎?當時一起 design review 的我,並沒有意會過來,也覺得 controller 確實可以當作是 main controller 啊,但後來考慮到 MVC 的 controller 主要是處理 UI 事件,main controller 則是處理系統事件,確實不合適在一起。

類似事件後來有再發生,一次是在 code review 學弟的程式碼時,發現大多數的商業邏輯被分散在用 Strust @Action 加註的函式中,於是我建議他將商業邏輯抽出來。另一次則是前陣子 code review 同事寫的後台程式,商業邏輯也是被放在用 Spring @RestController 加註的類別函式中,同樣,請同事再 refactoring 一下。老實說,在軟體設計的術語中,model 和 controller 恐怕是被用得最多的字眼,但也常常被搞混的,像 @RestController 本身有 controller 字眼,有人可能聯想成 MVC 的 controller,也可能想成處理系統事件的 main controller,但對我來說,它只是處理的 request 的 handler 而已。

所以,以 separation of concerns 的角度,API 的 request handler 該處理什麼事呢?簡單來說,就是商業邏輯以外的所有事:

  • 安全性控管
  • 資料格式檢查與轉換成 domain 物件
  • 呼叫 main controller 對應的函式
  • 處理例外與轉換成對應的狀態碼和錯誤訊息
  • 將 domain 物件轉成合適的輸出格式

當然,像 Spring framework 有提供蠻多的 filter 協助開發者處理像是安全性控管、資料格式檢查與資料轉換、例外的對應和資料輸出格式轉換,所以看起來好像只剩下一件事:呼叫 main controller 對應的函式,那不就只剩下商業邏輯的部分了?既然如此,不需要獨立一個類別,直接把商業邏輯的程式放到 request handler 中似乎也是可以?老實說,不是不行,對我來說就只是少了點跟框架的友善距離罷了,自己的習慣,@RestController 中的函式都蠻短的,大部分是 Spring framework 無法幫忙的格式檢查或資料轉換,然後就是呼叫 main controller 了,所以寫測試時,也相對輕鬆。

這次 client server 就談到這裡,不管是網頁應用程式或是 Andorid/iOS 上的 App 大多是 client server 的架構,也需要 API 的設計,希望大家都能設計出好的 API,有任何想法歡迎一起討論。

系列索引
上一篇《閒談軟體架構:休息時間
下一篇《閒談軟體架構:API Naming Style

 
about 2 years ago

看到標題是否覺得很奇怪?休息時間,如果改成 REST time 有沒有一種突然豁然開朗的感覺呢?每次看到 REST 就讓我想起以前念研究所和老師一起想論文題目時,曾提過國外常會玩這種文字遊戲,像是將 Representational State Transfer 變成一個很簡單的單字 REST,但我的東西不管怎麼想,卻想不出什麼有趣的東西 Orz

會想寫 REST,是過去在第一間公司工作時,曾聽到當時的架構師對著團隊說:『不要以為用了 JAX-RS 就是 Restful Web Services,你們 URL 命名根本就亂七八糟』,剛進去的時候,確實只是簡單地看了一下 Wikipedia 的說明,就開始用 JAX-RS 的 annotation 開發後台的 API,也沒多想什麼,所以後來自己在訂 Restful 的 API 總是特別小心,像是複數型的名詞、避免動詞、只使用 HTTP method (GET、POST、PUT 和 DELETE) 等等。

後來換工作,討論 API 時,當只有我特別這麼堅持時,總覺得好像是我比較龜毛似的,但 API 至少還是符合 Restful 風格,後來比較不是前線開發者後,等到 API 會議結束,看結果,幾乎所有的 API 都把動詞放在 URL 上了,曾經詢問了一下原因,但似乎沒引起什麼共鳴,後來想想算了,只要不對外宣稱這是 Restful API 其實也沒什麼不行。

半年後換工作,又從 iOS/Android 工程師變回後台工程師了,由於是我主導整個 API 的設計,在風格上沒什麼問題,但還是得和其他同事解說為什麼 API 是這樣命名的,但說的還是 Wikipedia 上的那一套,直到最近看完《建構微服務》後,才在思考要不要找出原始的論文來讀讀呢?

趁著中秋連假,把《Architectural Styles and the Design of Network-based Software Architectures》抓下來看,老實說,有點震撼到,像是使用 HTTP method 這件事其實根本就沒出現在論文中。

既然看都看了,就簡單和大家分享一下看完這篇論文的心得 (若我理解有誤歡迎更正) 吧!這篇論文用 Chapter 1 整章的篇幅,在說明怎麼描述一個軟體架構,過去在畫架構圖時,總是畫上幾個方塊和線條,然後就開始跟其他人說明,嚴謹一點就用 UML 的部署圖描述,從來也沒去想過要怎麼描述一個軟體架構。

A software architecture is defined by a configuration of architectural elements—components, connectors, and data—constrained in their relationships in order to achieve a desired set of architectural properties.

如上所述,論文倒是很清楚地用三個元素來描述一個軟體架構:

  • A component is an abstract unit of software instructions and internal state that provides a transformation of data via its interface. (也就是平常架構圖上的那些方塊)
  • A connector is an abstract mechanism that mediates communication, coordination, or cooperation among components. (圖上連接方塊的線)
  • A datum is an element of information that is transferred from a component, or received by a component, via a connector. (在元件之間傳遞的資料)

聽完可能有人覺得,啊~ 不就廢話,我平常就這樣描述軟體架構啊,但這一章花了不少篇幅探討這三個元素是否足夠,這倒是我過去不曾想過的。最後,一個軟體架構風格則是一組限制,限制上述元素的角色或功能與關係,任何架構只要符合這些限制就能稱作是某種軟體架構風格,而 REST 是由六個限制所組成的軟體架構風格。

An architectural style is a coordinated set of architectural constraints that restricts the roles/features of architectural elements and the allowed relationships among those elements within any architecture that conforms to that style.

Chapter 2 則是探討網路應用程式 (network-based application) 的架構,我覺得很有價值的是條列了七種網路應用程式架構的 properties:Performance、Scalability、Simplicity、Modifiability、Visibility、Portability 和 Reliability,也就是一般軟體工程書中稱之為 non-functional requirement attributes,並說明為什麼選這七種做為評估架構的依據。並在 Chapter 3 中使用這些 properties 評估了共 20 種 network-based application architectures,合計 7 類的軟體架構風格,值得一看。

Chapter 4 討論設計 Web Architecture 的需求與會遭遇的問題,在繼續之前,可能要想一下為什麼是 WWW 架構,不就是網頁應用程式嗎?如果仔細看 Wikipedia 上對 WWW 的解釋

The World Wide Web (WWW) is an information space where documents and other web resources are identified by URLs, interlinked by hypertext links, and can be accessed via the Internet

只要任何能以超連結串起來的資訊空間就能稱作是 WWW,沒有一定要用 HTTP 協定,也沒有說一定是 HTML,因此,論文提出一種分散式超媒體系統的軟體架構風格能滿足三個假設,這就不細討論三個假設,這是大多數論文的必備要素,說完一堆人就睡著了,基本上,那個方案就是 REST。

Chapter 5 描述 REST 是如何從無開始對 WWW 加入六種限制所形成的一種軟體架構風格。因為是限制,所有描述會變成:

  • 必須是 Client-Server,用意為 separation of concerns
  • 必須是 Stateless,request 與 request 之間是獨立的,server 不需要記住狀態以處理下個 request
  • 必須是可以 Cache 的,但那些內容可以 cache 是由 client 與 server 協調的
  • 必須是 Uniform Interface,這一點很重要,為了達成 uniform interface,論文對介面設計提出四個限制:
    • identification of resources
    • manipulation of resources through representations
    • self-descriptive messages
    • hypermedia as the engine of application state
  • 必須是 Layered System,基本上就是用 layer 隱藏實作的細節,client 只需要知道 uniform interface 即可
  • 可以是 Code-On-Demand,這是 optional 的限制,所以我說『可以是』,基本上是可以用這個方式讓 client 用取得程式碼的方式擴充功能。

剛剛不是說軟體架構可以用 component、connector 和 data 定義嗎?那 REST 風格的軟體架構呢?有的。

首先看 data,所有可被命名的資訊都是 resource,並給予 identifier (滿足 identification of resources),根據 client 的能力與需求 (透過 control data 描述),給予不同的呈現方式 (representations),像是 JSON、XML 等,並透過呈現方式操作資源,而不是直接操作資源,(滿足 manipulation of resources through representations),由於實際的資源與呈現方式可以相同也可以不同,所以 resource identifier 也可以視為資源與呈現方式 mapping (轉換) 的一種識別。resource 可以透過 representation metadata 與 resource metadata 加以描述,讓 client 可以自行決定如何解讀取得的 resources (滿足 self-descriptive messages)。

data 以 connector (client, server, cache, resolver, tunnel) 提供一致的方式存取,透過 identifier 存取不同的 resources (representation) 及轉換,完成目的 (滿足 hypermedia as the engine of application state)。component (origin server, gateway, proxy, user agent) 透過 connector 溝通,實際上,資源源如何儲存和怎麼處理則是隱藏在 component 中,可以直接提供 representation 或再透過 connector 與其他 component 請求 representation,但介面是一致的。

Chapter 6 則是經驗與評估,描述作者在參與 URI 與 HTTP/1.1 制定時,如何用 REST 找出問題並確定這些擴充沒有違反限制,個人認為有幾個重點:

  • 資源的 identifier 應該鮮少變動
  • 資源不是一個儲存物件,而是一種抽象的應對,一個 identifier 對應到一種抽象應對的實作
  • 辨識使用者的資訊不應包含在 representation 裡的 URI 中
  • HTTP 無法完整描述所有 REST 的語意
  • 使用 HTTP 本身的 header 描述 representation 及提供 metadata 與 control data
  • REST 不是 HTTP,HTTP 也不是 RPC
  • 對 resource 的操作,client 和 server 要有一致的認知

Chapter 6 之後便是結論了。

為什麼要花這麼長的篇幅介紹論文的內容呢?主要是,在設計 Restful API 時,有時候會有些猶豫,像是:

  • token 或 session ID 應該放在 query parameter 還是 HTTP header 中?這有點 tricky,雖說 URI 上不該帶有使用者資訊,但若 resource representation 中的 URI 不夾帶使用者資訊,而是在發送當下才夾帶,算是有違反嗎?不過,就安全性來說,query parameter 有時候會在 log 時被記錄下來,所以算是盡量避免的另一個原因。
  • API 版號應該出現在 URI 中還是 HTTP header 中?以 REST API Versioning 的描述兩種皆可,但看完論文後哪種較好呢?

當初在思考這些問題時,在網路上爬了很多文章和討論,但其實不管哪種作法都有,也有各自的擁護者,但等看完這論文後,我覺得我當初的選擇 (都用 HTTP header) 是比較接近論文的原意 (URI 不夾帶使用者資訊,並盡可能保持不變動,由 client 與 server 透過 control data 協調 representation),而自己對 URL 的命名,除了不太使用 controller 之外,也符合一般大多數的最佳實務

至於剛開始說的 URL 中該不該有動詞?以最佳實務來說,可以,用來代表概念性的 controller 資源,但不會用動詞描述 CRUD,因為 HTTP method 本身已經有這個語意了,個人認為 controller 這個概念性資源是對 HTTP 方法的一種擴充 (見論文的 Chapter 6),畢竟,完全用狀態與名詞描述行為確實不容易,以登入與登出這兩個行為來說,若觀察大多數的 API 會發現幾乎都是直接用 login/logout 這兩個動詞放在 URI 上,我當時也是想了很久,才以登入前和登入後狀態的差異,找到一個我覺得合適的名詞,取代用動詞放在 URI 上的做法。但之後是不是堅持不使用 controller 呢?我覺得還是視語意,挑選合適的方法比較重要。最重要的是,是否違反上述的限制,只要不違反就能稱作是 Restful API 了

相較於 SOAP、RPC 或更早的 RMI,Restful API 幾乎是目前最熱門的 API 設計風格,若想知道怎樣朝 Restful API 風格邁進,可以參考 Martin Fowler 在《Richardson Maturity Model - steps toward the glory of REST》文中所提的方式,逐步演進成為 Restful API:

  • level 0 就是什麼規範都沒有,或是把 HTTP 當成 RPC 使用的階段
  • level 1 定義 resource,讓系統能以 resource 描述狀態
  • level 2 善用 HTTP 動詞 (方法:GET、POST、PUT 和 DELETE)
  • level 3 能以超媒體的方式控制流程,或是從 response 中的資訊可以知道接下來可以做什麼

能做到 level 2 我就覺得很好了,想要做到漂亮的 level 3,若使用 XML 作為 resource 的 representation,那已經有一些標準可以遵循,但若是使用 JSON,目前我所知的有 Hypertext Application LanguageJSON API 兩種描述方式,哪種較好,我覺得只要能滿足 REST 的限制,可以依照自己的喜好使用。

所以大家的 API 能到 level 幾呢?其實到 level 3 也不代表 API 設計的好,API 的設計牽涉到很多層面,就如同論文 Chapter 6 中也探討很多要考慮的因素,希望這篇長文 (目前閒談軟體架構系列中最長的一篇),可以讓大家在設計 Restful API 時有更多的想法湧出。

因為是中秋連假,讓我有時間可以又看動畫又看論文,還可以寫這篇文章,若喜歡,歡迎大家分享出去。另外,我有時候會回頭改過去的文章,除了修錯字,有時也會加新東西,也算是一種對既有文章的持續改善吧,所以,有空可以回去看看就文章 (這是在賺點擊數嗎?笑~)。
系列索引
上一篇《閒談軟體架構:發生關係
下一篇《閒談軟體架構:Client Server

 
about 2 years ago

上一篇《閒談軟體架構:友善的距離》意外在 Facebook 上引起不少人的回應 (是的,我不但偷偷加了副標,還擴充了些內容),不過首先要澄清一下,接下來一系列文章 (如果能堅持住繼續寫的話),我不會討論哪個架構比較好,情境不同,本來對架構的選擇就不同,架構師本身也有所謂的偏好,所以討論哪種架構比較好,這種吃力不討好的筆戰,只是自找麻煩。因此,我只是把在一個抽象的架構中實作上可能會遇到的技術選擇或是設計選擇的種種因素放在文章中讓大家思考,嗯,通常我也不會寫答案,頂多是某些過去從經實驗的經驗分享。

在落落長的引言結束,還是回到本文來吧!如果覺得這篇文章的副標題有點エロ的人,應該都是被標題騙進來的,如果是被騙進來的,可以按上一頁離開,或繼續看完假裝自己不是XD,好啦,玩笑開玩了。不管用系統語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係

以目前主流的語言來說,大多可以用物件的方式描述可被使用的區塊,UML 的 class diagram 將物件之間的關係分成兩大類:實體階層的關係與類別階層的關係,實體階層有 dependency、association、aggregation 和 composition 四種,類別階層有 generalization 和 realization (implementation) 兩種,共計六種,雖說是六種,但在不同語言哩,實現的方式也不逕相同,所以我也不打算談怎樣的實作才算是哪一種關係。

我們就單純討論該如何讓一個物件知道另一個物件?你在開玩笑嗎?這不是很簡單嗎?別看這這樣,Martin Fowler 可是寫了篇 《Inversion of Control Containers and the Dependency Injection pattern》文章描述幾種 dependency injection 的方法: constructor injection、setter injection 和 interface injection,以及用來查找物件的 service locator。不過既然 Martin Fowler 寫得這麼詳細了,我又有什麼好寫的呢?就三個:自己的習慣、 container-based annotated dependency injection 和 singleton,後面兩種是我覺得有意思的東西。

先看我自己的習慣吧!我通常將必要的物件關係使用 constructor injection,例如 A 物件需要 B 和 C 物件才能運作,便會在 constructor 宣告 B 和 C 物件的參數,在建立 A 物件時,就要傳入 B 與 C 物件,但 constructor injection 會有缺點,首先,當需要的物件關係越多,常會造成 constructor 出現 long parameter list 的 bad smell,不過這也好,此時代表著 A 物件似乎做太多事了,可能也出現 low cohesion 的問題;再者,是建立物件的先後順序會受限,甚至會出現建立 A 物件時需要物件 C,建立 C 物件可能會需要 D 物件,但建立 D 物件時需要 A 物件,此時就出現雞生蛋、蛋生雞的問題了,這時,就只能用 setter injection 解開這個迴圈了。

Setter injection 則用在預計執行期間會換掉的關係上,例如根據外部輸入的條件,更換不同的演算法,像是根據購買物品的種類或是數量,使用不同折扣計算的演算法。至於,interface injection 則只用過二次,一次是使用 plug-in 架構時,為了讓 plug-in 能取得可能需要的 host resource 物件,只要載入的 plug-in 有實作特定 interface,就會替該 plug-in 注入物件。至於另一次經驗,等一下再提。

接著看 Container-based annotated dependency injection,有在用 Spring framework 或是使用 J2EE container 的開發者對於 @Autowired 或是 @Inject 等 annotation 應該很眼熟,自己在用 Spring framework 開發 Web service 時也用很多,像下面的程式,只要一個單純的 annotation,Spring framework 就會幫開發者注入合適的物件:

public class UserManager {

    @Autowired
    private UserRepository userRepository;

    // ... other code

}

但要能讓 Spring framework 注入物件, UserManager 本身必須是個 Spring bean 物件,不論是用 @Bean 或是用 xml 讓 Spring framework 將 UserManager 建立為 bean 物件,這些關係的注入才會生效。就如同《Spring Boot in Action》書中所說的 (其實是 Teddy 學長先在 Facebbok 提及,我才又回去書中裡找到的):

Like any framework, Spring does a lot for you, but it demands that you do a lot for it in return.

不過,就如前篇文章,我對框架都會保持友善的距離,因此,我很少會向上例那樣,直接將 @Autowired 這類框架專屬的 annotation 加到 domain 物件中,那該怎麼辦呢?一般來說 domain 物件我習慣放在有 -core 後綴詞修飾的 projet 中,真正提供服務或是與 Spring framework 整合的物件則放在 -ws-spring 修飾的對應 project 中,例如下圖:

UserManageracl-core 的 project 中,使用 Spring framework 提供 Web Service 服務的 UserManagerBean 則是在 acl-spring 的 project 中。

// in the acl-core project

public class UserManager {

    private UserRepository userRepository;

    public UserManager(UserRepository repository) {
        userRepository = repository;
    }
}

// in the acl-ws project

@Bean
public class UserManagerBean extends UserManager {

    @Autowired
    public UserManagerBean(UserRepository repository) {
        super(repository);
    }
}

雖然,UserManagerBean 沒有任何特殊的 method,只是一個單純的繼承,有點多餘的感覺,卻也多了點距離。另外,也讓測試變得簡單一點,因為測試時,不需要 Spring framework 的介入,在以前還沒有 SSD 的時候,光是等帶 Spring framework 啟動然後注入物件就要十幾秒,但一個單元測試 method 可能才執行 0.x 秒,即便有 SSD 將啟動時間縮短到數秒,整體來說,這個代價實在太高了,而且當測試越多,代價就跟著提高。所以測試 domain 物件的邏輯時,雖然 annotation 很方便,我還是喜歡回歸到單純的 constructor injection 或 setter injection。

最後,就是 singleton 了,我很刻意不使用的 desing pattern,或是說,我通常只有在真的只允許一個物件實體的情況下才會使用,但若看網路上 iOS 或 Android 大量的範例程式碼,singleton 被大量被當成 service locator 使用,特別是 Activity 是一個生命週期完全被 Android 掌控的物件,開發者既無法自己建立 Activity 物件,也不知道該在什麼時候使用 setter injection 注入所需要的 domain object,或是想在不同的 Activity 間傳遞複雜的物件,又或是不想一路傳遞物件到較深的物件中,於是能以 static 方式或是從全域取得物件的 singleton 就被大量使用了,但我覺得這完全不是 singleton 原本的意圖

在最近的專案中,我試著用 interface injection 的方式,搭配 Android 對 Activity 的 lifecycle callback,在有實作特定 interface 的 Activity 注入需要的物件,同樣地,Fragment 也能用這種方式注入物件,因此不需要依賴 singleton 作為 service locator。

以上,就是和大家分享的三個有趣的設計決策思考。雖然,目前規劃中的主題,大概還有四篇,如果平時還有想到有趣的東西會再加入,但如果有希望我分享和討論的主題,可以留言給我,若能幫上忙,有空就分享出來。

系列索引
上一篇《閒談軟體架構:友善的距離
下一篇《閒談軟體架構:休息時間

 
about 2 years ago

因為組織調整,我又變成 architect 了,早期對 architect 有很多憧憬,面對問題,心中總有一張設計圖,但實際當 architect 後,發現這是一個很不容易的職位,特別是要能說服別人使用某個設計或不用某個設計,另外,是在有時程壓力下,對一個有點歪的架構上,如何微妙地讓它保持像比薩斜塔般不至於垮掉,還能持續成長,又是另一個難題。

在讀《建構微服務》有段內容讓我玩味很久:

我們借用其他行業的名稱,稱自己為 software engineer 或 architect,但我們不是,對吧?architect 與 engineer 的嚴謹性和紀律性是我們在現實中無法冀求的,而且他們對社會的重要性是眾所周知的。記得有一次我和一位朋友聊天,就在他成為合格 architect 的前一天,他說:『明天,如果我在酒吧裡建議你如何建立某個軟體,而它是錯誤的,我就得承擔責任,我可能會被告,因為,我在法律上是一個合格的 architect,如果我犯錯,就必須負責』

怎樣才是稱職的軟體架構師呢?老實說,我恐怕還沒那資格說。

軟體架構有很多 pattern,坊間也有很多書探討,像是《Pattern-Oriented Software Architecutre》系列、《Software Architecture in Practice》、《Patterns of Enterprise Application Architecture》,但當我們套了這些 pattern 時,我們真的該用嗎?用對了嗎?還是只有有個殼而已,內容根本不是那麼一回事呢?

若深究每個 pattern 的形式,都會找到情境 (context)、遭遇的問題 (forces) 和對應的平衡解 (solution),但再仔細想想,這些解都圍繞在 separation of concerns 上,將不同的問題分離,以合適的方式處理,因為我們總是希望有個 high cohesion 及 loose couping 的系統,但在面對實際的設計抉擇時,有時反而會做出違背上述兩原則的選擇。

例如,為了不重新造輪子,我們使用第三方的函式庫,這聽起來很合理,但我們真的了解我們引入的函式庫嗎?我們對該函式庫的掌握度有多高呢?每個被引入的函式庫意味著一種 coupling,不論是在 Java 上使用 Maven 或 Gradle 或是在 Objective C 中使用 CocoaPods,在編譯時,會看到這些套件管理工具幫我們下載眾多的第三方函式庫,這意味著我們不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?

這是為什麼我蠻喜歡 Onion Architecture (洋蔥架構)Hexagonal Architecture (六角架構) 的原因了,在過去的專案中,我並沒有刻意使用這二個架構,畢竟我進去時,早已有龐大的程式碼基礎,不可能說改就改,只有在 Android 專案起始時,因為我是較資深的軟體工程師 (那時還不是架構師),主導整個架構走向,即便如此,我也只堅持 domain 要與 Android SDK 分離,維持使用而不相依的關係,而這也造就了後來開發PC 版時,有完整的 domain 核心可以直接使用不需修改,雖然這也不在當初的規劃就是了,但也省去了大量重複開發的時間。當要離職準備交接時,回頭檢視架構,上述二個架構的影子就穿插在程式碼之間了。

可能跟過去在學校的 OOAD 訓練養成有關,從 problem context 和 use case 中提取名詞,接著提取動詞找出關係與函式,然後利用 GRASP 逐步建構出整個 domain model 與 design model,這中間,完全沒思考過 UI (但如何與系統互動很重要) 與框架,可能是這樣,我後來在做設計時,很自然地就與框架保持距離,不論是用 C# 寫 Windows Forms 還是用 Java 寫 Web applications 。但這其實並不容易,我剛開始工作時,看《Hibernate in Action》時,有幾句話讓我印象深刻,第一句是:

We use transparent to mean a complete separation of concerns between the persistent classes of the domain model and the persistence logic itself, where the persistent classes are unaware of—and have no dependency to—the persistence mechanism.

其實,我畢業前就自學了 JPA,之後才學 Hibernate,上面那句話讓我開始思考,雖說 JPA 的 annotation 很方便,但不正是破壞了 transparency 嗎?但 Hbernate 也無法達到完全的 transparent:

We regard transparency as required. In fact, transparent persistence should be one of the primary goals of any ORM solution. However, no automated persistence solution is completely transparent: Every automated persistence layer, including Hibernate, imposes some requirements on the persistent classes.

因為再怎麼樣抽象化,資料庫對資料的描述與物件導向語言對物件的描述,存在著無法消除的 paradigm mismatch (有興趣可以找書來看,《Hibernate in Action》在第一章的第二節,花了整整一節說明這不匹配的情況),這讓我想起《約耳趣談軟體》第 26 章抽象滲漏法則中的一段:

所有重大的抽象機制在某種程式上都是有漏洞的。

而且有時候這些框架或工具會反過來影響 domain model 的設計,舉例來說,從 OO 設計的角度來看關係,若要好維護,一般會以單向關係 (unidirectional reference) 為主,但若要使用 ORM 或 CoreData 工具,為了確保工具能檢查資料完整性,會反過來在 domain model 上加上雙向關係 (bidirectional reference),但程式碼卻不見得需要去維護這雙向關係 (部分是 ORM 工具處理),這導致讀程式碼時會有點奇怪。

另一個例子是,過去在學 RDBMS 時,會學到正規化 (normalization)、主鍵 (primary key)、外來鍵 (foregin key)、索引 (index) 及一些 RDBMS 能在資料完整性幫上忙的工具,像是 cascade delete 等東西,因此 ORM 工具也常把這些資訊滲漏出來,滲漏也許還好,但把物件關聯的維護轉交給 ORM 工具上 (依賴 cascade delete 刪除不該存在的關聯),就是值得討論的設計,到底這物件間關聯的維護是商業邏輯層的責任,還是資料儲存層的責任?如果哪天,資料儲存層換了,偏偏不支援原有的 metadata (例如不支援 JPA 的annotation),那物件關聯的維護該怎麼處理?

所以重點是如何取得平衡?以上述的 ORM 工具來說,極端的兩邊:完全不使用 ORM 工具和毫不顧忌的讓 ORM 工具散布在 domain model 中,又或者是將 ORM 的滲透透過其他方式控管在特定的範圍中,例如:再建立一層抽象 (DAO 或 Repository),在實作中建立 ORM 所需的 data model;又或者是使用污染性較低的方案,例如:以傳統的 XML 取代 JPA annotation 描述 metadata,事實上,這沒有標準答案,每個軟體架構師的選擇都不同,上述二兩種折衷方案我都用過,也曾經完全不使用 ORM 工具,完全視情境而定,但原則到沒什麼不同,與框架及工具保持友善的距離,這同樣影響我之後在開發 iOS 時使用 CoreData 的方式。

或許是這樣,感覺自己比較像是 old-school 的軟體架構師,在選擇第三方函式庫或是框架時,相對比較保守,有時,還會為組織內部重新打造輪子,像是曾經在 Android 專案中復刻 iOS SDK 的 NSNotificationCenter,事實上,在 GitHub 上可以找到類似的第三方函式庫,像是 EventBus,但要不要採用一個第三方函式庫,除了該函式庫穩不穩定、文件夠不夠充足,還要看是否符合專案與組織的特性。

就專案來說,Android 專案分成兩個子專案:一個是只有 domain model 的 pure Java 專案,另一個是實際 Android UI 的專案,若要導入 EventBus,就只能在 Android UI 的專案中使用,因為 domain model 沒有相依 EventBus 所需的 Android SDK,這樣並無法滿足當初想用 NotificationCenter 減少 model 與 UI 之間 coupling 的初衷;另外,考量到希望 iOS developer 也能協助開發 Android,所當時以決定自己動手寫,並且在進行在復刻時,API 命名特意維持與 NSNotificationCenter 相似。

因此,軟體架構不是一旦決定了就穩固了,它需要後續開發時,時時想著當下這個設計是否會把架構搞歪了,架構需要細心的照顧,否則很容易歪掉,歪掉也許沒事,程式也可能還能繼續正常執行,但埋伏在裡面的技術債,何時會引爆則是未知,一旦反撲,對專案的影響不僅是時程,還有開發團隊對整體架構的信心。

系列索引
下一篇《閒談軟體架構:發生關係

 
over 2 years ago

第一章 微服務

在 《Single Responsibility Principle》中,Robert C. Martin 所闡述的單一責任原則特別強化這個論點,『將那些因為相同理由而改變的東西集合起來,將那些因為不同理由而改變的東西分離開來』。微服務採取相同的作法,達成它的獨立性。 p. 2

當論及多小才算夠小時,我喜歡從這個觀點思考:服務越小,微服務架構的好處與缺點就越被放大。 p. 3

我們盡量避免將多個服務整合到相同的機器上,... (中略) ...儘管這種隔離會增加一些開銷 (overhead),但它所產生的簡單性會讓分散式系統更容易理解,而且,較新穎的技術也能夠消弭許多與這種部署形式相關的困難與挑戰。 p. 3

不同服務之間透過網路呼叫來溝通,強化了服務之間的分隔,避免緊密耦合的危險性。 p. 3

這可能意味著,選用技術無關 (technology-agnostic) 的 API,確保我們不受限於特定技術選項。 p. 3

這允許我們針對每個任務選擇合適的工具,而不必選擇更標準、更一體適用的做法,那樣往往導致不甚理想的最『小』公約數。 p. 4

因為我能夠明確地限制潛在的負面影響。許多組織發現,這項能力讓他們得以更迅速地吸納新技術,成為組織的實際優勢。 p. 4

彈性工程 (resilience engineering) 的關鍵概念是隔艙 (bulkhead),如果系統的一個元件失敗,而該失敗並未蔓延,你就可以區隔問題,系統的其餘部分就能夠繼續運作。 p. 5

不幸的是,這也意味著,變更會在不同的釋出版本之間持續累積,最後,正式上線的新版應用包含一堆變更,而且,不同釋出版本之間的差異越大,犯錯的風險就越高! p. 6

使用微服務時,我們可以針對單一服務做變更,並且獨立於系統其餘部分進行部署,讓我們能夠更快速地部署程式碼,若有問題發生,它可以乾淨俐落地被隔離在個別的服務中,讓撤銷部署 (rollback) 更容易實現,同時,這也意味著,我們可以更快速地將新功能交給客戶手上。 p. 7

微服務方法衍生自現實世界的實際運用,讓我們更清楚地理解有助於妥善建構 SOA 的系統與架構,因此,你應該把微服務想成是 SOA 的特定解法,就像 XP 或 Scrum 被視為敏捷軟體開發的特定作法那樣。 p. 9

多個團隊可以圍繞著這些程式庫組織起來,而這些程式庫本身能被重利用,然而,這也存在著一些缺點。首先,你失去了技術異質性 (heterogeneity),程式庫通常必須以相同的語言寫成,或至少執行在相同的平台上。其次,你們彼此獨立地擴展系統零件的困難度變高。再者,除非使用動態連結程式庫,否則,沒有重新部署整個行程,就無法部署新的程式庫。 p. 9

無論如何,共用程式庫確實佔有一席之地,你會發現自己正在建立不屬於特定業務領域的通用程式碼,而且,你會想要在整個組織中重利用它,那正式成為共用程式庫的明顯候選人。然而,確實必須小心,被用來在不同服務之間進行溝通的共用程式碼會變成耦合點。 p. 9

第二章 進化的架構師

架構師顯著且直接影響系統的建構品質、團隊成員的工作情境、以及組織回應變更的能力,遠超過其他角色 p. 13

架構師必須改變想法,放棄想要打造最終完美產品的思維,改為聚焦在協助建立可從中衍生出合適系統的框架,並且隨著我們聊解更多資訊而持續壯大。 p. 15

代替針對所有可能性制定完善的計畫,我們的計畫應該著重於容許變更發生,避免想要周全考慮一切的衝動。 ... (中略) ...架構師有責任確保系統也能夠讓開發人員在那裡安居樂業。 p. 15

應該擔心不同服務之間發生什麼事,而放任服務裡面發生什麼事 p. 16

如果你是擘劃公司技術願景的人,這可能意味著,你必須花更多時間與組織 (或企業) 的非技術部門相處,弄清楚驅動企業的願景為何?以及它如何改變? p. 18

理想上不超過 10 個原則 --- 好讓人們能夠記住,或者放到小海報上。原則越多,相互重疊或矛盾的機會就越大。 p. 18

實務做法 (practices) 是確保原則被貫徹的手段,是一組關於執行任務的詳實指導方針,通常是技術特定的,並且夠低階,足以讓任何開發人員理解。 p. 18

當你在處理實務做法並且思考必須採取的權衡取捨時,要尋求的核心平衡之一,就是你的系統允許多少可變性 (variability)。 p. 20

知道個別服務是否健康確實有用,但往往只有在你試圖診斷更廣泛的問題,或瞭解更大趨勢的情況下才是如此。為了讓這件事情儘可能容易,我會建議讓所有服務以相同的方式留下與健康/一般監控有關的統計數據。 p. 20

務必與機器內部的技術相區隔,不要為了支援它而改變你的監控系統。在此,日誌機制也是一樣:必須將它集中一處,統一管理。 p. 20

即使不使用 HTTP,類似的顧慮也存在,我們必須能夠分辨下列幾種狀況:請求沒問題並且正確被處理、請求有問題並且因此防止服務用它來做任何事、以及請求可能沒問題但因服務器關閉而無法分辨,這是快速追蹤失敗原因與相關問題的關鍵,假如我們的服務把這些規則當作兒戲,最終只會得到更脆弱的系統。 p. 21

治理確保企業目標被達成,透過評估利害關係人之需求、條件與選項;透過排序與決策制定方向;針對約定的方向與目標監控效能、合規與進展。 --- COBIT 5 p. 24

我們想要聚焦於技術治理 (technical governacne) 的面向,我覺得這是架構師的職責。如果架構師的職責之一是確保技術願景存在,那麼,治理便涉及確保我們所建造的東西符合這個遠景.並且在必要時演進這個願景。 p. 24

架構師負責很多事情,必須確保有一組原則可以指導整個開發工作,而且這些原則需要符合組織策略,還得確認這些原則要求的實務做法不至於讓開發人員苦不堪言,他們必須跟上最新的技術,必且知道何時該做出是當的取捨。這是很大的責任。除此之外,他們還必須帶得動人 --- 亦即,確保一起合作的同事能夠理解他們的決策,必且投入心血,實現他們 p. 25

我非常贊同的模型係由架構師領導該小組 [註:治理小組],但主要的小組為每個交付團隊的技術專家 --- 至少是每個團隊的領導人。架構師負責確保小組順利運作,但由小組共同負責治理工作。 p. 25

第三章 如何塑模服務

我希望你聚焦在兩個關鍵概念上:鬆散耦合 (loose coupling) 與高度內聚 (high cohesion),貫穿全書,我們還會討論其他的觀念與實務,但假如把這兩個重點搞錯,其他努力皆屬枉然。 p.30

邊界上下文 (Bounded Context),想法是,任何特定領域 (domain) 皆由多個邊界上下文構成,每一個邊界上下文內涵無須與外界構通的東西,以及被其他邊界上下文從外部共用的東西。每一個邊界上下文都具有顯式介面 (explicit interface),他決定要跟其他上下文共享那些模型。 p. 31

老實說我不太喜歡將 context 翻成上下文

無論如何,一開始,先讓新系統保持在較為單體式的那邊;將服務邊界搞錯的代價可能很昂貴。 p. 33

過早將系統分解成微服務的代價可能很昂貴,尤其是在你對新領域不慎熟悉的情況下。基本上,將既有的程式碼基礎分解成微服務遠比從頭開始發展微服務來得簡單。 p. 34

這些功能可能需要資訊交換 --- 共用模型 --- 但我見過太多以資料的角度思考而導致貧血的 CRUD 服務,因此,請先詢問『這個上下文是做什麼的?』然後才問,『他需要什麼資料?』 p. 34

根據相同的業務概念來思考這些微服務之間的溝通也是很重要的,你的軟體塑模遵循你的業務領域,不應該侷限在邊界上下文的概念,在組織的各個單位之間共用的相同術語與想法應該被反映在你的介面上。 p. 36

第四章 整合

把整合 (integration) 弄對視微服務最重要的技術面向。將這項工作做好,讓你的微服務保有自主性 (autonomy),你就能夠以獨立於整體的方式變更及釋出它們。 p. 39

尋找理想的整合技術

  • 避免破壞性變更
  • 讓你的 API 保持技術無關
  • 讓服務消費者覺得你的服務很簡單
  • 隱藏內部實作細節 p. 40

資料庫整合讓服務很容易共用資料,但完全無法共用行為。我們的內部表示 (internal representation) 被暴露給服務消費者,很難避免破壞性變更,這必然會造成我們害怕進行任何變更。避開它,不惜 (幾乎) 任何代價。 p. 42

這跟我後來不喜歡將 JPA/Hibernate 或 Jackson 的 annotaton 放在 domain model 中的主要原因,這些內部呈現與外部呈現都與 domain model 的行為無關。

需要考慮的重要因素之一是,這些風格 [註:同步或非同步]有多適合用來解決通常很複雜的問題:我們如何處理跨服務邊界並且可能長期執行的流程? p. 43

orchestration 方法的缺點是,用戶服務會變得太過中央集權,成為系統的中央樞紐,以及邏輯蔓延叢生的中心點,我看過這種作法導致少數的『天神』服務指揮著一群基於 CRUD 的貧血服務。 p. 45

你必須思考網路本身。很多人都知道,分散式系統的第一個謬誤就是『網路是可靠的』,事實上,網路並不可靠,它們可能並且會失敗。 p. 47

有許多不同的 REST 風格存在,... (中略) ... 強烈建議你看看 Richardson Maturity Model,在當中,不同風格的 REST 被比較。 p. 49

傳統上,RabbitMQ 之類的訊息仲介者 (message broker) 試著處理這兩種問題,生產者 (producer) 使用 API 將事件發佈到仲介者 [非同步溝通的第一個問題:讓微服務發出事件的機制],仲介者處理訂閱,讓消費者 (consumer) 在事件到達時接獲通知 [第二個問題:讓服務消費者發現這些事件已經發生的機制]。 p. 55

事件驅動架構似乎導致更為解耦合、更具擴展性的系統,然而,這些編程風格確實增加複雜度,不只是管理發布及訂閱訊息所關聯的複雜度,還有我們可能面對的其他問題,例如,在考慮長期運作的異步請求/回應時,我們必須考量到回應返回時要怎麼做,是否回到發起請求的同一個節點?如果是這樣,萬一該節點停止運作呢?如果不是,我需要將資訊儲存在某處,而能夠據以做出式的回應嗎? p. 57

我們希望避免近似 CRUD 包裹器的愚蠢貧血服務,如果關於允許那些用戶變更的決策不是出自用戶服務本身,那就表示,我們正失去內聚性。 p. 58

DRY 是開發人員經常聽到縮略詞:don't repeat yourself。雖然其定義有時候被簡化為盡量避免程式碼重複,DRY 更準確的意思是,我們想要避免重複系統的行為與知識。... (中略) ... DRY 促使我們建立可重利用的程式碼,將重複的程式碼抽取出 來 ... (中略) ... 然而,在微服務架構中,這種做法看似危險。... (中略) ... 如果共用程式碼的使用洩漏到服務邊界之外,你已經引進某種潛在的耦合 ... (中略) ... 我的原則是:在微服務中,不要違反 DRY,但跨所有服務時,別太在意 DRY 是否被遵循。 p. 59

如果同一批人建立伺服器 API 與客戶端 API,那會有危險,應該存在於伺服器端的邏輯開始滲漏到客戶端 p. 60

最後,請確認確實由客戶端主導何時升級自己的客戶端程式庫:務必確保我們的服務能夠獨立於其他服務被釋出! p. 60

無論你是否決定傳遞實體曾是什麼模樣的記憶,請確認你還包含指向原始資料的參考 (reference),好讓新的狀態能夠被擷取。 p. 61

降低進行破壞性變更之衝擊的最佳方法就是從一開始就避免 p. 62

實作能夠忽略我們不在意之變更的讀取器 --- 就是 Martin Fowler 所謂的 Tolerant Reader

客戶端盡可能有彈性地使用服務的例子說明了 Postel 法則,也稱作強健性原則 (robustness principle):『發送時要保守,接收時要開放』 (Be conservative in what you do, be liberal in what you accept from others) p. 63

語意化版本管理 (semantic versioning)是讓你達成這個目的的一種規格,使用語意化管理時,版本編號的格式為 MARJOR.MINOR.PATH,當 MAJOR 編號遞增時,就表示非向後兼容的變更已經被產生;當 MINOR 編號遞增時,較表示向後兼容的新功能已經被增加;最後,當 PATCH 編號改變時,就表既有的功能已經完成臭蟲修復。 p. 64

我們總是想要保有獨立釋出微服務的能力,一種我已經成功運用來處理這項工作的方法是,讓新舊介面並存於同一個運行中的服務,因此,如果我們喜要發布破壞性變更,就不屬新版的服務,同時開放該端點的新舊版本。 p. 64

這事實上是擴展及收縮 (expand and contract) 模式的範例,允許我們逐步採納破壞性變更。我們擴展提供的功能,同時支援做某件事的新方法與舊方法,一旦舊的服務消費者以新的方法做事情,我們就收縮 API,移除舊功能。 p. 65

隨著時間推移,JavaScript 成為將動態行為增加到基於瀏覽器之 UI 的熱門選項,現在,有些甚至跟就是桌面客戶端一樣臃腫肥胖p. 67

如果有用 webpack 之類的工具打包 JavaScript 就知道,打包時間越來越長...

解決方法之一是讓服務消費者在提出請求時指名要擷取那些欄位,然而,這個解法假設每個服務都支援這種互動模式 p. 68

絞殺者模式 Strangler Application Pattern,透過這模式,你捕捉及攔截指向舊系統的呼叫,決定是否將這些呼叫繞送給既有的遺舊程式碼,或是導道你可能已經撰寫的新程式碼,讓你隨著時間推移逐漸替換功能性,而不需要突然重寫一大堆程式碼。

這算是進入職場後蠻常使用的 pattern 之一了

第五章 拆分單體式系統

大部分編程語言支援名稱空間的概念,讓我們將類似的程式碼組織在一起。Java 的 package 概念是相當薄弱的例子,但提供很多我們需要的東西,所有其他的主流編程語言皆內建類似的概念,但 JavaScript 基本上是一個例外p. 80

新標準一直在演進中,這個例外可能不會持續太久,但我個人確實沒有很喜歡 JavaScript

強烈建議你一點一滴慢慢處理它們,漸進式的做法會在過程中幫助你聊解微服務,同時也會限縮弄錯某是 (事情總會出錯!) 所造成的影響。 p. 81

這將我們帶往常常造成依賴纏繞的根源:資料庫p. 82

有時候,為了其他利益犧牲一點速度確實是正確的選擇,尤其是在速度綽綽有餘的情況下。 p. 85

從許多方面來看,這是所謂最終一致性 (eventual consistency) 的另一種形式,代替使用交易邊界確保系統在交易完成後楚瑜一致的狀態,我們改為接受系統會在未來某個時點讓自己變成一致的狀態,這種作法尤其適合於可能長時間執行的操作。 p. 91

分散式交易 (distributed transaction) 嘗試跨多筆交易進行確認,使用某種稱做交易管理器 (transaction manager) 的控制行程來協調由各個底層系統所進行的不同交易。 ... (中略) ... 分散式交易已經針對一些特定技術堆疊被實作,像是 Java 的 Transaction API,允許資料庫與訊息佇列之類的一直系統餐與同一個總體交易 (overarching transaction) p. 93

第六章 部署

即使採用短期分支 (short-lived branch) 來管理變更,也要盡量頻繁地整合到單一主線。 p. 104

沒有驗證程式碼行為是否一如預期的 CI 算不上真正的 CI。 p. 104

我們關心的是它的運作,那麼,我們可以將心力聚集在建立及部署這些映像的自動化,這也變成實現另一個部署理念的巧妙方法 --- 不可變伺服器 (immutable server)p. 113

基礎設施團隊的工作負擔往往與他必須管理的主機數量有關,如果更多服務被打包到一個主機,主機管理的數量不會隨著服務數量增加而增加。... (中略) ... 這模型也有一些挑戰,首先,它讓(精準)監控更困難 ... (中略) ... 另一個問題是,這個選項會限制我們的部署產出物,基於映像的部署被排除,不可變的伺服器也一樣,除非你將多個不同服務一起綁在單一產出物,但這是我們極力想要避免的事情。 p. 116

許多關於部署與主機管理的工作實務皆企圖以最佳化的方式處理資源稀有性。 p. 117

主機數量增加也具有潛在的不利因素,更多伺服器需要管理,而且,執行更多主機也隱含著一些成本。 p. 120

我們最不樂見的事情就是:因部署流程完全不同而造成我們在上線環境中遇到一些問題!在這個領域工作多年之後,我相信,觸發任何部署的最明智方法就是透過單一、可參數化的命令列呼叫,這可以透過指令搞觸發,由 CI 工具啟動或手動輸入。 p. 127

建立這樣的系統可謂工程浩大,需付出的心力往往集中在前期,但是對部署複雜性的管理可能是必要的。 p. 129

第七章 測試

近來的趨勢是避免大規模的手動測試,盡可能採用自動化測試,我當然同意這種做法,如果你目前正在進行大量的手動測試,建議你,在深入微服務之前,先解決這個問題再繼續往前走,因為,假如你無法快速且有效地驗證你的軟體,就無法從微服務獲得諸多利益。 p. 132

單元測試通常測試一個函示或方法呼叫,... (中略) ... 一般而言,你需要進行大量這類測試,如果處理得當,他們是非常快速的,而且,在現代化硬體上,你可以預期在不到一分鐘的時間內執行數以千計的這類測試。 p. 134

服務測試繞過 UI,直接測試服務本身,在單體式應用程式中,我們可能只測試一群提供服務給 UI 的類別,然而,對包含多個服務的系統來說,服務測試會測試個別服務的能力。我們想要測試單一服務本身的原因是,提高測試的隔絕性,以便更迅速地發係及解決問題。為了達成這種隔絕性,我們必須 stub 所有外布鞋作者,因此,僅僅服務本身落在測試範圍內。 p. 135

當較大範圍的測試 (如服務社或端到端測試) 發生失敗時,我們會試著撰寫快速的單元測試,捕捉相關的問題,依此方式,我們不斷地試著改善我們的回饋循環。 p. 136

基本原則是,每往金字塔下方移動一層,需要的測試數量可能多出一個層級,但重點是,知道你確實擁有不同類型的自動化測試,並且瞭解當前的平衡是否為你帶來問題! p. 136

變動元件越多,我們的測試可能越脆弱 (brittle),不確定性也越高。如果有一些測試偶爾失敗,而大家只是重新執行它們,因為稍後可能又會通過,這些就叫作詭異測試 (flaky test) ... (中略) ... 詭異測試是恐怖的敵人,在發生失敗時,它們並未提供我們很多資訊,我們重新執行 CI 建置,希望它們稍後不再出現,只是為了看到我們順利且持續地簽入程式碼,突然間,我們發現自己已經累積一大堆有問題的功能性。 p. 140

包含詭異測試的測試組會變成 Diane Vaughan 所謂的異常正常化 (normalization of deviance) 的受害者 --- 隨著時間推移,我們會對錯誤的事情習以為常,以致於開始接受它,視之如無物。 p. 141

Martin Fowler 主張下列作法。如果你有一些詭異測試,你應該記錄並追蹤它們,如果無法立刻解決,就設法將它們從測試組移除。看看你是否能夠改寫它們,避免以多執行緒的方式執行測試程式碼。看看你是否能夠讓底層環境更趨穩定。更好的是,看看你是否可以使用比較不會產生問題的較小範圍測試替換詭異測試。在某些情況下,改變受測軟體,讓它更容易測試,也是繼續向前行的好辦法。 p. 141

我很少看到團隊確確實實地妥善規劃端到端測試,以便減少測試涵蓋度的重疊,或是花費足夠的時間想辦法讓它們變快。 p. 142

這樣的緩慢,加上詭異測試可能經常存在的事實,會形成重大的問題。耗費一整天執行,並且經常包含與毀損功能無關之問題的測試組,絕對是一場災難,當功能真的毀損時,你可能得花好幾個小時才能找到問題。 p. 142

使用藍 / 綠部署 (blue/green deployment) 時,我們同時部署兩份軟體,但只有一個版本接受真實的請求。 p. 143

使用金絲雀釋出時,我們透過導入一部分的上限交通流量,看看新部署的軟體是否如預期般執行,『如預期般執行』可以涵蓋很多事情,包含功能性及非功能性的事項。 p. 150

考慮金絲雀釋出時,你必須決定是否將一部分上線請求直接導到金絲雀,或者只是複製上線負載。有些團隊能夠複製 (shadow) 上線流量,並且將它導到金絲雀,透過這種方式,既有的上線版本與金絲雀版本可以看到完全相同的請求,但是,只有上線版本處理的請求會被外部看到,這樣可以讓你對召集比較,同時去除被客戶看到金絲雀版本發生失敗的可能性,然而,複製上線流量的工作可能很複雜,尤其是在被重播的事件 / 請求並非等冪 (idempotent) 的情況下。 p. 150

有時候,投入相同的心力,建立更好的釋出補救機制,可能遠比增加更多自動化功能測試更有助益 [註:end-to-end 的自動化測試],在 Web 世界哩,這通常被稱作在最佳化平均故障間隔 (mean time between failures, MTBF)平均修復時間 (mean time to repair, MTTR) 之間的權衡取捨。 p. 151

當系統被分解成較小的微服務時,我們增加跨網路邊界的呼叫數量,原本可能涉及一個資料庫呼叫的操作,現在能需要對其他服務發出三、四個跨網路邊界的呼叫,以及相當數量的資料庫呼叫,這一切都可能降低系統的運作速度。因此,追蹤延遲來源尤其重要p. 152

第八章 監控

執行的主機數量成為一項挑戰,以 SSH 多工 (SSH-multiplexing) 的方式擷取日誌可能行不同,而且沒有任何螢幕大得足以容納所有主機的終端視窗 --- 相反地,我們指望使用專門的子系統來捕捉日誌,並且將它們集中起來。 p. 158

首先,有一句古老的格言,80% 的軟體功能從未被使用。

話雖如此,但更常聽到 PM / PO 一邊想像一邊說:這功能超棒超有用,一定會大受歡迎... 有人說,透過數據統計可以知道哪些功能沒人用,但已經投入的成本 (人力成本、時間成本和錯失市場的機會成本) 都已經無法挽回了。

一種有用的做法是運用相關性 ID (correlation ID),當最初的呼叫被產生時,你為該呼叫生成 GUID,接著,他被往下傳遞給所有的後續呼叫。並且能夠以結構化的方式被放進你的日誌哩,就像日誌級別或日期那樣。藉由合適的日誌工具,你可以一路穿過你的系統,追蹤這個事件。 p. 163

相關性 ID 的真正問題之一是,你經常不知道你需要它,直到你碰到問題之後, ... (中略) ... 雖然這可能看起來像是額外的工作,但我強烈建議你盡早將它們納入考慮,尤其是當你的系統即將運用事件驅動的架構模式時,那可能導致某種奇怪的緊急行為。 p. 164

目前想到可能較簡單的做法是將 task ID 當作 correlation ID,並將 task 當成參數傳遞,不管是否跨節點,每個處理過這個 task 的程式都可以輸出 task ID 到日誌中。

監控不同系統之間的整合點是關鍵,每個服務實例都應該追蹤並且揭露其下游依賴物的健康狀況,從資料庫到其他協作服務都是。 p. 164

就標準化而言,監控是一個至關重要的區域,透過多個介面,使用以各種方式提供功能給使用者的協作服務時,你必須以整體觀點看待系統。 p. 165

第九章 資訊安全

如果你要建立服務帳號 (service account),請盡量保持用途單純,因此,考慮讓每個微服務擁有自己的一組憑證,如果憑證被盜用,這樣會讓撤銷 / 改變存取變得比較容易,因為你只需要撤銷受影響的那組憑證。 p. 175

(客戶端) 憑證管理的操作挑戰甚至比使用伺服器憑證更艱鉅,不只是因為建立及管理較大量憑證的一些基本議題;更且,因為憑證本身的複雜性,你預計會花很多時間試圖診斷出服務為什麼不接受你認為完全有效的客戶端憑證,然後,我們還得考慮撤銷即在核發憑證的困難性。使用萬用憑證 (wildcard certificate) 會有幫助,但無法解決所有的問題。 p. 175

Twitter、Google、Flicker 與 AWS 等服務的所有公開 API 都利用 API 金鑰,API 金鑰讓服務能夠識別誰產生呼叫,並且限制他們可以做什麼事情,通常,這些限制不僅止於資源的存取,還能夠擴展到特定呼叫者的速率限制 (rate-limiting),以保證其他人的服務品質。 p. 177

有一種安全漏洞被稱作混淆代理人問題 (confused deputy problem),在此情況下,惡意的第三方欺騙代理人服務 (deputy service),讓它對下游服務發出呼叫。 p. 179

加密 (encruption) 依賴用來編密資料的演算法與金鑰,然後產生編密過的資料,那麼,你的金鑰要存放在哪裡? ... (中略) ... 一個解法是使用獨立的資安設備 (security appliance) 來加解密資料,另一個解法是使用你的服務在需要金鑰時能夠存取的獨立金鑰保存庫 (key vault),金鑰的生命週期管理是很重要的操作,這些系統能夠為你處理這項工作。 p. 181

我們擔心且想要加密的資料通常也需要備份!因此看起來可能很明顯,但我們必須確認我們的備份也被加密過,這也表示,我們必須知道處理那些版本的資料需要用到那些金鑰,尤其是,如果金鑰需要改變,擁有明確的金鑰管理機制變得相當重要。 p. 182

入侵偵測系統 (IDS, intrusion detection systems) 可以監控網路或主機上的可疑行為,在看到它們時報告問題。入侵預防系統 (IPS, intrusion prevention systems),除了監控可疑活動,還可進一步介入,阻止它們發生。 p. 183

以權限盡量低的 OS 使用者執行服務,確保萬一帳號被破解,造成的損傷會最小。 p. 183

如果我們讓生活過得更簡單些呢?何不盡量『淨化』可用來識別個資的資料,並盡早做到呢? ... (中略) ... 這裡的優點是多重的。首先,如果你不儲存的話,就沒有人能夠竊取它。其次,如果你不儲存,就沒有人 (例如,政府機構) 可以請求它!德語 Datensparsamkeit 代表這個概念,源自德國隱私法,基本概念是只能儲存對商業營運絕對必要,或者滿足本地法律的資訊。 p. 187

第十章 Conway 定律與系統設計

負責設計系統 (這裡採取更廣泛的定義,不限於 IT 系統) 之任何組織所產生的『設計結構』將無可避免地複製該組織的『溝通結構』。 p. 191

當協調變更的成本增加時,有兩種可能會發生,不是找到辦法減少協調 / 溝通成本,就是停止變更協調,後者最終導致難以維護的龐大程式碼基礎。 p. 194

如果建立系統的組織比較鬆散耦合,這個組織所建構的系統傾向於更為模組化,因此,可能比較不耦合。擁有多個服務的單一團隊請向隅發展出比較緊密耦合的整合,這種趨勢在較分散的組織中很難維持的。 p. 194

服務所有權 (service ownership)是什麼意思?一般情況下,這表示,擁有服務的團隊負責變更該服務,該團隊應該可以在必要時重新組織程式碼,只要該變更不破壞消費端服務。對許多團隊來說,所有權擴及服務的各個面向,從收集需求,到建置、部署和維護應用程式。 p. 194

在不能把燙手山竽交接給別人的情況下,自然就不會弄出什麼燙手山竽啦! p. 194

賦予團隊更多權力與自主性,同時讓它為其工作負起全責。我看過太多開發人員再把他們的系統送進測試或部署階段,就認為他們的任務已告結束。 p. 195

讓我們再次思考一下微服務是什麼:根據業務領域塑模的服務,而非技術領域,如果擁有特定服務的團隊大致與業務領域一致,很可能,團隊就可以維持用戶焦點,並且更能夠看清楚功能開發,因為它對於服務相關的所有技術都掌握著全面性的理解與所有權。 p. 196

萬一我們已經盡力,但就是無法找到辦法避開幾個共用服務呢?在此情況下,適度擁抱內部開源碼 (internal open source) 模型可能很合理。 p. 197

大多數開源碼專案傾向於不接受較大群不授信認知提交者的變更提交,直到第一個版本的核心功能完成。 p. 198

不管乍看如何,問題總是在人 --- Gerry Weinberg, The Second Law of Consulting

切記,若未考慮相關人員的感受,或者他們所具備的能力,逕自擘劃事情應該如何完成的願景,很可能將大火帶到錯誤的境地。 p. 202

你必須明確指出你的人員在微服務世界裡的責任,並且說清楚那些責任微和對你很重要,這樣可以幫助你看清楚可能存在那些技能缺口 (skill gap),必且思考如何消弭這些間隙。對很多人來說,這將是相當駭人的旅程,但只要記住,倘若無人承擔大任,任何你想要進行的變更可能從一開始就注定失敗。 p. 202

第十一章 大規模的微服務

在一定規模下,失敗變成統計上的必然。 p. 205

即使對我們當中不考慮極端規模的人來說,如果能夠接受失敗的可能性,事情會變得更好,例如,如果我們能夠優雅地處理服務的失敗,那麼,接下來,我們還可以就地升級服務,因為計畫下的運作中斷遠比意外的失敗更容易處理。 p. 205

我們也可以少花一點時間阻止無可避免的事情,多花一點時間設法以優雅的方式處理失敗,我很驚訝,許多組織設置大量流程與控制機制,試圖阻止失敗發生,卻很少或幾乎沒有付出心力,設法讓系統更容易從失敗中回復 (recover)。 p. 205

建構具回復力之彈性系統的重要關鍵是安全降級功能性的能力,尤其是在你的功能性分散於諸多或開啟或關閉的服務時。 p. 207

使用一個單體式應用程式時,我們不需要做很多決策,系統健康非黑即白,但使用微服務架構時,我們需要考慮更微妙的情況,在任何情況下都要做的往往不是技術決策,我們可能知道當購物車失效時技術上該如何處理,但除非了解業務背景,否則無法明白應該採取什麼行動。 p. 208

回應非常緩慢是你會碰到的最差勁失敗模式之一,如果系統壓根不存在,你很快就能夠找出答案,然而,當它只是緩慢時,在放棄之前,你最後會等待一段時間,但無論失敗原因為何,我們建立的系統確實脆弱,很容易發生連鎖失敗,下游服務 (我們對它幾乎沒什麼控制力) 會破壞我們的整個系統。 p. 209

說到底,我們發現,這些系統與其歹戲拖棚,倒不如快速失敗,那可能會比較容易處理一些。在分散式系統中,緩慢延遲實在是一個很棘手的問題。 p. 210

我們十座三種修補,避免這種情況 (緩慢服務造成的影響) 再發生:把逾期時間設對;實作隔艙 (bulkhead),劃分出不同的連接池;並且實作斷路器 (circuit breaker),從第一時間就避免傳送呼叫給不健康的系統。 p. 210

使用斷路器時,在一定數量的下游資源請求失敗之後,斷路器被熔斷,在此狀態下,所有後續請求快速失敗,特定時間之後,客戶端發送幾個請求,看看下游服務是否已經恢復,並且,如果得到足夠的健康回應,就重設斷路器。 p. 212

把設定弄至卻可能有點棘手,你不想要斷路器太容易熔斷,也不想要花太長時間才能熔斷它。 p. 212

如果有這樣的機制 (就像家裡的斷路器),我們可以手動操作它們,讓相關作業更安全。例如,如果我們想要在例行維護工作中卸除某個微服務,我們可以手動熔斷各個依賴系統的斷路器,讓它們在微服務離線時快速失敗,一旦該微服務重新上線,我們就可以重設這些斷路器,一切就會回歸正常。 p. 212

關注點分離 (separatin of concerns) 也是一種時做隔艙機制的方法,透過將功能拆解成多個獨立的微服務,減少一個區域的中斷影響另一個區域的可能性。 p. 214

從許多方面來看,隔艙是這三種模式中最重要的,逾期與斷路器幫助你在資源負荷超載或失效時釋放它們,但隔艙能夠在一開始就確保它們不變成負荷太重或失效。 p. 215

在等冪操作中,執行結果在連續呼叫多次之後並不會改變,如果操作式等冪的,我們可以重複呼叫多次,而不會產生不良的影響。 p. 215

一些 HTTP 動詞,如 GETPUT,在 HTTP 規格中被定義為等冪的,然而,這樣的話,它們依賴你的服務乙等密的方式來處理這些呼叫,如果你開始讓這些動詞變成非等冪的,但呼叫者以為他們可以安全地重複執行這些操作,你可能會讓自己陷入一團混亂。 p. 216

務必小心,別在太多地方快取! p. 230

有一個快取是你幾乎無法控制的:使用者瀏覽器裡頭的快取。 p. 230

特別是目前主流的瀏覽器像是 Chrome,之前開發時就因為快取,測試人員的瀏覽器總是用舊的 JavaScript 造成許多奇怪現象...

我們甚至有數學證明可以靠訴我們確實不行。你可能聽說過 CAP 定理,特別是在討論各種資料儲存機制的優缺點時,基本上,這個定理告訴我們,在分散式系統中,有三件事相互抗衡:一致性 (consistency)、可用性 (availability)、與分區容錯性 (partition tolerance)。具體來說,這個定理告訴我們,我們只能夠滿足三項當豬的二項,而無法滿足全部三項。 p. 232

跨多個節點維持一致性真的很難 p. 234

我們的系統不需要整體都是 AP (犧牲一致性) 或 CP (犧牲可用性) ... (中略) ... 然而,個別的服務甚至不必為 CP 或 AP。 p. 235

第十二章 全部組織起來

微服務的原則

  • 圍繞著業務概念塑模
  • 擁抱自動化文化
  • 隱藏內部實作細節
  • 將一切去中央化
  • 可獨立部署
  • 隔離失敗
  • 高度可觀察

這本書大概花了一個禮拜的零碎時間看完,其實收穫很多,很多原則不僅僅適用於微服務,也適用在單體式應用被部署在很多節點上,加上跟過去的經驗比較,更能體會到書中的觀點,接下來,要開始 stop starting, start finishing,將書架上很多看到一半的書,集中火力把它們看完吧!

 
over 2 years ago

今天被鬧鐘吵醒 (好久沒被鬧鐘吵醒,最近都是五、六點鬧鐘還沒響之前先自己莫名醒過來,然後差不多鬧鐘響時去按掉,又稍微睡一下回籠覺,到該出門前半小時趕緊吃早餐出門),手機的 Facebook 出現這張照片,是啊,離開學校已經四年了...

可能是因為很晚才步入職場,我的每次離職都是很果斷,這樣講好像我很常離職似的,其實才兩次,第一份工作只待了八個月,雖然對處長很抱歉,但思考後還是離開,第二份工作待了三年,老實說,那裡是不錯的地方,也讓我推薦了不少人進去,但在想著接下來要做什麼時,也許該離開舒適圈去闖闖了

畢竟和大學剛畢業或碩士剛畢業的人相比,我手上就時間的籌碼就少了六年,但我並沒有後悔,這六年確實在我這四年的職涯上帶來很多正面的影響,只是在規劃職涯時會讓自己想更多事情,像是畢業那年,雖然才面試了四家公司,卻也讓我寫了好長一篇分析 (現在回頭看,真的好長 XD )。

職涯規劃每個人想的都不一樣,而且會隨著時間改變,畢業時,是自己找工作,開始工作後,慢慢變成工作來找我,還曾寫了兩封信,婉拒對方的好意,第一次的理由有幾個,主要是不甘心,想看兩年的努力開花結果,另外是工作內容還蠻有趣的,想自己闖闖卻還沒想到要做什麼,然後就是想讓自己推薦進去的人都能獨當一面。

沒想到第二次時,上面的理由幾乎都不再是我想留下來的理由,推薦的人都已經是團隊的中堅成員,但等了三年,產品還是沒開花結果 (輾轉聽到似乎有機會了,希望這次不是再一次的狼來了,讓還在裡面努力的同伴能享受豐收的喜悅),我找出當初的婉拒信,最後婉拒的理由是,我想試試自己能發揮到什麼程度,因此我拒絕了我過去不曾聽過的年薪,選擇和理念相同的同伴一起闖闖看。

但命運很好玩,就在和同伴讓工作團隊開始步上軌道時,有了想併我們團隊的公司,而且就是我第二次婉拒的公司,在衡量種種條件後,團隊成員也不反對合併,對我來說這兩個多月 (從六月到八月),所見所聞的東西比過去要多很多,瞬息萬變很多。

雖然之後在以子公司的方式併入新公司後,薪水也許有成長 (老實說我也沒特別問這個),但相較之下還是會失去了某些空間,可是我還是想試試看當初婉拒時說的話:

是想嘗試一下,在這裡沒有完成的事:和夥伴一起摸索商業模式,用自己認為好的開發方式開發產品,和伙伴一起撞牆,若有機會,一起脫殼而出。

有點扯遠了,會提到職涯規劃,是先前不久在招募兩位學弟時,我改變過去面試的方式沒有問技術的問題,除了期望的待遇範圍外,只問了二個問題:

  • 你們的職涯規劃是什麼?
  • 從這離開後想帶走什麼?

相信這兩位學弟的專業能力所以沒問技術的問題,但一個新創公司需要的不只是專業的能力,還需要熱情,新創相較已經穩定的公司,更無法給進來的人什麼承諾,所以要能對自己有規劃,因此我問了這兩個問題。這其實也是問自己的問題,特別是現在的同伴當初找我作 CTO 時,我問了對方你覺得 CTO 是什麼角色?也問自己能否當好一個 CTO?

我曾覺得 CTO 是能幫公司看未來 10 年或 20 年後需要什麼技術,培養團隊能有有應付未來的能力,但老實說,我目前還沒有這能力,但想在的想法變了,我覺得所有 O 字輩的人,像 CEO 或 CTO,想的應該是如何讓整個團隊在未來能取代自己,不是說之前想的不重要,而是在自己達到那境界後,還要能讓後輩也能達到這境界,而不是讓自己成為拖累公司發展的人,這是一件超難的目標!

四年了,之前看《風起》時有一段話:人最黃金的創作時期只有10年,盡情去發揮吧!加上求學時期的強者學長,也加入我們了,要加緊努力,準備邁向下個階段。

最後,這是那兩位學弟的其中一位,在和我們談完後,傳給我的影片,他說那天聊到的東西在影片中有相似的論點,給想規劃自己下一步的人思考看看 (可別照單全收啊)。

沒想到自己在幾個關鍵點都留下許多的文字紀錄 XD,本來這文章是想寫來徵才的,但寫到最後覺得不適合了,所以只好放在這小小的後記裡,對我們接下來要做什麼有興趣想了解一下的熱血青年,歡迎跟我們聯絡。

 
over 2 years ago

雖然看過 Ruddy 老師的《精實開發與看板方法》,也整理了不少重點,但總覺得少了點什麼?碰巧趨勢科技的柯大哥在資策會有開課,加上又有公司全額補助,就決定跑去上課了,兩天的課程收穫很多,除了從看板遊戲體會到不少東西外,一些觀念也更加清楚,也發現到自己有西東西並沒有很清楚,只是上完課到實際回到團隊中使用,還是有很多小東西要注意。

什麼是看板方法?

首先看板方法不是軟體開發方法,也不是框架,原先是豐田為了達到零庫存,用來優化生產線使其能用最少的資源,在顧客下訂單後開始生產,以最快的速度交付給客戶,一種優化生產線的方法 (用抽象的角度來看,像是從生產線的末端,向前拉動生產線的生產,以減少庫存,不是從前端不斷地生產來推動下游,因此是一種拉動系統),後來衍生到軟體開發上,但本質還是一樣,看板方法是優化流程的方法,把看板方法與其他開發方法搞混了,就會常常撞牆鑽不出來。

精實精神

看板方法受到蠻多精實軟體開發的精神影響,所以在了解看板前先知道精神軟體開發的核心精神(或稱原則):

  • 消除浪費,軟體開發常見的浪費:
    • 半成品 (work in progress)
    • 額外過程,像是寫非必要的文件
    • 多餘功能,當時間緊迫時,能被捨棄或切割掉的通常就是多餘的 (假設產品負責人不是亂捨棄與切割)
    • 任務轉換,基本上就是避免 context switch 過多
    • 等待,這應該沒什麼好說的
    • 缺陷,如果沒 bug 就不用修,多好...現實中不可能啊~
  • 增強學習,軟體開發本身就是一種學習的過程
  • 盡量延遲決策,到資訊充足或是不做決策的成本要比做決策的成本高時才做決策
  • 盡快交付,我想應該沒有客戶喜歡等待...
  • 授權團隊,這點很有趣,幾乎所有的書都說,能自我管理的團隊會是最有效率的團隊,但在台灣通常還是有無形的手在後面...
  • 著眼整體,局部最佳化不等於整體最佳化,甚至會傷害整體最佳化

原則與實務

看板方法本身還是有些核心原則:

  • 從既有的流程開始,因為看板本身不是開發方法,也不是框架,所以要從團隊本身既有的流程開始優化
  • 同意持續增量漸進的變化,越是大幅度的變化,越容易引起團隊反彈,而且最好從痛點改起
  • 尊重當前的流程、角色、職責和頭銜,基本上優化不是搞革命,就這麼單純
  • 鼓勵各層級的領導行為,如果要層層上報才能做決定,那優化根本不會成功

上述是原則,但實際上執行可透過六個實務來優化流程:

  • 視覺化,將流程視覺化,協助找出問題在哪裡
  • 限制半成品 (WIP) 數量,凸顯瓶頸,並促使團隊成員解決瓶頸
  • 管理流程,基本上就是優化流程
  • 制定明確方針,越是簡單的方針,團隊成員越容易遵守
  • 落實回饋,鼓勵團隊提出建議,並讓建議成真
  • 協同改進,實驗性演進,分析問題時善用數據

前三項完成就有看板的雛型,後三項是強化優化的效果。

精實與 Agile 的差別

精實開發的精神與 Agile 在蠻多地方其實有點接近,事實上也不相違背,但個人覺得最主要的不同是,Agile 重在快速因應變化的能力,所以像 Scrum,以 sprint 衝刺的方式,讓 PO 與客戶在每個 sprint 結束後下個 sprint 開始前都有調整產品方向 (透過調整 story 的優先度),避免過度設計,透過每個 sprint 少量的時間進行 refinement,讓設計因變化所受到的衝擊減到最小。而精實重在持續快速交付價值,所以看板方法強調在優化流程,排除浪費,消除瓶頸,都是為了能快速交付。但並不是說 Agile 就不是快速交付,而是在因應改變與快速交付中取得平衡,所以 Scrum 也是有自省會議,讓團隊能更加進步。

設計看板

要使用看板方法,要先進行看板的設計,雖然說看板方法四個原則的第一則:從既有的流程開始,但既有的流程卻有很多解釋的空間,例如,可能已經有 Scrum 經驗的團隊,很直覺地就以 scrum board 的 planned、in progress、done 作為既有流程,若以 task 為工作項目的單位來看,這當然沒問題,但一個 task 進到 done 時,能為產品帶來什麼價值?當然有價值,但若整體的 story 沒有完成,就無法出貨,就是一種浪費。因此,設計看板時,要以系統性的角度 (著重整體) 去想:(1) 範圍、(2) 工作項目的顆粒度、(3) 狀態。但重點是,要與團隊取得共識

範圍

範圍決定看板的輸入與輸出。就軟體開發來說,最簡單的是需求作為輸入,實際可動的產品做為輸出。說是這麼簡單沒錯,但通常在考慮範圍時,會多考慮一件事:團隊是否能掌控。舉例來說,最近很紅的 DevOps,從開發、測試到部署以及營運,一條龍式 (最近這個詞好像變成貶意了) 整合在一起,那是不是能在設計看板時,就把部署放進去呢?那要看團隊是否在測試完成後,有權利決定立即部署,若可以,就適合放進去,若部屬需要層層的管理,或是有其他的流程決定,那就建議將部署另外獨立一個看板,不然,一堆工作項目放在 ready to deploy 的狀態也沒什麼意義。

同樣的,在考慮輸入時,也要考慮到是否能控制,或說得更清楚點,是需求的明確性有多高,一個客戶的 idea 在沒有進行任何討論或分析就放進來,那看板上在開發之前可能就要放一個分析的狀態,讓團隊有機會去討論與分析,但討論完若太大,可能又要切成若干個工作項目,此時,原始 idea 這個工作項目的角色就有點尷尬,要從看板上移除呢?還是保留呢?因此,一般建議是不適合放概念層級的工作項目到真正開發的看板中,也許可以有另一個看板用來進行分析與討論。

狀態

工作項目的顆粒度與狀態會彼此相依,例如剛剛所提的,若一個 story 依前端、後端與測試分拆成不同的 task,那 planned、in progress 和 done也許就可以當作狀態,但不見得能看出瓶頸所在,這也是為什麼上課時,柯大哥問大家一句話:你想從流程中看出什麼問題?

問題因團隊而異,有可能是前端與後端的資源不匹配,也可能是開發與測試資源的不匹配,也可能是分析與開發的資源不匹配,之所以都用資源不匹配稱為問題,是因為大多數團隊成員還是有各自的專長,即便是 T 型人才,也還是有特別精的部分與較不熟的部分,因此一個工作項目不太會是一個人從頭做到完 (假設是工作項目不是依職能切割),就會有交接的現象發生,當資源不匹配時,就不太可能很順地,一個人做完,馬上下一個人就接著做。

流程上的每個節點都需要消耗資源,沒有資源就只好等待,有資源完成後才會進到下個階段,因此看板方法想做的是用視覺化的方式去找出資源不匹配的地方,也就是瓶頸,然後才能解決問題,但解決問題不是靠看板方法,而是去調整流程、資源、開發方法,看板方法只是找出瓶頸,透過不斷地找出瓶頸,然後進行改變來優化流程。所以,在每個可能需要等待的狀態欄通常會建議再切成兩個子欄:進行中與完成 (作為緩衝)。好處是可以看到有哪些工作項目是處於『等待』的狀態,等待是我們試著要消除的浪費,若不凸顯出來,團隊就不會知道等待的狀況有多嚴重。

如果團隊的問題是開發與測試資源的不匹配,那可能較合適的狀態會是將『測試』這個狀態獨立出來,讓開發與測試在流程尚能被凸顯出來,相對地工作項目就不太合適是以前端、後端與測試的方式切割,而是一個可操作的項目,當前端與後端在開發時,工作項目進到『開發』的狀態,當開發完成後進到『測試』的狀態,因此可能的看板狀態會像下圖(但團隊可自行調整):

另外,由於累積流程圖 (CFD) 能協助觀察流程的順暢度,合適的狀態也是讓 CFD 能顯示出更有用的資訊的一種方式。

工作項目的顆粒度

看板方法沒有限定工作項目的大小形式,所以可以沿用既有的工作項目,例如 ticket、story 和 task 都是可以的,但剛有提到工作項目的顆粒度和狀態的切法有關聯,其實也會跟流程跑得順不順會有關聯,工作項目大小不一,設定 WIP 限制有時會出現產能下降或是多出盈餘時間,理論上工作項目的顆粒度相近,流程就會跑得越流暢,但不可避免,不可能永遠都可以把工作項目切得很相近,所以 WIP 限制是一個實驗值,要隨著執行成果調整。

上完課後,我個人比較喜歡的工作項目形式是 story,在流程每個狀態之間移動的單位也是 story,但實際執行時還是會切成更細的 task,但 task 怎麼辦?就變成 story 卡片上的 check list,check list 也分狀態,例如設計狀態的 task、開發狀態的 task、及測試狀態的 task,每個狀態下的 task 都滿足了該狀態定義 DoD (等等會提到)才能稱作完成,等到該階段的所有 task 都完成了,才會將 story 移到下個狀態,所以一張 story 卡片看起來可能像這樣:

Definition of Done

其實還真的蠻多人搞混 definition of done (DoD) 和 acceptance criteria 的,至少上課時就有不少人搞混 (不含我),簡單說,DoD 適用在所有工作項目上,可以根據狀態有不同的項目,也就是滿足了規定的 DoD 才能移到下個狀態,而 acceptance criteria 是針對特定工作項目所制定的,更簡單的說法是這個工作項目的規格,通過 acceptance criteria 才能跟客戶收錢。

會提到 DoD,是因為若看板空間夠的話,是可以將 DoD 直接條列在每個狀態的下方,例如,在分析與設計的狀態下可能的 DoD:

  • UI 流程圖已經完成並和 PO 或客戶代表確認過
  • 開發與測試的 tasks 已經切割完成

開發狀態下方可能的 DoD:

  • 都有單元測試,model 的 class/method coverage 到 100%
  • 程式碼至少由一個同儕 code review 過
  • 程式碼已 commit 到版控系統

而測試狀態的下方可能的 DoD:

  • 自動化測試腳本至少包含正常流程
  • 無法自動化測試的部分都有手動執行一次以上

當然,隨著團隊成熟,DoD 是可以一直增加的。最後,加上 DoD 的看板會像這樣:

急件

能插入急件,應該是許多團隊或管理層喜歡採用看板方法的一個原因,但要先說一下,雖然原則上 scrum 建議盡量不要在 sprint 進行中插件或調整 user story,但若因為市場因素 (例如,目前進行中的 story 已經不再需要) 或其他緊急因素 (營運中的服務發生問題),還是能將既有的 sprint baclog 做調整,注意,是做調整,不是僅僅把急件插入就好,sprint 週期就是固定這麼長,插件會排擠原有 story 的資源,所以就是用 story 換插件,插進急件就要把 story 移除 sprint backlog,不過有時候比較討厭的是,有些 story 已經進行到一半,要移除去就很討厭了,這即使是 scrum 加上 kanban 一起使用也是一樣的。

不論既有流程不是像 scrum 這種有固定時間周期的,在看板方法中讓將急件有獨立的渠道,而渠道上每個狀態的 WIP 限制都為 1,通常會避免設定很大,避免將所有的工作項目都設為急件,而濫用急件渠道。一但有急件,團隊就必須將該工作項目就當成急件,就像課堂中玩的看板遊戲中,講師偷偷提醒急件的定義,但有些組分配給該工作項目的資源甚至比普通件還少,有一組還因為有急件無法在指定時間內完成被扣 (遊戲中虛擬的) 錢,那就失去急件的意義了。

通常團隊要同時負責開發與營運的話,急件是需要的,因此,看板方法透過視覺化的方式,凸顯急件在每個階段是否都能順暢地被處理。加上急件後的看板會像這樣:

WIP 限制的設定

看板方法規定的東西很少,六個實務中,只有頭兩個是真正明確的要求,第三項則是根據問題去管理流程,沒有特定的方法,第一個視覺化流程,通常不太會遭遇到團隊的抗拒,但第二個設定 WIP 限制就不見得了,特別是『限制』這兩個字會讓人聯想到會不會影響到我平常的工作模式?但看板方法很明確地說,不能不設定 WIP 限制,設定 WIP 限制有幾個目的:

凸顯瓶頸

前面提到,切割 task 常會以職能切割,這無可厚非,即使是一個工作項目用多個 check list 項目也是同樣的概念,只是當以 task 做為狀態搬移的單位時,常會出現一種可能:局部優化,什麼意思呢?假設測試資源比開發資源要少,那開發人員在開發完某個 story 中與他職能相符的 task 後,很直覺地就會將下個 story 送進開發中的狀態,然後繼續開發與本人職能相符的 task,這樣確實不會讓工程師閒置,開發的 task 也消化的很快,達成了局部最佳化,但 story 完成了嗎?若測試資源還是稀少,那通常都要等到很後面才會看到 story 完成,這和盡快交付的精神違背。

要如何盡快交付?最簡單的就是 stop starting, start finishing。已經進行到一半的工作項目,就是盡快讓它完成,而不是在開啟新的工作項目,但這跟設定 WIP 限制有什麼關係?假設,測試的 WIP 上限是 1 (這是以一個工作項目有多個測試的 check list item 為例,若是以 task 為單位可以設為實際測試人員的數量作為上限),當有個工作項目停在測試階段,測試人員拼命側還是來不急測完,此時開發狀態的某工作項目已經完成,要送進下個狀態時就會因為違反 WIP 上限而無法送入,假設開發狀態的 WIP 上限還沒到,那開發人員可以再拉一個工作項目進到開發狀態,但如果也已經達到上限,就會發生卡住的情況。

這種卡住的情況是一種顯性瓶頸,和比較 task burn down 與 story burn down 曲線還更為明顯,由於已經卡住,所以團隊成員就必須想辦法解決問題,當然調高開發狀態的 WIP 上限是一種方法,但是一種笨方法,只是把問題往後延而已,事實上,看板方法就是強迫團隊去解決瓶頸,而不是視而不見,例如,開發人員可能不擅長寫自動化驗收測試腳本,但總可以幫忙手動驗收測試吧,或是就算寫得慢,還是可以停下新工作項目的開發工作,先幫測試人員寫自動化驗收測試腳本,戰鬥力雖弱也是一種資源啊!講義中有一個很有趣的比喻,工作項目是接力棒,團隊最重要的責任是讓接力棒不間斷地傳下去,直到終點,人有沒有閒置不是最重要的。

避免多工

課程中用翻銅板遊戲讓成員體驗多工不見得比較快,但我個人覺得翻銅板遊戲和平常軟體開發的多工其實不太一樣,於是能與現實工作產生的聯想就不明顯。自已的觀察,成員會常常多工,很大的原因是 task 切割得太細 (原因很多,就不在這細談),但彼此又有強烈的關聯,導致這些 task 一起做反而效率比較好。

當 task 切得異常細時,例如:我曾看過一個 story 切成數十個 task,story 紙卡上的便利貼貼到滿出來,以避免多工的角度出發,WIP 限制會是以 task 為單位去設定,例如:開發狀態的 WIP 限制為:開發團隊人數乘上 1.5,假設開發團隊為 4 人,則開發狀態的 WIP 限制為 6 個 tasks,這時候要小心,由於測試人員一開始可能沒事可做,所以開發人員把開發的 task 領到滿,然後持續這個情況,等到有某些 task 完成時,開發人員要領 task 時,卻無法領,因為已到 WIP 上限。又或者,因為 task 彼此關聯,想一起做時也會因為 WIP 上限出現排擠別的團隊成員能領的 task 數量。這些都是明顯卡住的警示,要團隊自己注意到該如何團隊合作以達到整體最佳化而不是局部最佳化。

另外一個導致多工的原因是,目前正在進行的 task 遇到阻礙,例如:要等其他部門的答覆,因為不想空等,所以領了新的 task 來做,此時這個人身上就會有多個 task ,當原有的 task 又可以進行時,成員又要進行 context switch 回到先前的工作狀態,如前所述,這也是一種浪費要避免。這時候會建議用個特殊的東西,像是磁鐵或是紅色的小便利貼貼在受阻礙的 task 以明顯標示該 task 目前是阻礙的狀態,若因為受阻礙的 task 多到一定的程度,團隊成員會很快達到 WIP 上限,於是團隊就必須思考如何加快排除阻礙,而不是等著它自行排解。

如果以剛剛我提到的喜好,以 story 當作工作項目的形式,每個 story 盡可能小,也許只是一個操作流程中的一個畫面,或是一個步驟。然後,將測試的 WIP 設為一個 story,若有 story 進到測試狀態,會希望團隊盡快將它完成;分析與設計狀態的 WIP 設為二個 stories,可以完成二個 stories 放在完成的子狀態,作為緩衝,開發狀態的 WIP 也是二個 story,保留一些執行上的彈性,但不希望有人同時執行超過二個 tasks。

不過這些設計都要與團隊取得共識,調整亦是如此。

落實

還在學校念書的時候,第一次接觸到《Applying UML and Patterns: An Introduction to Object-Oriented Analysis and Design and Iterative Development》書中的 GRASP 方法時,總覺得怎麼這麼麻煩?我的直覺很快就可以畫出 design diagram 了,這方法真的有用嗎?那時課程的助教 Teddy 學長說了一句話:『傻的願意相信』,先相信這套方法是有用的,等到弄清楚整個方法後,變成自己的東西時,那時你就不覺得是礙手礙腳的,反而會變得自然而然地,也不會刻意在用什麼方法,但在這之前要先相信它,願意使用它,不然這東西永遠不會是你自己的東西,就這樣,後來像是 CI、Scrum、SLM (ALM) 等概念都是這樣建立起來的,kanban 也是如此,先別去懷疑,先照著方法試試看。

觀察

看板方法大概都會提到 CFD,但實際上以人工畫 CFD,超累的,一般還是要資訊系統輔助會比較方便,理想的 CFD 是平滑的,什麼!第一次在書上看到這個形容詞時,我完全不知道那是什麼意思,但在玩過一輪看板遊戲後,再回頭看組員紀錄的 CFD,和其他組記錄的 CFD,就比較有感覺,越平滑的 CFD 就是流程跑得越流暢,也更容易預測工作項目所需要的時間。理想的 CFD 會像這樣:

但實際的 CFD 都不太可能是上圖,實際 CFD 會比較像下圖的,若以垂直切割的角度看 CFD,可以看到每個狀態下的 WIP,由於每個狀態都會有 WIP 的限制,所以最多不會超過 WIP 限制,但可以看出哪個狀態的 WIP 低於限制,哪些滿載,如此就可以發現流程哪裡不太順。

若以水平切割的角度看 CFD,可以看到下面兩個數據:

  • lead time = work item completed time - work item created time,簡單說就是工作項目整體花的時間
  • cycle time = work item completed tiem - work item started time,是工作項目真正處理的時間

從 CFD 看到的不是各別工作項目的 lead/cycle time,是團隊的平均值,所以當 CFD 越平滑,就越能預測 lead/cycle time,當有一個新工作項目排入時,大概可以猜出幾天後能完成,當然,這兩個數值會動態浮動,預測就只是預測,對我來說,若狀態切割的恰當,更可以看到工作項目在每個狀態的平均時間, CFD 最有用的還是用來分析瓶頸與優化流程的好用工具。

Coach

關於 coach 這個問題,是我在課堂中問柯大哥的,過去在跑 scrum 時,會有位 scrum master,當團隊的牧羊犬 (團隊成員是羊),除了保護羊不受干擾,也同時驅趕羊維持紀律,但在看板方法中,並沒有定義這樣的角色,所以需要嗎?其實還是需要的,若原本是 scrum team,看板方法只是用來優化流程,所以 scrum master 還是 scrum master,還記得『尊重當前的流程、角色、職責和頭銜』這原則嗎?但如果原本不是 scrum team,還是建議有個人扮演類似 coach 的角色,觀察上述的數字及圖表,協助團隊制訂與調整 WIP 上限。

要當好的 coach 其實並不容易,方法要不柔不剛,要讓團隊成員聽得懂,想得通,還要時時觀察是否走偏了,曾聽過一個說法:『 task 非得要估時數,是因為要給 scrum master 看』,我聽到時有點愣住,scrum master 什麼時候在意一個 task 要花多少時間了?task 的時數只是輔助團隊判斷是否能再拉更多的 story 進到 sprint backlog 中,實際上時數是多少,跟實際上差多少,擔任 scrum master 的我根本不在意,scrum master 觀察的是事情有沒有順利被消化完成,有沒有過度承諾,有沒有火力分散,有沒有過度保守,這些都是看整體的趨勢,而不是看各別 task 的時數,像這種就是一種可能走偏的信號,coach 可能要跟團隊再次溝通估時背後的精神。

自省與行動

如果是 scrum 搭配 kanban (或所謂的 scrumban),一個 sprint 一次的自省會議是很好的時間點,讓團隊針對該 sprint 出現的瓶頸或問題進行修正行動方案的討論,並在下個 sprint 中執行,若不是 scrumban 也沒關係,有固定的週期或是團隊有默契當什麼警訊發生時,就會召開會議檢討並調整流程就可以,不然實務的第三點就永遠不會發生,只是在討論時,有了視覺化的流程加上 CFD 等圖表與數據,更能清楚地針對對的問題去討論,而不是團隊成員的感覺。

最後,目前還在讓團隊嘗試摸索 kanban 中,也希望團隊能運作得更順暢。

上完課到現在也有兩個多月了吧!算是晚了很久的心得報告,不過,有些東西還是需要時間沉澱跟自己思考一下!有時看到自己敬重的幾個 coach 前輩常常參加一些 agile 的課程,時時補充自己當 coach 的材料 (火藥庫),最近碰巧自己的 title 有些更動,也在想是不是要少寫點程式?開始往 coach 的方向走呢?

 
over 2 years ago

還沒進新公司之前,對於要不要要求團隊估時與 CEO 曾有一些討論,當時討論的結論忘了 (路人:怎麼覺得這兩位是好隨便的 CEO 和 CTO,笑),不過,最後決定先照目前的流程跑,然後用 kanban 進行持續的觀察與優化,不過,對於 Scrum 團隊估時倒是有些個人想法可以分享。

為什麼要估時 (或估點)?

剛開始導入 Scrum 時,團隊大概最不能適應的事情之一,就是預估這件工作從 PM 移交給團隊了,過去由 PM 負責切 WBS,然後根據 WBS 規劃時程,有經驗的 PM 也許還好 ,沒經驗的 PM 則是用 dead line 天馬行空地認為團隊能在某個時間點全部開發完畢、然後進行測試等等,反正,一般來說,開發人員在水深火熱之中對於預估是無感的,甚至是排斥的,畢竟要花多少時間,開始開發才知道,說那麼多都沒用,即便 PM 規劃的時程多麼不合理都隨它去了。

在 Agile 方法中,預估的目的首要還是提供進度的可預測性度量,這是不可避免的,即便是粗略的估算,也是要有個大概的時程讓 stackholder 有個譜 (但是這譜有時會變成爭執的焦點又是另外的故事了),但在進行預估的會議中,像是 refinement meeting 或是 planning meeting,預估還有另一層意義:透過預估的方式,確定團隊對於工作內容都有一致的共識,個人認為,這才是將預估這件事交由團隊負責最有價值的地方。

至於可預測性量度,如果我們計劃得夠詳細了 (稍後會提到),是不是就能估算的很精確呢?答案是否定的,因為變異性是難以預測,在 Scrum 中的則是當團隊有穩定的預估穩定的開發能力後,相對來說就比較具有可預測量度。感覺很玄吧!難就難在穩定不是嗎?基本上,答案就是信賴團隊全體的智慧

以點數為例,當大小或複雜度差不多的 stories (或任何要進行預估的工作單位),團隊都能給予相近的點數,此時團隊的預估是彼此有共識的且穩定的;在有穩定的估預估前提下,當團隊能在一定的衝擊範圍 (有人突然重病無法上班、有人離職、新人加入或是遇上原先不預期的困難) 內,團隊能夠互相支援,依然能交付承諾的工作,此時團隊就有穩定的開發能力了。但這上述二點,不是瞬間就能擁有,是要團隊一定時間的磨合後才會穩定下來的。

預估的單位種類

穩定的開發能力需要多種 practice 組合和團隊組合的優化 (cross-functional team),不在這次討論的範圍,即便是有穩定的開發能力,但預估的大小變動範圍很大,也很難有一個穩定的數值可以參考,因此前面才會提到穩定的預估是很重要的前提,大多數 Scrum 書籍都會建議 user story 用點數當作單位,以小時當作 task 的估算單位,但這不是絕對,單位可以分成幾種:

時數

這是最直覺的單位,大家都習慣以『我要花多少時間』完成這個 story 或 task 的想法來估算,但也是最難估的準的單位,特別是用時數估算 story 時,除非 story 夠小,不然動不動就出現 20 或 40 以上的數字,意義就不太大了。如果團隊估時是用 planning poker 方法的話,有時也會容易造成 senior 與 junior 對於時數僵持不下的情況 (稍後再提),不過對剛開始使用 scrum 的團隊來說,用時數當成 task 的單位,在還沒有 velocity 參考數字之前,多一個數字作為一個 sprint 可以選多少 story 的參考。

通常是把每個團隊成員的實際可工作時數 (工作天數 * 5 - 休假時數 - 已知開會時數) 加總,作為一個 sprint 可選進 stories 的上限,從最優先的 story 開始挑,挑到時數已滿時,就不再加 story 了。

點數

和時數這種量測的單位相較,點數就有點抽象,但其實相對大小是比較容易比較的,點數代表複雜度的相對程度,也就是 2 點的 story 其複雜度會比 1 點的 story 要多複雜一倍,這也是為什麼撲克牌會是 1、2、3、5、8 的數列。那單看一個故事的點數有意義嗎?本身沒有,相對來看才有意義,在沒有 velocity 的情境下,也無法得知這個 story 要做多久。一般來說,點數可以用在 story 和 task 上,要使用點數作為單位估算,團隊必須先從過去開發的經驗中,找出一個不大不小的工作項目,訂下一個基礎點數 (3 點或 5 點),接下來所有的估算都是依照新工作比這個基礎複雜幾倍或簡單幾倍來估算。

有一種方法是找到一個最小的工作,當作是 1 點或 0.5 點,但『最小』有時候不容易找到,且也難保未來不會有更簡單的工作項目出現。

工作數

這個單位,我個人是比較建議團隊較成熟後,團隊的 task 切割有一定的慣性,例如切割方式都很類似,大小接近,此時可以簡化成 story 點數估完後,拆解完 task ,團隊對於 task 的內容有共識後,就可以開工了,PO 與 SM 看有多少 task 就大概知道時數的分布會在哪個區間,加快 planning meeting 的進行。由於缺少時數的 burn down chart,若真的想觀察更細微的團隊進度,可以用 task burn down chart 搭配 story burn down chart 來觀察。

個人的想法是,剛開始使用 Scrum 的團隊,還是用點數估算 user story,然後以時數估算 task,至少在一開始決定要納入多少個 stories 到一個 sprint 時,有比較好的參考點,當漸漸成熟時,是可以省去 task 時數的估算,但 story 的點數還是要有,因為團隊是會持續進步的 (當然也可能是退步),或是有人員異動,story 的消耗情況雖然是落後指標,但依舊可以觀察團隊的情況,即使有浮動或劇烈震盪,也應該要慢慢趨向一個穩定值才是夠成熟的團隊。

預防性措施

要讓團隊有較高品質的估算,agile coach 或 scrum master 可以觀察一些徵兆,若有發現盡早排除,免得讓團隊成員有壞習慣或是對估算這件事有陰影。

避免團隊成為橡皮章

首先,不論點數和時數都應該是由實際執行的團隊評估,PO 與 SM 都不要有建議,像是對團隊的數字說:『會不會估太少或估太多?』 (雖然,很多人都說遇到的 PO 大多是後者,但不管哪一種都不好)。最重要的是,不到非用不可的地步,PO 不要說:『不管怎樣,這些 stories 在這個 sprint 一定要做完。』這句話的殺傷力很大,因為就算團隊估算完後做不完也無法把 story 吐出來,會讓團隊之後更加不願意估算。而且為了趕上,犧牲掉的通常就是品質 (刪除某些細節的規格或是省略掉一些例外的處理),或堆積更多的技術債。但切記,對團隊信任度的傷害卻已經造成了。

就過去的經驗和參加其他活動聽到的經驗,很多的 dead line,都不是真的 dead line,通常是壓著說一定要做完,等到 sprint 最後一天,PO 看著牆面然後就冒出新的 dead line,或是當團隊勉強完成了,卻發現遲遲沒有真的上線,團隊才知道客戶根本沒這麼急著要。但 dead line 是一定會有的,像是奧運一定是在某一天開幕,作為支援的系統,勢必要在那一天之前上線,但如果真走到這一步,加班是避免不掉的,只是這應該是所有 agile 方法都在極力避免的 (透過減少未完成品的數量)。

避免換算

若同時使用點數與時數,要注意一點:點數與時數之間不一定有必然的關係,有可能某個 1 點的 story 切完 task 後估算結果是 8 小時,另一個 1 點的 story 估算後是 6 小時,這都是可以的。像剛剛我提到不大不小的 story,到底怎樣的 story 算是不大不小呢?我刻意避開像這樣的形容詞:『一人能在一天內完成的』story,原因就是避免讓團隊又落入以時間換算點數的情況,這樣就失去相對大小比較的好處了。

當排入 spring backlog,且 task 已經切好,也估完時數後,story 的點數雖然還是有一些統計上的意義,但沒那麼重要,因此不用拘泥於點數與實際的時數對不起來的問題,還特地回去修改點數或是調整時數,因為本來就沒有對應關係。當每個 spring 消化的點數趨於穩定,代表團隊的開發速度穩定,product owner是 是可以參考 velocity 大略估算時程,但這也不是絕對的時程。

即使事後統計發現有關係,也不需要拿出來跟團隊說,以後 1 點就是 5 小時,這會讓團隊在估算時礙手礙腳,一直在計算彼此之間的換算,或是讓團隊先估時數再回去推算點數。

避免僵局

若方法正確的話 (除了 planning poker 外,也可以參考 團隊估算遊戲),點數在估算上比較少會出現僵局,但 task 的時數就比較容易出現僵局,本來估點與估時的重點是釐清問題和尋找共識,但因為是以『如果是自己做這個 task 要花多少時間』就容易出現 senior 和 junior 就經驗上和能力上的不同有不同的數字,通常 junior 會比較擔心若在預估的時間內做不完,會不會讓自己的考績變差,因此會堅持較大的時數。此時也許可以考慮連 task 都用點數估算,所有的東西都是看相對的,這樣就可以排除不同人做同一件事時間不同,造成估算上的爭執。當全都以點數估算時,daily stand up meeting 就很重要了,因為那是團隊能知道實際還要多少時數的機會,這時就以真正做事情的人進行估算還需要多少時間,因為已經開始做了,這時候通常比較有把握,因此比較沒有爭議

一般來說,我說的是一般,但還是有些人本身很在意數字。團隊的氛圍通常是避免僵局最容易的方法,若對於多估 (實際執行時數比預估時數少) 或是少估 (實際執行時數比預估時數多),團隊能有開放性的檢討與改進,而不是批評或是做為考績的參考時,團隊也就比較不會對於數字斤斤計較,讓估時陷入僵局。

避免懼怕承諾

剛開始導入 Scrum 進行估時,通常都是估不準的,要不是過於樂觀就是過於悲觀,這都是正常現象,通常,團隊大概需要幾次的估算後才會趨於穩定,有點像下圖 (只是舉例,可能像任何數值),一開始沒經驗,因此承諾較多的點數 (40),但最後沒有完成, 於是下個 sprint 就曾諾較少的點數 (10),後來可能提前做完,因此又在下個 sprint 承諾較多的點數 (33),這震盪現象會持續一陣子,最後趨於一個穩定值 (大約20)。

所以在自省時 (稍後會說如何看過度悲觀或過度樂觀) 也不需要過度反應,非要團隊想出一個辦法或是要團隊在下次一定要估的很準,特別是承諾的 stories 沒有做完時 (這裡排除做錯被退的情況,單看因估計錯誤造成的沒做完),過度的反應有時會有反效果,像是花更多時間預估 (造成會議時間拉長)、數字灌水 (讓統計失真)、在估算時討論過多細節 (請參考後面的 mini-waterfall) 或是懼於給承諾。

在 TCP 網路協定中有個 Reno (Slow Start) 規則,當出現速度等異常時,傳送方會將傳送速度降到一半,然後慢慢再往上增加速度,當出現預估過度樂觀時,也許可以建議團隊先降低一個 sprint 能納入的 story 點數,當在 sprint 中提前做完,可以再添入 story 或是處理技術債,並在下次的評估中增加點數的上限,反覆幾次也能找到一個穩定值。

時時梳理

先前有提到,預估有一個重點是:團隊透過估點的方式釐清問題,看是否有不清楚的部分,因此若真要說一個 user story 本身點數有什麼意義的話,當點數很大時,例如:13、20,通常代表的是團隊根本不知道這個 story 要做什麼?或是太過複雜,應該再更進一步切小。一般來說 product backlog 中,高優先度的 story 點數應該都比低優先度的 story 要小,不然就應該安排 refinement meeting 將高優先度但點數仍然很大的 story 進行 refine,refinement 的重點也不是在切割,切割只是釐清問題後的結果,不是必然的過程。若團隊將一個不大不小的 story 點數是以5點來計算時,當進入 planning meeting 的 stories 點數都介在 1 ~ 8之間,時數估算的結果通常也會比較穩定。

切小還有一個好處是讓 PO 或 stackholder 有機會挑選真正最重要的功能來開發,agile 的 12 個原則中的一個是『將未完成的工作量最大化』,這一點看起來很矛盾,但這是因為一個軟體中,通常大部分的功能是使用者沒在用的,就算開發完成也只是浪費,同樣,一個需求,當初可能規畫得很完整,但切成小 story 後,可以根據時程與各項條件,把某些較不重要的 story 優先度調降,專注在更重要的 story 上,若擔心只有一個 product backlog 無法看到需求的全貌 (那些 stories 完成了,那些還沒),可以考慮 user story mappping 協助追蹤整個全貌

自省 & burn down chart 的解讀

其實從每天的 daily scrum meeting 就能觀察出預估與實際執行的落差,但即使出現落差,先別急討論預估的問題,這比較適合在每次 sprint 結尾的的自省會議中討論,自省會議 PO 可以參加,但如果要討論對預估的調整,我個人比較建議 PO 不參加自省,通常 PO (特別是兼主管職時) 參加會讓團隊不說話或是口頭上應付。討論時,scrum master 可以將該 sprint 的 story 與 task 的 burn down chart 作為會議討論的參考素材,也比較不會失焦。通常兩張圖擺在一起看可以看到三種類型:

Type 1 - 火力分散

在頭幾次的執行上,我個人的觀察是比較容易出現像下圖,團隊拼命地做自己擅長的事情,當一個 story 自己擅長的事做完後,就趕緊將下個 story 拉到進行中的狀態 (如果有累積圖的話更能印證),繼續做自己擅長的事情。因此,時數的消化很正常 (紅色折線),也貼近目標線 (黑色虛線),但真正完成的 story 很少 (藍色折線),此時檢討的重點恐怕不是預估的品質好不好,因為火力過度分散,導致團隊真正的效率還沒出現,所以,要先讓團隊能集中火力 (Stop starting, start finishing) 將已經開始的 story 盡量完成,讓 story 的 burn down 能持續下降。

Type 2 - 過度承諾

當線圖已經有比較順的下降,但到 sprint 結束時,總是有些 story 無法完成甚至還沒開始,如下圖一樣,這時候團隊就可能是過度承諾的狀態,這只是可能,story 無法完成的原因很多,像是中途改變了些需求,為了因應這些變動,團隊花了較多的時間導致無法處理別的 stories,或是遇到當初沒預期到的技術困難,這就是軟體專案上的變異性,光是技術困難這一件事就很難預期,例如:當初覺得 A 這個第三套件能完成 B 工作,但沒想到會影響系統,光是解決因第三套件引起的問題,就花了很多時間 survey,這通常是無法預期的。

因此,遇到下圖的情況,恐怕要 case by case 請團隊找出原因,而不是一開始就討論下次是否要少安排一點 stories,像是剛剛所說的第三方套件引起問題,那改進方向可能可以是下次在 refinement 後若有要使用不確定的技術,可以跟 PO 協調安插 prior study 的 story,在受控制的環境中實驗新技術;或是,太晚與 PO 確認完成的 story 導致 review 前一天才發現某個地方做錯了,那可能的改進方向就更不一樣了。

當其他原因都排除了,就可以重新檢視這次排入的 stories 數量與點數,未完成的 stories 點數,然後訂出下個 sprint 的 stories 點數上限,並在下次的 planning meeting 嚴格執行,否則團隊會覺得自省是玩假的:明明都做不完了,還是要我們吃下去。

Type 3 - 過度保守

可能有人覺得怎麼可能出現下圖?在 sprint 的一半時間就做完所有預估的事情?但事實上我擔任 scrum master 期間遇到過,而且不只一次,當出現下圖時,偶而出現是還好,畢竟團隊如果沒有一些盈餘時間,是無法學習新技術與改善團隊的。此時,scrum master 可以讓開發團隊與 PO 討論是否要再拉新的 stories 進來,或是用剩餘的時間處理技術債,又或是研究接下來可能用到的新技術,不論是哪種,都要與 PO 討論,確保真正重要的事被優先執行

但如果這現象是頻繁地出現,可能就是一個團隊過度保守的信號,在估點與估時的時候,數字帶有過多的 buffer,雖然說 PO 依舊可以在 sprint 期間加入新的 stories,但會讓本來穩定的 velocity 貶值,表面上 velocity 數字好像變大了,但其實整體來看完成的功能並沒有變多,此時,可能要決定是讓 velocity 續貶 (讓每個 sprint 能納入的 story 點數繼續增加),或是修正基礎點維持 velocity 的相對穩定 (從已完成的 story 中挑選一個作為新的比較基準),也可以跟團隊檢討,如何能有比較不虛胖的點數出現。當然,這也可能是團隊開發速度提升的信號,所以 scrum master 要小心應對,不然會讓本來開始進步的火苗又被澆熄了。

小心 mini-waterfall

Scrum 框架其實很簡單,卻是易學難精,若沒有理解藏在背後的 agile 精神,就容易變成只是依樣畫葫蘆卻沒有得到好處,例如,為了有『精確的』預估,為了有『漂亮的』的 burn down chart,一個 refinement meeting 開整整一天 (若是兩周為一個 sprint,一般是建議 refinement 不要超過 4 小時),把所有的設計細節全部寫下來,卻忘記當初使用 user story 的初衷是一個 placeholder 讓大家看到時,能用說故事的方式讓別人了解為什麼需要這功能,以及要完成什麼功能,怎麼完成是到 planning 及施工時,讓團隊發揮的,一次 planning meeting 要一天甚至兩天 (一樣,若是兩周為一個 sprint,planning meeting 也不建議超過 4 小時),task 切到小到不行,但卻又互相依賴,變成一次要領好幾個 task,這時恐怕是走火入魔,忘記當初這兩個會議的本意了。

從上面看下來,怎麼覺得開發團隊好像小朋友似的,都要 scrum master 像保母一樣照顧,但如果 scrum master 或 PO 真的像保母一樣帶團隊,那團隊就真的永遠是小朋友了,要讓一個團隊成長,要像對待成年人一樣,放手讓團隊走自己的路,上述很多觀察或是方法,能讓團隊發自內心提出來是最好的,甚至讓團隊想出更好的辦法,這時靠的是引導而不是教導,更不是指示,scrum master 要能從團隊的表現與現象觀察出問題,但切記別揠苗助長,適當地授權,團隊慢慢就能自我組織,自己解決問題。