# Bug是會傳染的
**Bug發現的越晚,修起來越痛苦**
1. 會寫出與bug有關的程式碼
2. 成為下游程式碼必須依賴的結果
### 不要指望你的使用者
* WHY?
有時候他們會回報一些問題,**但其實更多時候他們會在心裡假設,他們所看到的行為就是你要提供給他們的行為**
* HOW?
寫出更符合使用者取向的文件(軟體工程師與PM的溝通)
* BUT
實際使用者無法想我們一樣,明確的知道功能的用途,所以會把Bug當成功能的一部分
* WHAT?
使用自動化測試:TDD
### 自動化測試可能還蠻棘手的
有些東西很難進行測試:如有損型音頻壓縮編碼解碼器
其他例子:[有狀態服務(Stateful Service)](https://medium.com/brobridge/%E5%A6%82%E4%BD%95%E8%A7%A3%E8%80%A6%E7%85%A9%E4%BA%BA%E7%9A%84%E6%9C%89%E7%8B%80%E6%85%8B%E6%9C%8D%E5%8B%99-stateful-service-bfc14f865878)
### 無狀態的程式碼,更容易進行測試
* 程式碼內盡量不要用到"狀態"之類的東西 - 純粹只依賴"輸入",完全沒有"副作用",並且輸出結果完全可以預測(rails內的callback就是種副作用 - 被視為一種anti-pattern)
依賴外部狀態的簡單例子
```ruby=
# Good code
def add_numbers(a, b)
a + b
end
puts add_numbers(2, 3) # 输出 5
puts add_numbers(5, 5) # 输出 10
```
```ruby=
# Bad code
@counter = 0 # 外部状态
def increment(input)
@counter += input # 修改了外部状态
@counter # 返回了新的状态值
end
puts increment(5) # 输出 5
puts increment(5) # 输出 10,与第一次调用结果不同
```
Q:在rails service內,init之後的call/perform執行時,應該是投入參數比較好,還是用實體變數比較好?
```ruby=
class Service
def initialize(params1, params2)
@params1 = params1
@params2 = params2
end
def call
result = method1(@params1, @params2)
result = method2(result)
result
end
private
def method1(params1, params2)
end
def method2(result)
end
end
```
```ruby=
class Service
def initialize(params1, params2)
@params1 = params1
@params2 = params2
end
def call
@result = method1(@params1, @params2)
method2
@result
end
private
def method1(params1, params2)
end
def method2
@result
end
end
```
* 如果想對程式碼進行徹底而全面的測試,你就必須向他完整展示所有可能遇到的各種狀態,然後再針對不同的狀態,對相應的輸出逐一進行評估。如果是純函示,"函式的參數"就是唯一重要的狀態。但如果你引入了副作用/內部狀態/或向外調用任何函式,狀態數量就有可能暴增。
### 如果無法消除狀態,那就對他進行審查
```ruby=
class Character
# ...
def self.audit_all
@@all_characters.each_with_index do |character, index|
if index > 0
prev_character = @@all_characters[index - 1]
raise "Characters are out of order" unless character.priority >= prev_character.priority
end
character.audit
end
end
def audit
# Implementation of individual character audit checks
# For example:
raise "Invalid character state" if self.priority < 0
end
# ...
end
```
使用內部測試來確保內部狀態有沒有保持一至
### 別太信賴前來調用的那一方
* 並非我們得程式碼有bug,而是因為前來進行調用的程式碼寫了一堆bug
* 最容易造成bug的並不是前來調用得程式碼,而是被調用的程式碼,因為"你"所處的位置,確實才是更容易發現錯誤的位置
* 程式碼給出"未定義"的結果,這本身其實就是介面設計不良的一種跡象(ex: find_by會得到nil)
# 取個好名字,本身就是最好的說明
* 名字應該要能概括出這東西的重要之處,並引導那些看到它的人,讓大家知道應該把它想成什麼樣的東西
### 不要為了少打幾個字,而去簡化名稱
* 讀程式碼的次數,遠多於寫程式碼的次數
數值演算法風格
```c=
void cp(
int n,
float rr[],
float ii[],
float xr,
float xi,
float *yr,
float *yi)
{
float rn = 1.0f, in = 0.0f;
*yr = 0.0f;
*yi = 0.0f;
for (int i = 0; i <= n; ++i)
{
*yr += rr[i] * rn - ii[i] * in;
*yi += ii[i] * rn + rr[i] * in;
float rn2 = rn * xr - in * xi;
in = in * xr + rn * xi;
rn = rn2;
}
}
```
給予適合的名字
```c=
void evaluateComplexPolynomial(
int degree,
float realCoeffs[],
float imagCoeffs[],
float realX,
float imagX,
float *realY,
float *imagY)
{
float realXN = 1.0f, imagXN = 0.0f;
*realY = 0.0f;
*imagY = 0.0f;
for (int n = 0; n <= degree; ++n)
{
*realY += realCoeffs[n] * realXN - imagCoeffs[n] * imagXN;
*imagY += imagCoeffs[n] * realXN + realCoeffs[n] * imagXN;
float realTemp = realXN * realX - imagXN * imagX;
imagXN = imagXN * realX + realXN * imagX;
realXN = realTemp;
}
}
```
### 不要混用不同的約定慣例做法
```c=
template <class ELEM, int MAX_COUNT = 8>
class FixedVector
{
public:
FixedVector() :
m_count(0)
{ ; }
void append(const ELEM & elem)
{
assert(!isFull());
(void)new (&m_elements[m_count++]) ELEM(elem);
}
void empty()
{
while (m_count > 0)
{
m_elements[--m_count].~ELEM();
}
}
int getCount() const
{
return m_count;
}
bool isEmpty() const
{
return m_count == 0;
}
bool isFull() const
{
return m_count >= MAX_COUNT;
}
protected:
int m_count;
union
{
ELEM m_elements[0];
char m_storage[sizeof(ELEM) * MAX_COUNT];
};
};
```
同時使用FixedVector與C++內的vector時
```c=
void reverseToFixedVector(
vector<int> * source,
FixedVector<int, 8> * dest)
{
dest->empty();
while (!source->empty())
{
if (dest->isFull())
break;
dest->append(source->back());
source->pop_back();
}
}
```
* 第一個empty是清空dest這個向量,第二個則會檢查source這個向量是不是空的
### 不要搬石頭砸自己的腳
功能:把一堆視窗,用行列的排列方式塞進一個矩形區塊中,讓矩形區塊大致上保持長寬相同的比例
```c=
int split(int min, int max, int index, int count)
{
return min + (max - min) * index / count;
}
void split(int x0, int x1, int y0, int y1, int &r0, int &r1)
{
r0 = split(x0, x1, y0, y1);
r1 = split(x0, x1, y0 + 1, y1);
}
void layoutWindows(vector<HWND> ww, LPRECT rc)
{
int w = ww.size();
int rowCount = int(sqrt(float(w - 1))) + 1;
int extra = rowCount * rowCount - w;
int r = 0, c = 0;
HWND hWndPrev = HWND_TOP;
for (HWND theWindow : ww)
{
int cols = (r < extra) ? rowCount - 1 : rowCount;
int x0, x1, y0, y1;
split(rc->left, rc->right, c, cols, x0, x1);
split(rc->top, rc->bottom, r, rowCount, y0, y1);
SetWindowPos(
theWindow,
hWndPrev,
x0,
y0,
x1 - x0,
y1 - y0,
SWP_NOZORDER);
hWndPrev = theWindow;
if (++c >= cols)
{
c = 0;
++r;
}
}
}
```
* 調用split時x0與x1是視窗相應矩形左右兩側的座標。但在split函式內部x0與x1卻是不同意思(在A函式內是一般變數,但在B函式內是笛卡爾座標
```c=
int divideRange(int min, int max, int index, int count)
{
return min + (max - min) * index / count;
}
void layoutWindows(vector<HWND> windows, LPRECT rect)
{
int windowCount = windows.size();
int rowCount = int(sqrt(float(windowCount - 1))) + 1;
int shortRowCount = rowCount * rowCount - windowCount;
HWND lastWindow = HWND_TOP;
int rowIndex = 0, colIndex = 0;
for (HWND window : windows)
{
int colCount = (rowIndex < shortRowCount) ?
rowCount - 1 :
rowCount;
int left = divideRange(
rect->left,
rect->right,
colIndex,
colCount);
int right = divideRange(
rect->left,
rect->right,
colIndex + 1,
colCount);
int top = divideRange(
rect->top,
rect->bottom,
rowIndex,
rowCount);
int bottom = divideRange(
rect->top,
rect->bottom,
rowIndex + 1,
rowCount);
SetWindowPos(
window,
lastWindow,
left,
top,
right - left,
bottom - top,
SWP_NOACTIVATE);
lastWindow = window;
if (++colIndex >= colCount)
{
colIndex = 0;
++rowIndex;
}
}
}
```
* 容易追朔正在發生的事情
* 一看就可以知道變數所代表的概念,不用藉由程式碼去推斷其含義
* 類似的東西,類似的名稱。相同的東西,相同的名稱。
### 不要讓我花力氣去想
* 匈牙利命名標準(以ruby為例):早期C編譯器在進行連結時對於型別的檢查不夠嚴謹,因此創立出這種補救方法(避免型別錯誤)
```ruby=
user = User.new
objUser = User.new
def create
@user = User.create(name: params[:name], age: params[:age])
end
def create
strName = params[:name]
intAge = params[:age]
@objUser = User.create(name: strName, age: intAge)
end
def show
@intUserId = params[:id]
@objUser = User.find(@intUserId)
end
```