[實作筆記] Bowling Kata

前言

之前有和同事試過,並且用 Pair Programming 的方式進行,
代碼髒得很快,並且進入了死胡同,即使使用 TDD 有測試保護,
還是難以重構。

原因有以下:

  1. 不夠了解規則,開發到一半才重新解析
  2. 未經足夠的設計與討論
  3. Pairs 沒有相同的想法
  4. Test Case 粒度太大,不夠 Baby Step , Production 常常會多出多餘的代碼

在讀完 TDD By Example 後,我想再試一次,
用 Todo List 方式列下我想開發的項目再轉換成 Test Case,
不看網路上已有的 Solution 進行獨立開發(至少現在不會有 Pairs 的想法相異問題),
不刻意設計物件,讓測試自然趨動整體開發。

開始之前

先改善前幾次的問題,
由於這次由我一個人進行開發, 所以不會有想法不一致的狀況, 實務上或許需要更多的溝通,
規則的部份我參考,
詳細內容如下 :

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
- Each game, or “line” of bowling, includes ten turns, or “frames” for the bowler.
- In each frame, the bowler gets up to two tries to knock down all the pins.
- If in two tries, he fails to knock them all down, his score for that frame is the total number of pins knocked down in
his two tries.
- If in two tries he knocks them all down, this is called a “spare” and his score for the frame is ten plus the number
of pins knocked down on his next throw (in his next turn).
- If on his first try in the frame he knocks down all the pins, this is called a “strike”. His turn is over, and his
score for the frame is ten plus the simple total of the pins knocked down in his next two rolls.
- If he gets a spare or strike in the last (tenth) frame, the bowler gets to throw one or two more bonus balls,
respectively. These bonus throws are taken as part of the same turn. If the bonus throws knock down all the pins, the
process does not repeat: the bonus throws are only used to calculate the score of the final frame.
- The game score is the total of all frame scores.

# 中文

- 一個玩家(bowler),每場(line)有 10 個回合(frames)
- 每個回合玩家可以打兩次
- 如果兩次打完,沒有全倒,回合分數為擊倒的瓶子總數
- 如果兩次打完,全倒,簡稱 Spare,回合分數為 10 分加上下一次擊倒的瓶子分數
- 如果在第一次打完,全倒,簡稱 Strike,回合直接結束,回合分數為 10 分加上下二次擊倒的瓶子分數
- 如果在第 10 回合,
- 玩家擊出 Spare 可以有 1 次額外擊球機會
- 玩家擊出 Strike 可以有 2 次額外擊球機會
- 額外的擊球只是為了計算第 10 回合的分數
- 整場遊戲的分數是所有回合的加總

有了基本規則後, 我要參考 TDD By Example 一書的作法,
寫下 Todo List 用來記錄我要作的事情, 當然這也會是一份湧現式的清單.

第一次 Kata 的 Todolist

我想像中的 BowlingGame 會提供一個計算方法,
透過傳入一組整數列,回傳目前的分數,
過程中如果有目前 Todo List 沒考慮到的東西會逐步加上

  • 計算總分
  • 計算回合分數
  • 一回合兩次擊球,沒有全倒
  • Spare,加上額外一擊的分數
  • Strike,加上額外二擊的分數
  • API,給定一個數列,回傳一個分數
    • 0 分不等於沒有分
    • 初始分數是沒有分

第一次 Kata 中斷時的 Todolist

  • 計算總分
  • 計算回合分數
  • 一回合兩次擊球,沒有全倒
    • 前面有 Spare 或 Strike 的計算
  • Spare,加上額外一擊的分數
  • Strike,加上額外二擊的分數
  • API,給定一個數列,回傳一個分數
    • 0 分不等於沒有分
    • 初始分數是沒有分
    • 第一球就洗溝,0 分 沒有分
      • 第二球就打倒1瓶,0 分
    • 第一球就打倒1瓶,1 分 沒有分
      • 第二球就打倒 0 瓶,1 分
  • 最後一回合的計算
  • Frame 回合的概念
    • 消除重複的 null
    • 第一次就全倒就是一個 Frame
      • FirstTry
    • 打兩次就是一個 Frame
    • Strike Frame 的分數是 null

第一次 Kata 的檢討

設計上仍然不足, 單純只想靠測試 → 開發其實是有點鄉愿的,
TDD 的概念應該是以 Client(TestCase)的角度去使用 Production Code,
這個案例中, 我想設計的 API 是一次將目前擊倒的瓶數組合成一個 List 傳給 BowlingLine,
計算後回傳總分.

這樣的設計, 對 Client 來說簡單好用, 但是對 BowlingLine 來說似乎職責太多了,
另外 Frame 的概念就消失在 Client 的視野之中, 但 BowlingLine 應該要能夠區分出 Frame
所以我預計寫下 Frame 的測試案例. 再來, 我們發現分數在某些情況是尚未決定的,
比如說擊出 Strike/Spare 或是只擊出該 Frame 的第一次時, 是無法計分的.
經過第一次 Kata 後重塑對 Bowling 的認知

重塑認知

  1. 總分是 Frame 的分數的加總
  2. Frame 的分數由兩次 try 與 bonus 作計算
  3. 兩次 try 的加總等於 10 才有 bonus
  4. 有 bonus 的話必須計算完 bonus 才有分數

第二次 Todolist

  • Frame 的分數是 2 次 try 的加總加上 bonus
    • 一個 Frame 未 try 過 2 次的分數是 null
    • Try 的分數計算方式是加法
    • Bonus 的計算方式
    • 有 Bonus 但是還未計算的分數為 null
  • Game 的總分是 Frame 的分數的加總

第一次 Kata 的遺留代碼

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
public class BowlingLine
{
public int? Calculate(List<int> fellPins)
{
var firstTry = 0;
for (var i = 0; i < fellPins.Count; i++)
{
if (fellPins[firstTry] == 10)
{
continue;
}

if (fellPins.Count == 2 && fellPins.Sum() == 10)
{
continue;
}

if (fellPins.Count > 1)
{
return fellPins.Sum();
}
}

return null;
}
}

Frame 的實作

可以看到這些遺留代碼, 雖然可以通過目前的所有測試, 但是想更進一步的時候確寸步難行.
主因是我們的設計上缺乏 Frame 的概念, 由此我會先撰寫 Frame 的測試案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Fact]
public void TestFrameScore()
{
//Frame Playing
Assert.Null(new Frame().Score);
Assert.Null(new Frame(1).Score);
//Normal Frame
Assert.Equal(7, new Frame(4, 3).Score);
//Spare
Assert.Null(new Frame(4, 6).Score);
Assert.Null(new Frame(0, 10).Score);
//Strike
Assert.Null(new Frame(10, 0).Score);
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Frame
{
public Frame(int? firstTry = null, int? secondTry = null)
{
if (firstTry + secondTry != 10)
{
Score = firstTry + secondTry;
}
}

public int? Score { get; }
}

有了 Frame 之後我要來處理之前第一次 Kata 產生的遺留代碼
首先, Game 的總分是 Frame 的分數的加總 這條規則吸引了我,
理論上所有只有一個 Frame 的測試, 在我用 Frame 的寫法後,
測試應該都會通過. 而且幸運的是, 我之前的測試只有 2 個測試的情境進行到了 2 個 Frame,
所以頂多壞 2 個測試, 我可以嘗試修復它.

修改成使用 Frame 的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int? Calculate(List<int> fellPins)
{
var frames = new List<Frame>();

//todo remove this condition after pass single frame test
if (fellPins.Count == 2)
{
if (fellPins[0] != 10)
{
frames.Add(new Frame(fellPins[0], fellPins[1]));
}
}
if (fellPins.Count == 3 && fellPins[0] + fellPins[1] == 10)
{
var frame = new Frame(fellPins[0], fellPins[1]);
frame.SetBonus(fellPins[2]);
frames.Add(frame);
}
for (int i = 0; i < fellPins.Count; i++)
{
frames.Add(new Frame(fellPins[0]));
}
return NullableSum(frames);
}

有了 Frame 的概念後, 我們可以逐一將每個被擊倒的球瓶組成一個個 Frame

Loop 處理 Frame

先看一下目前的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public int? Calculate(List<int> fellPins)
{
var frames = new List<Frame>();
//todo remove this condition after pass single frame test
if (fellPins.Count == 2)
{
if (fellPins[0] != 10)
{
frames.Add(new Frame(fellPins[0], fellPins[1]));
}
}
if (fellPins.Count == 3 && fellPins[0] + fellPins[1] == 10)
{
var frame = new Frame(fellPins[0], fellPins[1]);
frame.SetBonus(fellPins[2]);
frames.Add(frame);
}
for (int i = 0; i < fellPins.Count; i++)
{
frames.Add(new Frame(fellPins[0]));
}

return NullableSum(frames);
}

我們建一個 For Loop 目標要將這些醜醜的 if 判斷式移到 Loop 之中
結果大致如下, 過程當然也是逐步的抽離

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
public int? Calculate(List<int> fellPins)
{
var frames = new List<Frame>();
var hasBonus = false;
for (int i = 0; i < fellPins.Count; i++)
{
var firstTry = fellPins[i];
int? secondTry = i < fellPins.Count - 1 ? fellPins[i + 1] : null;
if (hasBonus)
{
frames.Last().SetBonus(firstTry);
}
if (firstTry != 10)
{
frames.Add(new Frame(firstTry, secondTry));
if (firstTry + secondTry == 10)
{
hasBonus = true;
i++;
}
}
}

return NullableSum(frames);
}

前面的 Frame 在計算分數的時候, 並未考慮 Strike 或是 Spare 完成 Bonus 的情況 ,
所以接下來我們會用測試案例來趨動, 而最小的案例就是只有兩個 Frame 的計分狀況
比如說, 這樣的測試案例

1
Assert.Equal(13, _line.Calculate(new List<int> { 3, 7, 2, 1 }));

另一方面, 即使算分正確,
Frame 的個數也引起我的注意, 因為有時候 Frame 裡面只會有一次 Try Hit
擔心的事就用測試作為保護吧

1
2
_line.Calculate(new List<int> { 3, 7, 2, 1 });
Assert.Equal(2, _line.FrameList.Count);

Bonus

Bonus 也是我寫法改變最多的地方之一
我有用過 Flag, 計數器, 最後我選擇了 Type

1
2
3
_frames = new();
var hasBonus = false;
for (int i = 0; i < fellPins.Count; i++)
1
2
3
_frames = new();
var bonusCount = 0;
for (int i = 0; i < fellPins.Count; i++)
1
2
3
_frames = new();
var bonusType = string.Empty;
for (int i = 0; i < fellPins.Count; i++)

不過更重要的是, 為什麼 Bonus 與 BowlingLine 有關?
我們已知這個 Frame 與接下來兩次的擊球數與 Bonus 才有正相關,
所以我應該把這個職責移到 Bonus 身上, 原始判斷 Strike 與 Spare 的邏輯,
SetBonus 的邏輯, 也應該一併移到 Frame 身上, 這也是 OOP 的體現

原始代碼如下, 真是有夠糟糕的

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 int? Calculate(List<int> fellPins)
{
_frames = new();
var bonusCount = 0;
for (int i = 0; i < fellPins.Count; i++)
{
var firstTry = fellPins[i];
int? secondTry = i < fellPins.Count - 1 ? fellPins[i + 1] : null;
if (_frames.Any() && _frames.Last().BonusCount > 0)
{
_frames.Last().SetBonus(firstTry);
bonusCount--;
}
if (firstTry == 10)
{
_frames.Add(new Frame(firstTry));
}
else
{
var frame = new Frame(firstTry, secondTry);
if (firstTry + secondTry == 10)
{
frame.Spare();
bonusCount = 1;
}
_frames.Add(frame);
i++;
}
}
return NullableSum(_frames);
}

下面的代碼已經將 Bonus 相關邏輯移到了 Frame 之中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public int? Calculate(List<int> fellPins)
{
FrameList = new List<Frame>();
for (var i = 0; i < fellPins.Count; i++)
{
var firstTry = fellPins[i];
int? secondTry =
i < fellPins.Count - 1 ? fellPins[i + 1] : null;
if (FrameList.Any()) FrameList.Last().SetBonus(firstTry, secondTry);
secondTry = firstTry == 10 ? null : secondTry;
var frame = new Frame(firstTry, secondTry);
if (firstTry != 10)
{
i++;
}
FrameList.Add(frame);
}
return NullableSum(FrameList);
}

最後透過幾個重構的技巧可以讓這段代碼更好理解

  • 共用 Field(這個作法是否適合還可以討論)
  • Extact Method
  • Inline Variable

結果如下,更多可以參考分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public List<Frame> FrameList { get; private set; } = new();
private List<int> FellPins = new();
public int? Calculate(List<int> fellPins)
{
FrameList = new List<Frame>();
FellPins = fellPins;
for (var i = 0; i < FellPins.Count; i++)
{
var firstTry = FirstTry(i);
var secondTry = SecondTry(i);
if (FrameList.Any()) FrameList.Last().SetBonus(firstTry, secondTry);
if (firstTry != 10) i++;
FrameList.Add(new Frame(firstTry, secondTry));
}
return NullableSum(FrameList);
}
private int? SecondTry(int i)
{
return i < FellPins.Count - 1 ? FellPins[i + 1] : null;
}
private int FirstTry(int i)
{
return FellPins[i];
}

結語

這樣的結果其實還沒有完成, 我接下來將會測試一些邊際
或是不合理的輸入與呼叫. 過程中的幾個亮點仍然是讓人非常的開心

  • Frame 的概念
  • 沒有分的計算
  • Bonus 職責的轉移
  • 重構

其實還有一個概念沒有被寫出來,
那就是擊出 4 球計算一個 Frame 的分數,
再有一次 Kata 的話我或許會用這個概念下去實作.

參考

(fin)

[實作筆記] Kent Beck 的測試驅動開發 Ch12~Ch17

前情提要

最近疫情昇溫,公司開始 Work From Home,
少了通勤時間,少了晚上的聚會或運動課,
每天大概多了3~5個小時,趁現在多看點書囉,
「Kent Beck 的測試驅動開發」是一本 TDD 的經典,
加上是由 91 Chen 所翻譯,所以一出版我就買了,
不過一直到最近才有機會看,危機或許是就是轉機,趁現在補充一下自已的技能點數吧。

本書介紹

這本書分為三大部份,分別是第一部份貨幣範例,
第二部份 xUnit 範例, 最後一部份是介紹 TDD 模式。
本篇文章著重在第一部份 Ch12 與 Ch13 章的部份,
這裡有大量的實作, 書中作者是用 Java 實現的, 我是試著用 C# 與 xUnit 去實作。

這裡作者的思路對我來說實在是難以理解,
重構步驟更是讓我消化不良,
想了一下, 回歸初衷用我自已的步伐
試試看能不能「Driven」一些產品代碼出來。
特以此文記錄。

我在哪裡

我已經有了一些代碼, 並且開始了加法的計算
幣別或許是一個問題, 但在我們(在書中)先簡化這個問題, 只作美元的加法。
測試案例如下

1
2
3
4
5
6
7
8
9
[Fact]
public void testSimpleAddition()
{
Money five = Money.dollar(5);
IExpression sum = five.plus(five);
Bank bank = new Bank();
Money reduce = bank.reduce(sum,"USD");
Assert.Equal(Money.dollar(10), reduce);
}

five 是個簡單的 Money 物件,代表 5 美元。
這個測試案例存在一個運算式(IExpression)的隱喻,
這個隱喻我可真得想不出來, 作者也說他是經過 20 次以上的練習才有這樣的神來一筆,
我先接受這點繼續作下去

在書中我比較能接受是錢包裡面有很多國家錢幣的概念
不過我可能還是會用一個 List 丟給計算器,而不是透過運算式
運算式我會想像成我有 5 美元紙鈔跟 5 元法郎(現在先簡化成美元)‘five.plus(five);‘
然後請銀行依匯率(目前暫時沒有換匯的需求)算錢給我 ‘bank.reduce(sum,”USD”);‘

第一個問題是, 雖然測試綠燈, 不過其實是假的
Hard Code 寫死回傳 10 美元, return Money.dollar(10);
所以這裡開始會跟書上的發展有些不同, 但應該是殊途同歸才對

1
2
3
4
5
6
7
public class Bank
{
public Money reduce(IExpression sum, string usd)
{
return Money.dollar(10);
}
}

Step 1

參數名命怪怪的先改一下

1
public Money reduce(IExpression expression, string currency)

Step 2

讓 Bank 的 reduce 要有意義, 所以我們想像這個運算式應該是總和(Sum)
只有型別是 Sum 的時候, 才作運算,其它部份就拋出錯誤

1
2
3
4
5
6
7
8
9
public Money reduce(IExpression expression, string currency)
{
if (expression.GetType() == typeof(Sum))
{
return Money.dollar(10);
}

throw new NotImplementedException();
}

這裡有兩個問題, Class Sum 還沒有建立,
這是小問題, 下一個 Commit 我們就把他實作出來
另外一個問題是 NotImplementedException 並沒有被測試包覆,
但這不是我主要的情境, 讓我學習 Kent Beck 寫到待辦清單吧。

1
2
3
4
TODO List
- 只有 Sum Type 進行運算
- Hard Code 寫死回傳值
- NotImplementedException 並沒有被測試包覆

不過我拿到了一個紅燈, 看一下錯誤訊息

System.NotImplementedException: The method or operation is not implemented.

看來 expression 的 Type 並不是 Sum.

Step 3

我們試著先將 Client 端(也就是我們的測試案例)的部份,
轉換成 Sum

1
2
IExpression result = five.plus(five);
Sum sum = (Sum) result;

這個時候編譯會失敗, 原因是 Sum 未實作 IExpression 介面,
一樣一個Commit搞定他
重新跑一下測試, 還是紅燈但是錯誤訊息變了, 無法將 Money 轉型成 Sum

System.InvalidCastException Unable to cast object of type
‘Marsen.NetCore.Dojo.Tests.Books.TddByExample.Money’ to type
‘Marsen.NetCore.Dojo.Tests.Books.TddByExample.Sum’.`

Step 4

直接 new Sum() 回傳就好, 記得嗎?我目前仍未通過測試。

1
2
3
4
public Sum plus(Money money)
{
return new Sum();
}

到這一步就通過測試了, 現在只有 Sum 會回傳 return Money.dollar(10);
書中則是另外寫了一個測試, 但過程中我總會改壞另一個測試, 現在的步驟比較適合我

再看一下我們的待辦清單

TODO List
只有 Sum Type 進行運算
Hard Code 寫死回傳值
NotImplementedException 並沒有被測試包覆

Step 5

接下來我想處理 Hard Code 的部份,
但是目前的 test Case 有點凌亂, 稍微整理一下

1
2
3
4
5
6
7
[Fact]
public void testSimpleAddition()
{
Money five = Money.dollar(5);
Sum fivePlusFive = five.plus(five);
Assert.Equal(Money.dollar(10), _bank.reduce(fivePlusFive, "USD"));
}

Step 6

加上一個新的案例來產生紅燈

1
2
3
4
5
6
7
8
9
10
[Fact]
public void testSimpleAddition()
{
Money five = Money.dollar(5);
Money four = Money.dollar(4);
Sum fivePlusFive = five.plus(five);
Sum fivePlusFour = five.plus(four);
Assert.Equal(Money.dollar(10), _bank.reduce(fivePlusFive, "USD"));
Assert.Equal(Money.dollar(9), _bank.reduce(fivePlusFour, "USD"));
}

Step 7

回傳計算結果, 這個職責在此回到 expression 手上,
那 Bank 要作什麼?
在我的想像中將會是匯率與幣別的運算,
總之, 目前還輪不到它,

1
2
3
4
5
6
7
8
9
10
public class Bank
{
public Money reduce(IExpression expression, string currency)
{
if (expression.GetType() == typeof(Sum))
{
Sum sum = (Sum) expression;
return sum.reduce();
}
...

Step 8

讓 Sum 實作 reduce 邏輯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Sum : IExpression
{
private int augend;
private int addend;

public Sum(Money augend, Money addend)
{
this.augend = augend.Amount;
this.addend = addend.Amount;
}

public Money reduce()
{
return Money.dollar(this.augend + this.addend);
}
}

目前就可以通過測試,接下來我將試著接回書上的第 14 章.
完整分支請參考

20210602 補充

其實完整的代碼大約在 5 月底就已經補完,
雖然在最後整體的完整性與書上大致相同,
但部份的實作與心路歷程是不一樣的,
這也是我為什麼要不斷的練習相同的 Kata 的原因,
每次的 Kata 我可能都會有不同的想法, 這點是我覺得相當有趣的地方.
並不是所有的問題都只有唯一解, 而選擇的權利能帶來自由.

加上換匯的測試案例

這裡書上的範例會跑出 addRate 的方法,
以 TDD 原則上來說不應該先有這方法才對,
不過以 Todo List 的想法, 反而可以接受這樣的空方法(回傳或實作皆為空)
未了避免忘記, 我會加上 //Todo ,
這樣的作法更接近如果我是使用這個 API 的人我想怎麼用的設計理念,
接下來是 reduce 用假實作先完成.

重整檔案

接下來的步驟有點脫離實作, 總之, 我覺得代碼太肥了, 開始切割成不同的檔案,
甚至移到不同的 Namespace 之間, 這類的工作交給 IDE 作就對了.

離開書本

這一步是讓匯率能夠透過不同的幣別轉換(CH14),
與書中不同的作法是直接透過查表法(LookUp)來進行轉換,
我使用法朗兌美金(2:1)與美金兌美金(1:1)來作測試案例
書中的作法  與 Java 的語言特性有關, 而且與其時代背景有關, 所以跳過.

我想作者想表達的是,
你可以透過測試案例來掌握一些你不熟悉的語言特性,
甚至可以傳達意圖給閱讀這個測試案例的人.

回到書本

法郎與美金的加總,
這裡要掌握的還是 Expression 的隱喻,
使用錢包的概念我還是比較好了解, Expression 感覺在這之上墊了一層抽象.
最後將一些共用的/抽像的方法往上抽到介面就可以了.

雖然我在上一步驟離開了書本的實作, 但其實再次回到書本之中並不難,
原因是我走得步伐並不算大, 另一點是我的基礎設計仍與書中相同(第一個測試案例),
試想 Expression 的隱喻如果不是我一開始設計的理念,
而是單純的加法與轉換器的作法, 並有相對應的測試保護,
也實際應用到了產品之中, 我有能力不影響產品的情況下重構成 Expression 的作法嗎?(反思…)

(fin)

[閱讀筆記] 原子習慣/驚人習慣力/烏托邦的日常/象與騎象人

前情提要

最近看了一些類似的書籍,稍微整理如下:

  • 烏托邦的日常
  • 驚人習慣力
  • 原子習慣
  • 象與騎象人
  • 我們為什麼這樣生活,那樣工作

最近看的兩本是「烏托邦的日常」與「驚人習慣力」,
再來是「原子習慣」約是一年前的讀物,
「象與騎象人」與「我們為什麼這樣生活,那樣工作」則年代久遠不可考矣。
有點小小收獲,在此作個記錄。

兩個問題,我是誰,我會是誰

烏托邦一書裡面提到許多策略,
驚人與原子一書也都提供了各種策略,
這兩本書主要微型化工作項目,用以建立持續性行為,
或是反過來說,讓行為可以持續…

但是裡面最重要的事,卻都放在書的最後一個章節裡,
這件事就是身份認同,現在的我是誰 ? 過去的我會是誰 ?
我的答案是
你也可以想想你的答案會是什麼?

這也有點「把時間當朋友」一書中所說的「一切都是累積」,
改變自已的時間點跟種樹一樣,最佳時機是 20 年前,再來就是現在

現在是一個矛盾的時間點,「舊我」與「新我」並存的時間點,
對於現在的自已你總有不滿意的地方,但現在的自已又同是過去的你的總合。
所以你是誰 ? 什麼造就了現在的你 ?
你想成為誰 ? 那你應該作些什麼 ?
你真的想要成為一個有腹肌的男人嗎 ? 還是只是覺得那樣又酷又帥 ?
你真的想要成為又酷又帥的人嗎 ? 還是你只是想要成為萬人迷,又或是你只是不想孤單 ?
你必須找到你真正的動機

動機、意志力與習慣

在「驚人習慣力」一書中提到,

意志力與動機不是二擇一的問題,

動機與意志力都可以觸發新的行為發生,
但是動機(熱情)會遞減(反思:邊際效應),不適合習慣的養成(重複性的動作),
意志力是可靠(可以用系統強化,ex:待辦清單/行事曆),
但是意志力是有限的;
而五個消耗意志力主要的原因:

  • 程度努力
  • 認知困難度
  • 負面的影響
  • 主觀的疲勞
  • 血糖濃度

我相信動機遞減說,我自覺更多時候,其實是沒有找到我真正的動機,
而這部份與自我認同有關,自我認同跟你的生活習習相關,
很多人一邊減肥,一邊對自已說我就是瘦不下來,
其實就是一種負面的自我暗示,你可以透過不斷的自我暗示來改變自我認同,
你可以巧妙的使用它來改變自我認同,
一般的書通常會說要對自已說「我要瘦下來」或是「我是 XXXX 」,
我認為那樣的行為是不會產生暗示的,你必需找到自已人生中的金句,才能發揮強效,
試試看自問自答,當你找到時,自我認同將為源源不決動機
請務必對自已誠實,強者我朋友會這樣作,
「我就色,想把妹,想認識更多人,要有好的穿著,不能太胖,最好有點肌肉,才撐得起衣服」
後面買名貴的衣服、健身、不吃宵夜等習慣(行為)就都是後話了。

回到減少意志力的耗損,微型(原子)習慣請當作一種策略,
試著在自已身上執行,並追踪記錄,真的對你有效就持續執行。

策略、衝突與自我

烏托邦書中將人分為四類,「自律」「問責」、「質疑」、「叛逆」者。
要小心分類的陷阱,就像是星座或生肖一樣,
一個人身上同時間會有多種角色,只是比例的多寡
甚至面對不同的事情時,不同的類型的反應會特別突出,
舉例來說: 當你是福委安排尾牙活動時,我可能偏向「問責」,與朋友聚餐時我會偏向「自律」

烏托邦一書提到許多策略,
當中的追踪策略特別有感,沒有量測沒有改善,
有了測試後,可以結合各種方法論來實現目標(GTD/PDCA/KPI/OKR 甚至 Scrum etc…),
過程中記錄自已的心境變化,你想成為的人,不一定是你真正想成為的人

調整自已的節奏,使用明確策略寫下自覺得感受與期待,找到當中的衝突,
有時候你可以全都要,有時候你必需要取捨,記錄下來或是可視化
迭代自已的目標,認識真正的自已,透過明確策略告訴自已現在不作什麼,
將可以給你更多的掌握感。

策略清單

微型策略

驚人習慣力與原子習慣的主要策略,
讓日常所作之事儘可能的小,減少執行耗費的心力

優點: 極小化項目,使得持續策略得已完成
缺點: 太過微小的習慣,難以出現有效的進步。

持續策略

另一個知名的名稱稱作「Don’t Break The Chain!(不要打斷鎖鍊)」,
主要目的讓累積的成果視覺化,成為一種外在問責與獎勵。

方便/不方便策略

舉例來說,可以準備多套器材避免自已懶散,或是成為一個顯見的提示,
來觸發動機,反之可以避免惡習。

控制策略

在特定的時間點作特定量的事,以得到掌控感/儀式感。
像是飯前禱告、每日自省、冥想,這個策略與預先決策或是追踪策略可以很好的結合,
具體來說,你可以試著每日自省 10 分鐘,如果熟悉看板的話,
就可以在看板上預先預先決策追踪

預先決策策略

減少決策所消耗的心力,事先作好作定,這裡的概念是不要相信未來的自已
透過現在的自已設定目標,但當未來抵達的時候,你很有可能會軟弱的,
這時候控制策略所創造的掌控感也許可以助你一臂之力,
追踪策略則可以讓你看見進度。

追踪策略

前面提到追踪策略,只著重在看見進度,
但是這個策略的概念不僅僅是進度而,你也可以追踪其它任何東西,
比如說我也曾經用來追踪財務、時間與心理狀態,
優先養成追踪策略對自已會很有幫助。
具體的實踐可以參考 GTD 之類的作法

新的環境

趁機在新的一年、搬家、換工作等開始立志,
不過書中也提到,新的環境也很容易中斷故有的習慣,
如果有使用追踪策略比較不會中斷

其它

  • 行事曆/待辦: 我認為這是預先決策的具體實踐,
  • 若則: 這個策略是如果沒作到 A 就作 B,可以預先決策,但我認為實作上仍然有困難,也許要額外的心力
  • 近朱者赤: 接近你心中的實踐者
  • 明確目標: 寫下自已的目標,這裡也是視覺化的應用

獎勵

達到某個哩程碑時自我獎勵,但是在烏托邦一書有提到要小心這個策略,
根據你的獎勵,有可能會給心理負面的暗示,讓你覺得原本想養成的習慣變成一種苦難,
應該試著讓習慣本身就是一種獎勵,像是節食或運動,目的如果是健康或是外型的話
比起吃大餐,透過追踪的方式,讓自已的看到健康數值或外型的改變,
才是更好的獎勵方式。

行動

獎勵與動機應該緊密結合,
將所有外在的目標(想早睡、想瘦、想英文好)找到連結的內化目標(想健康、想賺錢…)
你可以試著探索什麼會是你現有的習慣與行為的內在動機 ?

比如說你喜歡打球,是因為其中的合作還是競爭,或是面對挑戰的感覺 ?
你喜歡購物,是因為對金錢掌控感 ? 還是可以滿足對新產品的好奇 ?
喜歡唱歌是因為會受到肯定 ? 喜歡看劇可以滿足幻想 的生活 ?

進一步,你可以寫下你想要培養的習慣,
你想成為的人,你想達到的目標,外在的動機是什麼 ?
內在的動機又是什麼呢 ?

願你我都能成為想成為的人

  • 挑戰
  • 好奇
  • 掌控
  • 幻想
  • 合作
  • 競爭
  • 肯定

參考

(fin)

[活動筆記] 令人失望的敏捷劇本殺

前情提要

活動連結
好久沒參加敏捷活動了, 一些”敏捷”活動的反思有機會再聊聊,
另一個原因是好奇如何結合劇本殺,所以蠻期待.

一些記錄

  • 收費部份採取現場收費,所以 KKTIX 連結

  • 時間被砍掉半小時,原本的設計是 1.5 小時.被砍到剩 1 小時.

  • 主持人自已都說沒有很 involve 在整場活動中,畢竟是有收費的活動,這樣的發言不是很洽當

    導演在活動過程中,除了拍照外就是在跟某大師聊天,
    實際上「導演」(場控) 有餘裕的話,我覺得並沒有不行,
    但更重要整場的引導.
    整場活動下來,包含結尾的自清,我覺得沒有作得很好

  • 某大師也是都在聊天,不過最後 End 提點的東西還是不錯的,可惜無法有更多的互動(因為要趕高鐵)

  • 破冰的效果不佳(工程師這群人的破冰難度的確是比一般人高)

  • 有些玩過(推測)的人,並沒有將引導新參與者,當然這樣不爆雷也是有好處

  • 其實只要透過問問題,可以在不爆雷的情況下讓人思考(也是引導的一種方式)

  • 說話和不說話的人太多了,問好問題的人太少了).

  • 劇本殺與角色設定薄弱,不過這本來就只是用劇本殺的”皮”在帶敏捷活動

  • 不是很是推薦你玩太多次,不過聽說有人玩 15 次,可能遇到很棒的導演

  • 玩之前很期待,玩之後覺得不符期待

End 作得不是很好,不如直接來個 Retro ,
如果沒有在活動中觸動參與者的內心,結尾很容易變成教條式的宣導,
然後還要賽認証機構的工商與贊助場地的招募,7~10點真正有價值的活動可能只有 1.5 小時
當然如果有人因此去考証或換工作或許對他們就是有價值的,只是對我來說就不符期待了

(以下可能含雷,請考慮是否繼續)
(以下可能含雷,請考慮是否繼續)
(以下可能含雷,請考慮是否繼續)

流程

  1. 入場到開場 : 小 delay 但是台灣人遲 30 分鐘內不算 ?

  2. 玩家 : 部份玩家應該是有玩過的, 但是沒有什麼引導新手進入遊戲的效果

  3. 選角 : 自由選角,因為可以掃 QR Code 看內容, 所以其實可以知道其它角色的人設, 但我個人是盲選

  4. 破冰 : 有結合後面劇本殺的內容這點不錯,但是效果我覺得沒有很好

  5. 分組 :

    • (我猜測)本來應該是遊戲的一環,但因為砍掉 30 min 所以改用事先分
    • 印象中職位有:PM、福委、設計、前端、後端、DevOps、測試(有嗎?)
    • 除了 PM、福委有其目的性以外,其它的職能分配在遊戲中意義不大
  6. 目的說明 : 呃…只是把投影片唸過一遍, 其實沒能幫助玩家更進入角色與遊戲

  7. Yes, & : 有特別說明這件事, 但不知道其用意

    • 我猜為了避免遊戲中出現 Yes Man 或 No Man)
    • 實際上進行遊戲中沒有人遵循
  8. 熱身 : 時間明顯控制不當,後面基本上都講一半就會強制打斷(也許透過資深玩家分小組進行可以縮短時間)

  9. 開始遊戲 :

    • 一開始的混亂是一定的
    • 然後來會有人跳出來領導(不見得是 PM)
    • 大部份的人只會看
    • 並沒有因角色的利益衝突演化出有趣的劇情/對話
  10. 結尾

    • 由主持人作 Ending 不如讓台下發表他們的覺察
    • 遊戲的目標其實沒有什麼難度(因為都可以靠幻想補足)
    • 前端、後端、DevOps、設計的職位設計沒什麼用(不確定別場會如何應用?)

檢討

社群的活動品質逐年下滑早有所聞,但那我就作個個人記錄吧
這活動是我覺得有點可惜, 希望下次能更好.
如果是我會怎麼作?

  • 我會在公司或朋友圈先試行
  • 我會專注在劇情的發展過程(或是提醒演員「你在演戲」)
  • 我不用那個破冰方式讓遊戲內容細節化
  • 我會作問卷回收回饋
  • 掌控時間
  • 儘量確保參與者有進入狀況

至於我個人

  • 不要看到標題就急著報名
  • 事前也可許可以 Google 一下
  • 想像一下自已是導演的話應該怎麼辦
  • 多參與不同的對話
  • 確保時間、場地以及內容的完整性

中國社群的心得

參考

(fin)

[實作筆記] 簡單工廠與工廠方法

前情提要

最近被要求介紹一下 Factory Method 這個 Design Pattern,
以前看大話設計模式的時候,這個 Pattern 總會跟 Simple Factory 一起講.
有了一些工作經驗後,現在回頭來重新看這兩個模式.

問題

我們為什麼需這個模式 ? 我們面臨什麼樣子的問題 ?
參考以下程式:

1
2
3
4
5
6
7
public class Email:INotification
{
void Send(string message)
{
////Send Mail
}
}

先說明一下這個程式,
在整個系統中依據不同情況中,會發送不同的通知,
比如說:電子郵件(Email)、語音電話(Voice Call)或通訊軟體(Slack)等…

所以我們實際使用的場景可能如下:

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
public class ProcessNotifyCustomerService()
{
var notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";

notify.Send(msg);
//// ....
}

public class ProcessNotifyDevOps()
{
var notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";

notify.Send(msg);
//// ....
}

public class ProcessNotifyManager()
{
var notify = new VoiceCall();
notify.server = "voiceCall.server";
notify.port = "9527";

notify.Send(msg);
//// ....
}

//// more ...

以上面的例子來說明一下簡單工廠想解決的問題:
當我們在整個系統中使用不同的 Notification 時,
我們建立整個物件的細節都在 Client 端之中,
這是非常擾人的, 特別當你要建立個複雜的物件,
你肯定不會希望每次都要重來一遍.
第一個想法就是把細節封裝起來,如下:

1
2
3
4
5
6
7
8
9
public class EmailFactory()
{
public static Email Create(){
Email notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";
return Email;
}
}

再進一步, 當我們有相同的行為時也可以封裝到簡單工廠之中,
在我們的例子中, 我們可以把 Email 與 VoiceCall 放在同一個工廠裡面
在這裡我們命名為 NotifyFactory

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
public class NotifyFactory()
{
public static INotification Create(string type){

INotification notify;
switch (type) {
case "DevOpsEmail":
notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";
return notify;

case "CustomerServiceEmail":
notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";
return notify;

case "VoiceCall":
notify = new VoiceCall();
notify.server = "voiceCall.server";
notify.port = "9527";
return notify;

default:
throw new UnsupportedOperationException("不支援該操作");
}
}
}

簡單工廠的好處在於,
當你想改變某一個功能時你只需要修改一個點,
而且你在改動的過程入無需再次涉入建構的細節

比如說:發信通知改為播打語音電話

只需要修改Client,無需處理建構細節

而另一個好處是,如果在建立物件的細節有所調整的話,
可以只要在一處就完成所有修正.

比如說: Email Notify 更換為客服的信箱

只需要改一個地方,就完成所有的修正

工廠方法

當我們需要加入(擴展)新的商業邏輯,會修改到不止一個地方
舉例來說:
我要加入一種新的通知叫飛鴿傳書(PigeonNotify)好了,
除了修改 Client 端使用工廠建立新的 notify 外,
也要在簡單工廠裡面修改。

1
2
3
4
5
6
7
8
9
public class EmailFactory()
{
public static Email Create(){
Email notify = new Email();
notify.from = "[email protected]";
notify.to = "[email protected]";
return Email;
}
}

再進一步, 當我們有相同的行為時也可以封裝到簡單工廠之中,
在我們的例子中, 我們可以把 Email 與 VoiceCall 放在同一個工廠裡面
在這裡我們命名為 NotifyFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class NotifyFactory()
{
public static INotification Create(string type){

INotification notify;
switch (type) {
//// 中間省略
case "Pigeon":
return new PigeonNotify();
default:
throw new UnsupportedOperationException("不支援該操作");
}
}
}

面對功能的擴展,也需要修改工廠的邏輯

簡單工廠中的 Switch 會與參數耦合,
每次加入一個新的 Notify 都會異動,
這違反開放封閉原則.
改用工廠方法, 我們只需要新增一個新的工廠方法,
並在 Client 呼叫使用工廠, 不再傳遞任何參數.

Client 相依於工廠介面上,需要呼叫指定工廠取得物件

TDD to Simple Factory

目的,透過 TDD 建立出不同的通知(Notification)類別,
再透過 TDD 趨動簡單工廠,再透過重構改變為工廠方法。

  1. 利用測試作為 Client 寫出測試案例

  2. 測試案例先簡單使用 new 通過測試

    • 因為是概念性的測試,所以會缺乏實作細節,實務上可能會不只有 new

    到這一步只是一般建立物件,
    下一步開始是趨動成為簡單工廠,
    但實際上你是可以跳過簡單工廠,直接 TDD 出工廠方法的

  3. 再寫一個測試案例,來製造(Notify)功能的重複(Email、SNS)

  4. 功能重複讓我們可以抽出介面

  5. 建立簡單工廠使用邏輯分支回傳不同的(Notify)功能實作

  6. 在簡單工廠的邏輯分支使用不同工廠方法實作

    • 因為封裝了實作細節,方法簽章應該不需要任何參數,回傳值應該為 void

    下一步開始趨動成為工廠方法

  7. 讓 Client 端直接呼叫不同的工廠

    • 簡單工廠類別就會變成多餘無用的類別
  8. 因為有相同的方法簽章,所以可以抽出工廠介面

  9. 讓 Client 端相依工廠介面

可以參考以下我的 commit 順序:

參考

(fin)

[實作筆記] 怎麼建立一個網站?(五) - Hexo 的 404 頁面

What

什麼是 404 頁面呢 ?
其實這是一個 HTTP 狀態碼,代表「網頁不存在」,
與另外一種常見錯誤代碼(500/503),代表的意義並不相同,
500/503 通常是指服務整個掛了,
而 404 是指所要的資源(頁面、檔案、圖片)並不存在.

以下提供一些常見的狀態碼與代表意義

  • 200 OK
  • 403 Forbidden
  • 500 Internal Server Error
  • 503 Service Unavailable

Why

為什麼我們需要一個 404 頁面呢 ?
我直接引述

當使用者不小心進入你某些不存在或者有錯誤的頁面,就會跳出這個 404 頁面,(中略…)
而這個頁面最大的用途在於增加使用者體驗,例如畫面上會有 Logo or 返回首頁按鈕,確保使用者不會因為看到這個頁面立刻關閉。
(中略…)
搜尋引擎也會依照你是否有這個當作一個加分評比 (中略…)

How

首先執行 hexo new page 404 , 這是 Hexo 用來建立新頁面的語法,
上述的語法執行後, 會產生一個 404.md 檔, 如果你不喜歡這個檔名你也可以換掉,
ex: hexo new page page_not_found

產生的頁面如下:

1
2
3
4
5
6

---
title: page_not_found
date: 2021-04-06 10:36:15
---

這個時候我們需要調整一個重要屬性 permalink: /404.html,
這樣當 Github Page 找不到頁面時,
才會出現我們設定的 404 頁面.

另外我們可以設定 layout:true 的布林值來決定是否套用原本的樣式(預設為 true),
這個主要也是為了讓使用者”感覺”他仍然在同一個網站之中,
而減低跳轉率.

下面可以使用 markdown 語法編輯

參考

(fin)

[生活筆記] 將來銀行與純網銀一些有的沒的

Next Bank

前情提要

將來銀行是台灣三大純網銀(Line、樂天)中,
官股成份最重的一間, 也因此備受各界期待.
2019 起成立籌備處, 大舉徵才…預計 2021 取得營業執照…
2020 年 12 月樂天取得營業執照,
2021 年 2 月LINE BANK取得營業執照

一些爭議

取得執照

2019 金管會審核三家純網銀皆取得執照後,
中時的報導,「國家隊吊車尾上榜 「將來」銀行真有將來?」
經濟日報的平衡報導 「純網銀審查幕後 將來銀行不是最後一名 」
是不是有點欲蓋彌彰 ? 相關報導只剩 PTT 的留存

人事

總經理劉奕成,號稱金融界跳跳虎(求出處,意指常轉換工作)
2018 LINE拚純網銀陣前換將,劉奕成確認離職,陳立人暫代,
2019 加入將來銀行籌備處,同年被爆高層團隊領高薪,
2021 年 3 月離職. 22日臉書首次公開貼文,
內文包含目前狀況

  • 資訊系統不穩定
  • 只有一半不到的人有金融背景
  • 資訊人員不會寫程式

性騷擾的事件

將來銀行爆性騷案處理不當 女經理患憂鬱症被離職【瞎事多1】

一些人看法

金管會主委黃天牧

將來銀行想要將來 有 3 點需讓金管會滿意

  • 營運制度的改善情況
  • 人事安排
  • ??

臉書一些回應

雷神講堂

將來銀行的徵才資訊中, 針對「資安維運中心工程師 SOC Engineer」的工作內容,
起薪僅25K至65K,且須配合輪夜班……

C 君

突然懂了什麼 #領7000萬的靠背2萬5不懂開發

R 君

不懂 Domain Knowhow 的話 JD 不要亂寫,
寫一堆又給不起。

更多…

我的看法

首先我覺得將來銀行的受到的鎂光燈好像比 Line 與樂天還多,
當然也可能是我的同溫層太厚的關係.
另外在將來銀行的官網或臉書上, 我找不到正式的公關回應.
所以不太確定新聞提到的將來銀行的回應是出自何處?
總之目前還有點渾濁, 我覺得要讓子彈再飛一會兒.

相比而言 Line 與樂天的新聞就少很多,
一路看下來我覺得比較像是一場只發生在將來銀行的媒體/行銷戰,
就我而言將來銀行的 Logo 或是營銷手段是迎合年輕人的,
但是我不了解的是,一個美美的官網或臉書你只用來介紹 Logo 的設計理念跟找網紅拍小短片 ?
撰寫本篇 Blog 時的臉書最後一篇文章是在跟風鮭魚時事哏,
而不對一些傳媒的新聞作出回應 ? (可能是我看漏,有人可以提供給我的話十分感謝)
有種「行銷成功卻公關完敗」的感覺.

想說說開發相關的問題, 但覺得人事可能也是一個大問題,
先說基層吧,將銀明顯是需要即戰力的,
但就我的面試經驗而言, 技術主管連使用什麼技術作為基本的架構將銀都無法決定,
在招募上是說你會什麼都可以, 但是你總得要團隊作戰,
你的團隊要用什麼戰鬥方式會影響你的戰術, 怎麼可以不確定呢 ?
招募進來的人員不會寫程式, 那招募者是不是有問題呢 ?
開發的產品有問題, 但總之金管會至少是有個產品可以被審核,
如果以劉奕成所言, 相比其它兩家, 將銀少了 10~15 月籌備時間,
那可以等待一段時間後再作評論, 只是以市場的角度來說算是失了先機;
開發人員一半非金融背景, 這是也是當初籌備招募的亮,
如果沒有好好整合, 甚至在團隊分派系才是糟糕的
現在有點以成敗論英雄.
再來,將銀在短短的 2 年招募了 300 人, 雖說不能確定是否有達到這計劃的人數,
即使只有一半 150 人也是碰到了鄧巴數,
這肯定會有相當的衝突.
有人的地方就有江湖, 這裡還是讓子彈繼續飛吧.

「敏捷」不 ? 我反而覺得是個假議題, 特別是在籌備了這麼久後的現在,

金融背景的人,會認為系統必須內部確認 100% 沒有問題,
再請金管會來審查,但是部分資訊部門的人卻認為,
就算有一些 Bug,也沒什麼關係,事後再來修改就好了。 — <<數位時代>>

上述這段話是來自數位時代的新聞, 也是敏捷圈最多引用一段話,
但這些自稱敏捷的人會露出一種「笑他的迂」的態度,覺得金融背景不懂敏捷 ?
這個態度讓我覺得十分荒謬.
我僅以下四點作為回應:

  • 個人與互動重於流程與工具
  • 可用的軟體重於詳盡的文件
  • 與客戶合作重於合約協商 
  • 回應變化重於遵循計劃

其實將銀在招募的過程說過因為沒包伏, 所以可以建立更敏捷的文化.
但如果新聞內容為真, 與其說金融背景的人不懂敏捷,
不如說這些當初號稱敏捷進入將銀的人, 並沒有成功的建立真正的敏捷文化.
將內部 100% 確認轉化成有明確事項的 PBI ,迭代執行檢驗,
隨這一年

金管會的回應中規中矩, 就不予置評了.

參考

(fin)

[A 社筆記] Sudo 與環境變數

前情提要

使用 Azure 的 pipeline 對專案進行 CI/CD,
在某一段執行語法中,因權限不足無法建立必要的資料夾,
而導致部署失敗。
有兩個思路,一個是讓現有的使用者擁有建立資料夾的權限,
另一個想法比較單純,使用 sudo 提供足夠的權限給 CI/CD 的執行者。

問題

但是 sudo 引發了另一個問題,
設定在 pipeline 的環境變數消失了,
原因是當我們在使用 sudo 的時候,會影響到環境變數。
這個時候需要參考 /etc/sudoers 的設定

env_reset

  • env_check # 當變數含有不安全字元 %/ 會被移除
  • env_keep # 當 env_reset 為 enable 時要保留的一系列環境變數。
  • env_delete # 當 env_reset 為 enable 時要移除的一系列環境變數。
1
2
3
4
5
6
7
8
9
env_reset   If set, sudo will reset the environment to only contain
the LOGNAME, SHELL, USER, USERNAME and the SUDO_*
variables. Any variables in the caller’s environment
that match the `env_keep` and `env_check` lists are then
added. The default contents of the `env_keep` and
`env_check` lists are displayed when sudo is run by root
with the -V option. If the secure_path option is set,
its value will be used for the PATH environment
variable. This flag is on by default.

解決方法

最簡單的方法加上-E, 將 User 的環境變數先載入

1
2
3
4
The -E (preserve environment) option indicates to the security policy 
that the user wishes to preserve their existing environment variables.
The security policy may return an error if the -E option is specified
and the user does not have permission to preserve the environment.

參考

(fin)

[生活筆記] 我對單元測試的想法

問題

你對所謂 “單元” 測試的想法爲何。

想法

單元測試是一種有效的方法、手段、工具 etc…
目的是為了「品質」,從兩個角度來說:

  1. 對客戶來說: 功能符合需求
  2. *對開發者來說: 好擴充易修改

額外的好處:

  • 快速揪錯、快速回饋 : 如果團隊有執行 UT 或是 TDD 的習慣,在開發過程中就可以發現部份的錯誤
  • Test Case 就是文件、就是 Use Case
  • TDD 要從 UT 開始寫,讓開發者優先考慮與其互動的 Service 或 Module 會怎麼使用這個方法
  • UT 是重構的基礎,有了 UT 作保護網,可以大膽重構
  • *重構才能往 Design Pattern 的方向走

Mindset

如上圖,
但是我現在卡在 Design 的部份,
我可以讓測試趨動「開發」,
但是沒有辦法產生良好的設計。

所以接觸了 DDD (一知半解的狀態),
然後想到以前學過的 Design Pattern ,但是實務上並沒有套用得很靈活,
很多時候為了 DP 而 DP, 而重構我可以作到小幅度的重構, 精簡程式碼
但是如果要重構成另一個 Pattern 時就又有點卡住了,所以我想我可能沒有掌握住軟體 Design 的技巧

反思與小結

最近參加了 Implementing Domain-driven Design 的讀書會,
像是導師所說,學習了單元測試與 TDD 後,
試著應用在實務上還是有所困難的,
所以我刻意建立了一些專案用來學習。

Test First 或 TDD 不應該省略設計的部份,
Domain-driven Design 常被縮寫成 DDD,
TDD 則為 Test-Driven Development
而當華語文人士整天說著 ATDD、BDD、DDD 與 TDD 時,
有注意到這個 D(Design) 不是 D(Development) 嗎 ?

進一步來說,TDD 的要求開發之前先寫測試,意味著要先寫測試案例,
這個步驟會讓你思考「你要怎麼呼叫你的代碼」,也就是說「你要如何設計的代碼」,

接下來,我會用一個購物車的開發作為案例,
試著用這個過程找到自已的盲點。
購物車 Sample

購物車的畫面如上,我會使用 C# 的 ASP.NET Core 進行開發,
雖然我會延用 ASP.NET 所提供的 MVC 框架,但我也會試著使用 DDD 的概念去設計,
我會設計一系列的 Domain Model 並且使用 Domain Service 作為隔離,
MVC 的 Controller 我會視為 DDD 的 Application Service,
這裡會出現 View Model,不同於 Domain Model,View Model 由 UI 所需要的資料決定。
更多的細節會記錄在後續的筆記當中。

DDD

(fin)

[實作筆記] SonarCloud Move analysis to Java 11 更新 Github Action

前情提要

長期使用的 SonarCloud 的 Github Action 突然執行失敗了.
原因是 SonarCloud 在 2021 年的 2 月 1 號開始,不再支援使用舊的 Java 版本(1.8.0_282),
至少要更新至 Java 11.
錯誤的訊息如下:

1
2
3
4
5
6
7
8
9
10
11
INFO: ------------------------------------------------------------------------

The version of Java (1.8.0_282) you have used to run this analysis is deprecated and we stopped accepting it. Please update to at least Java 11.
Temporarily you can set the property 'sonar.scanner.force-deprecated-java-version-grace-period' to 'true' to continue using Java 1.8.0_282
This will only work until Mon Feb 15 09:00:00 UTC 2021, afterwards all scans will fail.
You can find more information here: https://sonarcloud.io/documentation/upcoming/

ERROR:
The SonarScanner did not complete successfully
08:24:36.417 Post-processing failed. Exit code: 1
Error: Process completed with exit code 1.

調整方法

移除掉原本的 SonarScanner

1
2
3
4
5
- name: SonarScanner Begin
run: dotnet sonarscanner begin /k:"Marsen.NetCore.Dojo" /o:"marsen-github" /d:"sonar.host.url=https://sonarcloud.io" /d:"sonar.login="$SONAR_LOGIN
## 中略
- name: SonarScanner End
run: dotnet sonarscanner end /d:"sonar.login="$SONAR_LOGIN

在整個方案的根目錄加上一個檔案sonar-project.properties,

1
2
3
4
5
6
sonar.organization=<replace with your SonarCloud organization key>
sonar.projectKey=<replace with the key generated when setting up the project on SonarCloud>

# relative paths to source directories. More details and properties are described
# in https://sonarcloud.io/documentation/project-administration/narrowing-the-focus/
sonar.sources=.

sonar.organization 要如何取得呢 ?
登入 SonarCloud, 右上角頭像 > My Organizations 即可查詢到 Organization Key 值。
Organization Key

上方 My Projects > Administration > Update Key
即可查詢到 Project Key 值
Project Key

最後在 Github Action Workflow 加上這段,Github Action 就復活啦

1
2
3
4
- uses: sonarsource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

參考

(fin)

Please enable JavaScript to view the LikeCoin. :P