從 Clean Architecture 我們學到要把程式架構畫出來,把核心邏輯與其它細節分開來, 避免核心被汙染,達到最終目標:「最小化軟體生命周期的總成本+最大化程式設計師的生產力」。書本裡面給了一個四層的架構圖:Enterprise Business Rules, Application Business Rules, Interface Adapters, Frameworks & Drivers。簡單說前面兩個就是策略、業務規則、核心價值,後面兩個則是細節、資料庫、框架、通訊協定…等等。在這樣的分層規劃下,我們得以確認依賴關係是由外向內的。如果有需要,我們會利用依賴反轉(Dependency inversion principle,DIP)來維持核心的整潔,這樣的架構也允許我們延遲細節的決定。
事件風暴
第二堂課, Teddy帶我們體驗事件風暴(Event Storming),事件風暴是一種快速讓群體進入狀況, 跨部門達成共識的方法,不限於軟體,其實可以應用在各種業務領域,協助建構模型。事件風暴的介紹網路上可以查到很多,規則與範例就不在此贅述,因為事件風暴最重要的是溝通的過程,使用文字敘述很難體會,課程後我與YK(同事)為了讓更多同事也能了解這個溝通方法,因此辦了一個小workshop,邀請了三個不同部門的同事,其中包含測試部門、韌體研發、新技術開發部門一起共同討論「韌體自動化測試工具」這個主題。
共通語言
首先,大家想的真的都不一樣。我們先把12個人分成四個小組,讓大家各自列出這個系統中應該存在的Entity再來互通有無一下,結果發現:有一些東西大家都有共識(每一組都有塞一個專家),一定會有test case,也一定會有report,有一些東西則是我有你沒有,你有我沒有。但是即使是一樣的東西,各自表述之後就會發現,你的report跟我的report可能又不一樣,A組的包山包海,測試結果/細節步驟/結果統計圖,B組的只有成功與失敗,另外還有一個叫做debug log的會包含細節,沒有一個絕對的答案, 重要的是我們要找到共同的語言(ubiquitous language)。共通語言除了釐清彼此的想法以外,還有加速溝通的效果,建立共通語言後,可以提升溝通效率,不論是文件、程式或開會。例如當要做一個跟影像有關的專案的時候,我們可以在早期建模的溝通過程中,發現是不是所有成員都有3A的觀念,知道AE, AF, AWB是什麼,避免專案走到後期,才發現彼此有很大的誤解。
可視化流程
除了共通語言以外,事件風暴還提供了一個全面的可視化流程,我們同步大家的共同語言後,把四組合併成兩個組進行事件風暴,我們故意讓大家先用傳統方法從Entities開始發想,再補充上use cases,讓大家先跑一次流程。
事件風暴跟從核心出發不一樣,而是從領域事件開始展開(我們認為有點像是從use cases展開),兩組分別列出我們在乎的領域事件,看到大家不斷的貼上、移動、調整,對整個流程重新順一次,我想這就是事件風暴要的效果,最後產生一個看得到的用戶故事(User stories)。
我感覺event storming有點類似非coding版本的mob programing,用一種方法把群體的思維聚焦在同一個問題上,並且利用交流讓整體意識調整到相同頻率取得共識,共識後的產物是經過大家認可的業務邏輯與範圍,所以真正執行或實作的時候大家是朝同一個目標努力的。
|
在我們團隊快要一年了
一年的時間
寶寶可以長牙
台灣可以賣出 10 億杯飲料
營業額可以突破千億…
營業額可以突破千億…
| 經濟部統計處: 飲料店營業額在台灣逐年攀升 |
不說飲料
最近回顧了這一年的行事曆
我是有點吃驚的
我是有點吃驚的
OMG!
原來工作的方式,可以這樣影響生活
越工作越有活力
原來工作的方式,可以這樣影響生活
越工作越有活力
如何影響呢?
我先賣個關子,先來看幾個大部分的組織會遇到的現象。
第一個常見現象:開週會
Why are meetings so ineffective?
一個規模不大的四人團隊
一人報告一小時
一場 weekly meeting 就是四個小時
一人報告一小時
一場 weekly meeting 就是四個小時
如果你曾經有在電影院睡著的經驗
應該不難體會四個小時以上的會議有多累
應該不難體會四個小時以上的會議有多累
為什麼 weekly meeting 要開這麼久?
畢竟是一週的進度回報,如果每個人報告一個小時
相當於把他五個工作天,一天八小時的工作量
用 40 倍速的快進,講給你聽
奇怪的是,我們心理從來沒有出現這樣的聲音
“哇! 我聽到了 1/40 的超級濃縮精華/乾貨耶! ”
我們只希望他能再快個 40 倍
這不能怪同事,就像在電影院睡著其實也不能怪導演
這不能怪同事,就像在電影院睡著其實也不能怪導演
沒有哪個導演拍片是為了讓觀眾睡的
比較可能的是 —
那個議題和你沒有共鳴
Great movies tell stories that have deep emotional resonance
|
第二個常見現象:同事間的工作互換困難
Why is collective work difficult?
我們很常因為負責了某個案子,後續的東西都交給你來處理
因為這是短期看來最省時間的做法
因為這是短期看來最省時間的做法
當你在公司一陣子了,你會自然而然地有一個對照表:
“X module 的問題要找同事 A
Y module 的問題找同事 B”
“X module 的問題要找同事 A
Y module 的問題找同事 B”
你應該也對這種情況不陌生 —
同事 B: “A 請假耶,這問題要等他回來才能處理喔! ”
很少同事 B 會說: “A 請假,那你先把東西給我,我可以處理”
同事 B: “A 請假耶,這問題要等他回來才能處理喔! ”
很少同事 B 會說: “A 請假,那你先把東西給我,我可以處理”
但其實,這種工作互換、集體協作的例子,在生活中明明很常見
比如說便利商店的店員,A 忙著做咖啡、B 來幫忙結帳
從沒見過哪個店員跟你說:
“不好意思我不能幫您結帳,負責收銀的今天請假,我只會泡咖啡”
“不好意思我不能幫您結帳,負責收銀的今天請假,我只會泡咖啡”
上述這種情況在組織裡卻很常發生,原因是什麼呢?
可能是訓練文化
不知不覺把 ”人” 訓練成一台 “機器” (咖啡機/收銀機)
不知不覺把 ”人” 訓練成一台 “機器” (咖啡機/收銀機)
因此當我發現這裡的 team member 可以輕鬆的交換手頭上的工作
我是很驚訝的
我是很驚訝的
Machine learning or human learning?
|
第三個常見現象: 加班
Why work overtime?
你有玩過比手畫腳這類的傳話遊戲嗎? 通常結果都是各種歪樓
你可能會說,如果大家都能看到題目就不會有問題
No,還是不行
比如先前 U Think I Do 系列
就把朋友、家人、社會、學校、自己
看到一件事情的不同面向
放在一張圖裡,非常有趣
比如先前 U Think I Do 系列
就把朋友、家人、社會、學校、自己
看到一件事情的不同面向
放在一張圖裡,非常有趣
不同角度的生命科學系
|
撇除那種時程明顯不合理的情況
大多數的加班來自於
每個人對事物的理解不同:
大多數的加班來自於
每個人對事物的理解不同:
以為自己了解對方想要的功能
↓
發現不是對方想要的功能
↓
“盡量” 修改,改不完就加班
↓
發現不是對方想要的功能
↓
“盡量” 修改,改不完就加班
這個 “盡量” 可能妥協了很多事情
如果前面幾次靠著加班,都很幸運的在 deadline 前完工了
如果前面幾次靠著加班,都很幸運的在 deadline 前完工了
再遇到這種情況的時候,就會習慣性的把加班變成解決方法
而忘了去想該修正哪些流程
而忘了去想該修正哪些流程
又下班了?! 我還想工作
|
PM、RD、UX 都用自己的角度去理解、開發功能,在溝通不足、流程不透明的情況下,這個專案就會越來越像一個黑盒子
當老闆問這件事情什麼時候可以做完
是不會有人知道的
是不會有人知道的
高中物理就告訴我們,根據不確定性原理
專案的完成和沒完成,是同時存在的
只有打開黑盒子的一瞬間才會塌縮到一種可能性上
這種 “既完成又沒完成”
把人逼的 “既死又活” 的現象
把人逼的 “既死又活” 的現象
我們稱之為 —
薛丁格的專案
Schrödinger’s Cat 薛丁格的貓
|
本篇文章未完,你可以現在前往 Part2
也可以往下滑留下一些意見
The Year of Being Agile (Part 1)
我們再歸納一下,一般的組織常遇到三個現象:
1. 開冗長的週會
2. 同事間工作互換很困難
3. 加班
2. 同事間工作互換很困難
3. 加班
而我們 team 恰恰相反,有這三個很獨特的地方:
1. 不用開週會
2. 協作容易,你的同事就是你的分身
3. 不用加班
2. 協作容易,你的同事就是你的分身
3. 不用加班
這些特徵好像很難同時並存,你想,不開會怎麼知道同事在做什麼?
同事之間不了解,溝通成本就大
人越多就越複雜,花的時間也多
怎可能不用加班事情還能準時做完?
敏捷開發
Why OverCooked can teach you agile?
來講講 OverCooked 這個遊戲
一開始食物的訂單不多,每個人可以各司其職
專門切肉,專門煮菜,專門洗碗
而隨著訂單暴增和廚房地形改變
如果還是一個人只負責做一件事,時間肯定是不夠用的
一款具備敏捷精神的遊戲
|
Get back in sync
左上角可以看到現在的訂單有哪些
一回合的時間完成越多訂單得分越高
這是一款用嘴巴打仗的遊戲
你準備要拿料還是煮菜
需要什麼幫忙
哪邊需要修正
都要即時說出來
每個玩家都可以看到夥伴的狀況
這種快速 team sync 的方式就是 —
daily scrum meeting / standup meeting
Realtime review
這款遊戲一次最多四位玩家
如果有八個人玩呢?
一般情況就是多出來的四個會在旁邊
觀戰並且嘴砲給建議
等到下一回合再換這一組人拿搖桿
這種 driver & navigator 的方式 —
pair programming / mob programming
The programmer as navigator
|
介紹完遊戲,應該就不難回答我們一開始的問題
How to avoid boring meetings?
這個 team 不是不開會
而是不開沒有效率的會
daily meeting 有一些限制:
最多 15 分鐘,站著開
類似遊戲一回合的計時制
講求快速同步最新狀況
How to develop a cohesive team?
再來講協作:
關心不等於了解
想要了解一件事情
沒有什麼比實際參與還有效的
針對複雜的 task 安排 pair / mob
提倡 collective code ownership
簡單說就是共榮共享
但這樣講可能有點土
所以來看一下維基百科的定義:
合作或協作(英語:Collaboration)是指一種由兩個或兩個以上的個人或團體作為一個共同的目標而交集或在一起共同工作,舉例說明:一個知識分子可以透過合作的關係而去分享知識,再而經過學習和共識達到創作的目標
從上面描述看的出來
合作和凝聚力脫不了關係
那怎樣才能有好的凝聚力呢?
借用本公司的四大核心價值
誠信、 關懷、創新、當責
誠信、 關懷、創新、當責
肯定每個人的付出
讓每個人都看到自己的貢獻 —
一個人走得快一群人走得遠
Why collective intelligence matters?
電影環太平洋裡面
龐大的機器人無法由一人駕駛
需要兩位駕駛員連接彼此的腦神經
共享對方的情感、記憶、經歷
並且認同對方的處境之後
才能同步操控
Pacific Rim: Uprising (2018)
|
好的溝通和協作
就是用心和時間
灌溉的共同記憶
他會形成一個生命體
持續成長
或者說他就是一個生命
你們必須承擔起照顧這個生命的責任
也是這份承擔
讓你們之間的關聯
獨一無二
True collaboration yields better results
|
Iteration based method
Iteration 翻成中文『迭代』
在敏捷方法中是指
把一個大任務切成許多小週期可以完成的的任務
把一個大任務切成許多小週期可以完成的的任務
騰訊創始人馬化騰說:
小步快跑、快速迭代
小步快跑、快速迭代
這是他的產品思路
也成為互聯網創業法則
也成為互聯網創業法則
敏捷開發裡的迭代週期是一個 sprint
sprint 這字有短跑衝刺的意思
很符合迭代的概念
一個 sprint 一般為 2 ~ 4 週
sprint 這字有短跑衝刺的意思
很符合迭代的概念
一個 sprint 一般為 2 ~ 4 週
由團隊討論這個 sprint 能完成的功能
並在 sprint 結束時做個『試營運』
並在 sprint 結束時做個『試營運』
拿到客戶的 feedback
並且快速的衡量修正
並且快速的衡量修正
為了好懂,借個圖舉例
比較了傳統方法與敏捷開發法
如何在三個迭代週期完成蒙娜麗莎
如何在三個迭代週期完成蒙娜麗莎
Jeff Patton did an Iterative Mona Lisa.
|
先來看傳統的開發方法:
在第一階段可以看到蒙娜麗莎的頭
第二階段能看到蒙娜麗莎的左下半身
第三階段是完整的蒙娜麗莎
在第一階段可以看到蒙娜麗莎的頭
第二階段能看到蒙娜麗莎的左下半身
第三階段是完整的蒙娜麗莎
換成用敏捷開發會怎樣呢?
在第一階段是極簡主義派的蒙娜麗莎
第二階段是簡單上色派的蒙娜麗莎
第三階段是佛羅倫薩畫派的蒙娜麗莎
第二階段是簡單上色派的蒙娜麗莎
第三階段是佛羅倫薩畫派的蒙娜麗莎
不論哪個階段都是最小可行產品
很傳神地描述了敏捷的精神 —
很傳神地描述了敏捷的精神 —
接受反饋、迅速擴張
盡快給客戶有價值的產品
盡快給客戶有價值的產品
敏捷帶來的影響
How does agile affect my work and daily life?
所以回到一開始我賣的關子
那個讓我吃驚的行事曆
那個讓我吃驚的行事曆
我發現從以前到現在
晚上時間的利用有了很大的改變
從同事電腦辦公桌變家人散步吹吹風
以前晚下班總要負責關公司的門
有時候關得太順就會不小心鎖到別人
下圖的例外就是我補充的
關門 SOP
|
這一年有了固定的下班時間後
我開始能每天帶著媽媽去運動
我開始能每天帶著媽媽去運動
運動的地方是學校的操場
晚上學校警衛要關門的時候
看著警衛一邊關燈一邊趕人
看著警衛一邊關燈一邊趕人
這種熟悉又親切的感覺 —
等我退休了或許可以來當警衛吧 😝
前是把家當旅館現在是家事盡量管
The Year of Being Agile (Part 2)
平常 C++ 的開發工具是 Microsoft Visual Studio,然後現在的測試框架是使用 Google Test,以前都是一邊寫,一邊手動執行
test case 來驗證,沒有辦法跟 VS 內建的 Test Explorer 做整合,真的蠻原始的 =.=
最近因為 TDD 的關係,這樣子的開發環境真的太鳥了,所以認真研究了一下解決方案,發現了 Google Test Adapter 這個好東西:
https://github.com/csoltenborn/GoogleTestAdapter
很簡單,照著 github 上的說明安裝完後就可以使用。幾個要注意調整的地方:
public class E : VisualCommanderExt.IExtension
{
public void SetSite(EnvDTE80.DTE2 DTE_, Microsoft.VisualStudio.Shell.Package package)
{
DTE = DTE_;
events = DTE.Events;
documentEvents = events.DocumentEvents;
documentEvents.DocumentSaved += OnDocumentSaved;
}
public void Close()
{
documentEvents.DocumentSaved -= OnDocumentSaved;
}
private void OnDocumentSaved(EnvDTE.Document doc)
{
if(doc.Language == "C/C++")
{
DTE.ExecuteCommand("Build.BuildSolution");
DTE.ExecuteCommand("Test.RunAllTestsInSolution");
}
}
private EnvDTE80.DTE2 DTE;
private EnvDTE.Events events;
private EnvDTE.DocumentEvents documentEvents;
}
完工,現在只要存檔,就會自動 Build,並且在 Test Explorer 看到紅燈綠燈了
最近因為 TDD 的關係,這樣子的開發環境真的太鳥了,所以認真研究了一下解決方案,發現了 Google Test Adapter 這個好東西:
https://github.com/csoltenborn/GoogleTestAdapter
很簡單,照著 github 上的說明安裝完後就可以使用。幾個要注意調整的地方:
- 原本它會搜尋結尾為 test / tests 的執行檔來分析裡面的 test case,所以如果你的 Unit Test 程式不是 xxxTest.exe 的話,請記得到 [TOOLS] –> [Option] –> [Google Test Adapter] –> [General] ,裡面有一個 Regex for test discovery,我是使用 UT_[\w]*.exe
- 在同一頁中還有一個 Working directory & PATH extension,如果你的執行環境不是在預設的 output directory,也請記得修改。
- 修改 Unit Test Project 會自動跑 test case,但修改原本的 project 並不會被偵測到
- 希望可以每次存檔後,就自動 Build & Test ,這樣只要專注綠燈/紅燈就好了。
public class E : VisualCommanderExt.IExtension
{
public void SetSite(EnvDTE80.DTE2 DTE_, Microsoft.VisualStudio.Shell.Package package)
{
DTE = DTE_;
events = DTE.Events;
documentEvents = events.DocumentEvents;
documentEvents.DocumentSaved += OnDocumentSaved;
}
public void Close()
{
documentEvents.DocumentSaved -= OnDocumentSaved;
}
private void OnDocumentSaved(EnvDTE.Document doc)
{
if(doc.Language == "C/C++")
{
DTE.ExecuteCommand("Build.BuildSolution");
DTE.ExecuteCommand("Test.RunAllTestsInSolution");
}
}
private EnvDTE80.DTE2 DTE;
private EnvDTE.Events events;
private EnvDTE.DocumentEvents documentEvents;
}
完工,現在只要存檔,就會自動 Build,並且在 Test Explorer 看到紅燈綠燈了
Google Test Adapter
起因
Diro 分享了一個影片
以下簡單記錄我看完的心得
內容簡介
Speaker 是 Brian Lonsdorf (CTO of Loop/Recur, 多麼 geek 的公司名 XD)
LinkedIn: https://www.linkedin.com/in/drboolean
GitHub: https://github.com/DrBoolean
整個影片的重點在說明 Underscore 這個 library 雖然宣稱提供了一些有用的 functional programming helpers, 但是它的介面設計讓使用的人很難寫出真的 functional language 特性的 codes
Currying
以下他用幾個例子來說明 Underscore 的問題要看以下他舉的例子前, 先說明一個東西 - Currying
假設你有一個 function 叫 add
var add = function(x, y) {
return x + y;
}
假設你希望可以這樣寫var add2 = add(2); // return: function()
add2(10); // return: 12
add2(5); // return: 7
那你就要修改一下你的 add 定義var add = function(x) {
return function(y) {
return x + y;
}
}
var add2 = add(2); // return: function(y) {...}
add2(10); // return: 12
add2(5); // return 7
有更 generic 的作法, 請參考 http://blog.rx836.tw/blog/javascript-currying/作者也有提到一個幫你把你的 function 轉成 curried function 的 library (wu.js)
所以假設這樣寫會把你的 function 轉成 curried function\
var add = function(x, y) {
return x + y;
}.autoCurry();
舉幾個實際點的例子 (畢竟 add 有點沒感覺)var modulo = function(divisor, dividend) {
return dividend % divisor;
}.autoCurry();
modulo(3, 9); // return: 0
var isOdd = module(2);
isOdd(6); // return: 0
isOdd(7); // return: 1
var filter = function(f, xs) {
return xs.filter(f);
}.autoCurry();
filter(isOdd, [1, 2, 3, 4, 5]); // return: [1, 3, 5]
var getTheOdds = filter(isOdd);
getTheOdds([1, 2, 3, 4, 5]); // return: [1, 3, 5]
講者提的範例
- 範例一
var firstTwoLetters = function(words) {
return _.map(words, function(word) {
return _.first(word, 2);
});
};
firstTwoLetters(['jim', 'kate']); // return: ['ji', 'ka']
因為 Underscore 的參數順序是先 array 再 function所以沒辦法寫得更好
他認為如果 Underscore 能把參數順序顛倒且 API 是 curried functions
就可以寫成這樣
// _.first(n, array) 參數顛倒
var firstTwoLetters = function(words) {
return _.map(words, _.first(2));
};
// 更進一步, _.map(f, array) 參數顛倒
var firstTwo = _.map(_.first(2));
filterTwo(['jim', 'kate']);
- 範例二
var sortedPhones = function(users) {
return _.chain(users)
.sortBy(function(user) { return user.signup_date; })
.map(function(user) { return user.phone; })
.value();
};
sortedPhones(users);
他將其改寫為var sortedPhones = _.compose(
_.map(function(user){ return user.phone; }),
_.sortBy(function(useR){ return user.signup_date}));
sortedPhones(users);
更進一步var dot = function(prop, obj) {
return obj[prop];
}.autoCurry();
var sortedPhones = _.compose(_.map(dot('phone')),
_.sortBy(dot('signup_date')));
sortedPhones(users);
結論
我個人認為, 看完這個影片比看 Functional Javascript 這本書還更能感受 functional language 的特性 XD不過我也還不是很懂, 大家就一起學習吧
Hey Underscore, You're Doing It Wrong! 觀後感
https://blog.qt.io/blog/2013/04/15/evolution-of-the-qml-engine-part-1/
這一系列 QML engine 的文章(其實目前也只有一篇....)深入探討了 QML engine 的內部運作機制。Lars Knoll 指出了目前 QML engine 比較大的問題包括:
- Several object models
- 也就是一個 QML item 必需有3個object models,分別存在於 V8 engine, QML engine, Native Qt (以QObject 形式存在),不但耗費很多記憶體,要 sync 三個物件的值也是一大工程
- Bindings done through JS closures
- 每個 property binding 都是一個 JS closure,因此要先重新把 binding expression 重組成 JS closure,然後 V8 再 parsing 一次這個 closure。很慢,因此提出了一個輕量、快速的 V4 engine 來幫忙處理簡單的 expression(這也是為什麼官方的 performance tuning 文件叫你要盡量簡單化 binding expression)。
- Data type conversions
- Qt 跟 V8 之間的 data type 轉換很花時間,此外如果是 string 的話,memory copy 的成本也不低。當然,有些改善方法,但那就是拿記憶體或程式複雜度換來的了
- QML scoping rules
- QML element 實際上是樹狀結構,跟傳統的 JS engine 其實不大一樣,QML engine 用了很複雜的方法來處理這段(through nesting slow and deprecated with() statements)。
- Throwing away QML type info
- type info 會被丟棄..
- iOS and WinRT support
- V8要求系統記憶體可以被設定成 executable,但這在iOS及WinRT是做不到的...所以只能另外做一套 JS engine 了..
- Working with upstream V8
- Qt 裡面的 V8 跟標準版的 V8 程式碼已經差很多了..而且好像很難 merge 了...
Qt/QML Performance Tuning #1
在 Scrum Team 中很強調 透明(transparency),這次跟大家分享的是 Developer Resource 透明化帶來的好處。
傳統團隊
在傳統的開發方法上,常常都是以”月”、”季”為時間單位,因此常會聽到 PM 說:可不可以再加這個 X 功能、這個 Y 功能,還有那個 Z 也順便加進來好了,這一季完成這些,應該沒問題吧?
身為 developer 能拒絕他嗎?有點難,因為感覺一季的時間應該可以做蠻多事情的,就算你覺得不可以,PM也很容易這樣感覺:一季耶,三個月你竟然沒辦法幫我多做 X+Y+Z?
在 Developer Resource 不透明的的狀況下,還真的很難回答。而不透明的原因,其實是因為週期太長、功能多且不明確,倒不是因為什麼溝通或互信的關係。
敏捷團隊
但在敏捷團隊中,我們在每個 Sprint Planning Meeting 時,便會先算出這個 sprint 有多少資源可以用:6(hours) * 6(developer) * 8(days) = 288 points
接著在 Story 介紹、Story Point 估算、決定 priority 等等活動之後,便開始討論 PO 這次最後決定的 stories:A,B,C,D,E,F,把這 6 個 stories 再細拆分成各個 tasks,例如 Story A 可能會被拆分成下列 tasks:
- QML:這個功能有 UI 介面,需要實作 QML
- C++:實際功能由 C++ 完成
- Unit Test:要對 C++ 實作單元測試
- UAT:要完成自動化驗收測試
- Code Review:這次的 C++ 還蠻難的,雖然有 Unit Test,但還是要進行 Code Review
然後,接下來團隊成員根據每一項 tasks 開始打牌,決定每一項的 tasks 所需的時間。這裡是關鍵,因為要由這過程去弄清楚 PO 真正想要的東西,也讓 PO 了解這個 task 為什麼需要這些時間來開發。
以 C++ 這項 task 來看,如果一開始大家出的點數差異有點大,例如:3,3,3,5,20,3,5,便要請出3及20的成員們分別說明一下他認為是3及20的原因。
張飛(RD):我覺得這個 雙向語音 很簡單,用原本單向語音模組來改就可以了!
關羽(RD):可是他不是全雙工嗎?這樣我們要改很多地方才能做全雙工..
劉備(PO):耶,不用考慮全雙工,我們這個應用基本上只要雙工就可以了!
關羽(RD):原來如此,是我想太多了..
在一番討論後,大家才能真正清楚 PO 想要的東西,而不會多做或少做,或著方向不對。此時再出一次牌,通常就能取得共識了。
在經過撲克牌大賽後,可以得到每個 tasks 需要的時間:QML 5, C++ 20, Unit Test 20, UAT 20, Code Review 8,合計73小時。
剩餘的 Story B ~ Story F 也是照一樣的方式去得到需要的時間,可能是 B:80, C:100, D:64, E:38, F:48,這樣 PO 就很清楚,在這個 sprint 有限的資源內(288),可以去完成那幾個 stories(D應該做不完,E跟F就不要想了...除非運氣很好。
在 PO 很清楚知道資源有限的狀況下,他就必需去做抉擇。如果現在只有 1 個 sprint 的時間,你沒辦法 A~F 全做,你要考慮清楚最重要、對客戶最有價值的到底是那幾個!全做當然最好,但資源攤在你眼前,很清楚的告訴你:不可能!
所以我覺得透明性的帶來的好處,除了明確了解需求外,也可以讓 PO 理解資源有限,他必需非常認真的選擇他要的功能!
註1:我們認為每位 developer 一天當中完全專注在工作上的時間是六小時左右,扣掉上廁所、休息、上網,工作轉換的overhead等)。
註2:一個 sprint 為二週,扣掉第一天的 Planning Meeting 及最後一天的 Review & Retrospective meeting,剩下8天。
註3:估算的過程重點在”溝通”,實際上的時間可能還是會不一樣。
敏捷當中的 透明性 帶來什麼好處?
「不要試圖覆蓋所有使用案例。Spec.不是用來替代組合回歸測試的。」 - Spec by Example 中文版 p.151
但我們明明覺得現在用 Robot Framework 實現自動化 UAT 來替代組合回歸測試是很正確的做法,為什麼作者覺得這樣不對?
我認為是因為作者在討論的是 Spec by Example ,或著說想用 ATDD 的方式來開發軟體,而一旦變成回歸測試,就失去了最初的目標了。
但是..
我認為是因為作者在討論的是 Spec by Example ,或著說想用 ATDD 的方式來開發軟體,而一旦變成回歸測試,就失去了最初的目標了。
但是..
耶,這是怎樣,其實要拿來做 regression tests 也是可以的哦!?
我覺得其實是可以的,而且我們現在也在這樣做,但是用 UAT 做回歸測試有一些副作用,例如需要很久的時間,因此解決這些副作用才是最重要的!現在雲端主機要租用非常方便,我認為可以用平行處理,把 UAT 分散在很多台機器去跑,這樣就可以讓測試時間縮短。
應不應該用 UAT 做回歸測試?
在做自動化 UAT 時,最常做的事便是拿 HANDLE,這在以前傳統的 GUI Framework 只要用 Spy++ 或其它開發工具都很容易做到。
但現在 QML 不一樣了,QML 裡頭已經沒有所謂的 window handle,比較接近的是 objectName,你可以用 objectName 來對該元件進行操作(get property, call method…),問題是要怎麼樣拿到 objectName 呢?最直覺的作法就是直接看 QML source code,有錢一點的可能是用 Squish 之類的工具去做。但是直接看 QML source code 其實是比較花時間的,而 Squish 則是價格比較高,因此我實作了一個 JavaScript Library 可以讓你在 UI 上顯示每個 QML component 的 objectName。
例如你的 GUI Application 原本看起來是這樣:
在呼叫了 LabelQML.addObjectNameFlag(this); 之後,它便會在所有有 objectName 的元件上顯示一個小紅點:
當你滑鼠移至小紅點上方時,便會在畫面中央顯示它的 objectName:
完整程式碼及範例:
https://github.com/diro/LabelQMLComponent
其實作法很簡單,基本上就是 recursive 去列舉所有的 component,並檢查是否有 objectName,若有則建立一個 dynamic QML component(小紅點),當滑鼠移至小紅點時,便會在預先建立的 displayBoard 元件中顯示 objectName。
但現在 QML 不一樣了,QML 裡頭已經沒有所謂的 window handle,比較接近的是 objectName,你可以用 objectName 來對該元件進行操作(get property, call method…),問題是要怎麼樣拿到 objectName 呢?最直覺的作法就是直接看 QML source code,有錢一點的可能是用 Squish 之類的工具去做。但是直接看 QML source code 其實是比較花時間的,而 Squish 則是價格比較高,因此我實作了一個 JavaScript Library 可以讓你在 UI 上顯示每個 QML component 的 objectName。
例如你的 GUI Application 原本看起來是這樣:
在呼叫了 LabelQML.addObjectNameFlag(this); 之後,它便會在所有有 objectName 的元件上顯示一個小紅點:
當你滑鼠移至小紅點上方時,便會在畫面中央顯示它的 objectName:
完整程式碼及範例:
https://github.com/diro/LabelQMLComponent
其實作法很簡單,基本上就是 recursive 去列舉所有的 component,並檢查是否有 objectName,若有則建立一個 dynamic QML component(小紅點),當滑鼠移至小紅點時,便會在預先建立的 displayBoard 元件中顯示 objectName。
How to get and display QML objectName in application
一直都是使用 Robot Framework 來進行自動化的 UAT,但因為完整的 UAT 還包括了手動測試的部份,因此在 UAT 中 Test Case 的管理上就比較麻煩一點,可能完整的 test case 是存在 excel 裡頭,再由開發人員手動填入 Robot Framework 的測試結果。
這樣真的太低級了,這不但浪費時間、易出錯,而且很難管理啊,所以必需有一個更好的管理方式。
為了解決這個問題,我們導入了 TestRail。TestRail 是一套相當好用的 Test Case / Test Plan 管理系統,而且 API 相當完整,因此拿來跟 Robot Framework 整合真是再適合不過了。使用的流程為:
- 在 TestRail 中建立Test Case
- 如果該 Test Case 有對應的 Robot Framework 自動化測試 test case,記得在 test case 中的 Tags 欄位加上 CID:n,這個 n 便是在 TestRail 中的 CID(Case ID)
- 當產品要進行完整測試時,便建立一個 Test Run,然後由 CI Server 呼叫 Robot Framework 開始進行自動化測試,接著再自動把測試結果更新到 TestRail 中對應的 Test Run Result 中(passed/failed)。接著團隊便可以知道還有那些 Test Case 未經測試(untested),只要針對這些 test case 去補足手動測試即可。
How to Integrate Robot Framework with TestRail

http://www.taaze.tw/sing.html?pid=11100723738
本書獲得《Dr. Dobb’s Journal》肯定,榮獲第21屆Jolt獎。
打造優良的軟體開發團隊,除了要大家有敏捷的觀念外,基礎建設也是非常重要的。前一陣子唸完了 Continuous Delivery 之後, 便開始計畫要把不足的部份補完(如果不知道 Continuous Delivery 有什麼好處,可以先上網查查,或著看看書 XD),在幾位同學的努力之下,花了一個多月把缺的部份補上,包含架設CI Server、夠好用的build script、Robot Framework for QML、Unit Test integration。
- CI
- 我們選定的 CI 是用 TeamCity,其實 Jenkins 也不錯,但是比較起來商業化的軟體在易用性及介面上,還是略勝一籌,加上之前也都是在用 TeamCity,我覺得用 TeamCity 可以更快上手。
- Unit Test
- Unit Test 使用 Google Test,為何捨棄原本的 Boost Testing Framework?一個原因是它在 mock/stub 這一部份還不及 Google Test 完整,因此我們選用了 Google Test。
- UAT (User-Accaptance-Testing)
- UAT 的部份則是使用 Robot Framework,沒有特別原因,只是因為有其它同事正在用,就一起用吧 XD
- Qt/QML Testing Framework,
- GUI Application 的測試,有一點很重要的是要以 HANDLE、object ID 來操作,而避免使用”圖形”的方式來操作,因為你的 GUI 有非常大的機率會調整,只要一改,你的 test case 就要全部重來了。但是 QML Application 裡頭是沒有所謂的 window handle 的,跟傳統的 Windows Application 不大一樣,而目前市面上只有 Squish 有辦法去對 QML Application 做到使用 object ID 來操作。(Ubuntu 的 testing tool 似乎也有,但沒有深入研究)
- 但我們並沒有使用 Squish,而是讓我們的程式原本就具備這樣的”可測性”,也就是說我們自己做了一個 python library,可以讓開發人員透過這個 python library 來操作我們的 QML Application。而 Robot Framework 就是透過這個 python library 來完成 keyword-driven testing。不知道什麼是 keyword-driven testing 的話,可以參考 http://kb.froglogic.com/display/KB/Article+-+Keyword-driven+testing+with+Squish+and+Robot+Framework
就這樣,透過上面四個元件的合作,我們完成了 Continuous Delivery 的目標。這個系統會自動完成下面的工作:
- 有團隊成員 commit code 之後,開始進行 build,然後執行全部的 unit test
- 通過 unit test 之後,自動打包成準備釋出的 installer.exe
- 接下來自動依序發佈到不同平台的 VM 上去進行測試。
- 系統呼叫 sikuli,自動在各個 VM 裡頭執行 installer.exe,並完成軟體的安裝。
- 安裝完畢,會呼叫 Robot Framework 開始進行 UAT (User-Acceptance-Testing)。
- 完畢,產生報告。
有了 Continuous Delivery,現在 RD/UX/PO 都隨時可以下載最新版的”已測試過"軟體來使用,再也不用等人手動去 build 出來了。
Continuous Delivery
很多書都在強調 code review 的重要性。在我們的軟體部門剛開始成立,並沒有 code review。這個狀況持續了幾年,一直到前年,我才開始認真思考,不做 code review 真的對嗎? 於是我們從那個時候開始逐步推動 code review。
在沒有 code review 的時代,有兩件事情讓我印象深刻。第一件事情已經七、八年了,有一個週末,我們因為程式在下一週初要 release,想當然爾,工作是做不完地。所以我決定週末來加班。那時候我自詡應該當規劃者,所以平常是不寫程式碼地。可是真的來不及了,所以我得跳下來自己幹。沒想到那個週末,當我打開 project 的程式碼一看,我快嚇傻了。一個函式長達一千多行,眼睛東瞄西瞄看到的是許許多多重複的程式碼。這個該怎麼改呢? 到最後就是能頭痛醫頭腳痛醫腳,可想而知,那一個週末沒有什麼好下場了。第二件事發生在三年前左右,我們一個產品內的某一個 service,在屢次的進測,總是領到最多的紅牌。最後在產品被追殺的階段,我們發現那一個 service 無法收斂了。這時候我們開始投人進去那一個 service 進行 code review。不看不會怕,看了之後才知道多可怕。那一個 service 竟然高達一萬四千多行。可怕的是,這一萬四千多行在同一個檔案。是地! 一個 service 就一個檔案。最厲害的地方是,這個 service 破了之前的一千多行記錄,有一個函式高達三千多行。最後那一個 service 只能靠硬測的方式讓他自然地收斂。然後在後面安排 refactor。Refactor 後剩下八九千行,當然也拆了不少的檔案。可以看出來原來的作品中重複的程式碼有多嚴重。我們稱這種大函式叫:"吾碼一以貫之",這種一個函式殺到底的寫作方式,對於寫的人來說當然過癮,不用想太多次該怎麼擺變數,想一次就可以用 "很久"。但是對於看的人來說,實在是痛苦至極,看到第三頁的時候,已經不記得這個變數什麼時後宣告、什麼時候改值。諷刺的是一個函式不能太長這件事,在公司的 coding rule 當中是有提到地,為什麼還會看到這樣的事情出現?問題就在於事情沒有人去看,就容易歪掉。
這種事情就是這樣,開始總是最難地。前年開始推 code review 的時候,第一個遇到的難題就是人力的問題。這其實是一個弔詭的問題,本來寫 code 寫得很用力的團隊,突然要 "插" 一件 "本該做的事",卻變成當時團隊多出來的事情。於是我只能動用手上所有的資源幫忙 review。然後因為遇到大家的時間不容易湊在一起,所以也請成員架上 Reviewboard 來協助分散式 code review。然後先是一個團隊持續地看到 review 活動,然後是另一個團隊的重要修改也開始 review。最讓人驚艷的是去今年初,已經有一個團隊將 Pre-commit review 自動化架好了 (用 Git + Gerrit + Jenkins)。只要 commit code,系統會自動驗證可否 build,然後會寄出 review request 給設定好的人。所以現在信箱一天會收到將近十封的 review request (說實在,加上 build 成功的訊息,一天信箱會被灌入 3, 40 封信,很快就會爆了 XD)。這樣的自動化,最有趣的地方在於讓你的 code 沒有被 review 過就不能夠 commit。因此現在對那個團隊而言,已經不是 optional, 而是 must。所以 review 活動就會被持續執行。而現在也會發現,因為 code review 的工作被拉出來,組長的工作裡頭自然會加入此項 task,所以比較不會像剛開始一樣覺得根本排不進去。
那麼到底這樣強推 review 有沒有什麼效果呢? 說句老實話,如果你期待 code review 可以大量減少 bug 數量,是比較不切實際地。但是他對於防範 non-trivial 的 bug,尤其是架構性問題、易維護問題、後續加新功能的 side effect 的防範,是有比較大的效果。另一個重點是,因為強制 code review,你可以在無形中強迫 member 去看到其他人被要求了哪些東西,下次他自己寫的時候就不容易重複同樣的錯誤。另外,自己寫的東西有人在看的時候就會比較小心一點,這些都是無形的效果。
現在開始有團隊在導入 scrum 了,在該團隊中更增加了 peer 的 code review。不再單純是上對下的 review。我覺得這樣做又有更進一步的效果,那就是懂 code 的人更多了,對於未來 task 的相互支援,也會變得比較有可能性了。這是我們在 code review 開始前享受不到地。
所以 code review 重要嗎? 非常重要。你們開始做了嗎?
Code review 真的重要嗎?
Unit test (單元測試) 在我剛開始加入團隊的時候,有寫過一陣子。那時候使用的是內部人員自己開發的 framework。當時寫 unit test 的方法可以說相當原始,我們沒有特別去研究 unit test 可以怎麼作,stub 或 mock 更是沒有研究過的議題。大家就只是很努力地在每個模組完成後,補完大部分可能的 test cases。但是久而久之,隨著專案越來越大,架構越來越複雜後,寫 unit test 變成一個令人沮喪的活動,至少當時對我而言是這樣。原因是甚麼?
多年後,我在團隊中又剛好擔任到導入 unit test 的第一線角色。現在因為開發經驗比較豐富,對於測試應該怎麼寫,或是寫 unit test 的人應該專注於哪一部分有了一點點的感覺。在我還是個懵懵懂懂的 RD 時,覺得每次寫一個新模組的 unit test,前置大概要花好幾個小時,在寫一些該模組與其他元件「假」互動的部分,例如與資料庫溝通的模組,透過網路傳送訊息的類別,跟串流伺服器傳輸影音的函式庫。因此,寫久了會問自己,到底是在寫單元測試還是在寫假類別的程式。
![]() |
| [圖片來源: www.wowwiki.com] |
從目前的狀況看來,我們只是「有」了 unit test,但是我們還沒有思考覆蓋率,或是維護的問題,持續改善這一部分將是我們要繼續努力的地方。好消息是,我們的 CI 已經整合了 unit test 的部分,這多多少少也會鼓勵大家寫 unit test。只有當 RD 覺得 unit test 很好用時,他們才會願意花時間且認真的撰寫 unit test。我本人也不例外 XD
Unit test 的美麗與哀愁
這次在 hackathon.tw 中為 QML 做了初步的介紹,選定這個主題,主要是容易上手,而且初學者很容易做出一個很有感的成品,例如用 MediaPlayer、Camera 搭配現成的ShaderEffect,很容易做出很炫麗的作品。希望藉由這個介紹,讓許多新手也可以試試看 QML 這個比較年輕的架構:)
QML - Introduction of QML Multi-Media

https://github.com/lemirep/QtLeapMotionLibrary
LeapMotion 算是這幾年在 User Interaction 領域上頗受注目的產品之一,提供的 SDK 也相當完整,而現在 github 上有個 QtLeapMotionLibrary 更是完整的把它的 SDK 包裝成 QML 元件供大家使用,讓 Qt 開發者在使用 LeapMotion 時又更加輕鬆寫意了 XDD
請先照這三步安裝 SDK 及編譯 library:
- 安裝 SDK
- 將安裝完畢後的 SDK 中的 Leap 資料夾複製一份到 QtLeapMotionLibrary/QtLeapMotion/Leap 中,照這目錄結構放可以省掉不少調整 project file 的功夫。
- 不過原本的版本並不支援 Mac (project file 沒有處理mac build),因此請在各 example 中的 .pro 檔中加入粗體部份:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
linux-g++: {
!contains(QMAKE_HOST.arch, x86_64) {
LIBS += -L../../QtLeapMotion/Leap/x86/ -lLeap
LIBS += -L$$OUT_PWD/../../QtLeapMotion/ -lQtLeapMotion
} else {
message("x86_64 build")
LIBS += -L../../QtLeapMotion/Leap/x64/ -lLeap
LIBS += -L$$OUT_PWD/../../QtLeapMotion/ -lQtLeapMotion
}
}
macx{ LIBS += -L$$OUT_PWD/../../QtLeapMotion/ -lQtLeapMotion LIBS += -L$$PWD/../../QtLeapMotion/Leap/lib -lLeap }
|
完成!現在可以開啟 example 來玩看看了 :)
QtLeapMotionLibrary
Qt 原本就已經提供了 QTest 可以用來開發單元測試,但整體感覺還是沒有 google test 完整,因此實務上我們還是使用 google test 來進行單元測試。
Google test 搭配 Qt 一般來說有二點要處理:
1. Qt Signal
2. Event Loop
Qt Signal
因為你的 slots 沒辦法用 MOCK_METHOD 製造出來,或著說QObject::connect 無法連結到 MOCK_METHOD 做出來的偽slot ([苦主](https://groups.google.com/forum/#!topic/googlemock/RTgynKPa6ew))
因此,最好的方式仍舊是搭配 QTestLib 裡頭的 QSignalSpy 使用([前輩已經說過了](http://stackoverflow.com/questions/22390208/google-test-mock-with-qt-signals))。
QSignalSpy 可以幫助你檢查某個 signal 被 emitted 的次數,以及它每次 emit 所帶回的參數是什麼,算是蠻方便的:
QCheckBox *box = new QCheckBox(0);
QSignalSpy spy(box, SIGNAL(clicked(bool)));
box->animateClick();
EXPECT_EQ(1, spy.count())
QList<QVariant> arguments = spy.takeFirst();
EXPECT_TRUE(arguments.at(0).toBool());
不過,在用QSignalSpy的時侯要想一下,你有 event loop 嗎?不是每個unit test都像範例這麼單純的,如果你串起來不會動,就想想 event loop 吧 :)
Event Loop
因為許多功能都必需有 Qt 的 event loop 才能運作,因此勢必要呼叫 QCoreApplication::exec() 才能跑。那麼這個原本都是放在main()裡頭的 QCoreApplication::exec() 現在要放在那裡呢?當然就是放在每個 test case 之中了。
這邊的範例是我會呼叫 trackEvent() 功能,並等待 signal TrackEventFinished 回來時再離開 event loop:
TEST_F(PluginMixPanelFixture, Send_1_Event_With_Extra_Properties)
{
**int argc = 0;
QCoreApplication app(argc, NULL);**
auto conn = std::make_shared<QMetaObject::Connection>();
*conn = QObject::connect(pTestingPlugin, &PluginMixPanelClient::TrackEventFinished, [ = ]()
{
QCoreApplication::quit();
});
QString event = "[test] login function with extra properties";
static QVariantMap property;
property.insert("prop_size", 1000);
property.insert("prop_alias", "diro");
pTestingPlugin->TrackEvent(event, property);
**app.exec();**
QObject::disconnect(*conn);
}
當 Qt 碰上 Google Test

在 debug 時最常用到的就是 symbol file / symbol server,然而相信常有 load 不到 symbol file 的經驗,不管怎麼按,怎麼換,不對就是不對,有時覺得明明就放對了,還是不能用 Orz。如果你用的是 VS.NET,應該永遠都只能繼續怨天尤人、哭天搶地了,但如果你用的是 windbg 的話,那麼請照著下面步驟來解決這個問題。
例如我在追查memory leak的時候,去分析一個 address 的 call stack
!heap -p -a 0x12344545結果得到的 callstack 非常詭異,一看就覺得不可能,或著它很明顯的告訴你「Following frames may be wrong」,那們八成是 symbol 不正確了
首先要先判斷為什麼 load 不到,先用 sym 打開 symbol 的詳細訊息
!sym noisy此時重新執剛才的指令 !heap -p -a 0x12344545, 此時它便會告訴你它有那些 symbol file 是有問題的
- checksum 不正確
- 如果你確定 symbol (.pdb) 是對的, 或著你是重新用一模一樣的 compiler setting 所編出來的 pdb,那麼你可以試看看強制忽略 checksum
- .reload /f /i [xxxxx.exe]
- 根本找不到 .pdb
- 請注意看 message,它會告訴你它在那些目錄下搜尋過 pdb,看一下你的 pdb 是不是真的放對位置了吧! (如果你連 symbol path都不會設,趕快去翻一下手冊吧)
最後別忘了用 !sym quiet 把 message 關閉,才不會看到一堆沒有用的訊息。
丟掉VS.NET,用 WinDbg 才是王道 Part 1
上次介紹的 TeamCity 預設是不支援 Boost Unit Testing Framework 的,需要額外的 plugin 才能將Boost UTF 的測試報告整合進 TeamCity 中。
首先先到 TeamCity 官方網站下載 plugin,檔案在 http://confluence.jetbrains.net/display/TW/Cpp+Unit+Test+Reporting 裡面有一個 teamcity-boost-1.2.zip for Boost.Test library,下載回來之後解壓縮可以得到三個檔案
teamcity_boost.cpp teamcity_messages.cpp teamcity_messages.h
只要將這三個檔案加入你原有的 testing project 中即可,不需更改任何設定。
咦,這麼神奇,為什麼這三個檔可以達到這種效果,答案是它是用了 UTF 中的Global fixture,如果你不知道什麼是 fixture,可以先去查詢一下軟體測試的相關文章。這個 Global fixture 會去處理每個Test Case的測試結果,並加以回報給 TeamCity,整體的整合度還蠻高的,可以看到有那些 Test Case,有那些 PASS 以及那些 FAILED,當然FAILED 的訊息也會完整的呈現。
如何整合 TeamCity 與 Boost Unit Testing Framework
這本是前一陣子發現的好書,本書點出了許多程式員的迷思及觀念:
- 你可能剛學會了一堆Patterns,便迫不及待的在任何專案中開始導入Patterns,但這真的是你要的嗎?你要的是更好的設計,而不是更多的範式」,一味的追式範式,只是造成過度設計。
- 範式只是一個結果,而重構是到達之路,了解這個過程才能讓你真的領悟範式的意涵,進而想出更好的設計。
- 這本書將帶領你去了解每個範式背後的動機,也讓你了解何時該To(成為), Toward(近接), Away(遠離) 範式
這本書有許多內容是我一直想要跟團隊成員表達的,但是自己一直沒有機會(也沒有能力)把這些東西整理的這麼完整,我想它對整個團隊的意義非凡,將能幫助團隊更往上一層。
重構 - 向範式前進
VIVOTEK Digest
這裡是 VIVOTEK 晶睿通訊 的技術部落格,我們在這裡分享與敏捷、Qt/QML及其它軟體相關技術
Trending
-
「不要試圖覆蓋所有使用案例。Spec.不是用來替代組合回歸測試的。」 - Spec by Example 中文版 p.151 但我們明明覺得現在用 Robot Framework 實現自動化 UAT 來替代組合回歸測試是很正確的做法,為什麼作者覺得這樣不對? 我認為...
-
平常 C++ 的開發工具是 Microsoft Visual Studio,然後現在的測試框架是使用 Google Test,以前都是一邊寫,一邊手動執行 test case 來驗證,沒有辦法跟 VS 內建的 Test Explorer 做整合,真的蠻原始的 =.= 最近因為...
-
針對常用的match role, 可利用QHash (or QMap) 多存一份, 來加快 search 效率 我們在設計model通常都會定義一個存unique string value的IdRole當作model item的索引, 且會很頻繁對這個role作ma...
Categories
Programming
19
Culture
11
Testing
6
scrum
6
Agile
5
performance tuning
4
Book
3
Code Quality
2
Qt
2
Event Storming
1
database
1
file system
1
virtual machine
1










