很久很久以前(講得好像很老一樣),剛學寫程式的時候,我有個習慣是寫一點點就會執行起來看對不對。在那個連用迴圈做輸入都不太熟的超新手時期,連輸入都會做這種「測試」。這習慣延續了很久,直到現在,在剛開始學一個新語言或者環境許可的狀況下(編得過、可以跑、測起來不會太複雜),還是會這樣──寫一點點就跑起來看對不對。

今天晚上寫小玩具的時候,新語言嘛,不熟,就又出現這種習慣。就在我還沒把程式 run 下去、正覺得這種驗證很囉唆的電光石火(?)之間,我突然想到──幹嘛不把這種驗證寫成 unit test 就好?這樣就不用跑很多次、人眼校對很多次了啊!

於是乎,這次改變方式,先把要寫的東西全部寫完,再寫 unit test,直接 run unit test 看 code 有沒有寫對。在這種很瑣碎但又得確認是對的事情上,用 unit test 真是神清氣爽啊!雖然不知道之後重複利用的機會多大,但是不用在那裡對到快脫窗真好。也還真給我抓到一個漏掉 assign 的 member…

看來我的寫 code 方式終於跟 test 連上了一點,不然每次想要硬套 TDD 之類的時候,就像學了個新方法,可是並沒有改變舊習慣,兩邊有點接不起來。這次是注意到舊習慣令人厭煩可以改善的地方,再套一點新東西進去變成好一點的方式。

最近在想,獨立開發的時候,更該利用自動化測試減少花在重複性工作上的時間。我想,運用科技讓人能更妥善運用時間,才是科技的本意吧。

QMap 內的資料會以 key 的值 sort 好。以自訂 class 或 struct 作為 key 需要提供 operator<。所以將資料 insert 進 QMap,再用 iterator 取出就可以做到 sorting。

1
2
3
4
5
6
7
8
9
10
11
12
QMap<int, QString> sortMap;

sortMap.insert(31, "31");
sortMap.insert(3, "3");
sortMap.insert(2, "2");
sortMap.insert(5, "5");
sortMap.insert(7, "7");

for (QMap<int, QString>::iterator iter = sortMap.begin(); iter != sortMap.end(); ++iter)
{
// sorted
}

key 有多個欄位時可以做到不同欄位有不同 priority 的能力,如:

1
2
3
4
5
6
7
8
9
bool operator<(const XXX& rhs) const
{
if (fieldA != rhs.fieldA)
return fieldA < rhs.fieldA;
if (fieldB != rhs.fieldB)
return fieldB < rhs.fieldB;
...
return false;
}

這表示在比較上,fieldA 的 priority 比 fieldB 高,也就是先比 fieldA 再比 fieldB。

x < yy < x 都為 false 時表示 x == y

今年,專業上:

  • 開了這個 blog
  • 看完《Effective C++》(跳過 template 就是了,那塊太不熟根本看不懂)
  • 《深入淺出物件導向分析與設計》一到九章(剩一章,耶!)
  • 正在看《程式設計師的自我修養──連結、載入、程式庫》
  • 邊做邊學 Ruby on Rails
  • 對 C++ 有再熟一點,吧?
  • 設計架構崩崩中
  • 有比較會切細工作,吧?
  • 多會一點 Git

2015 年目標:

  • 吃掉《深入淺出物件導向分析與設計》
  • 啃掉《程式設計師的自我修養──連結、載入、程式庫》
  • 看《深入淺出設計模式》
  • 看《Clean Code》(寫到這裡我在想是不是在找死?)
  • 寫個 Ruby Gem
  • 練習 OOAD
  • 練習「想清楚再做」跟「避免分析癱瘓」(有時候會一直在那裡想然後做不了決定 X_X)
  • 找些 open source project 來 follow(好像有點抽象)
  • 填補理論跟實務之間的空虛地帶(具體上還沒決定,先列些主題)
    • OS
    • Compiler
    • Multi-Thread Programming

我也不知道寫這些目標會不會照著做,很具體的應該會,寫得越抽象的越容易歪掉就是了。明年底再來看哪些有做到、哪些歪掉、又有什麼是吃飽太閒在亂玩。:P

一個檔案 include 另一個檔案時,這兩個檔案便形成 compilation dependency。一個被 include 的檔案一旦修改,跟它有 compilation dependency 的檔案(無論直接或間接 include)都需要重新 compile。假設 A.h include B.h,當 B.h 改變或 B.h include 的檔案改變時,所有 include A.h 的檔案都需要重新 compile。

應該盡量降低 compile dependency,以減少修改一個 header 而需要重新 compile 的檔案數量,免得改個檔案全世界都要重新 compile。(工程師無數青春在哭泣)

原則上盡量以 declaration 的 dependency 取代 definition 的 dependency

  • 可以用 object pointer 或 object reference,就不要用 object。

    可以在 class 裡宣告 pointer 或 reference 作為 member,就不要定義 object 當 member,因為定義 object 就需要該 class 的 definition。

  • 分開 class 的宣告與實作

    讓 include(提供 definition)可以從 header file 移到真正使用這些 class、function 的 cpp file。

    • Handle class

      使用兩個 class,一個代表宣告,一個是真正的實作。前者通常稱為 Handle class,只宣告 function 並有個 pointer 指向後者。外界使用 Handle class,而 Handle class 會轉 call 真正 implement 的 class 達到功能。這個方式用到第一點的概念──避免使用 object,改用 pointer。

      因為兩個 class 會有相同的 function,會造成同時 maintain 兩份類似 class 的問題。

    • Interface class(abstract base class)

      interface class 沒有 member data、沒有 constructor,只有一 virtual destructor 跟一堆 pure virtual function。實作的 class 繼承 interface class 進行功能實作,以 virtual function 的機制讓外界用 interface class 的 pointer 使用。

    這兩種方法的 trade off 是速度會慢一點、多用點 memory,不過普遍上來說好處比 trade off 重要。

Ref

  • 《Effective C++》item 31

program by constract(按契約程式設計)就是寫 code 跟用那份 code 的人說好,這些 function 要做什麼、負責什麼、使用的人要怎麼用,大家照說好的去做。

defensive programming(防禦性程式設計)則是大家互相不信任,會做許多檢查以確保自己不會當掉、使用的人用錯也不會當。defensive programming 會產生許多用於檢查的 code,讓 code 變得冗長、效率也可能較差,所以一般建議在 input 要進入系統時做檢查,系統內部互相 call 時就不再做檢查。當檢查發現問題、出現不該出現的錯誤時,較建議盡快讓程式 fail 好找出錯誤,而非忽略錯誤、繼續讓系統執行下去。

Ref

Linux 本身沒有 thread 的概念,它把 process 跟 thread 都視為 Task

fork() 用來生 child process,生出來的是概念上的 process。

clone() 可以生出跟 parent process 共用 memory space、file descriptor 的 table、signal handler 的 table 等東西的 child process,也就是概念上的 thread。Linux 用 clone() 實作 thread 概念。

clone() 有 glibc 包過的:

1
2
3
4
5
#include <sched.h>

int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

也有純 system call:

1
2
3
long clone(unsigned long flags, void *child_stack,
void *ptid, void *ctid,
struct pt_regs *regs);

UML class diagram 只是個概觀,不會說明所有細節(例如如何實作),可以為需要建立的 class 進行 modeling。

用一張圖解釋:

繼承

1
2
3
4
class Parent
class Child

Child -up-|> Parent

實作(implement)

1
2
3
4
interface Interface
class Concrete

Concrete .up.|> Interface

class 間的關係

抽象上來看 class 之間的關係,程式語言裡不見得有對應的語法,但是釐清抽象上的關係有助寫 code。只要對系統來說是合理的,class 間的關係可能因為系統的演進而改變。

關聯強度:composition > aggregation > association

Association

class 間有關係但關聯性較弱。

1
2
3
class Foo
class Bar
Foo -right-> Bar

class Foo 知道 class Bar

Aggregation

class 間是 has a 的關係。class A 包含 class B,但 B 不是 A 的一部分。

main object 不負責 composed object 的 create 及 destroy,composed object 可以獨立於 main object 之外。例如車子跟輪胎間的關係(Car has a Tire),輪胎可以獨立於車子之外(可以從車子上拆下來)。

UML 以空心菱形表示:

1
Car o-right- Tire

Composition

class 間是 own 的關係。(碎念:own 跟 has 根本沒辦法用中文區分…)

main object own composed object.

composed object 是 main object 的一部分(a part of),composed object 不會獨立存在於 main object 之外。main object 負責 composed object 的 create 及 destroy,main object destroy 時 composed object 也會 destroy。例如生物跟細胞間的關係(Animal owns Cell)。

UML 以實心菱形表示:

1
2
3
4
class Animal
class Cell

Animal *-right- Cell

composition 跟 aggregation 最大的不同在於 composed object 的生命週期是否與 main object 有關。

Dependency

class 間是依賴關係時,UML 以虛線箭頭表示:

1
2
3
4
5
6
class Animal
class Water
class Air

Animal .right.> Water
Animal .left.> Air

這個原則跟何時該使用繼承有關:derived class 必須能替代 base class。

使用繼承時必須確保 base class 的所有特性及 method 對 derived class 仍成立,反過來說,derived class 必須有 base class 所有特性。一個 derived class 的 object 是一個(is-a)base class 的 object,原本對 base class 的 object 所做的所有操作可以直接換成對 derived class 的 object 操作。

C++ 的 public 繼承在意義上即是 LSP 所說的繼承觀念。

繼承的替代方案

使用繼承一般是希望擴充原有 class,但如果套用繼承時發現會違反 LSP,有什麼替代方案呢?

class 應該只負責一件事、只有一個改變的理由,class 聚焦於實作該責任。避免一個 class 有多重責任,或者一個責任分散在多個 class 中。SRP 的主要目的是增加 class 的 cohesion(內聚力)。

OO 中會以 class 為單位,但可以提升到 process level。舉例來說,將多個 process 啟動與結束的控制分散在各個 process 以及 watch dog,就讓「控制 process 的啟動與結束」這個責任分散在多個地方。這會導致較難理解控制流程、不知道某個時間點是誰在控制,修改時也容易出 bug。

一個 class 可以負責一個「大」責任。運用 SRP 應該會讓系統中的 class 比較少但比較大,因為會將功能性集中在負責該功能的 class。在思考一個 class 應該負責什麼時,用較抽象的方式去想會比較好,想這 class 要做什麼?它的名稱顯示它該負責什麼?某件事究竟關不關它的事?

我覺得區分 class 的責任蠻困難的,取決於設計者怎麼想、怎麼看待一個 class 以及其想法的合理性。

SRP 分析

分析一個 class 是否符合 SRP。把 class 跟 method 名稱套到以下句子,用人類語言看看通不通順。

The [class] [method] itself.
The [class] [method] [arg] itself.
該 [class] [method] 它自己
該 [class] 自己 [method]

想起來順表示該 method 屬於這個 class、符合 SRP,反之則表示這 method 應該移到其他地方。

分析時要注意參數可能在 construct 已先傳入,不一定是 method 的 argument。

名稱就說明了它的意思──不要重複。

這也是概念上的原則。不僅限於不重複相同的程式碼,也包括不要重複相同的程式碼、相同的功能(例如多個軟體間不重複相同的功能)等等,確保一件事情、一項資訊只存在一個地方。

遵守這個原則的好處是當一件事情需要修改的時候,只需要修改一個地方,不會因為需要改很多地方而漏掉、造成問題。

DRY 也跟如何好好分解系統有關──如何將每項資訊、功能、程式碼放在單一且合理的地方。