使用 embedded struct 做 json 的 marshal 跟 unmarshal 時,json 欄位會省略 struct embedded 欄位的中間名,以比較簡潔的形式呈現。如果 struct 有寫出欄位名稱,json 欄位就會多那一層。

使用 embedded struct

1
2
3
4
5
6
7
8
9
10
11
type Serving struct {
Amount float64
Unit string
}

type Food struct {
Name string
Serving // embedded struct
NutritionInfo
Comment string
}

marshal 結果:

1
2
3
4
5
6
7
8
9
10
{
"Name":"Banana",
"Amount":100,
"Unit":"g",
"Calorie":0,
"Carb":0,
"Fat":0,
"Protein":0,
"Comment":""
}

不使用 embedded struct

1
2
3
4
5
6
type Food struct {
Name string
Serving Serving // not embedded struct
NutritionInfo
Comment string
}

marshal 結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Name":"Banana",
"Serving":
{
"Amount":100,
"Unit":"g"
},
"Calorie":0,
"Carb":0,
"Fat":0,
"Protein":0,
"Comment":""
}

不用 embeded struct 就會有一層 Serving,用 embedded struct 就會省略 Serving 這層。

submodule 是在 git repos 中使用別的 repos 的方式之一。

git 的 submodule 是記錄一個指到別人 repo 某個 commit 的指標。對主 repo 來說,記錄的只是一個 submodule commit hash。

切到 submodule 的目錄時做 git 操作會是在操作另一個 repo。

加入 submodule

1
$ git submodule add <repo path>

clone 含有 submodule 的 repos

clone 含有 submodule 的 repos 後,submodule 的目錄會是空的,要做以下動作來初始化:

1
2
$ git submodule init
$ git submodule update

git submodule update 會讓 submodule 的內容回到記錄的 commit。

更新 submodule

submodule 的 repo 更新或者想用不同版本(commit)的 submodule 時,要做以下操作:

1
2
3
4
5
$ cd submodule_dir
$ git pull
$ cd ..
$ git a submodule_dir # 更新主 repo 記錄的 submodule commit hash
$ git ci

概念是把 submodule 的 repo 更新或者 checkout 到想要的 commit,再在主 repo 更新記錄的 submodule commit hash。

移除 submodule

1
2
$ git rm -rf submodule_dir
$ vim .git/config # 移掉 submodule 相關設定

.gitmodules

檔案 .gitmodules 會記錄有哪些 submodule。

任何檔案可以擁有任意數量的 init() function:

1
2
3
func init() {
// ...
}

init() 會在程式啟動時自動以宣告的順序執行,但不能被 call 或參考。

假設有以下兩個 go 檔案:

foo.go
1
2
3
4
5
6
7
package main

import "fmt"

func init() {
fmt.Println("foo.go first init")
}
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func init() {
fmt.Println("main.go first init")
}

func init() {
fmt.Println("main.go second init")
}

func main() {

}

go run 以不同的順序指定 source file 會有不同結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ go run foo.go main.go
foo.go first init
main.go first init
main.go second init

$ go run main.go foo.go
main.go first init
main.go second init
foo.go first init

# 不指定 file
$ go run .
foo.go first init
main.go first init
main.go second init

不指定 file 的話 go 會將 file 以其名稱排序。

如果嘗試直接 call init() 則會 compile error:

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func init() {
fmt.Println("main.go first init")
}

func main() {
init() // compile error: undefined: init
}

要為一段 code 寫測試,首先要在測試中建立它所屬的 class 的 instance,接著為要修改的 method 寫測試。這章要解決的是「難以在測試中執行要測試的 method」的情境。為一個 method 寫測試可能會遇到的問題:

  • 無法在測試中 access 那個 method,例如它可能是 private 的或是有其他存取限制。
  • 很難建立所需參數,導致很難 call method,例如參數是一包 XML。
  • 要測試的 method 可能產生糟糕的 side effect,例如修改 DB、發射飛彈等等,所以無法在測試中執行它。
  • 需要透過該 method 使用的 object 進行 sense,才能知道這個 method 做了什麼事。

The Case of the Hidden Method

假設我們要修改的是一個 private method,想要測試它,該怎麼辦呢?

首先,能透過 public method 來測試它嗎?如果可以,就這麼幹吧~用 public method 去測試,就是按照程式中 private method 如何被使用的方式去測試它。如果有天要把 private method 改成 public,把它變成 public 的人應該寫一系列的測試說明這個 method 的用途以及 caller 該如何使用它。

這邊有提到一點 method 設計實作上的小概念:

雖然 general 的 method 對 caller 來說蠻有用的,但每個 method 的功能應該剛好可以滿足 caller 並且易於理解與修改。

有時候呢,我們就是想直接為 private method 寫測試(任性),可能是因為我們想用測試來知道如何使用 private method,或者用 public method 來測試它實在太難太痛苦啦~

例如一個擁有商業邏輯並且會 call third-party API 的 class 做的事情是:call API 取得一包 XML 資料,parse XML 得到商業邏輯需要的資料,再做商業邏輯上的計算或操作。我們想知道 parse XML 的 private method 是否正確,但它埋在整個流程裡,而用 public method 做整個的 call API、parsing、商業邏輯的測試難以只測試到 parse XML 的部份。

所以,想為 private method 寫測試時該怎麼辦呢?

如果需要測試一個 private method,就該把它設成 public。

看到書上這句話我蠻驚恐的,想著:「等等等,不是吧?就這樣直接把 private method 變成 public 好嗎?這不會在 class 上開出看起來突兀或者不知如何使用的 method 嗎?」

如果不方便將其設為 public,大多數情況下意味著我們的 class 做太多事了,應該進行調整。

喔~原來是這樣~這倒是真的~像上面那個例子,一個 class 既 call third-party API 又 parse XML 又做商業邏輯,太多事情了。

如果我們想測試一個 private method,首先看它是不是個適合在這個 class 當作 public 的 method?如果是,直接改成 public。否則看看是不是這個 class 做太多事了,有些事可以交由另一個 class 處理。例如我們把 parse XML 有關的 method 放到另一個 parser class,這些 method 到了新 class 會變成 public,原本的 class 就能 new 一個 parser 出來做事。

好的設計應當是可測試的,不具可測試性的設計是糟糕的。

如果我們遇到上面這樣的狀況,想拆解 class、將職責分開,卻沒有多少現成測試呢?假設我們想拆解這個包山包海的 class,但它本身卻沒有什麼測試,而 refactor 應當要有測試保護,雞生蛋蛋生雞的問題出現啦~

又或許,我們正在開發週期的後期,軟體已經接近 deploy,我們沒有多少時間去做拆解的 refactor,而且沒有測試又讓 refactor 的風險大幅提昇。儘管 refactor 可以改善程式結構、有好處,但需要考慮目前處於開發週期的哪個階段、時間多寡與風險高低,才能決定要不要 refactor 以及 refactor 到什麼程度。

時間不夠或風險太高的時,我們無法拆解 class。退而求其次,至少幫我們要修改的 private method 加上測試,讓這個 class 開始有測試保護也是好的。

想為 private method 加上測試,表示要能在測試中直接 call 到這個 method,要怎麼做呢?

又用到 Extract and Override 了~這招也太萬用…

假設我們有個與機票搜尋、訂購有關的 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Flight {
public function search($searchParams) {
// call third party api and get response as xml
$searchResults = $this->parseSearchResponse($xml);
// other impl.
}

private function parseSearchResponse($xml) {
// impl.
}

public function booking($bookingParams) {
// impl.
}

// other methods
}

search() 裡先用 http client 向 third party API 送 request 並收 response,response 是一包 XML,我們想 parse 出其中需要的資料而 call parseSearchResponse()。因為 XML 相當複雜,我們希望能單獨確認 parsing 結果是否正確。

這個 class 除了 search、booking、parse 各種 XML 之外還會做許多事情,它的職責太多了,如果我們現在沒有時間去拆解它,卻想測試 parseSearchResponse() 的結果該怎麼做?

首先將 parseSearchResponse()private 變成 protected

1
2
3
4
5
6
7
8
9
10
11
12
class Flight {
public function search($searchParams) {
// call third party api and get response as xml
$searchResults = $this->parseSearchResponse($xml);
// other impl.
}

// 變成 protected
protected function parseSearchResponse($xml) {
// impl.
}
}

接著在測試中繼承它:

1
2
3
4
5
class FlightForTest extends Flight {
public function parseSearchResponse($xml) {
return parent::parseSearchResponse($xml);
}
}

這樣就能在測試 call 到 parseSearchResponse() 進行測試了:

1
2
3
4
5
6
7
8
class FlightTest extends TestCase {
public function testParseSearchResponse() {
$target = new FlightForTest();
$xml = 'blabla';
$ret = $target->parseSearchResponse($xml);
// assertions
}
}

這麼做雖然沒有立即改善 Flight 做太多事的問題,但至少幫修改的地方加上測試,確保目前的修改是正確的。並且為將來拆解 class 的 refactor 鋪路――因為加了些測試而減少之後 refactor 所需的 effort 跟時間,使之後 refactor 成為可能。

補充

在 Debian,先安裝:

1
$ sudo apt install mariadb-server

裝完做些跟安全有關的設定跟 root password:

1
$ sudo mysql_secure_installation

Summary

  • 自動化重構工具
    • 像 JetBrains、Visual Studio 等 IDE 的 refactor 功能。
    • 在工具夠好的情況下,可以在沒有為 code 寫測試的情況下用工具進行自動化 refactor。但有時工具可能有問題,refactor 卻會修改到程式行為,所以使用工具進行 refactor 前,如果能先寫測試還是比較好的。
    • 要注意工具在 refactor 時會跟不會進行哪些檢查
      • 例如 extract method,如果將新 method 命名為已存在 method 的名字,工具會不會顯示錯誤?
  • Unit Test 工具
    • xUnit framework
      這類型 framework 執行測試大致的作法:找到 test class 裡所有 test method(依據語言不同有不同作法,有些語言可以用 reflection),為每個 test method 產生一個單獨的 object,該 object 的任務是去執行那個 test method。利用不同 object 隔離 test case,讓 test case 不會互相影響。
  • 一般測試控制工具
    • FIT (Framework for Integration Test)
    • Fitnesse
    • Selenium
    • ……等等其他

Seam & Enabling Point

seam(接縫)是指程式中一些特殊的點,在這些點上你不需要修改它本身就可以變動程式行為。

例如某段程式 call 了一個 function 來計算商品價格,現在想改變計算價格的 strategy。想在不改變 call 計價 function 的情況下,改變計價結果(程式行為)。

又例如程式 call 到牽連龐大子系統的 function,我們希望在 test 中避免執行到那些複雜的 code(不然很難測或無法測),又要在 prodcution code 裡照常執行到。如果這段 code 有 seam,便能在不改動到原本 call function 的情況下,換掉該 function 的行為來避免在 test 中碰到子系統。

每個 seam 都有一個 enabling point,在這裡你可以決定使用哪種行為。

seam 是可以讓你改變程式行為的「縫隙」,enabling point 則是決定那個 seam 要是什麼行為。在 enabling point 給不同的值,可以讓 seam 有不同的行為。例如物件 seam,物件 method 的參數列表是 enabling point,因為我們可以傳入不同的物件來改變程式行為。

為一團亂的 code 測試時,最好別去修改它本身,盡可能透過 seam 去解開 dependency 來測試。

Seam Types

seam 有很多種類型。不同的語言,可以使用的類型也不同。一個語言從程式碼轉換到 machine code 的各個階段,擁有不同種類的 seam。

  • preprocess 時期 seam
    • 在 C/C++ 裡可以用 macro 直接把字換掉來達到改變行為,例如直接把有 dependency 的 function call 用 #define#ifdef TESTING 時換成空的 function。
    • enabling point 是 #define TESTING
  • link 時期 seam:在將多個 object file link 起來時的 seam
    • dynamic link:直接換掉 dynamic link 要去找的 library 或 object file。
    • static link:透過 build script(像 Makefile),在測試環境去 link 抽換的 object file 而非原本 production code 的 object file。
    • enabling point 通常在 build 或 deploy script 裡,因為這時候才決定 link 誰。
  • object seam
    • 一般用 interface、polymorphism 來做 dependency injection,以及 extract and override。

先安裝 ibus:

1
$ sudo apt-get install ibus ibus-chewing

~/.bashrc  加入:

1
2
3
export GTK_IM_MODULE=ibus
export XMODIFIERS=@im=ibus
export QT_IM_MODULE=ibus

寫個 shell script:

1
2
#!/bin/bash
ibus-daemon -d -x

在 KDE Setting 的 Autostart 設定啟動時執行 shell script 來啟動 ibus

這天很多在介紹 C,尤其是 pointer,作者用組語的角度去看 C,很有趣。這邊只寫些簡單的筆記。

組語跟 C 一起使用時,只有 EAXECXEDX 三個 register 可以使用,其他 register 只能 read 不能 write,因為它們存著 C 語言程式的相關資料。

C 語言中,普通數值跟表示 memory address 的數值被當作兩種不同的東西。

根據 C 語言規定,組語執行 RET 指令時, EAX 中的值就被看作是 function return value。

調色盤設定顏色

在 8 bit 彩色模式下,顏色以 8 個 bit 表示,也就是 0~255。

每個數字表示什麼顏色,是由 developer 來決定的,不像 RGB 的 #ffffff 固定代表某個顏色。使用 0~255 表示顏色前,developer 要先幫這些數字指定好對應的顏色,例如 25 對應 #ffffff 等等。這種方式稱為「調色盤」。

設定調色盤的步驟如下:

  1. 先 block interrupt
  2. 依據設定調色盤的方式,對 IO device 的某些 port 寫入資料
  3. 恢復對 interrupt 的處理

向 IO Device read/write

先來看怎麼對 IO device 讀寫。

CPU 與 IO device 相連,CPU 要能控制 IO device 當然有向 device 發送訊號與從 device 接收訊號的指令。

向 device 發送訊號的指令是 OUT,反之從 device 接收訊號的指令是 IN。就像 memory 用 memory address 區分不同位置,device 以 device port 區分不同 device。組語實作從 device read 與 write 到 device 的 function 們:

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
; read 8 bit
_io_in8: ; int io_in8(int port);
MOV EDX,[ESP+4] ; port (port number 只有 16 bit)
MOV EAX,0 ; 把 return value 清成 0
IN AL,DX ; read 8 bit (AL)
RET ; EAX 的值是 return value

_io_in16: ; int io_in16(int port);
MOV EDX,[ESP+4] ; port
MOV EAX,0
IN AX,DX ; read 16 bit (AX)
RET

_io_in32: ; int io_in32(int port);
MOV EDX,[ESP+4] ; port
IN EAX,DX ; read 32 bit (EAX)
RET

; write 8 bit
_io_out8: ; void io_out8(int port, int data);
MOV EDX,[ESP+4] ; port
MOV AL,[ESP+8] ; data
OUT DX,AL
RET

_io_out16: ; void io_out16(int port, int data);
MOV EDX,[ESP+4] ; port
MOV EAX,[ESP+8] ; data
OUT DX,AX
RET

_io_out32: ; void io_out32(int port, int data);
MOV EDX,[ESP+4] ; port
MOV EAX,[ESP+8] ; data
OUT DX,EAX
RET

我們要設定調色板,就是找出對應 device 設定調色板的指令,然後照著做~

CLI 與 STI

CLI 是將 interrupt flag clear 為 0 的指令, STI 則是將 interrupt flag set 為 1 的指令。

interrupt flag 為 0 時,CPU 遇到 interrupt 會忽略它、不處理,flag 為 1 時 CPU 就會處理 interrupt。

EFLAGS register

EFLAGS 是由 FLAGS 16 bit 的 register 擴展而來的 32 bit register。

FLAGS 儲存 carry flag(進位 flag)與 interrupt flag 等 flag,不同 bit 代表不同 flag(有 1 個 bit 表示一個 flag 也有 2 個 bit),如下所示:

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
NT IOPL IOPL OF DF IF TF SF ZF AF PF CF

IOPL 是第 12 跟 13 bit 放在一起處理。

EFLAGS 沒有 MOV 指令,只能使用 PUSHFDPOPFD 來讀寫。

PUSHFD 是 push flags double-word 的縮寫,以 double word 的長度(32 bit)將 flag 的值 push 進 stack,等同 PUSH EFLAGSPOPFD 是 pop flags double-word,將 double word 長度的值從 stack pop 出來到 flag,等同 POP EFLAGS

如果想把 EFLAGS 內的資料放到 EAX 裡,不能用 MOV,而要先 PUSHFDPOP EAX。反過來想把 EAX 的資料放進 EFLAGS 裡則是 PUSH EAXPOPFD。操作 EFLAGS 的組語 code 如下:

1
2
3
4
5
6
7
8
9
10
_io_load_eflags:	; int io_load_eflags(void);
PUSHFD ; PUSH EFLAGS
POP EAX
RET

_io_store_eflags: ; void io_store_eflags(int eflags);
MOV EAX,[ESP+4]
PUSH EAX
POPFD ; POP EFLAGS
RET

設定調色盤要先執行 CLI,為了在設定完後恢復 interrupt flag 的值,要先把原本的值記下來。我們可以直接把整個 eflags 記下來、執行 CLI、設定調色盤,最後直接把整個 eflags 的值再存回去,達到恢復 interrupt flag 的效果。

在螢幕上畫圖

螢幕上每個像素都對應到 VRAM 中的一個 address。向 VRAM 指定值,便能指定螢幕上像素的顏色。