[實作筆記] Macbook SSH 設置與疑問

前情提要

最近轉換了一下跑道,
剛好又了一點時間就想說順便換一下 OS 學一點新東西,
就敗了一台 Macbook, 這幾天就忙著整理開發環境 。
在 ssh 上卡住了點, 就作點記錄順便上來提問 。

情境

我安裝了 git-fork 作為我的 Git GUI 工具。
裡面有一個很方便的功能可以快速的設定 ssh ,

fork

於是我很輕鬆娛快的設定了一組 ssh key 我命名為 Macbook
也可以很正常的在 fork 裡面作一些 git 的操作 ,
比如說 fetch push pull 等。

不過人在江湖飄, 哪有不挨刀。  
身為一個開發者與一個 Git 的愛用者,
有很多情境是會離開 GUI 工具操作 Git 的 。
但是很奇怪,只要離開了 fork 我的 Git 部份指令就會失效,
錯誤訊息如下

1
2
3
[email protected]: Permission denied (publickey).  
fatal: Could not read from remote repository.
Please make sure you have the correct access rights and the repository exists.

很明顯是權限不足的原因。
但是我查找了 ~/.ssh 資料夾,
我設定的 private key Macbook 確實存在。

所以我透過一個指令來取得更多資訊

ssh -vT [email protected]

輸出如下

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
47
48
49
50
51
52
[email protected] ~ % ssh -vT [email protected]
OpenSSH_7.9p1, LibreSSL 2.7.3
debug1: Reading configuration data /etc/ssh/ssh_config
debug1: /etc/ssh/ssh_config line 48: Applying options for *
debug1: Connecting to github.com [192.30.253.112] port 22.
debug1: Connection established.
debug1: identity file /Users/marsen/.ssh/id_rsa type -1
debug1: identity file /Users/marsen/.ssh/id_rsa-cert type -1
debug1: identity file /Users/marsen/.ssh/id_dsa type -1
debug1: identity file /Users/marsen/.ssh/id_dsa-cert type -1
debug1: identity file /Users/marsen/.ssh/id_ecdsa type -1
debug1: identity file /Users/marsen/.ssh/id_ecdsa-cert type -1
debug1: identity file /Users/marsen/.ssh/id_ed25519 type -1
debug1: identity file /Users/marsen/.ssh/id_ed25519-cert type -1
debug1: identity file /Users/marsen/.ssh/id_xmss type -1
debug1: identity file /Users/marsen/.ssh/id_xmss-cert type -1
debug1: Local version string SSH-2.0-OpenSSH_7.9
debug1: Remote protocol version 2.0, remote software version babeld-a69101e9
debug1: no match: babeld-a69101e9
debug1: Authenticating to github.com:22 as 'git'
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: curve25519-sha256
debug1: kex: host key algorithm: rsa-sha2-512
debug1: kex: server->client cipher: [email protected] MAC: <implicit> compression: none
debug1: kex: client->server cipher: [email protected] MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ssh-rsa SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8
debug1: Host 'github.com' is known and matches the RSA host key.
debug1: Found key in /Users/marsen/.ssh/known_hosts:1
debug1: rekey after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug1: rekey after 134217728 blocks
debug1: Will attempt key: /Users/marsen/.ssh/id_rsa
debug1: Will attempt key: /Users/marsen/.ssh/id_dsa
debug1: Will attempt key: /Users/marsen/.ssh/id_ecdsa
debug1: Will attempt key: /Users/marsen/.ssh/id_ed25519
debug1: Will attempt key: /Users/marsen/.ssh/id_xmss
debug1: SSH2_MSG_EXT_INFO received
debug1: kex_input_ext_info: server-sig-algs=<ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-rsa,rsa-sha2-512,rsa-sha2-256,ssh-dss>
debug1: SSH2_MSG_SERVICE_ACCEPT received
debug1: Authentications that can continue: publickey
debug1: Next authentication method: publickey
debug1: Trying private key: /Users/marsen/.ssh/id_rsa
debug1: Trying private key: /Users/marsen/.ssh/id_dsa
debug1: Trying private key: /Users/marsen/.ssh/id_ecdsa
debug1: Trying private key: /Users/marsen/.ssh/id_ed25519
debug1: Trying private key: /Users/marsen/.ssh/id_xmss
debug1: No more authentication methods to try.
[email protected]: Permission denied (publickey).

幾個奇怪的地方
一個是 identity file 或是 Trying private key 的檔案我都找不到 ,
另一個奇怪的點是 ~/.ssh/Macbook 這組我剛剛建立的 private key 明明存在 ,
我確找不到他顯示在 identity file 或是 Trying private key 的記錄之中
總而言之,最後的訊息仍然是

1
[email protected]: Permission denied (publickey).

解決方法

指令 ssh-add -K Macbook 執行後, 就可正常運作。
小小猜測一下, 應該是 fork 運作的環境與一般 terminal 的環境有所差異,
所以需要額外透過指令加上 private key 才能夠執行。

不過我仍有疑問, fork 的設定在什麼地方可以調整呢 ?
另一個問題是 log 裡面這些不存在 private key 是在哪裡設定的 ?
查找過 /etc/ssh/ssh_config 裡面並沒有相關的設定

1
2
3
4
5
debug1: Will attempt key: /Users/marsen/.ssh/id_rsa
debug1: Will attempt key: /Users/marsen/.ssh/id_dsa
debug1: Will attempt key: /Users/marsen/.ssh/id_ecdsa
debug1: Will attempt key: /Users/marsen/.ssh/id_ed25519
debug1: Will attempt key: /Users/marsen/.ssh/id_xmss

希望有人能有答案或提供文件可以參考一下。
不求甚解繼續玩 Mac 去。

(fin)

[實作筆記] 從 TDD 到 TDD ,Todo 到 Test 趨動開發(一)

前情提要

上次作完金流後,這次換作物流,
在開發的過程中首次全程用 TDD 進行(?),
因為某些因素,測試沒有進版控,所以稍微記錄一下心路歷程。
但是那個 T 是什麼? 就請各位看下去了…

需求說明

出貨流程為:

  1. 訂單成立
  2. 透過物流服務配號
  3. 出貨
  4. 透過物流服務查詢物流狀態
  5. 狀態正確,通知消費者取貨
  6. 狀態不正確,通知商家,人工處理。

這次 TDD 進行的部份為流程上的第 4 步,
往下看我怎麼做的

整體流程如下

流程

Step0 . 這次不是從無到有,而是在遺留代碼之中建立測試,
所以我會準備一些「遺留代碼」來呈現我面臨的狀況。

Step1 . 這次 TDD 先不是 Test 而 TODO,
直接對 Production Code 寫下 Todo List,
這個 TODO 的過程其實就是一種分析,一種需求拆分。

這裡我先簡單拆成兩步,

  1. 打 API 問狀態
  2. 將問到的資料轉換成回傳資料

Step 2 . 隨著過程把 TODO 拆的更細

  1. 打 API 問狀態
    1. 建立 HttpClient
    2. 建立 auth
    3. 準備 HttpContent 資料
    4. 指定 API URL
    5. 呼叫

以往這個時候,我就會進開發了,
這次我不打算刻意改變我的開發習慣,
但是我會多作一件事,寫測試。
這個測試會直接呼叫我即將開發的方法,
而我的方法會真的去打 API 存取 DB 讀 Config Files 諸如此類的事情。

測試

第一個測試,但是沒有 Assert

我目前對測試案例沒有任何的想法(這是個壞味道),
但是我打算直接透過測試呼叫我的 Prodction Code

1
2
3
4
5
6
7
8
[Fact]
public void Case1_Just_Run()
{
var target = new PickupService();
long storeId = 0;
List<string> waybillNo = new List<string>();
target.GetUpdateStatus(storeId, waybillNo);
}

因為沒有想法,所以沒有 Assert
這不算是測試,頂多是一個小工具可以隨時呼叫我的 Prodcution Code 而已

Do Todo 建立 HttpClient

這裡依造我以前的開發習慣,直接開幹,
把 HttpClient new 出來,刪除 Todo Comment
Commited 然後發 Pull Request

1
2
-            //// TODO 1.建立 HttpClient
+ var httpClient = new HttpClient();

Do Todo 建立 auth

通常在串接第三方服務的過程中,
第三方會提供沙盒(SandBob)作開發人員測試使用
這裡我加了一點遮罩,但實務上如果是沙盒的 auth 資訊
我可能會直接 Commit 進去(壞味道)。

注意!! 這時候還是 Production Code 喔
我可以在測試加個 TODO ,
未來這段應該被 Mock 而不是 Hard Code 寫死。

1
2
3
4
5
-           //// TODO 2.建立 auth
+ //// TODO login id 抽參數
+ httpClient.DefaultRequestHeaders.Add("login_id", "testId");
+ //// TODO authorization 抽參數
+ httpClient.DefaultRequestHeaders.Add("authorization", "testAuth");

Do Todo 準備 HttpContent 資料

準備 HttpContent 有很多種方式,
這裡我選擇 StringContent 來實作。
所以要包含物件轉換成 Json String 的行為,
需要參考 JsonSerializer 。
如果是不太熟悉的開發人員可能會另開 TODO,
但是我這裡就一次性的作掉了 。

1
2
3
4
-           //// TODO 3.準備 HttpContent 資料

+ var requestContent = JsonSerializer.Serialize(new { Type = "DeliveryOrder", waybillNo });
+ var httpContent = new StringContent(requestContent, Encoding.UTF8, "application/json");

Take a break

稍微休息一下,這裡我的開發流程基本上沒有改變,
除了多寫一個(整合)測試,而且每次都會稍微跑一下測試,
這個測試其實沒有 Assert ,唯一的幫助只能驗証執行方法時沒有 Exception

重構

重構應該落在開發之中,我看到兩個小問題

  1. 我會 inline 掉多餘的參數 requestContent
  2. Type = “DeliveryOrder” 對我來說是個 magic variable ,我會加 TODO 預計未來抽成常數(壞味道,Why not now ?)

Do TODO 4.指定 API URL & TODO 5.呼叫

修改代碼如下,拿到第一個紅燈
因為我沒有指定 url

1
2
3
//// TODO 4.指定 API URL
string url=string.Empty;
var responseMessage = httpClient.PostAsync(url, httpContent).Result;

為了修正這個紅燈我會指定 url,
實務上我會使用沙盒的 url ,
這裡我先用 mock api 取代 ,
mock api 的服務為 mocky
類似的服務很多,也不是本篇的重點,就不贅述了。

這次一次處理掉兩個 TODO ,
表示當初我 TODO Task 切的過小,
下次可以切大一些。

不算壞味道,就當學個經驗。

第一次 TODO 作完之後

當初規劃的 TODO Task 都作完了,
但是其實工作並沒有完成。

我會再作進一步的分析,
可以看到原本的 TODO 產生了更多的 TODO ,
另外打完 API 後的處理也是個問題。

這邊要用到 walking skeleton 的概念,  
簡單說就是,先打通再迭代。

前面產生的 TODO 項目並不是「最」重要的,
我應該先處理回傳的資料,讓整件事情串通。
開立 TODO 如下

1
2
3
+ //// TODO Parse Response Entity
+ //// TODO Switch Status
+ //// TODO Return ShippingOrderUpdateEntity List

可以得知,我最終會回傳一包 List,
這個時候我可以 Assert 了

修改第一個測試案例

這個階段我開始撥雲見日,我要很明確的寫下第一個測試案例,
第一個案例我會直接作 Happy Case ,
也就是目前的呼叫的 API
只打一筆,回傳 Done 的資料。

這裡進一步作需求分析,
呼叫完 API 我會收到一大包 JSON 資料,
需要轉成我可以處理的物件,
其中最重要的欄位 lastStatusId 會回傳各種狀態,

  • DONE
  • FAIL
  • Arrived
  • Shipping
  • SMS
  • Expiry

我只處理

  • 已取貨(DONE) 系統狀態為 Finish
  • 失敗(FAIL、Expiry) 系統狀態為 Abnormal
  • 貨到待取(Arrived) 系統狀態為 Arrived
  • 出貨中(Shipping) 系統狀態為 Processing

分析後,我的測項將會是向 API 循問一筆資料
且回傳一筆為 Done 的 ShippingOrderUpdateEntity 給我。

1
2
3
4
5
6
7
8
9
[Fact]
public void Case1_Query_Done_waybillNo()
{
var target = new PickupService();
long storeId = 1;
List<string> waybillNo = new List<string> {"TEST2002181800010"};
var actual = target.GetUpdateStatus(storeId, waybillNo).FirstOrDefault().Status;
actual.Should().Be(StatusEnum.Finish);
}

拿到紅燈 ,Do TODO Parse Response Entity

如果是 Test Driven 我可能會速解再加案例,
但是我現在是 Todo Driven 所以造著 Todo 作事,
透過 json2csharp 快速產生 Entity 來轉置 JSON 資料。

如何從 T(odo)DD 到 T(est)DD

寫到這裡我已經開始感覺 Todo 的挶限性了,
由於這個方法職責不分,所以要測試是困難的,
但是 Todo 的作法是無法趨動改變的。

現在開始,試著把每個 Todo Task 改變成 Test Case

第二個測試案例開始之前

再次說明一下商務需求,我可以提供 waybillNo 向 API 循問物流的狀態。
理想上我會擁有一堆不同貨態的 waybillNo ,剛好可以作我測試的案例
但是在實務上,我拿不到這些案例, 我折衷的作法是透過 Dummy API 來偽裝回傳值。
這仍是整合測試的一種,雖然我可以 Mock API 的回傳值,
但在網路狀況異常下,測試仍會紅燈

在作 Dummy API 的前提下,要能抽換 URL
所以我會先作 TODO url 抽參數

重構如下:

Production Code

1
2
3
4
-        //// TODO url 抽參數
- //// string url= "http://www.mocky.io/v2/********";
- string url = "http://www.mocky.io/v2/********";
+ string url = this._configService.GetAppSetting("pickup.service.url");

Test Mock Return Value

1
2
3
4
var configService = Substitute.For<IConfigService>();
configService.GetAppSetting("pickup.service.url")
.Returns("http://www.mocky.io/v2/********");
var target = new PickupService(configService);

進一步增新測試的可讀性

可讀性真的是一個很抽象的觀念,之後有機會再深入探討
我的修改如下,主要的想法是「讓測試案例可以像對話般被閱讀」

1
2
3
4
5
6
[Fact]
public void Case1_Query_Done_waybillNo()
{
var actual = QueryWithDoneWaybillNo();
actual.Should().Be(StatusEnum.Finish);
}

第二個測試案例 - 出貨中(Shipping)

這裡就是用簡單的 Test Case 趨動 Production Code 的代碼生成

Test Code

1
2
3
4
5
6
[Fact]
public void Case2_Query_Shipping_waybillNo()
{
var actual = QueryWithShippingWaybillNo();
actual.Should().Be(StatusEnum.Processing);
}

Production Code (部份)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-           //// TODO Switch Status
- //// TODO Return ShippingOrderUpdateEntity List
- result.Add(new ShippingOrderUpdateEntity {Status = StatusEnum.Finish});
+ foreach (var c in obj.content)
+ {
+ switch (c.lastStatusId)
+ {
+ case "DONE":
+ result.Add(new ShippingOrderUpdateEntity {Status = StatusEnum.Finish});
+ break;
+ case "Shipping":
+ result.Add(new ShippingOrderUpdateEntity {Status = StatusEnum.Processing});
+ break;
+ }
+ }

順手再重構了一下測試,
希望能提高可讀性

1
2
3
4
5
6
[Fact]
public void Case2_Query_Shipping_waybillNo()
{
var actual = QueryWaybillNoWith(UrlMockShipping);
actual.Should().Be(StatusEnum.Processing);
}

剩下的測試案例

  • 失敗(FAIL) 系統狀態為 Abnormal
  • 失敗(Expiry) 系統狀態為 Abnormal
  • 貨到待取(Arrived) 系統狀態為 Arrived

剩下的 TODO 項目

這個時候基本的功能都好了,來收拾一下剩下的 TODO 項目吧
主要都是取得設定值的功能,實務上這些設定值可能來自不同的服務
Database、Config Service 或 Settring API 等…
我在這裡先簡化成 IStoreSettingService 取值就好。

1
2
3
4
5
-           //// TODO login id 抽參數
- httpClient.DefaultRequestHeaders.Add("login_id", "testId");

+ var loginId = this._storeSettingService.GetValue(storeId,"pickup.service","loginId");
+ httpClient.DefaultRequestHeaders.Add("login_id", loginId);

同時測試代碼也要修改,
注意這裡的 testId 其實是 Pickup Service 提供給我們的測試 Id
在 Production 你需要整合進你的 Database、Config Service 或 Settring API 裡
在整合測試可以直接 mock 它

1
_storeSettingService.GetValue(_testStoreId, "pickup.service", "loginId").Returns("testId");

Auth 也是相同的處理

1
2
3
4
-           //// TODO authorization 抽參數
- httpClient.DefaultRequestHeaders.Add("authorization", "testAuth");
+ var auth = this._storeSettingService.GetValue(storeId,"pickup.service","auth");
+ httpClient.DefaultRequestHeaders.Add("authorization", auth);

Testing Mock

1
_storeSettingService.GetValue(_testStoreId, "pickup.service", "auth").Returns("testAuth");

心得小結

TDD 不一定要用單元測試,
你可以試著從 T(Todo) Driven Development 到 T(Test) Driven Development
思考一下你目前的開發方式,
不要急著 TDD ,想像一下你開發到什麼程度會想要驗証(測試),
試著在這個時間點加上測試就好,
這樣的開發方式,會比較貼近你的開發方式,
同時也可以練習寫測試,你會發現很多問題。
比如說:

  • 你跟本沒有足夠了解需求,導致你寫不出驗收條件。
  • 你根本不熟悉測試框架或是相關的 Library。
  • 甚至你跟本不熟悉你賴以為生的開發工具與程式語言。
  • 或是你跟本不會 Debug 跟 Google 而且以前你一直以為你會。
  • 你把代碼搞得一蹋糊塗,而且沒有人愛你

下一次試著先寫測試吧,改變一下自已的工作方式。
平順把你的開發方式轉換成 TDD 吧 。

下一篇我會透過整合測試,發現一些異常狀況,
並試著加上測試案例,並試著趨動。

(fin)

[實作筆記] 用 TDD 寫一個 API Pay

需求說明

金流系統透過打 API 與第三方介接來進行付款,
為了追蹤金流,在打 API 的過程中,業務單位要求要帶著 RequestId 。
再進行付款。
而 RequestId 由另一個專門負責提供 RequestId 的 API 來提供。

整體流程如下:

  1. 打 API 取得 RequestId

    1
    GET {{url}}/api/{{version}}/requestId
  2. 組合付款資料與 RequestId

  3. 打 API 完成付款

    1
    POST {{url}}/api/{{version}}/pay/CreditCard/{{transationId}}

第一個 Case,Pay 的時候應該呼叫 GET reguestId 1次

問題,我需要驗証 HttpClient 呼叫的 url次數

一開始會寫成這樣,

1
2
3
4
5
6
7
[Fact]
public void pay_should_Get_requestId()
{
var target = new PaymentService();
target.Pay();
httpClient.Received().GetAsync("https://testing.url/api/v1/requestId");
}

我本來就預計使用 HttpClient 來呼叫 API,
但是直接使用 HttpClient 會直接產生耦合,
所以我建立一個介面來包裝它。

1
2
3
4
public interface IHttpClient
{

}

接著馬上建立類別 HttpClientProxy 實作 IHttpClient,
這個時候我會知道我會使用 GetAsync 的方法,
所以我會讓 IHttpClient 長出這個同名方法,
實作很單純,就是呼叫 HttpClient().GetAsync 方法。

幾個想法,
這樣算是 Proxy Pattern 嗎 ? 我覺得算是:P
另一點,這個階段我會擔心 HttpClient 的問題,
不處理是對的嗎 ?
如果不刻意處理的話 HttpClientProxy 好像會長不出來

1
2
3
4
5
6
7
8
9
10
11
public interface IHttpClient
{
Task<HttpResponseMessage> GetAsync(string requestUri);
}
public class HttpClientProxy : IHttpClient
{
public Task<HttpResponseMessage> GetAsync(string requestUri)
{
return new HttpClient().GetAsync(requestUri);
}
}

完成這階段的修改後,我才可以透過 Framework 來 Mock IHttpClient,
寫好的測試如下,順利拿到第一個紅燈:

1
2
3
4
5
6
7
8
[Fact]
public void pay_should_Get_requestId()
{
IHttpClient httpClient = Substitute.For<IHttpClient>();
var target = new PaymentService(httpClient);
target.Pay();
httpClient.Received().GetAsync("https://testing.url/api/v1/requestId");
}

馬上修改 Production Code ,拿到綠燈。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PaymentService
{
private readonly IHttpClient _httpClient;
public PaymentService(IHttpClient httpClient)
{
_httpClient = httpClient;
}

public void Pay()
{
_httpClient.GetAsync("https://testing.url/api/v1/requestId");
}
}

第二個 Case,Pay 的時候應該呼叫 POST Pay CreditCard 1 次

測試案例:

1
2
3
4
5
6
7
8
[Fact]
public void pay_should_Post_Pay_CreditCard()
{
IHttpClient httpClient = Substitute.For<IHttpClient>();
var target = new PaymentService(httpClient);
target.Pay();
this._httpClient.Received().PostAsync("https://testing.url/api/v1/pay/CreditCard", Arg.Any<HttpContent>());
}

修改 Production Code

1
2
3
4
5
6
public void Pay()
{
var readAsStringAsync = this._httpClient.GetAsync("https://testing.url/api/v1/requestId");

this._httpClient.PostAsync("https://testing.url/api/v1/pay/CreditCard", null);
}

重構測試

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
[Fact]
public void pay_should_Get_requestId()
{
WhenPay();
ShouldGetRequestId();
}

[Fact]
public void pay_should_Post_Pay_CreditCard()
{
WhenPay();
ShouldPayByCreditCard();
}

private void WhenPay()
{
var target = new PaymentService(_httpClient);
target.Pay();
}

private void ShouldGetRequestId()
{
this._httpClient.Received(1).GetAsync($"{_testingApiUrl}requestId");
}

private void ShouldPayByCreditCard()
{
this._httpClient.Received(1).PostAsync($"{_testingApiUrl}pay/CreditCard",
Arg.Any<HttpContent>()));
}

第三個 Case,Pay 的時候應該先呼叫 Get RequestId 再 POST Pay CreditCard

1
2
3
4
5
6
7
8
9
10
[Fact]
public void pay_should_Get_RequestId_Before_Post_Pay_CreditCard()
{
WhenPay();
Received.InOrder(() =>
{
ShouldGetRequestId();
ShouldPayByCreditCard();
});
}

想法,有了第三個案例,我還需要前面兩個案例嗎 ?

下一步,調整 ShouldPayByCreditCard 的 Assert 邏輯,
原因是實務上我必須將 RequestId 帶入 Post Pay 時的 HttpContent 裡面。

1
2
3
4
5
private void ShouldPayByCreditCard()
{
this._httpClient.Received(1).PostAsync($"{_testingApiUrl}pay/CreditCard",
Arg.Is<HttpContent>(x => x.ReadAsStringAsync().Result.Contains(_testRequestId)));
}

Prodouction Code 因而長出 PayEntity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void Pay()
{
var requestId = this._httpClient.GetAsync("https://testing.url/api/v1/requestId").Result.Content
.ReadAsStringAsync().Result;
HttpContent content = new StringContent(
JsonSerializer.Serialize(
new PayEntity
{
RequestId = requestId
}));

this._httpClient.PostAsync("https://testing.url/api/v1/pay/CreditCard", content);
}

public class PayEntity
{
public string RequestId { get; set; }
}

Case4 組合資料邏輯

4.1 組合 PayEntity 的邏輯

這次我假設外部的元件已組合好 PayEntity 傳入 PaymentService.Pay 方法,
唯一的組合邏輯就只剩 RequestId。
至於外部的 PayEntity 組合邏輯如何用 TDD 長出 Production Code 可以參考這篇

1
2
3
4
5
private void WhenPay()
{
var target = new PaymentService(_httpClient);
target.Pay(new PayEntity());
}
1
2
3
4
5
6
7
8
9
public void Pay(PayEntity payEntity)
{
var requestId = this._httpClient.GetAsync("https://testing.url/api/v1/requestId").Result.Content
.ReadAsStringAsync().Result;
HttpContent content = new StringContent(
JsonSerializer.Serialize(
payEntity.RequestId = requestId));
this._httpClient.PostAsync("https://testing.url/api/v1/pay/CreditCard", content);
}

最後,將 api 的 url 也抽成可參數化。

1
2
3
4
5
6
7
8
9
10
11
12
public PaymentServiceTests()
{
this._httpClient = Substitute.For<IHttpClient>();
this._httpClient.GetAsync(Arg.Any<string>()).ReturnsForAnyArgs(
Task.FromResult(
new HttpResponseMessage
{
Content = new StringContent(_testRequestId)
}));
this._configure = Substitute.For<IConfigure>();
this._configure.Setting("PayService.Url").Returns(_testingApiUrl);
}

Production Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public PaymentService(IHttpClient httpClient, IConfigure configure)
{
this._httpClient = httpClient;
this._configure = configure;
}

public void Pay(PayEntity payEntity)
{
var apiUrl = this._configure.Setting("PayService.Url");
var requestId = this._httpClient.GetAsync($"{apiUrl}requestId").Result.Content
.ReadAsStringAsync().Result;
HttpContent content = new StringContent(
JsonSerializer.Serialize(
payEntity.RequestId = requestId));

this._httpClient.PostAsync($"{apiUrl}pay/CreditCard", content);
}

小結

在 HttpClient 那邊卡蠻久的,用介面的方法包裝起來也不知道是否合適。
網路上有提供許多不同的作法,單元測試的 TDD 好像趨動不太出來 Production Code
是否需要加入整合測試,甚至是透過呼叫 Production Code 去打 API 作端到端測試,來趨動開發 ?
TDD 的 T 是不是不只是 Unit Test 呢 ?

參考

(fin)

[閱讀筆記] 開刀房裡的沉思 -- 一位外科醫師的精進

前言

因為「學徒模式」在最後的章節多次提到這本書,
於是去了圖書館借來翻了一下,內容大多與醫療與醫生相關,
作者在醫療上如何追求卓越,精益求精 ?
作為一個開發人員,能否借鏡不同產業的精進旅程帶來一些反思 ?

簡單先作個小結,作者認為精進有三個基本要素

  1. 努力不懈,簡單卻最困難,如何堅持?
  2. 務求正確, 這裡面臨了人性的挑戰,包含貪心、防衛心理和堅持與適時放棄
  3. 創新,勇於認錯、願意改變、見賢思齊

引述一小段。

我們不只是活在一個人的世界,
必須和別人一起工作,與科學一起進步,
也活在混亂、複雜的現實世界裡,而且任重道遠。
……
光是擔責任還不夠,選要想想如何能做得更好。

章節概要

CH 1 禍手

簡單卻作不到的事。

創新者的困境

山姆維茲雖然推論出醫生不洗手是產婦致死的原兇,
但在同業同事間推動卻因批判與溝通而大大的失敗,
成為了悲劇英雄。

現場執行的困境

  • 洗手的 SOP :首先缷下手錶,戒指…其次,用溫水潤濕雙手…最後沖水至少 30 秒…然後拿擦手紙…事實上沒有人能夠謹守這樣的步驟。
  • 「不問你為什麼不洗手,而是問你為什麼不能洗手 ?」答案多半是:沒時間。
  • 把病房變得像開刀房,使維持清潔變得容易,當他另謀高就後,原單位故態復萌、改革挫敗、白費功夫
  • 「正向偏差」:透過內部推廣取代指示改變,詢問現場一線取代宣導與命令(現場改善、由下而上)

反思:
現場如何推動單元測試與 Clean Code ?
只靠個人 ? 明星開發者 ? 嚴格的規定 ? 無處不在的口號標語 ?

Ch 6 薪事誰人知

醫師的困境是按件計酬,而存在與之對立的醫療保險業者,
原本醫療保險的立意是良善的,避免有的醫生漫天喊價。
但是時至今日,美國的醫療保險規則太過複雜,
引入第三方(保險業者)不但沒有幫助,反而變得更加複雜,
醫生無法得到合理的收費,沒有保險的病人無法得到醫療照護。

馬修.松頓保健計劃,在初期簡化與保險公司打交道的複雜性,
提昇了照護病人的成效,但在組織成長變大後,
諸多的規定與合約讓這個保險計劃也由保險集團接手,最終宣告結束。

Ch 9 艾卜佳評分表

對人類來說,生產一直是一件困難的事。
對新生兒與孕婦在生產的過程中一不小心就有生命危險,
單鈎、產鉗、多達數十種的助產法、剖腹產等…

二十世紀複,隨著知識與器械的進步,研究卻表明醫院生產的新生兒死亡率高於產婆接生
生產流程的革新與標準化才進一步確保了新生兒的生存率
艾卜佳評分表在這當中擔任關鍵的角色,而且相當簡單。
膚色、哭聲、呼吸、四肢活動力、心跳次數等,簡單的分為 0、1、2 三種評分,
這讓主觀評量變成了客觀,有了評量才有改善的方向。
新生兒加護病房、產婦脊髓麻醉與胎兒心跳監視器也相應而生。

反思 1 :
如何客觀評量績效 ?
書中說明產科的進步並不在嚴謹的「實證醫學」而是一個簡單的評分表。

那麼如何評量代碼質量 ?

  • 如何建立一個開發人員的評分表 ?
  • 如何建立代碼的艾卜佳評分表呢 ?

反思 2 :
產鉗在上個世紀是一門技藝,
儘管有研究表明剖腹產未必優於產鉗,但這門技藝仍就消失了。
如果產鉗是因為標準化的流程而消失,那軟體工程呢 ?
TDD 會是軟體工程裡的產鉗嗎 ? 2014 年的 TDD is died 仍值得回味再三。

績效又該如何轉換成合理的收入呢 ?

Ch 10 醫師的成績單

辛辛那提的醫生全力以赴,仍然只是中等平庸的醫院,
在本書中,醫生的技藝是呈現鐘型曲線。
作者表達的意思是,病人都想找最好的醫生,但在醫療的這個領域,
大多數都是平庸的。
在學徒模式書中,工程師是左傾的曲線,暗示大多數工程師都是拙劣的。
左傾的曲線

反思
雖然手頭上沒有完整數據佐証,但是一般醫生至少需要 7 年的專業訓練,
而軟體工程業界,中途出家的非本科工程師比比皆是,
一線從業人員的水平參疵不齊,甚至有達克效應

這個行業本身歷史很短也很年輕,
影響範圍卻相當的廣大,
整個世界各行各業軟體都能參予其中。
缺乏調研、認証相關機構,但是我相信在未來這點能夠逐步改善

Dunning-Kruger Effec

其它章節

  • 一個都不能漏
  • 浴血
  • 纏訟
  • 死刑室醫師
  • 戰鬥
  • 我的印度之旅

後記

2020 過年寫下這篇文章,幾個事件與書共鳴,
反思記錄一下。

先是 2020 年館長的新聞

館長已經揚言要提告了,他的需求是能同時十萬人上線的網站,
報價要 300 萬 + 30 萬壓測費,透過熟人介紹接案。
個人小小碎唸,在軟體業界常常有亂喊價,亂報時程的常態,
最後損失的往往是品質。

  • 盲信的後果,你沒有專業要如何相信對方呢 ? 反過來,你有專業要如何讓客戶相信你呢 ?
  • 本例中館長揚言要提告工程師,但我疑惑為什麼不是中間人、接案公司或業務而是工程師 ? 窗口真的是工程師嗎 ?
  • 壓測應該收多少錢 ? 10萬人同時上線搶購,要作到什麼程度 ?
  • 資安避險應收多少錢 ? 要作到什麼程度 ?
  • 功能的缺失(帳務、發票 etc…)。如何驗收呢 ? 為何驗收失敗仍要行銷 ?

第二個反思,公開透明的組織運作,將會成為組織是否能夠進步改善的關鍵
2020 的武漢肺炎是反向的例子,

  • 武漢市長表示未被授權, 依法不能批露疫情。
  • WHO 錯誤評估風險等級
  • ICAO 屏避支持台灣的帳號
  • 陳秋實、面具人、口罩哥與黨與組織的各種輿情論戰。

這件事還沒有結束,就不多作評論了。
後續再作追踪。

最後,今年我的侄子出生了,
因為剛好讀到產婦相關章節,所以特別有感
願他平安成長。

(fin)

[實作筆記] 記錄用 TDD 寫一個 Entity Parser

如何用 TDD 寫一個 Entity Parser

情境,假設有一包 JSON 檔案如下

1
2
3
4
5
{
"FirstName": "Tian",
"LastName": "Tank",
"BirthDate": "1989/06/04"
}

我想轉換成 C# Entity ,
這個 Entity 裡面包含兩個商業邏輯

  1. 提供全名
  2. 提供年紀

ex:

1
2
Name = Tank Tian
Age = 35

如何透過 TDD 的工序達到這個目標 ?

Arrange

傳入的 JSON 字串如下

1
2
3
4
5
{
"FirstName": "Tian",
"LastName": "Tank",
"BirthDate": "1989/06/04"
}

Act

執行 Parse 方法之後

Assert

回傳一個 PersonEntity ,裡面的資料應該要是

1
2
3
4
new PersonEntity {
Name = "Tian Tank"
Age = 30
}

工序

Step1

第一個案例,我會只驗証 Name 的組合邏輯

1
2
3
4
5
6
7
8
9
10
[Fact]
public void CovertName()
{
////Arrange

////Act
var actual = _target.Parse(testJson).Name;
////Assert
actual.Should().BeEquivalentTo("Tian Tank");
}

Production Code 簡單寫可以這樣,
這個可以說是極致的 Baby Step 吧 ?

1
2
3
4
5
6
7
public PersonaEntity Parse(string json)
{
return new PersonaEntity
{
Name = "Tian Tank"
};
}

不過這個階段可以透過 IDE 工具長出 PersnaEntity

1
2
3
4
public class PersonaEntity
{
public string Name { get; set; }
}

Step1.1

透過分析過需求,我們應該可以理解到 PersonaEntity.Name
其實是 FirstName 與 LastName 的組合,
所以我們可重構一下 Production Code,
這個步伐會大步一點,如下

1
2
3
4
5
6
7
public PersonaEntity Parse(string json)
{
return new PersonaEntity
{
Name = "Tian" + "Tank"
};
}

Step1.2

Oops !! 我拿到了一個紅燈,因為我忘了空白
修改 Production Code 如下,得到綠燈

1
2
3
4
5
6
7
public PersonaEntity Parse(string json)
{
return new PersonaEntity
{
Name = "Tian" +" "+ "Tank"
};
}

Step1.3

開始重構,這樣的 Baby Step 會不會真的太小步了 ?

1
2
3
4
5
6
7
8
9
public PersonaEntity Parse(string json)
{
var firstName = "Tian";
var lastName = "Tank";
return new PersonaEntity
{
Name = firstName + " " + lastName
};
}

Step1.4

長出新的 Entity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PersonaEntity Parse(string json)
{
var firstName = "Tian";
var lastName = "Tank";
var originEntity = new PersonaOriginEntity
{
FirstName = firstName,
LastName = lastName
};
return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}"
};
}
1
2
3
4
5
public class PersonaOriginEntity
{
public string LastName { get; set; }
public string FirstName { get; set; }
}

Step1.5

重構

1
2
3
4
5
6
7
8
9
10
11
12
13
public PersonaEntity Parse(string json)
{
var originEntity = new PersonaOriginEntity
{
FirstName = "Tian",
LastName = "Tank"
};

return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}"
};
}

Step1.6

改用 JSON Parser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PersonaEntity Parse(string json)
{
//var originEntity = new PersonaOriginEntity
//{
// FirstName = "Tian",
// LastName = "Tank"
//};
var originEntity = JsonSerializer.Deserialize<PersonaOriginEntity>(json);

return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}"
};
}

Step1.7

移除無用代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public PersonaEntity Parse(string json)
{
//var originEntity = new PersonaOriginEntity
//{
// FirstName = "Tian",
// LastName = "Tank"
//};
var originEntity = JsonSerializer.Deserialize<PersonaOriginEntity>(json);

return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}"
};
}

Step2

第二個案例,我會驗証 Age 的計算邏輯

Case 2. Age 是現在時間減去生日的年份差

首先,要如何處理 現在時間 ??
一般的 Production Code 會用 DateTime.Now
這是一個 struct 的 static 方法。

Step2.1

1
2
3
4
5
6
7
8
9
[Fact]
public void CovertAge()
{
////Arrange
////Act
var actual = _target.Parse(testJson).Age;
////Assert
actual.Should().Be(30);
}

透過測試逼出 Age 欄位

1
2
3
4
5
public class PersonaEntity
{
public string Name { get; set; }
public int Age { get; set; }
}

快速讓測試綠燈

1
2
3
4
5
6
7
8
9
10
public PersonaEntity Parse(string json)
{
var originEntity = JsonSerializer.Deserialize<PersonaOriginEntity>(json);

return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}",
Age = 30
};
}

Step2.2

實作計算 Age

1
2
3
4
5
6
7
8
9
10
public PersonaEntity Parse(string json)
{
var originEntity = JsonSerializer.Deserialize<PersonaOriginEntity>(json);

return new PersonaEntity
{
Name = $"{originEntity.FirstName} {originEntity.LastName}",
Age = originEntity.BirthDate.Year - DateTime.Now.Year
};
}

這個測試案例會在 2019 年為綠燈,而在 2020 年變為紅燈
這不是一個好的測試,好的測試應該符合可重複性(Repeatable),
所以我們要試著 Mock 掉 DateTime.Now

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public struct SystemDateTime
{
private static DateTime? _mockDateTime = null;

public static DateTime Now
{
get => _mockDateTime ?? DateTime.Now;
internal set => _mockDateTime = value;
}

internal static void Reset()
{
_mockDateTime = null;
}
}

Step2.3

改寫測試案例

1
2
3
4
5
6
7
8
9
10
[Fact]
public void CovertAgeTodayIs2019()
{
////Arrange
SystemDateTime.Now = Convert.ToDateTime("2019/12/28");
////Act
var actual = _target.Parse(testJson).Age;
////Assert
actual.Should().Be(30);
}

Step2.4

Mock 時間後再寫一個測試案例

1
2
3
4
5
6
7
8
9
10
[Fact]
public void CovertAgeTodayIs2020()
{
////Arrange
SystemDateTime.Now = Convert.ToDateTime("2020/12/28");
////Act
var actual = _target.Parse(testJson).Age;
////Assert
actual.Should().Be(31);
}

Step2.5

改用 Given When Then,
測試案例如下,這樣會比較好讀嗎?:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Fact]
public void parse_name()
{
AfterParseJson().Name.Should().Be("Tian Tank");
}

[Fact]
public void parse_age_today_is_2019()
{
GiveTodayIs("2019/12/28").Age.Should().Be(30);
}

[Fact]
public void parse_age_today_is_2030()
{
GiveTodayIs("2030/05/06").Age.Should().Be(41);
}

後續

  • 如果未來有多新的欄位再逐步加上測試。
  • 但在實務上我極有可能會同時驗証大量的欄位 ,比如說欄位是一對一的 Mapping
  • 想省略 PersonaOriginEntity ,有可能用 Dynamic 嗎 ?

(fin)

[實作筆記] 使用 Request Bin 測試第三方 Webhook 與 Callback

案例說明

最近串接 Stripe ,一個相當方便的國際化金流服務,
情境是主子帳號的綁定,
Stripe 可以透過 Oauth 的方式綁定不同的 Stripe 帳戶,
對於電子商務的平台來說,相當的方便,
它可以透過簡單的授權機制,取得客戶的金流資訊,

Strip Oauth Flow

這時候問題來了,營運部門發現有的時候客戶會不小心將授權解除,
這會導致帳務上的問題,所以需要在第一時間被通知,
而 Stripe 其實也有提供 Event Driven 的解決方案。

透過 Webhook 監聽指定的事件 (Event),可以串接各種通知服務。
ex: Line、E-mail、簡訊甚至是電話,
開發這樣的通知服務並不難但是要時間的。

而其中最大的風險在於 Event 送到 Webhook 再到通知服務的這段流程。
如果這段不通,就算 Event 確實會發生,就算通知務服務是正常的,
仍然會收不到通知。

有沒有辦法快速驗証Webhook呢?
有沒有辦法快速驗証Webhook呢?
有沒有辦法快速驗証Webhook呢?

網路上的 Solution

這個時候就要推薦一下網路上的這個服務 Request Bin
登入後只需要一鍵,就可以快速建立一個 EndPoint
而且立即生效,有任 Request 進來都會完整記錄。

好處

  1. 快速建立 EndPoint
  2. 立即生效
  3. 免費

如果有其它類似的服務,請推薦。

參考

(fin)

[實作筆記] Reshaper Code Template

前情提要

我通常會作一些 Code Snippet 來加速開發,
避免同樣的代碼要重複的寫,
除了避免重複,能交給工具的儘量交給工具去作,
只要是人為的操作就有可能犯錯,就算是簡單的剪下貼上,
讓工具作事,用心去檢驗,這是我在 2019 練習的小心得。

比如說我使用無蝦米輸入法、刻意練習英打速度,
購買 Reshaper 等…,都是為了提昇生產力。
今天就是要說說 Reshaper 的 Template ,
這是一個跟 Code Snippet 相同的功能 。

本文

Template 與 Code Snippet 並不使用共同的範例檔,
所以要如何將已有的 Snippet 轉移過來呢 ? 如果有人知道請跟我說。
另一個問題是,他真的比原本的 Snippet 好用嗎 ?

因為我的 Snippet 並不多,大約 10 個左右
為了馬上增加生產力,我手動將主要有用到的 2個重建了
剩下的如果真的很少用,就讓他自然淘汰吧。
常用的如果有用到再來重建。

第二件事,我使用上手感是差不多的
據說 Resharper 會更智慧化,所以我會選用 Code Template

如何新增

以 Visual Studio 2019 與 Resharper Ultimate 為例,
在搜尋框輸入 Template 找到 Template Explorer
點擊新增圖示,進行編輯,
其它就跟原本的 Snippet 差不多,也可以提供參數化入與 short cut
Create Template

補充

如果已經安裝了 Resharper Ultimate 但是 Template 不工作的話記得看一下設定

Resharper > Option > Intellisense > General

Option

參考

(fin)

[學習筆記] SQL 大小事 Isolation Level 與 SARGs

ACID 與 Isolation Level

什麼是 ACID

  • Atomicity(原子性):
    一個交易(transaction)中的所有操作,要嘛全部完成,要嘛全部不完成,
    不會在中間某個環節中斷。交易在執行過程中發生錯誤,會被回滾(Rollback)到交易開始前的狀態,
    就像這個交易從來沒有執行過一樣。即,交易不可分割、不可約簡。
  • Consistency(一致性):
    在交易開始之前和交易結束以後,資料庫的完整性沒有被破壞。
    這表示寫入的資料必須完全符合所有的預設約束、觸發器、級聯回滾等。
  • Isolation(隔離性):
    資料庫允許多個並發交易同時對其數據進行讀寫和修改的能力,
    隔離性可以防止多個交易並發執行時由於交叉執行而導致數據的不一致。
    交易隔離分為不同級別,
    包括未提交讀(Read uncommitted)、
    提交讀(read committed)、
    可重複讀(repeatable read)和串行化(Serializable)。
  • Durability(持久性):
    交易處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失。

問題,欄位 price 現在為 100,兩筆交易同時發生,A 交易為 price+10, B 交易為 price-15 請問最終的值為何

MsSQL 預設的 Isolation Level 為 READ COMMITTED

Isolation Level

情境題 & SQL

建立測試資料

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE [dbo].[ACIDSample](
[ACIDSample_Id] [bigint] IDENTITY(1,1) NOT NULL,
[ACIDSample_Name] [nvarchar](100) NULL,
[ACIDSample_Price] [Decimal] NULL)

INSERT INTO [dbo].[ACIDSample]
([ACIDSample_Name]
,[ACIDSample_Price])
VALUES
('Toy',150),
('Shoes', 120)

GO

Read Committed(MsSQL 預設值) vs Read Uncommitted

差別在於會不會讀到髒資料,
Read Uncommitted 不會使用 locked 來避免未 commit 的資料被讀取,
參考以下範例:

假設目前 ACIDSample_Price = 100

Sample READ COMMITTED

Session 1,執行 Update 後延遲 10 秒後 Rollback

1
2
3
4
5
6
7
BEGIN TRANSACTION
UPDATE ACIDSample
SET ACIDSample_Price = ACIDSample_Price + 10
WHERE ACIDSample_Id = 1

WaitFor Delay '00:00:10'
Rollback Transaction

Session 2,
設定 TRANSACTION ISOLATION LEVEL 為 READ COMMITTED,
目前 ACIDSample_Price = 100
執行查詢,在 READ COMMITTED 的情境況下,會等待延遲結束後取得資料

1
2
3
-- SET TRANSACTION ISOLATION LEVEL
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SELECT ACIDSample_Price FROM ACIDSample WHERE ACIDSample_ID = 1

Sample READ UNCOMMITTED

Session 1,與前一個範例相同,執行 Update 後延遲 10 秒後 Rollback

1
2
3
4
5
6
7
BEGIN TRANSACTION
UPDATE ACIDSample
SET ACIDSample_Price = ACIDSample_Price + 10
WHERE ACIDSample_Id = 1

WaitFor Delay '00:00:10'
Rollback Transaction

Session 2,執行查詢,在 READ UNCOMMITTED 的情境況下,
會立即取得未 Commit 的資料(Dirty Data),不需要等待10秒

1
2
3
-- SET TRANSACTION ISOLATION LEVEL
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT ACIDSample_Price FROM ACIDSample WHERE ACIDSample_ID = 1

Sample Repeatable Read

Session 1
設定 TRANSACTION ISOLATION LEVEL 為 Repeatable Read
執行以下SQL後,立即執行 Session 2 的 SQL,
等待 10 秒(Delay)後執行 Session 3 的 SQL

1
2
3
4
5
6
7
8
9
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
SELECT * FROM ACIDSample

WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
COMMIT Transaction

Session 2 新增一筆資料

1
2
INSERT INTO ACIDSample (ACIDSample_Name,ACIDSample_Price)
VALUES ('Cat',200)

Session 3 刪除一列資料

1
DELETE ACIDSample WHERE ACIDSample_Name = 'Cat'

等待 Delay 結束後,可以看到查詢結果如下
第2次的查詢會與第3次一致,但是實際上的資料已經被刪除了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ACIDSample_Id        ACIDSample_Name                                                                                      ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120

(2 rows affected)

ACIDSample_Id ACIDSample_Name ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120
7 Cat 200

(3 rows affected)

ACIDSample_Id ACIDSample_Name ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120
7 Cat 200

(3 rows affected)

試著把 Session 的 ISOLATION LEVEL 調成 READ COMMITTED

Session 1 (READ COMMITTED)

1
2
3
4
5
6
7
8
9
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
BEGIN TRANSACTION
SELECT * FROM ACIDSample

WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
COMMIT Transaction

結果在同一筆交易內會相同的會查詢到不同的結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ACIDSample_Id        ACIDSample_Name                                                                                      ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120

(2 rows affected)

ACIDSample_Id ACIDSample_Name ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120
9 Cat 200

(3 rows affected)

ACIDSample_Id ACIDSample_Name ACIDSample_Price
-------------------- ---------------------------------------------------------------------------------------------------- ---------------------------------------
1 Toy 132
2 Shoes 120

(2 rows affected)

Serializable

上面的範例,雖然確保了查詢的一致性,但實際上會查詢到不存在的資料(Phantom),
為了避免這樣的情況可以考慮使用 Serializable,但實際上會帶來效能的耗損,
在這樣嚴格的限制下,所有查詢將有序的執行。

Session 1

1
2
3
4
5
6
7
8
9
SET TRANSACTION ISOLATION LEVEL Serializable
BEGIN TRANSACTION
SELECT * FROM ACIDSample

WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
WaitFor Delay '00:00:10'
SELECT * FROM ACIDSample
COMMIT Transaction

也就是說在執行了 Sesson 1 的 SQL 之後 ,
Sesson 2 即使在 Delay 的時間中執行也必須等待 Sesson 1 結束才會執行,
這樣確保了資料一致性同時也不會拿到 Dirty Data 或 Phantom

Session 2 新增一筆資料

1
2
INSERT INTO ACIDSample (ACIDSample_Name,ACIDSample_Price)
VALUES ('Cat',200)

Session 3 刪除一列資料

1
DELETE ACIDSample WHERE ACIDSample_Name = 'Cat'

SARGs (Search Arguments)

SQL 效能調校的一些 GuideLine

建議使符合「查詢參數(Search ARGument SARGs)」規則

  • SARGs的格式:

    • 「資料欄位 部份運算子 <常數或變數>
    • 常數或變數> 部份運算子 資料欄位」
    • 運算子:=、<、>、>=、<=、BETWEEN、LIKE ‘關鍵字%’
  • 非SARGs格式:

    • 對資料欄位做運算
    • 負向查詢:NOT、!=、<>、!>、!<、NOT EXISTS、NOT IN、NOT LIKE
    • 對資料欄位使用函數
    • 使用OR運算子

參考

(fin)

[生活筆記] 2019 InterView 心得分享

前言

在 N 社 4 年了,一直以來都有一個習慣,就是每年都會定期開 CV,
有好的機會就去面試看看。
部門主管 C 哥在這部份相當開明。
我印象深刻是剛進公司時,他要大家寫 1 年後的履歷寄信給他再跟他 1-1。
他也鼓勵有好的機會就去試,重點是「以終為始」,搞清楚自已要的到底是什麼 ?

理由

有比沒有好一點的敏捷

我是一個理論的實踐者,如果有我認同的理想,
就會試著去實踐,可惜「理想很豐滿,現實很骨感」,
理論與實踐往往總是有一條橫溝。
這不是我想走的主因,但是如果我能找到橫溝一處較窄的地方。
我想我會毫不猶豫的走過去。

打不敗的通膨

另一個理由就更現實了,「薪資」基本上4年來是沒有調整的,
當然我的計算方式比較特別。我是會考慮通膨率,
此外,在這個大前端的時代不知道為什麼我好像選錯了邊,
不得不說 F2E 的價金較高機會也較多。
而我在 C 社時期,其實是前後通吃的,但是不包含切版。
在 N 社我不止一次想轉換到 F2E 卻有以下幾個卡點,

  1. 缺乏實際行動,僅止於與 F2E主管聊聊的階段
  2. 切版在 N 社 F2E 是必備技能,但不是我職涯規劃中準備投資的項目
  3. 過舊的技術棧 Angular 1.0 與 KnockOutjS 還有 KendoUI 等…
  4. F2E 與 RD 部門的 Silo

重點我想還是在 1 ,其它項目其實都有漸漸在改變。

工作內容

再延申到最後一個原因,想作的事與正在作的事。
其實我很清楚,這兩者要相同並沒有那邊容易,
想作的事還很模糊,而正在作的事很無聊。
過去一年基本上只是在抽字串,
或是將功能搬到 A 市場, 再搬到 B 市場,
而且真的只有搬,有沒有人用都不知道,業績如何也不知道,團隊作起來很沒有成就感。

而跟主系統(台灣市場)比較起來,A/B市場規模相對小,人力也少,
當有產品需求調整的情況,往往犧牲的都是A/B市場的,再由我們開發補丁作修正。
而不是一開始就以一個國際化的產品在設計產品,這樣作下來漸漸就提不勁了。

就業一段時間應該都知道,只有功勞沒有苦勞。
不賺錢,沒資源,更不賺錢,更沒資源是一種惡向循環。

我的解決方案,重新定位團隊目標,
所以我非常強力推動幾個項目的國際版標準化,
因為我認為這才是我們的核心項目。

彼得效應

彼得效應其實是我在 C 社的功課,簡單的說就是碰到天花板了。
不論在能力上、薪資上或是發展性上都到了一個瓶頸,
這問題不只在我身上,在我的同儕、我的主管以及我周邊所有的人身上,
我以前會考慮,要如何才能延續我技術人員的生涯?
特別是在這個變化如此快速的時代。

目前我還沒有一個很完整的答案,不過我周邊的人離開了原本公司之後,
又遇到了相同的問題,討厭的主管/討厭的政治環境/討厭的工作內容,
在不同的公司,卻有相同離開的理由…
我目前的想法,不再只以「技術人員」作考量,
在薪資可接受的範圍下,我作為一個人,作為「我自已」我要如何讓自已盡可能的發展 ?
純技術是不是自我設限 ? 主管不會領導,那我能不能反過來引導他 ?
環境不喜歡,我能不能改變它 ? 人不喜歡,我能不能找到志同道合的夥伴 ?

身體因素,眼花花心慌慌

去年 5 月身體微恙,其實自已有點被嚇到,
劇烈的頭痛外加短暫的視力消失,
N 社一直以來都有鼓勵用下班時間進行一些工作的文化,
包含午休時間的「分享」或讀書會,對我來說沒有不好,
但是貪多不得,反而傷害了身體,
這次病後,比較會注意自已的飲食與作習,
畢竟留得青山在不怕沒柴燒。

面試記錄

講了一堆現在才要進到主題。
先講結論,我愛的人不愛我,愛我的人我不愛。

Nxxx Bxxx

純網銀的公司。慕名 N 社前 HR 大主管之名而去,
面試機會好像很難拿到,但是我運氣蠻好的,拿到了面試機會。

總共有三面,
第一面是技術職的面試(60mins),只有閒聊工作上的經驗,沒考什麼技術。
有三個技術主管,聊起來的感覺人都蠻和善的。
技術棧並不確定,但是有很多機會,什麼都可以嚐試的感覺。
不太熟悉敏捷,但是宣稱一定會跑測試與自動化等功能。

第二面是 N 社的前主管(30mins),合作的機會不多,所以互相聊了一下公司的進況。
另外我問了一些新聞八掛(肥貓啊、排名最後啊…),也很坦率的回應,
他敢說我就敢信,基本上沒有逃避閃躲問題我覺得就 Ok 了

第三面是總經理,基本上不問你問題,
而是請你問問題,但是我覺得我沒有準備的很好,
只有問一下目前的目標與挑戰,回答是「活下去」

Offer : Get

Txxxx

社群上有名的「敏捷」公司,遊戲產業,
剛好有認識的人介紹之下去面試。

面試的內容,程式題一題,DB題一題,
不過相當的簡單,不太像是在找 Senior 的題目,
剩下的就是閒聊,不過可能是我聊的沒有很好,另外一個可能性,就是薪資的問題所以沒有上。

感覺比較差的兩點,
一個是 T 社找了我兩次,
兩次 HR 都說不論是否錄取都會通知,
但是兩次都沒有通知,
第二次去面試前我還有特別在通訊軟體上與 HR 確認是否會告知結果,
但是仍然是無聲卡。

第二點是兩次面試的題目一模一樣,
完全沒有變化,面試官也知道但好像也不是很介意,
但是以我面試者的角度會覺得這間公司在招慕上沒有什麼鑑別度。

Offer : 無聲卡 * 2

Cxxxxxxxx

主要作工程師媒合平台,是我非常喜歡的一間公司,
也是第一次線上面試,問題蠻廣泛的,但是我覺得我都回答得出來
只有問了一題 ACID 我沒有回答的很好,我有另文記錄。
總之就是我對 DB 的認識停留在預設值,而沒有考慮應該商業需求可能的調整。

另外我作了一些線上面試的小小 Retro,稍稍記錄一下

  • 面向光源,不然你會臉會很黑
  • 保持背景單調乾淨
  • 跟同居的事先說好有面試,避免臨時打擾
  • 儘可能讓攝影機高過你的頭,不然看起來會很肥
  • 就算是凌晨面試也要梳洗,看起來比較有精神
  • 網路可能有延遲,可以稍等一下再回答問題

Offer : Reject

Fxxxxxxxxx

後面好像有個富爸爸的收據公司,數位轉型作 CMS,
而且只針對 mobile 製作,聽起來工作內容蠻輕鬆的。
不過他們的瓶頸在 DB ,所以要找一個後端懂SQL的人。
只有一面,介紹產品還有面臨的問題,
技術棧算蠻新的,不過沒有CI/CD與自動化等工程。

CMS 是我蠻喜歡的 Topic , N 社現在也有在作,
但是我最後還是拒絕了。

主要的原因可能是,我覺得技術上好像沒有辦法在那邊得到進步。

Offer : Get

axxxxxx

微軟 Teams 服務的合作廠商,主要作數位電話相關。
也是朋友介紹的公司,應該是第二次找我,
因為之前沒有很強烈的離職意願,所以沒有去面試。
特色的早上班早下班,大概 5 點就能下班了,據說不太加班,
一面,與美國主管線上訪談,這次有吃到 Cxxxxxxxx 的經驗,
所以聊得蠻順利的,美國主管有技術底,會講中文所以溝通無礙,
不過我一直以為他是美國人,其實是台灣人的樣子,只是在美國待久了有點口音。

二面,跟台灣當地主管面試,介紹產品與閒聊,
老實說工作環境與地點還有薪資我都蠻 OK 的,
當初有考慮過接收 Offer 的。
最後就是跟朋友閒聊,朋友是 C 社的前同事。
基本上就是一個小公司,產業蠻穩定的,那個職位也有很多機會,
但是成長就要靠自已了,現在想想還是會覺得放棄可惜。

Offer : Get

小結

我覺得定期作面試是很有幫助的,
他能帶來以下的好處,

  • 了解市場需求,直接面對第一線的招幕才知道現在含金量最高的技術是什麼
  • 了解自已,過程中不斷評估自已要的東西(薪資、成就感、穩定、福利、名氣…),什麼都是假的,離開(或留下)才是真的
  • 了解不足與市場的差距(特別是技術面)
  • 與不同產業的技術主管聊天,技術只是一種手段,不要忘記背後的商業目的

家家有本難唸的經,不同的公司也有不同的難處,
時時檢視自已所處的環境,換或不換你都可以選擇,
成為更好的自已。
開始準備 2020 的履歷囉。

最後謝謝所以推薦我與面試我的人。

(fin)

[生活筆記] Swing Dance 雜記

學習 Swing 大概滿周年了,最初的理由已經有點忘記了;
為了避免忘掉更多東西,還是隨手記錄一下自已的一些小小想法。

波動的感覺

在新手期的階段有一件有趣的事,在牽起 Follower 手的時候,
大概就知道這個人有沒有跳過舞,基本上會有波動從對方的手傳遞過來,
正常來說這個波動應該是上下的,有時候會遇到左右或是扭扭的波動,
也有波平如鏡的狀況。

Lindy 的基本動作之一是 Bounce,它帶來的波動應該是上下,而且是下律動的。
所以如果感覺到不太一樣的波,大概可以猜這個人之前有跳過別的舞蹈,
準確率還蠻高的,只是現在我好像感覺不太到了。

與武術的關係

上面提到了波,就聊聊舞與武的關係,因為不知道為何我腦海總會出現「波動拳」三個字。
除了波以外,練基本步跟 Bounce 就好像是在蹲馬步,跟 Follower 練習就好像在練套路(Kata)
而 Social 場合就像是賽際過招,年度大型 Event 就是天下武術大會的概念。

目的

跳舞的目的很抽象,不像其它的運動,有明確的目的,例如把球投進籃子裡或是率先衝過終點線。
我給自已訂了一些目標與原則,
首先要讓 Follower 感到安全,
同時不能讓他感到無聊,整個過程必須是有趣的,
最後一定要讓 Follower 跳得好看。
如果 Follower 被誇獎跳得很美,Leader 才算成功。

規則

一開始學習給的一些自我約束,不一一說明原委,反正我就是喜歡沒事找事作

  1. 不要找同一個人一直跳舞,儘可能每首都換
  2. 全場通殺,小一點的 Social 場合儘量跟每個人都跳到
  3. Follower 永遠是對的,如果 Follower 跟錯了,想想是不是自已的 Leading 不夠明確
  4. Last Song , 可以的話最後一首一定要跳

教室

Big Apple(BA)、Swing Taiwan(ST)、YM Swing(YM)、Lindy Island(艾倫)、JF Swing(JF)、Naughty Swing

台北我知道的教室大概就這幾間(2019),第一次是在 ST 上的,後來時間對不上就去 YM 學了一陣子(半年以上),
Lindy Island 上過一期 Swing Out(四堂),Naughty Swing 上過一次 Solo 。
上面的教室括號內是教室的簡稱,然後我是按照某種神祕規則排序的,不知道有人看得出來嗎 ?

給一年前的我的建議

  1. 不要先學 Charleston 。
  2. 照教室開課的順序去學,但是覺得困難的話,與其往下上,不如上複習班
  3. 不同教室類似的課通常會錯開,時間許可的話都去上。
  4. 常去 Socail 多聽音樂,練習、練習、練習
  5. Solo 比想像中的好玩,一定要試試
  6. Keep Learning

(fin)