l

2019年10月18日 星期五

學習DDD有感

Oct. 18 14:46~16:18


客戶的發問

昨天下午到客戶家介紹領域驅動設計(Domain-Driven Design;DDD),以及如何運用事件風暴(Event Storming)、Clean Architecture、TDD的工具落實DDD。

活動結束時有同仁問Teddy:「你演講中提到DDD的好處我們都可以理解,具體上有沒有什麼實際的經驗可以分享?」

***

第一次接觸

Teddy首先想起幾年前剛接觸DDD,讀到《Domain-Driven Design: Tackling Complexity in the Heart of Software》這本書的情景。由於多年來Teddy已經習慣物件導向分析與設計(Object-Oriented Analysis and Design;OOAD)方法,當時覺得DDD強調建立領域模型(domain model)這件事,和OOAD並沒有什麼不同。另外書中提到的ubiquitous language、bounded context等觀念,其實就是pattern發明人Christopher Alexander在《Notes on the Synthesis of Form 》與《The Timeless Way of Building》等書中所提到的設計方法。至於DDD的Tactical Design(戰術設計)所包含的entity、value object、aggregate、factory、service、layered architecture這些patterns,也不過就是物件導向設計中常見的作法。

結論就是,這本書稍微翻一下就沒再理它了XD。

***

第二次接觸

過了一陣子,可能是因為微服務(Microservice)流行的緣故,DDD也跟著流行起來,Teddy同溫層的「臉友」談論DDD的人漸漸多了起來。這也引起Teddy的好奇,想說光看《Domain-Driven Design: Tackling Complexity in the Heart of Software》可能不夠,又買了幾本DDD的書,花了不少時間用力讀了一下。此時對於DDD所提出的這堆patterns的內涵有比較完整的認識,但總覺得,要開發軟體我就用熟知的OOAD就可以了,找不到一個施力點來使用DDD。

***

第三次接觸

兩年前在指導教授的建議之下,Teddy停掉在北科大資工所兼任的「軟體生命週期管理」課程,改教「軟體架構」。當時剛好Robert C. Martin 的新書《Clean Architecture: A Craftsman's Guide to Software Structure and Design》 出版,Teddy就用這本書當教科書。兩年來為了學習與教學的目的,Teddy設計兩個不同的專案,用了好幾種方式來詮釋Clean Architecture的實作。後來發現搭配DDD的domain model、ubiquitous language、bounded context、event storming、entity、aggregate、repository、service、domain event等pattern,以及Clean Architecture,加上採用TDD/BDD/SBE的方式實作系統,可以很漂亮的串起這些軟體開發方法。

此時,Teddy才慢慢感受到DDD的威力,特別是ubiquitous language in code的實踐,對於軟體的開發與維護所帶來的好處。另外,aggregate與domain event的使用,讓系統模組自然支援現今主流的分散式架構,這是傳統OOAD方法需要特別而外去關注才做得到的地方。

***

第四次接觸

約一年前Teddy帶幾位研究生開發一個看板系統,當初一開始設定的目標是要採用Clean Architecture作為此系統的架構。隨著開發活動進行,慢慢發現使用到DDD patterns越來越多,像是原本Clean Architecture就很強調的domain model,以及DDD的ubiquitous language in code、aggregate、repository、service、domain event等。

多年來Teddy也帶過好多組學生開發軟體,每次都立下宏願,希望用畢生所學,讓學生可以開發出「高品質」軟體。坦白說,多年來這個宏願一直無法實現。一方面研究生時間不多,要修課、寫作業、過日子,而且每年都有學生畢業,真正有時間投入專案開發頂多就是一個寒暑假而已。

另一方面學生的軟體開發能力與經驗也不足,還在學習當中,不容易要求在短時間內就可以做出設計良好的系統。

但是,這次開發看板系統Teddy感受到一絲曙光。初期Teddy花了很多時間與學生code review,先確認他們的Clean Architecture沒有走偏(目前還是有點偏,但至少沒有歪的太厲害)。後來隨著一個個user story實作的機會,幫助學生認識如何落實domain model與ubiquitous language in code。Teddy覺得光是這兩點做到,再搭配Clean Architecture,整個軟體系統的可讀性提升好幾個檔次。

Teddy希望有朝一日能做到「即是是學生所開發的軟體,也能夠達到專業的水準」。

***

老外幫我們做實驗

Teddy後來告訴發問的同仁:「DDD外國外已經提出10幾年了,有很多老外已經幫我們做過實驗。落實DDD的難度相對而言是比較高的,但是一旦公司與團隊能夠掌握這項方法,以我切身的經驗,我相信可以讓軟體開發更接近『讓軟體變軟』這個目標。」

落實DDD除了技術上的門檻以外,還有一點非常重要,就是團隊中有沒有領域專家,以及這位(或這一票XD)領域專家是否願意與開發團隊緊密合作,採用迭代與增量的模式,共同建立軟體系統的domain model與ubiquitous language。

DDD的技術門檻,可以來上泰迪軟體的〈Clean Architecture這樣學就會了實作班〉補足。至於有沒有領域專家願意與團隊緊密合作,就只能靠緣分了XD。

***

放下偶包

整個學習DDD的過程讓Teddy有一個很深刻的感觸:「很多時候以往的成功經驗可能會阻礙自己學習新的事物。」OOAD太熟導致一開始忽視DDD、Waterfall太熟導致覺得敏捷無用、壓專案時程太方便導致沒有給團隊自主決定的機會、成功的大公司導致無法冒險也沒有容忍失敗的空間。

所以武俠小說裡面的橋段:「欲練神功,揮劍自宮」都是真的。

***

友藏內心獨白:該不會下一頁是不用自宮,也能成功吧!

2019年7月2日 星期二

領域驅動設計學習筆記(6):Aggregate (中)

July 02 21:45~22:48


在上一集〈領域驅動設計學習筆記(5):Aggregate (上)〉提到為了避免破壞aggregate invariant(聚合不變量或聚合規則),aggregate root在回傳資料的時候,需要考慮:

  1. 完全禁止回傳Aggregate內部的Entity。
  2. 可以回傳Entity,但只回傳immutable Entity或是Entity的deep copy。
  3. 設計immutable interface,讓Entity實作immutable interface並透過immutable interface回傳給客戶端。

今天討論這幾種做法的優缺點,並展示一個透過自動產生immutable proxy的工具來解決這問題的範例。

***

禁止回傳Aggregate內部的Entity

  • 優點:這個方法因為禁止aggregate root回傳內部entity,所以客戶端不可能透過aggregate內部entity來破壞aggregate invariant。
  • 缺點:為了提供資料給客戶端,aggregate root可能需要將內部entity轉成DTO(data transfer object)再往外傳。這種做法有點類似clean architecture中,use case層定義雙向介面將entity往外傳遞。從架構的角度來看可以切得很乾淨,但是需要花費額外的功夫包一層DTO。

***

回傳immutable entity或是entity的deep copy

  • 優點:用相同的domain model(直接回傳aggregate內部的entity)來處理程式邏輯,而且因為回傳的entity是不可修改的物件,或是原本物件的複製版本,所以也不可能透過這個entity破壞aggregate invariant。
  • 缺點:無論是實作immutable entity或是deep copy(GoF的Prototype設計模式),都需要花費額外的設計、實作與測試的功夫。特別是deep copy,不小心沒有實作好變成shallow copy就GG了。

***

設計immutable interface

Immutable interface是一種設計模式,請參考c2。簡單講,immutable interface是一個只有getter沒有setter的介面。設計好immutable interface之後,讓你的entity實作此介面。如果aggregate root要回傳entity,只能傳回entity所實作的immutable interface,如此一來客戶端就不可能修改到aggregate的資料。

Immutable interface的缺點和回傳DTO類似,domain model裡面除了原本的entity,又多了另一種代表「不可修改」的immutable interface。

***

折衷作法

如果程式語言可以支援自動回傳immutable object,這個問題就不是問題。Teddy不知道有沒有哪個程式語言有這種功能,目前只能從上述三種做法中挑一種來實做。

Teddy想用方法二。方法二有兩種實作,考慮到回傳entity的deep copy比較不好實作,所以決定優先選擇回傳immutable entity的方式。

要實作immutable entity,可以套用Proxy設計模式,概念如下:

Ministage是一個mutable介面,MinistageImpl是這個介面的實作,而ImmutableMinistage則是將MinistageImpl包裝一層,只要客戶端呼叫到setter的函數,就丟出例外。

這種方法的優點是,從客戶端的角度,他看到的就是Ministage這個domain model的概念。至於它是mutable還是immutable,則只是實作細節。

這種做法當然也有缺點,首先設計上當然比沒套用Proxy設計模式要來的複雜。其次,因為在編譯期間(compile time)客戶端不知道拿到的Ministage是mutable還是immutable,如果不小心拿到immutable但卻呼叫到它的setter函數,則會出現runtime exception。

程式設計師都有偷懶的天性,Teddy想回傳immutable entity但又懶得手動實作proxy,於是google了一下,找到一個Java語言的reflection-util工具,可以自動產生immutable proxy。


▼使用方法很簡單,以maven建構工具為例,首先加入reflection-util的參考。



▼接著在程式中透過ImmutableProxy.create()方法,可以直接產生immutable entity,非常容易使用。

***

從Clean Architecture的角度來看…

使用reflection-util這種工具,可以幾乎無痛產生immutable entity。但是從clean architecture的角度來看,在最核心的entity層引用外部工具,違反了相依性原則,你的架構就變得不再那麼乾淨了。

Teddy覺得這個「小違規」目前是可以接受的折衷方案。因為你可以透過將reflection-util再包一層,讓aggregate root透過間接的方式呼叫reflection-util來獲得immutable entity。有朝一日如果真的不想使用reflection-util,也可以在影響最小的情況下,以其他實作方式將它取而代之。

所以,雖不完美,但還可接受。

***

友藏內心獨白:設計就是取捨後的結果。

2019年6月17日 星期一

領域驅動設計學習筆記(5):Aggregate (上)

June 17 15:36~16:42


問題

在物件導向的domain model裡面,物件和物件之間經常有著複雜的關聯(associations)。如下圖所示,一個看板系統的domain model,Board擁有若干的Stage,一個Stage擁有若干個MiniStage,一個MiniStage擁有若干個SwimLane,一個SwimLane擁有若干個WorkItem。

當修改這些物件狀態時,如果不小心很可能會違反系統的不變量(invariant)或固定規則而導致系統狀態不正確(產生bug)。例如,看板系統的一個不變量是:「SwimLane的WorkItem數量不可以超過WipLimit數值。」

試想在一個多人使用的狀況下,為了滿足上述不變量,當修改SwimLane的狀態時,你可能要鎖住SwimLane,甚至是鎖住整個系統,導致系統無法使用或效率很差。

***

Aggregate Pattern

Aggregate是領域驅動設計(Domain-Driven Design;DDD)裡面很重要的pattern。依據《Domain-Driven Design: Tackling Complexity in the Heart of Software》書中的定義,Aggregate想要解決的問題與解決方案分別為:

Problem

It is difficult to guarantee the consistency of changes to objects in a model with complex associations. Invariants need to be maintained that apply to closely related groups of objects, not just discrete objects. Yet cautious locking schemes cause multiple users to interfere pointlessly with each other and make a system unusable.

很難保證具有複雜關聯的模型中物件更改的一致性。 需要維護不變量,這些不變量適用於密切相關的物件組,而不僅僅是離散的物件。 然而,謹慎的鎖定方案會導致多個用戶無意義地相互干擾並使系統無法使用。(中文修改自google翻譯)


Solution

Cluster the ENTITIES and VALUE OBJECTS into AGGREGATES and define boundaries around each. Choose one ENTITY to be the root of each AGGREGATE, and control all access to the objects inside the boundary through the root. Allow external objects to hold references to the root only. Transient references to internal members can be passed out for use within a single operation only. Because the root controls access, it cannot be blindsided by changes to the internals. This arrangement makes it practical to enforce all invariants for objects in the AGGREGATE and for the AGGREGATE as a whole in any state change.

將ENTITIES和VALUE OBJECTS聚集在一起形成AGGREGATES並定義AGGREGATES的邊界。 選擇一個ENTITY作為每個AGGREGATE的根,並透過根控制對邊界內物件的所有存取。 允許外部物件僅保留對根的引用。可以傳遞對內部成員的瞬時引用,以便僅在單個操作中使用。 因為根控制存取權限,所以內部的更改不能繞過它。 這種安排可以確保任何針對AGGREGATE裡面的物件以及AGGREGATE本身的狀態修改都不會違反不變量。(中文修改自google翻譯)

***

實作細節

以上述看板系統為例子,Stage、MiniStage與SwimLane形成一個Aggregate,Stage是Aggregate Root。依據DDD的定義,為了確保不變量:

  1. 所有對於Aggregate內部物件的操作都要透過Aggregate Root。也就是存取MiniStage與SwimLane都要透過Stage。
  2. 外部物件只可以擁有Stage物件的reference(因為它是Aggregate Root),不可以擁有MiniStage與SwimLane的reference。
  3. 但是,Stage可以將MiniStage與SwimLane往外傳,只不過客戶端程式只能在單一method中操作這些entity,不可以把它們的reference保存起來(例如,不能存在data member裡面)。

前兩點很容易理解與實作,但第三點就需要進一步討論。因為如果把MiniStage與SwimLane往外傳,既使在單一method裡面使用,也可能繞過Stage直接呼叫MiniStage與SwimLane,因此改變Stage這個Aggregat的狀態而破壞不變量。

所以,在實作Aggregate的時候,就有幾種可能的選擇:

  • 完全禁止回傳Aggregate內部的Entity。
  • 可以回傳Entity,但只回傳immutable Entity或是Entity的deep copy。
  • 設計immutable interface,讓Entity實作immutable interface並透過immutable interface回傳給客戶端。

無論採取哪種方式,在實作時都需要特別留意,以免Aggregate的不變量一不小心就被違反了。

***

廣告

對於DDD的Aggregate與Event Storming(事件風暴)有興趣的鄉民,可參考泰迪軟體的【Clean Architecture這樣學就會了實作班】,2019年7月份課程已確定開課。

***


友藏內心獨白:還是要看定義。

2019年5月31日 星期五

如何用看板管理卡住的工作項目?

May 31 10:30~11:53


狀況分析

使用看板的團隊經常會遇到工作做到一半卡住的問題,卡住的原因不外乎:

  • 遇到技術困難不知道該怎麼做下去。
  • 遇到相依性問題,需要等待其他資源(工作、人、設備、材料)完成才可以繼續。

依據卡住的時間長短,卡住的狀況又可分為:

  • 短期:團隊投入資源想辦法排除卡住的原因,每日站立會議追蹤卡住工作的進度,儘量讓工作可以順利往下游移動。
  • 遙遙無期:預期工作會卡住很久,或是在特定時間才有辦法處理該工作。不需要在每日站立會議追蹤卡住工作的進度,一周、兩周甚至一個月更新一次進度即可。

***

用看板管理卡住的工作項目

▼針對短期卡住的工作,在該工作卡片上面貼上一張粉紅色便利貼,提醒團隊該工作項目發生阻礙,需要特別關注排除阻礙讓工作順利流動。


▼每日站立會議時追蹤阻礙項目的狀況,如果尚未排除則在該阻礙項目上用筆點一點,類似progress bar的概念。如果阻礙卡片上的progress bar越長,表示該阻礙停留時間越久。

***

針對被卡住「遙遙無期」的工作項目,短時間團隊並不會也沒辦法去處理它。如果直接把它放在看板上不管,會佔據一個WIP,這樣也不好。

▼一種常見的作法是在看板上建立一個獨立的Ice Box(冰箱)工作階段,把卡住遙遙無期的工作項目移入Ice Box。如此一來原本的工作階段便空出一個WIP,而團隊只需要定期檢視Ice Box裡面的工作卡片有沒有「臭酸」或「超出保存期限」即可。

當Ice Box中的工作項目其卡住的原因排除之後,只要原本的工作階段未達WIP上限,便可移回原本的工作階段繼續施工。

***

如果怕整個看板只有一個Ice Box不容易分類不同工作階段卡住的工作項目,可以參考《Agile Project Management with Kanban》書中的做法,直接在工作階段增加一個追蹤(Track)欄位。

▼如下圖所示,追蹤欄位和Ice Box類似,(通常)沒有WIP限制,放在其中的工作項目數量不會佔據實作工作階段(WIP Limit  = 5)的WIP。

***

結論

看板透過小批量生產消除變異性讓工作流動得更順暢,縮短產品交期。卡住的工作代表產生阻礙,也就是工作流程產生變異,需要特別關注,才不會發生「人進不來,貨出不去,公司發不了財」的問題。

***

友藏內心獨白:貨暢其流。

2019年5月29日 星期三

設定看板的WIP

May 29 11:52~13:41


前言

很多剛開始跑看板的鄉民都有一個疑問:「要如何設定每個工作階段的WIP?」今天介紹《Agile Project Management with Kanban》書中提到的方法。

***

準備工作

要決定如何設定WIP,以下資料需要先準備好:

  • 視覺化工作流程,例如分析、開發、測試。
  • 知道每個工作項目(work item)完成的平均時間,例如分析工作一周完成六個,開發工作一周完成兩個,測試工作一周完成三個。
  • 每個工作階段有多少人負責,特別是瓶頸工作階段的人數(詳見下面說明)。

***

開始計算

有了以上資訊,就可以計算初始看板的WIP。參考下表,計算步驟如下:

分析

實作

測試

平均每人完成工作速度

6/week

2/week

3/week

工作階段人數


3


產能


6/week


WIP


3 * 1.5 = 5


  • 找出瓶頸,也就是速度最慢的工作階段,在這個例子中實作速度最慢。
  • 算出瓶頸的產能:平均每人完成工作速度 * 工作階段人數,2 * 3 = 6,代表實作階段每周可完成6個工作項目。
  • 將實作的WIP訂為:工作階段人數 * 1.5 (四捨五入),實作階段的開發人員有3人,3 * 1.5 = 5。乘上1.5的目的是讓每個人手邊有1.5件工作可以做,以免WIP太小任何一件工作卡住就必須等待。但也不會因為WIP太高導致context switch的浪費。這個技巧在《Kanban in Action》書中也有介紹。
  • 其他工作階段配合瓶頸速度:由於屬於瓶頸的實作階段每周只可完成6件工作,因此分析只需要1人即可搭配實作階段的消耗工作速度。同理,而測試階段只需要兩人。

分析

實作

測試

平均每人完成工作速度

6/week

2/week

3/week

工作階段人數

1

3

2

產能

6/week


6/week

6/week

WIP


3 * 1.5 = 5


  • 計算其他工作階段WIP:分析工作階段WIP = 1 * 1.5 = 2。測試階段WIP = 2 * 1.5 = 3。

分析

實作

測試

平均每人完成工作速度

6/week

2/week

3/week

工作階段人數

1

3

2

產能

6/week


6/week

6/week

WIP

2

3 * 1.5 = 5

3

***

人太多怎麼辦

剛剛的例子是由瓶頸階段的人數與平均每人完成工作的速度,回推其他工作階段需要多少人。但很多情況是團隊人數已經確定,例如分析、實作、測試各2人,請參考下表:

分析

實作

測試

平均每人完成工作速度

6/week

2/week

3/week

工作階段人數

2

2

2

產能


12/week


4/week

6/week

WIP

3

3

3


在這種情況下,因為一周完成12件分析工作但卻只能消化4件,因此分析階段的工作會累積在實作之前。而測試會沒有工作可做,導致人員閒置。

但是,幸好有WIP限制,分析工作不可能無限增加。如下圖所示,假設分析與實作階段已達WIP上限,此時分析人員便不可再從待辦事項中拉取工作來分析,只能停下來看看如何幫助工作流動得更順暢。

同理,測試階段可能沒有多餘的工作可以做,一樣需要思考如何幫助工作流動得更順暢。最直接的方式就是所謂的swarming,分析與測試階段的人到實作階段幫忙,提高實作階段,也就是瓶頸階段,的產能。

如果能夠從每個工作階段的產出可以互相匹配的角度來安排人力與設定WIP,會讓工作流動比較順暢。

當然,實務上平均每人完成工作速度變異性可能很高,受到品質、施工人員的技能與身心靈狀態、工作項目大小、需求不確定性等因素影響,導致需要控制的因素很多,因而需要時時檢討人力、WIP的設置是否恰當,以及透過流程改善來讓工作流動更加順暢,降低交期(lead time)。

***

友藏內心獨白:小批量生產與消除變異性。

2019年5月28日 星期二

不要只聽半套

May 28 23:11~23:54

▲這句怎麼都沒人聽進去XDD


故事一

有一個公司派了整個團隊來泰迪軟體上Scrum敏捷方法實作班,回公司之後團隊跑了一陣子Scrum。有一天因為產品出了包,非常緊急,Product Owner希望團隊加班趕緊把這個問題解決。沒想到,團隊中有成員居然回答PO:「我們去上Teddy的課,Teddy說跑Scrum不能加班。」

公司老闆得知後非常氣憤,有一次遇到Teddy,用開玩笑的語氣說:「我員工去上你的課,回來之後都不加班了」。

這,員工不想加班就大聲說「我不想加班」,幹嘛扯到Teddy頭上哩?!Teddy上課時是說:

敏捷開發講求「穩定的開發步驟」(sustainable pace),早期XP對於穩定開發步驟有一個簡單的說法,就是每週工作四十小時。我自己還在與Scrum團隊的時候,也是儘量利用上班專心把工作完成,以不加班為原則。

假設公司因為產品出問題都火燒屁股了,作為員工此時還在「我跑敏捷我不加班」,如果Teddy是老闆,也會發火啊。古語有云:「善戰者無赫赫之功」,這也是好的敏捷團隊期望達到的狀態。但是,天有不測風雲,總是會遇到需要「江湖救急」的時候。此時願不願意跳下來解決問題,就不是敏不敏捷的問題,而是心態的問題了。

***

故事二

某PO質問Teddy:「為什麼你跟團隊成員說retrospective沒用?」聽到這樣的質問,Teddy也是滿頭黑人問號???

細問之下,原來是Teddy和團隊成員聊天時提到:

如果retrospective會議沒有訂定具體的改善執行計畫(action plan),或是改善計畫都沒有落實,相同的問題便會一再出現,導致每次retrospective都討論一樣的議題。有迭代沒有增量,久了之後團隊就會覺得retrospective沒有用。

不知道這位團隊成員是不是buffer不夠,只擷取到「retrospective沒有用」這幾個字。也未免斷章取義的太厲害了一點吧。

***

問題是自己狀況的投射

相信很多人都有這樣的經驗,聽了一場演講,或是經過一次對話,腦袋中留下來最深刻的只有「與自己現況不滿相關的記憶」。因為這些不滿的力量非常強大,強大到足以「扭曲力場」,把別人的意思曲解成自己的內心獨白,然後採用「借刀殺人」的手段,將自己一直想說卻不敢說出口的話,假借「別人的名義」說出來。

這種斷章取義、扭曲本意、沒有脈絡(context)的引用,並非全無意義。它往往透露出講話者內心真正的感受,只不過平常不敢說出來罷了。

為什麼敏捷開發的始祖XP要提「勇敢」?因為太多人不敢面對自己,不敢正視問題。真、善、美,只有真誠的表達自己的感受,改善才有可能發生。只有改善發生,才有可能逐步趨近完美。

***

友藏內心獨白:Teddy有說多讀書你怎麼都沒聽進去?!

2019年5月24日 星期五

一個Scrum各自表述

May 24 00:40~01:27

▲每個component team都在跑Scrum,啊不就好棒棒…Orz


沒事不要找Teddy聊天XD

去年在某個演講場合,演講結束後某位鄉民找Teddy聊天….

鄉民:Teddy你好,我是你部落格的忠實讀者。

Teddy:你好、你好,謝謝捧場。

Teddy:你看我的部落格,是因為你們有跑Scrum嗎?

鄉民:對,我在用戶研究團隊跑Scrum。

Teddy:喔……用戶研究團隊跑Scrum…..這麼神奇。這是什麼意思?

鄉民:就是我們團隊會在專案開始前期用迭代、增量的方式,跑幾個sprint產出用戶研究報告。

Teddy:然後把產出的報告交給開發團隊去實作?

鄉民:對。

Teddy:怎麼我聽起來好像是瀑布式開發?

鄉民:不是、不是,我們是跑Scrum。

Teddy:你們做出用戶研究報告交給開發團隊之後,需求都不會改變嗎?

鄉民:會啊。

Teddy:那之前花時間所做的用戶研究報告需要重做嗎?

鄉民:這……….

Teddy:你們就是waterfall啊。

鄉民:我們是Scrum啦……至少在公司內我們要說我們在跑Scrum。

Teddy:那你們當初為什麼想「跑Scrum」?

鄉民:因為總經理下令說要跑Scrum。

Teddy:喔,對、對。幾個月前我有看過你們公司發的新聞稿,說你們Scrum跑得很成功。


▲有功大家練,有敏大家捷。

***

高興就好

▲圖片來源在此


陳近南:反清復明 敏捷只不過是個口號,跟阿彌陀佛其實是一樣的。Waterfall一直欺壓我們公司,浪費我們的銀兩,所以我們要反Waterfall!

韋小寶:要反Waterfall搶回我們的錢,是不是?說來說去敏不敏捷根本就是脫褲子放屁!關人鳥事呀?行了,大家聰明人,了解!繼續!

陳近南:嗯。總之,如果成功的話,就有無數的銀兩,你願不願意去呀?

韋小寶:願意~!只不過你剛剛那句九死一生太嚇人了。

***

友藏內心獨白:敏捷就是專案出得去,錢進得來,公司發大財XD。