[生活筆記] 拋棄 LastPass 擁抱 BitWarden

前情提要

2021 年 3 月, 看到了一則新聞, 指出 LastPass 將改變它們的經營策略,
免費版本將只支援一種裝置, 行動裝置或電腦。
產品收費是天經地義, 但消費者找尋更高性價比的產品也是。
讓我想離開 LastPass 的主因在於, 突襲式的宣布讓我幾乎沒有反應時間,
免費版的限制太過苛刻, 一樣在限制裝置的功能上,
EverNote 這款筆記軟體也是逐步降低免費版本可跨平台的數量。
但是 LastPass 更狠, 瞬間降到僅限一種, 這基本上是在玩文字遊戲,
因為大多數的人應該都同時具備手機與個人電腦, 一般上族班只能被迫選擇電腦,
或是付費了事。付費當然不是問題, 但宣告期極短且粗爆的方式讓人覺得被當韭菜割。

當我稍微作了一下功課, 發現這類的服務已經多如繁星, 甚至內建在作業系統或是瀏覽器之中。
再進一步比較功能, 與自已所需要的功能(密碼保管、密碼產生器、跨裝置etc…)
有更多免費的軟體已經能滿足我的需求,比如說: BitWarden

比較心得

BitWarden 的好處,

  1. 免費帳戶即可跨裝置存取密碼
  2. 可以匯入 LastPass 匯出的密碼
  3. 有密碼產生器
  4. 有瀏覽器外掛,支援自動填入
  5. 開源方案

最後一點我是覺得最吸引我的地方,這表示如果你有一定的程式語言能力,
你將可以自行搭建一個密碼管理的伺服器,這點甚至對企業也是有相當的吸引力,
而 BitWarden 使用的語言為 TypeScript、C# 與 Python。

在轉換上是非常容易的,可以在一小時內完成這項作業
實務操作上也沒有遇到太大的困難,除了原本 LastPass 右鍵產生密碼的方式,在 BitWarden 上面沒有外,
其它功能或是外觀設計上,我都覺得 BitWarden 的表現更加優秀,
甚至有點相見恨晚的感覺。

至於沒有的功能,不是什麼大事,而且開源專案誰都可替他加上這個功能。
但對我來說,我已經忘了 LastPass 有什麼優點,
轉移到了 BitWarden 6 個月,從未再次開啟,於是今天正式將帳號刪除了。

參考

(fin)

[實作筆記] Azure Functions 存取 Key Vault

前情提要

新的工作有機會接觸到微軟的 Azure Cloud 服務,
包含 AWS 、 GCP 的話,世界的三大雲服務都碰過一輪了,
不過也只是皮毛而已, 紀錄下來免得遺忘 .

說明一下需求,在實作授權的

在這裡我會使用到 Azure Cloud 的三個服務,

  1. Azure Functions
  2. Managed Identity
  3. Key Vault

使用的服務簡介 TL;DR

說明一下我理解的三個服務:

Azure Functions 是微軟無伺服器(Serverless),
Serverless 的概念是讓開發人員專注開發, 減少對伺服器維運的成本,
並且提供極度彈性的可擴展性,並且可以使用更少的資源(更低的成本),
最大的限制是運算時間(timeout),
但就我個人而言的 Best Practice 在 300 秒內完成不了的運算, 應考量整體架構的瓶頸在哪裡,
而不是採取無限制的付費方案(Plan Type)。
稍微作個 ORK 評量,應該在 0.2 | 10 | 300 秒內完成計算。

Managed Identity 是微軟用來作身份認証與授權的服務,
與其它雲服務最大的差別應該是它是基於 Azure Active Directory (Azure AD) 之上,
一般來說會建議使用遵循(Role-based Access Control),
下面的圖片很好的說明了其概念。

如何使用適用於 Azure 資源的受控識別?

Key Vault 是個相對簡單的概念, 在開發中我們會接觸到需多的密鑰、証書、連線字串等等資訊…
這些資訊有敏感性, 但對開發過程又不是最重要的東西,
這些資訊在單體架構(Monolithic)時常常面臨開發與資安的兩難,
而 Key Vault 可以解決這個問題(當然不同的 Cloud 也有類似的解決方案)

案例說明

這次的案例,使用者會在瀏覽器提供給我們一組 Token ,
而我們會拿這組 Token 與第三方互動換回我們需要的 Key,
最後我們會把這組 Key 存進 Key Vault 給其它服務使用.
流程簡介

你可能會好奇, 在上圖中 Managed Identity 扮演的角色為何 ?
就是在 Function Apps 存取 Key Vault 這一段,
我們將提供一組 Identity 給 Function Apps 讓它有權限存取 Key Vault .

實作

我將步驟大致分為以下幾步

  1. 建立 Function App 與 function
  2. 啟用 Function App 的 Managed Identity
  3. 建立 Key Vault
  4. 設定 Key Vault Access policies
  5. 設定 Function App Configuration
  6. 修改 Function

讓我們開始吧.

建立 Function Apps 與 function

開始前先作名詞解釋, 因為微軟的命名會讓剛接觸的人十分混淆

  • Azure Functions : 微軟的 Serverless 產品名稱, 我們可以在這個服務下建立許多 Function Apps 的實體
  • Functions Apps : 主要運作的實體, 也是我們要設定的地方,每個 App 裡面可以有多個 function
  • Functions : 主要邏輯所在的地方, 以開發的角度來說, 我們應該只關心這塊

建立 Function Apps 的步驟較為簡單,請依照微軟文件即可,
接下來選擇 Function Apps > 選擇剛剛建立的 Functions App > Create > Http Trigger C#,
選擇這個範本我們將以 C# 語言實作,授權等級我選擇 Anonymous .

建立好後,可以在 Code + Test 裡面查看預設的程式.
這裡建立的是 C# 指令碼, 開發上的細節請參考官方文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System.Net;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");

// parse query parameter
string name = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
.Value;

if (name == null)
{
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
name = data?.name;
}

return name == null
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
: req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
}

啟用 Function App 的 Managed Identity

移動到 Functions Apps 的 Settings > Identity,
可以看 System assigned > Status 將它切換為 On.
這一步的設定是為了讓服務之間可以受到 AAD 控管而不需要你自行處理.

建立 Key Vault

在建立之前也是需要一些名詞解釋,

  • Key Vault: 微軟的服務名稱
  • key vault section: Key Vault 的實體區塊(可以想像成一個實體檔案,裡面可以存很多 Key, 並且我們可以對這個檔案進一步設定,也可以參考 官方的文件快速建立)
  • Keys : 在 key vault section 的 settings 中有三種類型的資料,Keys、Secrets 與 Certificates,

找到 Key Vault > key vault section > Settings > Secrets , 建立一組 Secret

建立 Key Vault

點選剛剛建立的 Secret 並且選擇 CURRENT VERSION 取得 Secret Identifier(複製下來, 待會會用到)

設定 Key Vault Access policies

一樣在 Key Vault > Settings > Access policies > Add access policy ,
在 Select principal 找到剛剛建立的 Function App,
在 Configure from template (optional) 我們直接套用 Secret Management,
這允許我們的 Function App 會取得管理 Key Vault Secret 的權限.

設定 Function App Configuration

移動到 Function App > Settings > Configuration > Application settings
我們要在這裡新加一個 Application Setting

Name 自已取但是請記得,我們稍後就會用到,這裡我先命名為 TestKV,
Value 請參加下面的範例, 將前面步驟取得的 Secret Identifier 填到 SecretUri 之後.

1
@Microsoft.KeyVault(SecretUri=https://mykv.vault.azure.net/secrets/RefreshToken/xxxx)

這裡有小朋友問我, 那為什麼不把資料設定在 Function App Configuration 就好,
這樣子開發人員也碰不到資料, 要修改也是很有彈性的.
我的回答是, 這樣子其它的服務也需要這個資料怎麼辦呢?
Copy-Paste 會造成維護上很大的困難, 我們會希望維持一組就好.
放在 Key Vault 可以讓我們不同的服務共用.
Overview

反思 Best Practice 應該怎麼作???
設定檔散落在不同的層級, 還是應該集中管理, 再用 Config 橋接???

修改 Function 的代碼

下列我們用 TestKV 這組設定與 C# 作為範例 :
透過環境變數取得 Key Vault 的資料

1
2
3
// Get
var testKV =
Environment.GetEnvironmentVariable("TestKV", EnvironmentVariableTarget.Process);

而如果你需要寫入請使用 Azure SDK Client Library

1
2
3
4
5
6
7
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
/// Skip ...
/// Set
var kvUri = $"https://{KeyVaultName}.vault.azure.net";
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
client.SetSecret($"{SecretKey}", $"{SecretValue}");

其它語言可以參考

參考

(fin)

[實作筆記] 自我簽署憑証, localhost run with https

前情提要

最近有一批學生在學習 Express 開發網站,
他們需要透過 facebook 應用程式作登入。
遇到了以下的問題
Facebook 偵測到 所使用的網連線並不安全

最簡單的解決方式,重新選擇你的 Facebook Application Type 為無

如下:
!Facebook Application Type 選擇無
這樣足以解決學生們在練習開發上的問題了。

不過我有點好奇背後的真正原因是什麼 ?

Facebook 的政策

Facebook 的文件說明

使用 HTTPS

使用具有加密功能的 HTTPS 作為網際網路通訊協定,而非 HTTP。HTTPS 會維護傳送資料的隱私,保護其不受竊聽攻擊。
此外,也能保護資料在傳送過程中不遭到置入廣告或惡意程式碼的竄改。

在 2018 年 10 月 6 日,所有應用程式都必須使用 HTTPS。

可能是一個原因,因為學生們的開發環境建置出來都是單純的 http ,同學也有作出相關的猜測。
那麼我們要怎麼快速驗証這個想法呢 ?

https-localhost

這是一個最快速的驗証方法,不過需要修改部份的代碼,而且遮蓋了需多細節。
以下以 MacOS 操作為主

首先,需要安裝相依的 NSS Tool

brew install nss

安裝 https-localhost

npm i https-localhost

改寫在 Express 網站的 app.js

1
2
//const app = express()
const app = require("https-localhost")();

基本這樣就完成了。

  • 優點
    • 改動步驟少
    • 無需理解細節
  • 缺點
    • 要改動原始代碼

自我簽署憑証

接下來我們會深入一點細節,並了解背後的機制。

憑証機制簡介

我不打算從 TCP、Http 講解到 Https,網路上已有相當多的資訊,
或是未來有機會再補充。
總而言之,當你使用 Https 時,有一個很重要的觀念是,
你的 Server 應該提供一組公鑰給 User 作為資料的加解密。
那延伸的問題就是怎麼確認這組公鑰是可以信任的呢?

簡單說就是透過第三方的公正單位進行認証,所謂憑証認証機構(CA)。
我們相信這些機構,而機構也會幫我們進行審核。
而一般來說你的應用程式,比如說瀏覽器會先安裝好可信任的根憑証。

一個經典的說明是這樣的:

鮑伯可以隨便把憑證向外發布。
鮑伯與愛麗絲事先可能互不認識,但鮑伯與愛麗絲都信任伊凡,
愛麗絲使用認證機構伊凡的公鑰驗證數位簽章,如果驗證成功,便可以信任鮑勃的公鑰是真正屬於鮑伯的。[1]
愛麗絲可以使用憑證上的鮑勃的公鑰加密明文,得到密文並傳送給鮑伯。
鮑伯可以可以用自己的私鑰把密文解密,得到明文。

以開發者而言,你可以自我簽署憑証,
這裡你要同時扮演鮑伯,愛麗絲與伊凡,
所以常有人會在其中迷失方向,角色錯亂。

伊凡(local CA)

伊凡是一個 CA,我們只要安裝 mkcert
就可以建自一個本地端的 CA

brew install mkcert

我們來對 localhost 發行憑証

mkcert localhost

會產生下列兩個檔案,
分別為憑証 localhost.pem
與私錀 localhost-key.pem

鮑伯(Server)

這個時候我們角色切換至鮑伯(Server)
鮑伯在這個過程中最重要的發行公錀,
私錀是用來加密傳輸的資料的。

這裡需要修改一下 Server 的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require("fs");
const https = require("https");

/// 中略…

https
.createServer(
{
key: fs.readFileSync("localhost-key.pem"),
cert: fs.readFileSync("localhost.pem"),
},
app
)
.listen(3000, function () {
console.log(
"Example app listening on port 3000! Go to https://localhost:3000/"
);
});

這樣我們就可以允許愛麗絲透過 Https 與我們溝通囉。

愛麗絲(Client)

基本你只要開啟瀏覽器,輸入 https://localhost:3000/ 你就可以正常瀏覽了。
但我們要了解背後發生的過程,

安裝 mkcert 完畢後,其實會在你的機器(與瀏覽器)上安裝根憑証,
這樣愛麗絲才會信任伊凡
可以透過以下語法查詢安裝路徑(不同 OS 會有差異)

mkcert -CAROOT

這個作法改動的程式也不會比較少,
但至少不再有黑魔法(當然你也可以去追蹤 https-localhost 的代碼)

Recap

快速回顧一下 mkcert 作了哪些事

  1. 建立一個 local CA
  2. 替你的瀏覽器安裝 CA
  3. 對 localhost 發行憑証

而你的程式需要

  • 改使用 https 啟動網站

別忘了解決學生問題

回到 facebook 應用程式登入的問題,
別忘了去設定 callback 網址,並且確定是走 https
用戶端 OAuth 設定 > 有效的 OAuth 重新導向 URI
不然會得到以下錯誤

忘了設定 callback URL

小計

文末的聯結有需多是透過 OpenSSL 的作法供參考,
但是會多一步驟需要安裝手動憑証(愛麗絲該作的事)。

另外仍然需要修改程式這點我不是很滿意,
應該可以掛上其它的 Proxy Server,由 Proxy Server 處理 https 就好,
內網單純走 http。
最近也有看到零信任網路
不過那應該是另一個議題了

Zero Trust 零信任架構近年來日受重視,在 Zero Trust 框架底下,
即便使用者在內網環境,也不會給予沒有期效或過高之存取權限。

參考

(fin)

[活動筆記] 利用單元測試來開發『猜數字』小遊戲

前情提要

一直有在收集一些 Kata 用來練習 TDD ,
剛好台中的敏捷社群有這樣的活動, 第一時間就報名參加,
也很幸運的搶到票, 下面是我的記錄與一些心得反饋。
不過我沒有 Php 的經驗, 或許會有一些偏誤。

希望有機會可以整理出一系列的 Kata ,
讓人可以在學習 Coding 過程中將 Testing 變成一種首選(反射行為、自然反應 etc …)
可以參考我之前的 Blog 從 TDD 到 TDD, Todo 到 Test 趨動開發

活動流程

  • 主持人簡單介紹, 並邀請大家輪流發言, 說明參加的目的

    這個引導技巧蠻好的, 有 Check-In 與 Warm-up 的效果
    雖然是還是有人不發言或是後續引發的討論有點少(直接用肯定句結束對話)
    但是相信多試幾次是可以改善的

  • 主講者介紹題目
  • 主講者實作題目
    • 拆分「猜數字」與「隨機數」
    • 使用 Injection 的技巧
    • 解題
  • 分組解「隨機數」
  • 主講者講解隨機數
  • 問問題

心得

看到各地開始有不同的講者來說明 Unit Test 或 TDD 感覺是很棒的,
希望能夠有更多的社群與講者來介紹. 這個工程師的基本技能

在講解的過程中, 可能是我誤會了, 但是我感覺到講者不確定是不是能講更多,
畢竟測試、單元測試(Unit Test)、測試趨動開發(TDD)的重心都有一些不同,
也許在報名前作一次篩選, 或是同時準備基本進階的教材,
Check-In 的階段, 或隨著課程進行由淺入深的介紹,
就以猜數字來說 Injection 也許可以放入進階的部份再介紹,
反而有位與會者在 Check-In 提出的問題我覺得蠻好的。

如何對猜數字這個題目, 進行需求分析, 可惜沒有進一步的研討。
第二部份的猜數字, 包含了隨機的部份, 分組討論我沒有參與到其它組別,
不太確定討論的效果為何? 但是在我的組別是有人完全不說話的,
總之線上活動與會者的參予是非常重要的,
另外在實作猜數字的測試案例時, 講者使用了我們這一組的代碼,
講者似乎有不同的看法, 它將 3 個案例合併成了 1 個,
但是在移除案例時, 並沒有實測可以保護原有案例的部份.
這裡可以處理的更細緻一些.

最後如果能有完整流程的 commits 與錄影的話更好,
當然主辦與講者可能有不同的考量就不予置評了.

用 C# 重新跑一次 Kata

我打算用 C# 重新進行一次這個 Kata 並稍作記錄

規則說明

猜數字是一個經典的遊戲, 分為出題者與答題者,
出題者必需隨機設定一組四碼不重複的數字, 答題者嘗試去猜出正確的數字,
如果答題者猜對數字且位置正確, 出題者必需給一個 A,
如果答題者猜對數字但位置不正確, 出題者必需給一個 B,
如果 4A0B 就代表答對, 遊戲結束.

設計

出題的部份, 我們可以開發給出題者輸入, 或是隨機產生題目,
隨機出題的部份我們可以拉出來當另一個功能, 所以我們先專注在猜數字的邏輯上
先是 A 的邏輯, 表示位置與數字相同, 所以我會需要定位與數字比較
B 的邏輯, 表示位置與數字相同, 這裡我會發現不存在數字比較的邏輯,
而是字串或字符的比對.
大概就這樣, 很簡單來設計我們的 Case 吧

猜數字案例

前設條件:答案為1234

  1. 5678 應該得到 0A0B : 目的是產生方法的介面與參數
  2. 1678 應該得到 1A0B : 最簡單的方式可以檢查第 1 個數字是否符合
  3. 1278 應該得到 2A0B : 最簡單的方式可以檢查第 2 個數字是否符合, 產生重複
  4. 重構 2、3 案例, 消重複使用 ACounter 作為記錄
  5. 1238 應該得到 3A0B : 目的是確定上面的重構有效
  6. 1283 應該得到 2A1B : 最簡單的方式檢查是否符合包含 3
  7. 1243 應該得到 2A2B : 最簡單的方式檢查是否符合包含 4, 產生重複
  8. 重構 6、7 案例, 消重複使用 BCounter 作為記錄
  9. 2143 應該得到 0A4B : 確認上面的重構有效

出題者的設計

這裡我覺得不適合只使用單元測試, 應該搭配整合測試可以有更好的效果

設計隨機出題

  1. 每次都是隨機的
  2. 是 4 個數字
  3. 4 個數字不重複

隨機答案案例

  1. 呼叫應得到數字 : 定義介面, 無傳入值, 回傳字串, 但是每個字元都是數字
  2. 呼叫應得到長度為 4 的字串 : 限制回傳長度
  3. 重構:使用正規表示式合併兩個 Case
  4. 4 個數字不能重複

後記

  1. 將 SetAnswer 由 public 轉為 private,內化為由程式隨機出題(也就是沒有出題者,實務上這層級的改動應參考需求)
  2. 第二層抽象, 將 SetAnswer 實作包裝一層介面, 抽象為取出不重複數字,
    但是將字串職責留在 SetAnswer 之中
  3. 實作介面, 測試轉移到這個實作之中, 猜數字的測試改用相依性注入作 Mock
  4. C# 語言特性並沒有對集合的洗牌擴充方法, 抽出這一部份單獨實作, 並使用整合測試進行測試
  5. 改寫隨機的洗牌方法以符合 Fisher–Yates 的演算法

參考

(fin)

[實作筆記] terminal 設定 alias

前情提要

在換了新電腦後, 越來越常使用 terminal,
在學習了 Vim 之後, 常常不開任何的 IDE 直接在 terminal 的編輯檔案,
指令的使用頻率也更高了, 所以試著設定一下 alias 來提昇開發的效率.
什麼是 alias ? 它可以幫助我們使用較短指令, 達到與完整指令一樣的效果,
當我們常用的指令縮短了, 就可以加開我們開發的速度.

設定檔案

需要修改 .bashrc 或是 .zshrc 檔, 因為我使用 iTerm 所以要在 .zshrc 裡設定.
目前我最常使用的為 git,此外 hexo (一種靜態網站的發佈策略, 我用來設定我的 blog),
設定如下:

git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
alias gac="git add . && git commit -m" # + commit message
alias gi="git init && gac 'Initial commit'"
alias gp="git push" # + remote & branch names
alias gl="git pull" # + remote & branch names
alias gs="git status"
## Pushing/pulling to origin remote
alias gpo="git push origin" # + branch name
alias glo="git pull origin" # + branch name
## Pushing/pulling to origin remote, master branch
alias gpom="git push origin master"
alias glom="git pull origin master"
## Branches
alias gb="git branch" # + branch name
alias gc="git checkout" # + branch name
alias gcb="git checkout -b" # + branch name

Hexo

1
2
3
alias hxg="hexo g"
alias hxs="hexo s"
alias hxd="hexo d"

參考

-Git aliases for lazy developers

(fin)

[實作筆記] SendGrid 設定 on Appsmith

前言

appsmith 是一個簡單網站應用, 可以快速客製表單應用,
背後使用的技術棧有

  • docker
  • nginx
  • redis
  • mongodb
  • certbot
  • watchtower
  • sendgird

Sendgird

  1. 註冊建立帳號

    • 需準備手機
    • 需安裝 Authy 作兩階段驗証
  2. 前往 Dashboard

  3. 建立一組 API Key

    • Integrate 選擇 SMTP
    • API Key 大概長這樣(僅供參考已作廢):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
    • 驗証你的 Sender 信箱
    • 驗証 Application 的寄信功能, 通常應使用你的應用程式或系統進行測試
    • 這裡我使用 Appsmith 登入頁面的「忘記密碼」功能進行這個驗証, 參考下面 Appsmith Email 設定
  4. 補充,可以管理(CRUD)你的 API Key

設定 Appsmith

  1. 使用 docker 部署你的環境
  2. 設定 sendgird,也可以參考上面的說明
  3. 設定 docker.env
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

## **\*** Email ****\*\*****

APPSMITH_MAIL_ENABLED=true
APPSMITH_MAIL_FROM=YOUR_SENDER_IDENTITY_EMAIL_ID
APPSMITH_REPLY_TO=YOUR_SENDER_IDENTITY_EMAIL_ID
APPSMITH_MAIL_HOST=smtp.sendgrid.net
APPSMITH_MAIL_PORT=587

## **\*** Set to true if providing a TLS port **\*\***

APPSMITH_MAIL_SMTP_TLS_ENABLED=true
APPSMITH_MAIL_USERNAME=apikey
APPSMITH_MAIL_PASSWORD=YOUR_SENDGRID_API_KEY #把 sendgrid 的 api key 放這裡
APPSMITH_MAIL_SMTP_AUTH=true
``` 4. 重啟 docker 與服務

```shell
sudo docker-compose rm -fsv appsmith-internal-server nginx && sudo docker-compose up -d

參考

(fin)

[實作筆記] MacBook Terminal 美化與設計

2024更新

重寫此文,請參考,部份資訊仍有用途故,舊文不刪

前情提要

3 年前寫過如何讓 windows 也有美美命令提示視窗,
不過在去年我開始使用 MacBook Pro 了, 當然也想要美美的 Terminal 啦.
但是這個想法一直在放心裡面沒有實踐, 畢竟只是一個浮誇的東西.
不過在防疫期間重新看了一遍 高見龍的即將失傳的古老技藝 影片,
除了把 Vim 再熟悉一遍外, 同時也觸動了心中浮誇的那塊.
實作上比 Windows 簡單很多, 在這裡稍作記錄.

Overview

  • 下載 iTerm2
  • 設定 iTerm2
  • 安裝 oh-my-zsh
  • 安裝 powerlevel10k

第一步 下載並安裝 iTerm2

設定 iTerm2 的外觀

  1. 從網站 iTerm2-Color-Schemes (https://github.com/mbadolato/iTerm2-Color-Schemes) 下載你喜歡的配色方案,例如 “DimmedMonokai.itermcolors”。
  2. 開啟 iTerm2,點擊菜單欄的 “iTerm2”,選擇 “Preferences”。
  3. 在偏好設定視窗中,選擇 “Profiles” 選項卡,然後選擇你要更改配色方案的會話配置文件。
  4. 在 “Colors” 選項卡下,點擊 “Color Presets” 按鈕,選擇 “Import…”。
  5. 找到剛才下載的配色方案檔案,例如 “DimmedMonokai.itermcolors”,點擊 “Open”。
  6. 選擇 “DimmedMonokai” 作為你的 iTerm2 配色方案。

import .itemcolors

Profiles 裡有更多的設定, 字型、顏色
比如說, 調整啟始視窗大小與背景透明度, 可以前往 Windows 進行設定.
更多的細部設定可以自行摸索. 記得有些效果需要手動重啟 iTerm

第二步, 安裝 oh-my-zsh

1
sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

安裝相當簡單, 更多資訊可以參考 Github
這裡要了解如何對 ~/.zshrc 進行編輯, 在後面安裝我們會需要編輯這個檔案,
可以確認一下檔案內容

1
vim ~/.zshrc

安裝 Powerlevel10k

前往 Powerlevel10k
我們使用 oh-my-zsh 的方式安裝

1
git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k

安裝後編輯 ~/.zshrc 設定參如下:

ZSH_THEME="powerlevel10k/powerlevel10k"

之後重啟 iTerm2 將會有一連串設定問題, 依照喜歡的設定即可,
可以參考這個影片,
如果設定完後不喜歡, 可以執行 p10k configure 重新設定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    This is Powerlevel10k configuration wizard. It will ask you a few questions and
configure your prompt.

Does this look like a diamond (rotated square)?
reference: https://graphemica.com/%E2%97%86

--->  <---

(y) Yes.

(n) No.

(q) Quit and do nothing.

Choice [ynq]:

其它

這裡記錄一下我會使用的設定

~/.zshrc

1
ZSH_THEME="powerlevel10k/powerlevel10k" # 設定使用 powerlevel10k 主題

安裝完 oh-my-zsh  後如果有資料夾問題, 會出現以下訊息

1
2
3
4
5
6
7
8
9
10
11
12
13
[oh-my-zsh] For safety, we will not load completions from these directories until
[oh-my-zsh] you fix their permissions and ownership and restart zsh.
[oh-my-zsh] See the above list for directories with group or other writability.

[oh-my-zsh] To fix your permissions you can do so by disabling
[oh-my-zsh] the write permission of "group" and "others" and making sure that the
[oh-my-zsh] owner of these directories is either root or your current user.
[oh-my-zsh] The following command may help:
[oh-my-zsh] compaudit | xargs chmod g-w,o-w

[oh-my-zsh] If the above didn't help or you want to skip the verification of
[oh-my-zsh] insecure directories you can set the variable ZSH_DISABLE_COMPFIX to
[oh-my-zsh] "true" before oh-my-zsh is sourced in your zshrc file.

一般來說可以執行 compaudit | xargs chmod g-w,o-w 指令再重啟 iTerm2 即可以排除此問題.
如果無法排除, 又覺得每次出現這個訊息很煩人的話, 請至 ~/.zshrc 設定以下資訊

1
ZSH_DISABLE_COMPFIX="true"

活用 Terminal 的左右空間

在一般的終端機上, 隨著深入資料夾結構之中,
左側顯示路徑是會越來越長, 可以透過設定讓其顯示縮短, 更加的簡潔
編輯 ~/.p10k.zsh 以下參數

  • POWERLEVEL9K_SHORTEN_DIR_LENGTH = 1
  • POWERLEVEL9K_DIR_MAX_LENGTH = 0

而右側的留白空間, 反而很適合放入一些有用的參數
編輯 ~/.p10k.zsh 以下參數

  • POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS (右側的顯示內容)

未來如果有要調整相關的設定, 找.p10k.zsh.zshrc的相關設定就對啦

附上成果圖
my iTerm2

20211114 補充

但是在一些工具預設會用原本的 Terminal (終端機)開啟,
比如說 git-fork, 就會開啟原始的終端機,
如果沒有更換字型, 就會看到一些不正常顯示的?符號, 可以在
終端機 > 偏好設定 > 描述檔 > 文字 > 字體 作更改
我使用的字體是: MesloLGS NF
另外,git-fork 可以在

  • Preference > Integration > Terminal Client 調整開啟的終端機為 iTerm2

20211114 補充 2

我已經將 .vim 資料夾入版控,
未來只要在

  1. 在 ~ 目錄下 Clone .vim Repo 即可

  2. 在 ~ 目錄下設定連結

    ln ./.vim/.vimrc .

20211114 補充 3

讓命令呈現 highlight 語法,
使用 zsh-syntax-highlighting
這個設定可以帶來的好處是, 視覺上可以第一時間知道你有沒有打錯命令

Oh-my-zsh 的安裝方法

Clone this repository in oh-my-zsh’s plugins directory:

1
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting

編輯 ~/.zshrc 開啟外掛

plugins=( [plugins…] zsh-syntax-highlighting)

example:

plugins=( git zsh-syntax-highlighting)

最後重啟終端機即可

20211116 補充

如何讓錯誤訊息 Highlight,

iTerm > Help > Trigger 或是
iTerm > Preferences > Advanced > Trigger > Edit

以下是我的設定:

(?i:.error.) // Yellow on Red
(?i:.(warning|warn).) // Yellow
(?i:.FATAL.) // Red

20230419 補充

無限歷史行數,一般來說預設為 1000 行,
這個數量隨便執行一些語法就到頂了。
iTerm2 > Settings > Profiles > Terminal,打勾 Unlimited scrollback

參考

(fin)

[實作筆記] 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)