[閱讀筆記] 大話設計模式 --- 橋接模式(Bridge Pattern)

前情提要

因緣繼會下參加了一個線上讀書會,
讀的是一本老書,「大話設計模式」
簡體書應該是 2007 年出版,但是國內有繁體書,
我是在 2010 年左右入手這本書的,號稱簡單易懂,
當時翻了幾遍,但是對 DP 與 OOP 並沒有很深刻的認知。

如果對 OOP 沒有什概念的人,這本書的附錄也有簡單的介紹。
算是適合當作入門的書。

不過在實務上,很少用 Design Pattern 在解決問題,
更多是前人怎麼作,我就怎麼作。
一直到我學習了 TDD 與重構,我才漸漸了解 Design Pattern 是怎麼一回事,
書中小菜有幸遇到大鳥而我沒有,只能更多努力了。

本書小得

這篇 blog 主要是寫書中的第 22 章–橋接模式,
但是我想提一下心得,也許之後會寫別的章節,也許不會,
但心得就先收錄在這裡了。

首先是這本書面向的讀者應該是小菜,
同時是書中的主角,對物件導向與設計模式不熟悉的人,
所以書中用了很多現實生活中的例子來舉例,
當然我們可以將現實投影到程式當中,但我實際上的感受還是有差異的。
比起生活實例,我現在可能更希望是代碼實例吧。

第二點,書中的背景是 2007 年,所以時空背景已經不太相同了,
以本章(22章,橋接模式)為例,書中提到的手機遊戲跨平台問題,
在 2020 年已經不存在了,現在的智慧型手機與書中的「掌上電腦」功能描述差不多了。

第三點,網路上有簡體版的書在流通,但是用語與正體中文有所差異。

想讀這本書的人參考一下上述幾點,
或許結合一些其它的書籍或是網路資源,
對你來說可以對設計模式有更好的理解。

橋接模式

我猜想命名的原因是來自完成後的類別圖看起來的樣子,
在本書中舉了手機品牌(Brand)與手機軟體(Soft)的關係作為例子,
你可以以 Brand 作為分類,也可以用 Soft 作為分類,
但最後都會長出三層繼承的類別圖,
然後其中都會包含奇怪的職責; Soft 包含 Brand 的資訊(或是 Brand 包含 Soft 的資訊)。

而真正的問題是,難以擴充,每當我們需要一個新的 Soft 或是 Brand
我就需要為所有的 BrandSoft 新增一整組的類別。
而這問題背後的原因就是過度繼承。

書中的例子,有一處我覺得不佳的地方,
在使用橋接模式前,主邏輯如下

1
Console.WriteLine("Run N Brand Game");

使用橋接模式後的代碼如下,

1
Console.WriteLine("Run Game");

作者可能想強調其橋接的觀念,而不討論橋接的兩端實際有資訊互通的需求,
但是我認為這是不正確的,我們不應該套用了某種 Design Pattern 而改變其原始行為。

接下來我會以下面這張圖,Demo 一下怎麼重構到橋接模式

手機品牌

首先,先寫測試,由於我視作這個結構是遺留代碼的產物,
所以我不認為他會有完整的單元測試,當然如果有的話就要考慮這些測試是否仍然適用。

1
2
3
4
5
6
7
8
var handsetNokiaAddressBook = new HandsetNokiaAddressBook();
handsetNokiaAddressBook.Run();
var handsetNokiaGame = new HandsetNokiaGame();
handsetNokiaGame.Run();
var handsetMotorolaAddressBook = new HandsetMotorolaAddressBook();
handsetMotorolaAddressBook.Run();
var handsetMotorolaGame = new HandsetMotorolaGame();
handsetMotorolaGame.Run();

我寫了一些 End To End 測試列舉出所有的情境。

延申問題

  1. 目前只是 2 x 2,所以要寫 E2E 測試似乎不難,如果是 3 x 3 或更多呢 ? 你會怎麼作 ?
  2. 這裡隱含著一件事,當你看到你的繼承鏈與商業邏輯的交互,
    已經開始出現 2 x 2 的現象時,是一個明示你應該重構它了。

下一步,我可以很明顯的發現中間層的類別,其實一點意義也沒有

1
2
public override void Run()
{}

所以我們要將繼承鏈中最葉端(leaf node)的類別繼承關係移除,

1
public class HandsetNBrandGame : HandsetNBrand

改成

1
public class HandsetNBrandGame

當然我不認為實務上有這麼簡單能移除一個繼承關係,
所以要達到這一步之前,我們也許要先創造無意義的中間層。
因為這步是對葉端類別的處理,所以我喜歡稱它為「修剪枝葉節點」。

下一步,當我把所有葉端的類別剪除後,我會先作分類,
實務上我會更傾向在腦海中作好分類再剪除,然後一個分類一個分類重構。
比如說 Game 類別:

1
2
3
4
5
6
7
8

public class Game
{
public void Run(string brand)
{
Console.WriteLine($"Run {brand} Game");
}
}

可以看到我已經將 brand 抽出來作為方法變數,
作為過渡時期多載(overloading)或許是個手段
此外,可以看到我透過參數傳遞來解除相依,
這個手段甚至可以套用在 delegate 或是複雜型別。

同樣的步驟再作一次,

1
2
3
4
5
6
7
8

public class AddressBook
{
public void Run(string brand)
{
Console.WriteLine($"Run {brand} Address Book");
}
}

我們可以明顯發現重複的項目可以抽出介面,
實務不需要特別介意是 interfaceabstract class
依最小改動為原則,選擇適當的手段進行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public interface Application
{
void Run(string brand);
}

public class AddressBook : Application
{
public override void Run(string brand)
{
Console.WriteLine($"Run {brand} Address Book");
}
}

public class Game : Application
{
public override void Run(string brand)
{
Console.WriteLine($"Run {brand} Game");
}
}

這時候我們必須將 brand 傳入,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public class HandsetNBrand
{
protected HandsetSoft Soft;
protected string Brand;
public HandsetNBrand(HandsetSoft soft)
{
Soft = soft;
Brand = "NBrand";
}

public void Run()
{
this.Soft.Run(Brand);
}
}

而 Client 端就可以組合起來使用。

1
2
var game = new HandsetNokia(new HandsetGame());
game.Run();

同樣的手法作在 HandsetMBrand 之中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

public class HandsetMBrand
{
protected HandsetSoft Soft;
protected string Brand;
public HandsetMBrand(HandsetSoft soft)
{
Soft = soft;
Brand = "MBrand";
}

public void Run()
{
this.Soft.Run(Brand);
}
}

這時候可以發現重複,重構後如下。
除了各自的品牌資訊,大多可以共用的方法與欄位,
我們就抽到父類別。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

public class HandsetBrand
{
private readonly Application _app;
protected string Brand;

protected HandsetBrand(Application app)
{
_app = app;
}
public void Run()
{
this._app.Run(Brand);
}
}

public sealed class HandsetNBrand : HandsetBrand
{
public HandsetNBrand(Application app):base(app)
{
Brand = "NBrand";
}
}

public sealed class HandsetMBrand : HandsetBrand
{
public HandsetMBrand(Application app):base(app)
{
Brand = "MBrand";
}
}

完成

Recap

  1. 修剪枝葉,
  2. 製造重複,重構並產出 Implementor
  3. 如果橋接的兩端有需要傳遞的資訊,考慮使用方法參數
  4. Abstraction 抽象呼叫 Implementor 來建立橋接
    Bridge

參考

(fin)

[閱讀筆記] 物件導向的心得與隨筆

  • Object 泛指所有的物件,Instance 是明確指透過 Class 創建的 Object

  • Class 是 Object 的藍圖/設計書 ( Class 是 Object 的抽象 )

    • Object 是 Class 的具像化
    • 通常透過 new Class
    • 一種常見的應用是 Object 裡面只有欄位,用來作為資料的載體
    • 另一種常見的應用是 Object 包含可執行的 Function
    • Static 另作討論
  • Abstract Class 是 Class 的藍圖/設計書 ( Abstract Class 是 Class 的抽象 )

    • Class 是 Abstract Class 具像化
      • 通常透過繼承方式
    • Abstract Class 會定義共用的欄位與方法給它的子類別
      • 當某一個方法,實作細節必須由子類別處理時,會宣告成抽象方法 abstract method
      • 子類別繼承後必須實作 abstract method
      • Abstract 無法直接具像成 Object
  • Interface 是 Method 的藍圖/設計 ( Interface 是 Method 的抽象 )

    • Method 是 Interface 的具像化
      • 但是 Method 必須生存在 instance 上
      • 所以不會是 Static Object
      • 所以 Interface 必須與 Class 共存,才能在 instance 上實現

(fin)

[實作筆記] Tennis KATA 與 State Pattern

前情提要

Tennis Kata 是我最常練習的一個題目,
就我個人而言,這個題目源起 91 大的極速開發,
目前最快只有在 17 分左右,使用 Rider with Mac 的話可能還會再慢一些。
我很熟悉,所以很少作需求分析,Test Case 也大多有即定的寫法。
手動得很快,腦卻不怎麼動了,明明這是一個相當經典的題目,
不過我卻被定錨了。

今年 5 月上了「測試驅動開發與持續重構」,
第一天 91 大也有透過 Tennis Kata 展示了一下火力,
那個時候又有提到可以使用 State Pattern 來實作,
最近工作上又恰巧有使用到 State Pattern。
於是我便決定要試著用 State Pattern 來進行 Tennis Kata 。

有兩種方法,一種是無到有的 Kata,
一種是將現有 Tennis Production Code,
透過重構轉換成 State Pattern,
這次我選擇從無到有。

第一次失敗

總歸來說,需求分析作得不夠徹底,
Test Case 設計不良,所以很難自然而然的讓 State 產生

第一次失敗

上圖是我第一次畫的 State ,
現在回過頭來想想,圖型上其實可以很明顯看出重複的壞味道。
但是我當下完全沒有「覺察」,明明是想要消除 if else,
卻在 State 裡面產生了大量的 if else。

第二次不成功

總而言之是作完了,但是不是很順暢。
第二次不成功

如上圖,我蠻粗暴的將所有比分轉換成可能的 State,
一樣我沒有注意到重複,但是比較起第一次的失敗,
這次的狀態機是將所有可能的狀態攤平,
這樣作是為了符合 Tennis 的規則。

Test Cases

  • LoveAll
    • 產生 Context 類別與 Score 方法
    • 產生 LoveAll State
    • 產生 IState 介面,包含 Score方法,讓 LoveAllState 實作 IState 介面
  • FifteenLove
    • 產生 ServerScore 方法
    • 產生 FifteenLove State
    • 產生 SetContext 方法
    • 產生 ChangeState 方法
    • 轉換 IState 介面成為 State 抽象類別
  • LoveFifteen
    • 產生 ReceiverScore 方法
    • 產生 LoveFifteen State
  • LoveThirty
    • 產生 LoveThirty State
  • LoveForty
    • 產生 LoveForty State
  • FifteenAll
    • 產生 FifteenAll State
      略…
  • 覺察重複,重構
    • 產生 NormalState
    • 產生 ServerPoint
    • 產生 ReceiverPoint
    • 使用 Dictionary 消除 if else
    • 產生 SameState
  • Deuce
    • 產生 DeuceState
      以下略…

Final State

最後的狀態會如上圖,當大量的 State 產生之時,心裡真的有點慌慌的,
這次的 TDD 仍然不算順暢成功,設計階段就可以在腦中模擬的問題,
我拖到了開發階段,雖然後來收斂成 NormalState 時頗有一回事,
但如果在實務上,這段不確定且發散的時間仍然是太長。

第三次成功,仍然不足夠

參考第二次所作的 State Diagram。
可以看到缺少了 Normal to Normal 的線條。
第三次將它補上了。
Final State

States Sample Next States
Same 0-0,1-1,2-2 Normal
Normal 0-1,0-2,1-2,1-3 Same、Normal、Deuce、Win
Deuce 3-3,4-4,5-5 Advantage
Advantage 3-4,5-6 Deuce、Win
Win 5-3,5-7

參考上表製作測試案例,
這裡我想強調的是狀態改變的動線,
狀態由 SameState 開始。

簡單筆記一下測試與重構的幾個亮點
完整的 commit 可以從 d792b2e開始看

Highlight Test Cases

測試的案例的設計會依 State Diagram 的箭頭來設計,
也就是狀態的改變,初始狀態為 SameState 比分為 0 - 0
並將 Design Pattern 作為指引重構出 SameState

第二個測試案例也很簡單,
因為 SameState 只會往 NormalState 移動,
所以只要使用 1 - 0 或是 0 - 1 這個案例,我就可以建立出 NormalState 類別。
並且可以觀察到兩個 State 的共通性,這個時候就會重構出 IState 介面。

一樣看圖開發,
NormalState 是最為複雜的一個狀態,他的狀態可能為

  • 保持原樣 : NormalState
  • 退回平手 : SameState
  • 進入決勝 : DeuceState
  • 直接獲勝 : WinState

而剩下的狀態都算是相當簡單,
WinState 的狀態不會再改變,
DeuceState 只會往 AdvState 狀態前進,
AdvState 是相對複雜的狀態,可能變成 WinState 也可能降回 DeuceState
NormalState 並沒有直接的關聯路徑,所以在案例設計上,應該放比較後面。

第三個案例,我會讓 NormalState 變回 SameState,除了可以完成一條狀態改變的路徑外,
好處是我不用新增類別,作到最小異動。
一開始我的邏輯會放在 GameContext 之中,但是不論是依循著 Design Pattern 的設計,
或是單純考量職責,很自然地將這裡的邏輯移至 NormalState 之中。
這裡案例會選用 1-1

但是要將邏輯移到 NormalState 之中時,會面臨一個問題。
NormalState 之中,為了判斷是否會回到 SameState
需要知道 GameContext 的資訊 ( ServerPoint 與 ReceiverPoint ),
依循 Design Pattern 的指引,
將會在 IState 之中建立 SetContext 方法。
儘管 SameState 其實不用 ServerPoint 或 ReceiverPoint 的資訊。
但是因為我們相依於介面 IState 之上,導致 NormalStateSameState 都要實作 SetContext 方法。

再進一步,SetContext 在不同的 State 實作上是不會有變化的,
所以我會轉換 IState 為抽像類別 State
而我也需要在比賽初始時呼蹈後 SetContext 並將初始狀態設為 SameState

案例 4、5、6 我選擇了 0-1 、0-2 、0-3 等案例
這裡實作的是 Normal to Normal 的狀態改變(其實是沒變),透過 Dictionary 消除 if 的手法就略過不提。
但是也許下次我不會急著完成 NormalState 而是先完成 WinStateDeuceState

接下來讓 3-3 這個案例,帶我們走到 DeuceState
稍微繞了一點路作 4-4 這個案例,雖然一樣是 DeuceState
我一開始的設計案例上並沒有考量清楚,從 3-3 走到 4-4 必須經過 AdvState
下次應該先選擇 3-4 或 4-3 的案例
再透過 4-4 的案例實作 AdvState 回到 DeuceState 的這條路徑。
最後再將透過案例 5-3 讓 AdvState 走到 WinState案例 4-1 讓 NormalState 走到 Win 就可以結束主要的流程了。

收官的部份就是簡單的重構,與補足一些特殊比分的案例,
特別要注意的是 NormalState 的 Score 仍然有許多的 if 讓我想重構移除,
目前暫時沒有想法。
此外,實際上 Tennis 會特別重視得分的順序,
比如說 100-100 Deuce 這種極端案例,交互得分的情況應忠實呈現在測試之中。
所以在寫測試時,就要特別注意得分的順序性。

後記

這次還是有用到一些常用的重構套路,
比如說,用 Dictionary 消除 if else 的手段。
另一個則是 Template Method Pattern
讓我們看看以下的 commit

ServerScore 方法為例,
本來是在定義在 Abstract Class State 之中的抽像方法,
由各個 State (Normal、Same、Deuce、Adv 與 Win)實作,
但是我們可以明顯發現, Context.ServerPoint++ 是重複的,
ChangeState 才是真正抽像的地方,
所以我們可以在 Abstract Class State 加入以下的方法與抽像方法,

1
2
3
4
5
6
7
public void ServerScore()
{
Context.ServerPoint++;
ChangeState();
}

protected abstract void ChangeState();

這不就恰巧是 Template Method Pattern 嗎 ?
同樣的手段可以放在 ReceiverScore 方法再重構一次。

實務上 Design Pattern 本來就應該星月交輝,而非千里獨行。
在學習 Design Pattern 的路上,不是硬套,而是找出適用場景。

也是我比較建議的作法,透過重構自然走向 Pattern,
透過限制改變來提昇品質,首先要有測試保護,再找尋套路或壞味道重構,
最後讓 Design Pattern 成為指引,讓代碼自動躍然而上。

參考

(fin)

[A社筆記] KATA 跟本是個屁,練不出什麼鬼東西,不服來戰

前情提要

前幾天有個小朋友問我說
「想問問 Tennis KATA 練習的重點是重構嗎
有沒有什麼重構的目標 ?」

反思

這讓我想起一年前寫的文章,[N社筆記] 在公司小規模玩 Coding Dojo
順便記錄一下後續,那個實驗大概在三月隨著參與的人員忙碌而停止了,
然後在後續有再重啟一次,那次是使用 Production Code 進行,
有多一些新的成員,但是也是隨著「忙碌」與「沒時間」而停止,
雖然我心裡比較想直接譙那些不出席又不出聲的人渣一些髒話,
不過我想這就是現實吧。這也引發我後續對 TDD 產鉗的反思

回答

簡答版

Kata 可以幫助你學習到。

  • 需求分析
  • 寫測試案例
  • TDD 與 Unit Test
  • 找到壞味道與重構
  • Design Pattern

詳答版

Kata 就是一個簡單的需求,當需求來的時候
你必需先作需求分析,透過需求分析找到測試案例。
如果你想學習 TDD 你可以試著讓每個測試案例趨動你的代碼生成。
也就是說分析測試案例的時候,你要考慮案例的順序與帶來代碼的改變。

試著遵守 TDD 的 紅-綠-重構 準則,
重構包含「測試代碼」與「產品代碼」,
如果是 OO 語言試著去遵循「SOLID原則」,
試著自已隨著 Kata 的過程流動可以看到代碼的變化。

Design Patten 可以當作目標或指引去重構代碼。
不過以 Design Patten 為目標的話,
有兩種可能:

  1. 在「需求分析」階段覺察適合的 Patten 透過設計案例來趨動
  2. 在已完成的代碼中覺察適合的 Patten 在不破壞測試的情況下重構(也有可能需要加測試案例)

原始回答

原始回答

新的問題

要花多少時間才能培育一個懂「OO、TDD、Refactoring、DP」的工程師 ?
這個性價比符合商業利益嗎 ? 職涯規劃上值得嗎 ?
業界真的有需求嗎 ? 還是只要會剪下貼上就好 ?
真正的大公司是這樣開發的嗎 ? 業界很多公司不這樣作不也活得好好的 ?
目前的趨勢會更加的專業分工,這些準繩依舊適用嗎 ?

我還沒有答案,但我還在路上。

(fin)

[閱讀筆記] 回饋隨筆—《SCRUM敏捷實戰手冊》

前情提要

朋友在一個知識型部落客的團隊中工作,
作了一個有關 Scrum 的影片,
恰巧我最近的工作與 Scrum 的導入有比較深的關係,
影片我也蠻喜歡的,就稍微作個筆記當作回饋。

回饋隨筆

第一點,讓我可以看到非業界人員初探 Scrum 的角度,
我第一次「知道」Scrum 已經是在 C 社的事了。
有點忘了第一次接觸的「初心」,台灣非軟體人說這方面的非常稀少。
能夠提供不同視角是很重要的。

橄欖球列陣這部份我蠻喜歡的; 雖然直譯就知道 Scrum 原本的意思,
但是離「光速蒙面俠」太遠,已經有點忘了那些規則,
現在對比起來真的很像,為了一次的達陣,你需要多少衝刺 ?
戰術又該怎麼選擇 ? 球員平時的訓練與臨場反應是不是也可以映射到這個業界呢 ?

整個影片很淺顯易懂,口齒清晰即使放到 1.5 倍速還是聽得清楚,
可惜的是汽車廠 5 年轉型的故事我想知道更多細節或出處,
不是很喜歡「一半的時間,作兩倍的工作」的結論,
我覺得是書商的行銷手段,不是 Scrum 的目標與強項。
產能的提昇不過是個結果,重點是三大支柱對團隊帶來的效應。
沒提到透明性、檢視性與調適性我覺得蠻可惜的,

另外個人的工作流程我不覺得需要跑 Scrum ,
可以試試 GTD 或 PDCA ,而且你會發現很多類似的觀念與原則。
Scrum 設計上是適合小型團隊的,業界說法是 3~9 人,不含 PO 與 SM
多人的公司也有很多別的方法論(EX: LeSS or SAFe),這裡我不多開戰線,敏捷無它「務實」而已。
方法沒有不好,只有適不適合。

差異

有些用字跟業界不太一樣有點可惜,硬要翻譯不如保留英文或是業界常用翻譯。
Scrum 跟規格固定應該沒有什麼關係,不過工程上的確這樣作會帶來一些好處。
Scrum 跟 Agile 本質上還是有些不同, Agile 比較像願景、原則比較抽象的層次。
複習一下 Agile 的宣言

  • Individuals and interactions over processes and tools
  • Working software over comprehensive documentation
  • Customer collaboration over contract negotiation
  • Responding to change over following a plan

Scrum 是業界較常見的框架而且與其它工作法相容性非常的高,
就我個人而言擷取過 XP、GTD、番茄鐘、ORID 與 ToC 等方法。
作為一個框架,有彈性的包容各種方法是它如此廣佈的原因之一。

最後 Scrum 無法擺脫加班、Scrum 無法擺脫加班、Scrum 無法擺脫加班
不要盲從,Google 是先成為 google 才有導入 Scrum,你導入 Scrum 也不會成為 Google。
丟書前多想 3 秒鐘,丟桌上不如丟臉上,丟書不如丟辭呈。

參考

(fin)

[A社筆記] Introduce Unit Test --- 心法篇

Why Unit Test 心法篇

網路上很多,自已找(兇)。

我的想法 about Unit Test

  • 有驗收才有品質,所以我需要測試
    • 黑箱
    • 白箱
    • 整合
    • 單元
  • UT 不過是驗收的最基本的單位。
  • 開發人員一定會測試的,不論用何種方法
    • Debuger
    • Console Output
    • Break Point
  • 程式人員的好品德
    • Laziness
    • Impatience
    • Hubris
  • 程式是照你寫的跑,而不是照你想的跑

  • 既然你會測試,那麼為什麼不讓它可以[重複/一鍵]被執行

  • 只是為了重複執行,何不使用現有的測試框架與工具?

先訂驗收標準,再進行開發是正常不過的作法,
只不過你太聰明而在腦中測試過了。
但是程式是照你寫的跑,而不是照你想的跑
不如先寫下驗收標準(測試左移/DoD/TDD),再進行開發。
錦上添花的話,就透過工具讓「執行測試」可以快速重複。
剩下的問題是,這些驗收標準寫到多「鉅細靡遺」?
有沒有什麼方式可以提昇我撰寫的速度 ?

假設我有了測試保護,那麼重構將是一件安全的事。
壞味道可以給我提示,而 Design Pattern 可以是改善程式的一個指引。

Unit Test

3A

程式寫的是 AAA (正序)心裡想的是AAA(逆序)

  • Arrange
  • Act
  • Asset

紅、綠、重構

如果以 TDD 開發,寫完新的測試後,得到的一個「綠燈」反而是一個壞味道。

第一天分享

pair programming 30 min,QA 約 20分鐘。
先請同事實作 1+1 的 Unit Test 看一下他對測試的理解。

  • 建立測試專案,可以選用 xUnit

    1
    2
    不選 MsTest 的理由是,我比較喜歡建構子與解構子的寫法,  
    勝過 TestInitialize/TestCleanup 的 Attribute 的寫法
  • 引導由測試寫出方法。

    1
    2
    3
    logic → function → class method  
    透過寫測試讓 Production 在思考中產生
    引導的沒有很成功
  • 3A 的寫法,未來會介紹沒有 3A 的寫法(為了更好的理解)

  • 方法測試案例的命名

  • 介紹 Assert.Equal 取代 Debug.Assert

  • 簡單提到三種邏輯,回傳值、改變值、互動。

  • 為什麼我討厭 Static

[未排序]預計要講的題目

  • 範例是否為偶數 → 牛奶是否過期 → 如何透過一些手法,控制不可控的類別
  • 怎麼對 Legacy Code 作解耦 ?
  • 介紹 Mock Framework
  • 怎麼寫出好理解的 Assertion ?
  • 介紹 Assert Framework
  • 建立 A 社的道場 Repo
  • more …

問題

  • 開發後,自已會完全不自已測試就丟給 QA 或客戶嗎 ?
  • 什麼是 Dojo ?
    • 日文的道場,把寫程式想像成是在練功,建立一個練功的環境。
  • 什麼是 Kata ?
    • 日文的形,或是可以說是套路/招式,一樣透過練習招式來強化自已的能力。

參考

(fin)

[實作筆記] Elasticsearch Insert Data with .Net

前篇

上次使用 Nlog 直接與 ElasticSearch 作結合,
這次來看看怎麼賽入資料給 ElasticSearch 。

NEST

一般來說,ElasticSearch 只要透過呼叫 API 就可
但是我將使用 C# 的 Nuget 套件 NEST 來簡化呼叫 API 的行為。

步驟

安裝套件

1
Install-Package NEST

建立連線

1
2
3
var node = new Uri("http://localhost:9200");
var settings = new ConnectionSettings(node);
var client = new ElasticClient(settings);

寫入資料

1
2
3
4
5
6
7
var json = new
{
name = "test name",
timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK")
};
string indexName = $"test-index-{DateTime.Now:yyyy.MM.dd}";
client.Index(json, idx => idx.Index(indexName));

Create Index Pattern

連線進入 Kibana (http://localhost:5601/),

Setting > Kibana > Index patterns > Create index pattern

Step 1 of 2: Define index pattern
輸入 test-index-*

Step 2 of 2: Configure settings
記得選取時間軸(x軸)為 timestamp,這裡會透過 automap 對應欄位,
故在產生測試資料時,記得先產出時間資料,
這個範例我使用的格式為

1
DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK")

設計共用型別

1
2
3
4
5
6
public class Record
{
public DateTime Created { get; }
public string Type { get; }
public Object Record { get; }
}

想法是通用類別,或許用抽像類別繼承也可以 ?
Created 欄位用來記錄資料產生時間,
Type 用來記錄 Record 的型別,
Record 用來記錄實際的資料。

預計未來可能會需要取出 Json 資料再轉成物件操作。

大概就醬。

6/8 補充 .Net Logger 分級

Trace 0 包含最詳細訊息的記錄。 這些訊息可能包含敏感性應用程式資料。 這些訊息預設會停用,且永遠不應在生產環境中啟用。
Debug 1 開發期間用於互動式調查的記錄。 這些記錄主要應包含適用於偵錯的資訊,且不具備任何長期價值。
Information 2 追蹤應用程式一般流程的記錄。 這些記錄應具備長期值。
Warning 3 醒目提示應用程式流程中異常或未預期事件的記錄,這些異常或未預期事件不會造成應用程式執行停止。
Error 4 在目前執行流程因失敗而停止時進行醒目提示的記錄。 這些記錄應指出目前活動中的失敗,而非整個應用程式的失敗。
Critical 5 描述無法復原的應用程式或系統損毀,或需要立即注意重大失敗的記錄。
None 6 不會用來寫入記錄訊息。 指定記錄類別不應寫入任何訊息。

(fin)

[A社筆記] 閒聊 Product Backlog

前言

Although implementing only parts of Scrum is possible, the result is not Scrum.

雖然實施部分的 Scrum 是可能的,但結果並不是 Scrum 《Scrum Guide》

之所以在文章一開頭就引述這段文字,是因為世界上太多 Scrum But 了;
而我下文所敘的則是 Not Scrum
未來有機會再深入討論兩者的差異,對我來說主要是心態上的差異。

我們透明、檢核、調試,我們擁抱改變,我們有所堅持,我們有理想但我們也務實。
好啦,說不定只有你自已。

在 A 社的導入的過程中,我開場白通常會說「這不是 Scurm」然後開始拿 Scurm 的東西說嘴。
至少對我來說,這比那些號稱「我們跑 Scrum」「But #^%!~…」要好得多,
與其讓你難以分辨斷句在哪,或是誤會怎麼 Scrum 這麼糞,不如讓我明確的告訴你「這不是 Scurm」。

背景

蠻小巧的團隊,小到我覺得也許不用任何「敏捷」
分為兩個組成 QA 部門與 RD 部門,遠端工作的 RD 主管兼職 PM
QA 團隊的主管除了目前這個團隊,也要兼者作其它團隊的測試,
此外還有一個大主管,在國外有時差,每周固定會與成員們開 3 次的會。
RD 團隊成為會有一個 Daily Sync 的會議,每天約 5~10 分鐘。
整個團隊有使用 Azure DevOps 的看板,但是 QA 只會用來開 Bug。
但不知道什麼時候 Bug 才會被修復。

問題

Azure DevOps BackLogs 太像文件

Azure DevOps 的 BackLogs 放著許多 Feature ,
依據不同的功能,再將每個 Sprint 的 User Story 放進去
這些 Feature 永遠不會被作完,隨著時間過去,
儘管完成了許多 User Story ,但是也會有新的 User Story 被加進去。
而作為文件,他的巢狀結構又不足以面對複雜的需求內容,
分散式存儲對於維護與修改上也是相同不便。

Bug 隨時蹦出

QA 的工作就是不斷的測試,一有發現問題就開立 Bug,
RD 有空就會去領來作,有時候也會有 RD 主管確認過後再分配給 RD
這導致不同面向的問題,RD 自領的情況可能會無序的作,
導致真正有價值的 Bug 無法第一時間被修正。
而如果每件事情都要 RD 主管確認後再進行動作,
RD 主管恐怕會變成瓶頸所在。

BackLogs 簡介

下面是我在向團隊介紹 Backlogs 後的筆記。

在 Azure DevOps 上會有 BackLogs 與 Sprints > Backlog。
恰巧與 Scurm 的 Product BackLogs 與 Sprints Backlog 可以一一對應。
如果以 XP 來說,可以投射到發佈計劃會議與迭代計劃會議中討論的「用戶故事清單」。
如果以 GTD 的方法論來說 BackLogs 可以當作收集一切事務的 Inbox,
而 Sprints > Backlog 可以視作專案裡面的工作項目。

總的來說,這些方法論的名詞故有所不同,但本質上卻是非常接近的事務。
我們總是有許多新的想法,但是沒有足夠的時間作所有的事。
所以我們必須補捉這些想法,放入 BackLogs 之中
補捉了之後,必須排序,這裡的概念有「重要且緊急」先作 ( 參考 Eisenhower Matrix ),
或是「價值高」的先作,或是有影響力的先作 ( 參考 Impact Mapping )。
GTD 的概念是 5 分鐘能完成的想法,立刻去作不然就丟到 BackLogs ( Inbox ) 之中,
我認為團隊不適合這樣作,畢竟 GTD 是針對個人的工作法。
團隊反而要刻意不作為,才能讓真正重要的事情浮現,而不是被一堆雜訊 DDos,
所有想法我的建議是「丟到 Backlogs 之中」,然後定期梳理吧。

理論上大部份事情會由模糊到清晰,這是個過程,之後才會進入到一個可以被執行的階段。
在 Scurm 之中,常用「遠光燈」作例子,我個人比較喜歡用「通勤」舉例。

留個問題給你,誰來衡量 ? 團隊 ? PO ? 客戶 ?

想像一下你要上班了,在家裡你對公司的方向與位置其實有個大概的想法,
要走過哪些路口,經過某個大樓,穿越了一座橋後你需要待轉,
再過幾個紅綠燈,看到速食店後就可以開始找車位了。
實際上路後,也許路上施工,你必需繞個路,或是運氣很好一路綠燈。
哦,不!! 你的車拋錨了,你必須改搭公車,而它的路線是……

有個大方向就是你的目標,路口、橋、大樓,就是你的每個迭代,
每次的煞車、轉彎或催油門就是再被細分下來的工作項目,
紅燈、拋錨、下雨天就是你真實上路後才會體驗到的事。
有個概念「Road Map」其實也是在說差不多的事。

回到 Product BackLogs 與 Sprints Backlog ,
我們也就是不斷的細化這些項目,一直到可執行然後被執行為止。

至於怎麼變快呢 ?
也許你要選一條最優路線,最短的距離最少的車流,沒有紅綠燈之類的,
也許你要有最棒的工具,最好的機車、定期保養之類的,
也許你要改變的開車習慣,不要危險駕駛、不任意切換車道等等…

透過的調整工作順序、找尋最佳工具輔助與保持正確心態,是我目前的答案。

(fin)

[實作筆記] Dotnet Logger 整合 Kibana Kibana 與 Elasticsearch

前情提要

我將 ASP.Net Core 的 API 服務加上 Log。
Logger 使用 NLog ,載體我使用 Elasticsearch ,
使用者操作介面使用 Kibana。

然後面對開發者,我希望用 AOP 方式,
讓 Logger 與主邏輯分離。

  • 這裡會用到簡單基本的 docker 技術
  • 透過 docker-compose 建立 Kibana 與 Elasticsearch
  • 假設你已經會使用 Dotnet Core DI 注入 Logger
  • 用最原生的方法實作 AOP
  • 對你可能沒有幫助

Run ElasticSearch & Kibana with docker

在本機建立 ElasticSearch & Kibana
首先建立 docker-compose.yaml 檔如下,
簡單說明一下內容:

  • 起一個 elasticsearch,port : 9200
  • 起一個 kibana ,port : 5601,設定環境變數 ELASTICSEARCH_URLhttp://localhost:9200
  • 網路名命為 elastic 使用 birdge 讓兩個 container 連起來
  • elasticsearch-data: 實務上我想需要指定一個 storage(硬碟或 File System 之類的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
version: '3.1'

services:

elasticsearch:
container_name: elasticsearch
image: docker.elastic.co/elasticsearch/elasticsearch:7.6.2
ports:
- 9200:9200
volumes:
- elasticsearch-data:/usr/share/elasticsearch/data
environment:
- xpack.monitoring.enabled=true
- xpack.watcher.enabled=false
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.type=single-node
networks:
- elastic

kibana:
container_name: kibana
image: docker.elastic.co/kibana/kibana:7.6.2
ports:
- 5601:5601
depends_on:
- elasticsearch
environment:
- ELASTICSEARCH_URL=http://localhost:9200
networks:
- elastic

networks:
elastic:
driver: bridge

volumes:
elasticsearch-data:

執行

1
docker-compose up -d

完成後在本機瀏覽器瀏覽以下網址。
確定功能正常。

雷包

第一次啟動 Kibana 要 5 分鐘左右,但是我不確定是 docker 或是 Kibana 的問題

Dotnet Core NLog with ElasticSearch

首先必需安裝相關套件

1
2
3
4
5
Install-Package NLog.Web.AspNetCore

Install-Package NLog

Install-package NLog.Targets.ElasticSearch

設定 appsettings.json
(請見 20200518 的補充)

1
2
3
4
5
6
{
"ConnectionsString": {
"ElasticSearchServerAddress": "http://localhost:9200/"
},
///...
}

設定 nlog.config,注意以下路徑

  • nlog > extenstions > assembly
  • nlog > targets > target
  • nlog > rules > logger

這裡的重點是 ElasticSearchServerAddress 這組字串需設定成你的 ElasticSearchServerAddress
(請見 20200518 的補充)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="Info"
internalLogFile="c:\temp\internal-nlog.txt">

<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
<add assembly="NLog.Targets.ElasticSearch"/>
</extensions>

<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target xsi:type="File" name="allfile" fileName="${currentdir}\log\nlog-${shortdate}.log"
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
<!-- ElasticSearch -->
<target name="ElasticSearch"
xsi:type="ElasticSearch"
ConnectionStringName="ElasticSearchServerAddress"
index="dotnetcore-nlog-elk-${date:format=yyyy.MM.dd}"
documentType="logevent"
includeAllProperties="true"
layout="[${date:format=yyyy-MM-dd HH\:mm\:ss}][${level}] ${logger} ${message} ${exception:format=toString}">
<field name="MachineName" layout="${machinename}" />
<field name="Time" layout="${longdate}" />
<field name="level" layout="${level:uppercase=true}" />
<field name="logger" layout=" ${logger}" />
<field name="message" layout=" ${message}" />
<field name="exception" layout=" ${exception:format=toString}" />
<field name="processid" layout=" ${processid}" />
<field name="threadname" layout=" ${threadname}" />
<field name="stacktrace" layout=" ${stacktrace}" />
<field name="Properties" layout="${machinename} ${longdate} ${level:uppercase=true} ${logger} ${message} ${exception}|${processid}|${stacktrace}|${threadname}" />
</target>
</targets>

<!-- rules to map from logger name to target -->
<rules>
<!--All logs, including from Microsoft-->
<logger name="*" minlevel="Trace" writeTo="allfile" />
<logger name="*" minlevel="Trace" writeTo="ElasticSearch" />
</rules>
</nlog>

我的 Logger 代碼可能會類似這樣:
我想調整 Logger 代碼,不要與商務邏輯混在一起。

  • 一般的 Logger 我會用 AOP 的方式作成 Audit Log 記錄
  • catch Exception 的 Logger 我會統一處理
1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

public Result MyMethod(Context ctx)
{
this.logger.LogInformation("Hello Marsen");
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

20200518 補充

nlog.configappsettings.json 內容調整。

使用Elasticsearch Cloud(以下簡稱 ESC)當作服務的儲存體。
踩到了一個雷包,當我把 ElasticSearchServerAddress 修改成 ESC 服務的 EndPoint
我發現 ESC 並未接收到 Log ,更奇怪的事情是,在我本機端 docker 所建置服務仍然收到了 Log。

查詢了一下最新的Nlog ElasticSearch Wiki後,
應該改用 uri 屬性設定 EndPoint ,而沒有設定的情況下預設為 localhost:9200

1
2
uri - Uri of a Elasticsearch node. Multiple can be passed comma separated.  
Ignored if cloud id is provided. Layout Default: http://localhost:9200

而要使用 ESC 的 EndPoint,除了設定 uri 外,還需要啟用授權。
requireAuth 設定為 true(預設為false),另外還要設定 usernamepassword
appsettings.json 的 ConnectionsString 就可以刪除了。
nlog.config 修改大致如下

1
2
3
4
5
6
7
8
9
10
11
12
<!--略-->
<target xsi:type="ElasticSearch"
name="elastic.co"
index="dotnetcore-nlog-elk-${date:format=yyyy.MM.dd}"
documentType="_doc"
includeAllProperties="true"
layout="[${date:format=yyyy-MM-dd HH\:mm\:ss}][${level}] ${logger} ${message} ${exception:format=toString}"
uri="https://**************elastic-cloud.com:9243"
requireAuth="true"
username="**********"
password="******************">
<!--略-->

Dotnet Core AOP

用 AOP 的方式作成 Audit Log 記錄,
想法是方法的 in / out 我將想知道資訊記錄下來。
比如輸入的參數或是回傳值。
題外話,因為 AOP 的特性,如果方法處理到一半想要記錄是作不到的,
這是不是意味著必須重構將方法一分為二 ?

參考這篇,我會使用最基本的 Filter 實作 AOP,
Filter 簡介
依據 Filter 的特性我在這裡會實作 IActionFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AuditLogAttribute : Attribute, IActionFilter
{
private readonly ILogger logger;

public AuditLogAttribute(ILogger<AuditLogAttribute> logger)
{
this.logger = logger;
}

public void OnActionExecuted(ActionExecutedContext context)
{
this.logger.LogInformation("Result Filter End");
}

public void OnActionExecuting(ActionExecutingContext context)
{
this.logger.LogInformation("Result Filter Start");
}
}

我的最終目標是透過掛載 Attribute 的方式來讓 Logger 與解耦,
參考下方的程式碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

[AuditLog]
public Result MyMethod(Context ctx)
{
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

這裡就有一件討厭的事,因為我的 Logger 都是透過建構子注入產生,
而自已本身也比較傾向不要使用公開 Property Injection 的方式*。
但是使用建構子在這裡會產生另一個問題,
我將無法使用掛載 Attribute 的方式處理 Logger
解決的方式是透過另一個 Attribute ServiceFilterAttribute 作間接掛載

1
2
3
4
5
6
7
8
9
10
11
12
13
private readonly ILogger logger;

[ServiceFilter(typeof(AuditLogAttribute))]
public Result MyMethod(Context ctx)
{
try{
//// do some thing
}
catch
{
this.logger.LogError("What's a Wonderful World");
}
}

這樣作還是有一些缺點,最明顯就是掛載的 Attribute 變長,
然後視覺上又是末端文字才能表達意涵,
另一點是原本約定成俗可以省略的*Attribute後綴不能省略了。

我想使用 CastleAutoFac 重作一次。
應該可以變得更加簡潔。

參考

(fin)

[實作筆記] Github 結合 Stryker 作變異測試

前情提要

前年初次接觸變異測試,去年在看重構時偷換了概念,
將兩者結合了。

這次我想更進一步,將 CI Server 與之結合。

Stryker.Net

跟據Stryker Handbook
Stryker 主要有三個專案,

  • Stryker (Javascript & TypeScript)
  • Stryker4s (Scala)
  • Stryker.NET (.NET)

我會使用我的練習用專案作為目標,
所以很理所當然的我會選擇 Stryker.NET,那我們就開始吧。

開始

安裝 Stryker

1
dotnet tool install -g dotnet-stryker

執行 Stryker

1
dotnet stryker -tp "['./test/Marsen.NetCore.Dojo.Tests/Marsen.NetCore.Dojo.Tests.csproj','./test/Marsen.NetCore.Dojo.Integration.Tests/Marsen.NetCore.Dojo.Integration.Tests.csproj']" -p="Marsen.NetCore.Dojo.csproj" -dk=$STRYKER_DASHBOARD_API_KEY

注意幾個 cli 參數,
-tp 明確指定測試的專案有哪些,可以用中括號[]傳入多個測試專案名稱,使用,作為分隔符
-p 專案名稱
-dk dashboard-api-key 這組 key 是用來與 https://dashboard.stryker-mutator.io/ 互動的,
最主的功能是將報告上傳。
$STRYKER_DASHBOARD_API_KEY 是 Github 的 Secrets

你可以執行 dotnet stryker -h 查看更多原始說明

1
2
3
4
-tp|--test-projects Specify what test projects should run on the project under test.
-p|--project-file <projectFileName> Used for matching the project references when finding the project to
mutate. Example: "ExampleProject.csproj"
-dk|--dashboard-api-key <api-key> Api key for dashboard reporter. You can get your key here: https://dashboard.stryker-mutator.io

專案設定

新增一個 stryker-config.json
對我來說,最重要的是要記得設定 stryker-config > reporters > dashboard 這筆資料。
詳細的說明的可以參考這篇

1
2
3
4
5
6
7
8
9
10
{
"stryker-config": {
"dashboard-project": "github.com/marsen/Marsen.NetCore.Dojo",
"dashboard-version": "master",
"reporters": [
"json",
"dashboard"
]
}
}

在 Github 上工作

首先你必須在 Dashboard中 Enable Repository

Enable Repository

然後產生一組 Key

產生一組 Key

接下來到 Github , 我們將這組 Key 設定到 Secrets 之中

將 Key 設定至 Github

再到 Github Actions 中,把 workflow 指令設定完成,

Github Actions

觸發 CI 完成後,就可以在 Dashboard 上看到報表啦。

結果呈現

參考

(fin)