# 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 ```