2020年4月29日 星期三

The Clean Code Talks - Don't Look For Things! 整理

重點整理:
  • 用細緻的方式回到基本去討論依賴注入(Dependency Injection)
  • 測試系統時需要實例化物件,實例化物件時常放了很多測試替身(Test-double)進去,讓測試更困難
  • 類別的靜態變數有時牽涉到單例(Singleton)設計模式,要一起實例化,讓測試更麻煩
  • 實例化很困難意味著難以測試
  • 超文本文件類別為例,文件只在乎客戶端產生的文件,不需要瞭解客戶端如何產生文件
  • 車類別為例,車只在乎產生的引擎,不需要了解引擎怎麼產生
服務定位器模式(Service Locator pattern)的問題:
  • 違反得墨忒耳定律(Law of Demeter a.k.a. LoD,簡單說即只拿直接需要的物件)
  • 將服務定位器傳給要實例化的物件,看建構子參數不夠,還得追建構子程式碼才知道相依的物件,這會隱藏物件間真正的相依性
  • 以房子類別為例,實例化房子卻傳入整個家的狀態,追建構子才知只要門、窗與屋頂
  • 給房子還要給整個家的物件,導致物件重用性變差
  • 解決方式是不透過定位器,明確將門、窗與屋頂作為參數傳給房子
  • 此舉能讓獨立測試變得容易
  • 服務定位器把查詢與建立物件的功能混淆
  • 還需要額外的介面、覆寫或仿造服務定位器的設定方法(setter)
  • 以致一旦依賴服務定位器,也需要依賴其他跟服務定位器有關的東西
  • 服務定位器有很多別名,本質上都是只需要部分物件但給了全世界
得墨忒耳定律:
  • 以結帳為例,一般來說只會拿錢(直接需要的物件)給店員(請求物件的物件),不會整個錢包(全域狀態)給店員
  • 只請求直接需要的物件
  • a.getX().getY()亦即間接取得物件即違反得墨忒耳定律
  • 好萊塢法則(Hollywood principle, that is "Don't call us, we'll call you")
破解依賴注入的迷思
  • 子物件新增一個參數,則父物件要一路傳遞參數直到給子物件
  • 事實上父物件並不負責建構子物件,父物件並不知道子物件怎麼建構的,所以也沒有傳遞參數的問題
  • 房子類別為例,房子並不知道門有門把,房子只需要建構好的門物件
  • 工廠模式(Factory pattern)負責將物件組合起來,與業務邏輯分開
  • 同一個生命週期的類別們只要同一個工廠組合就夠,並不用一個類別一間工廠,所以不會導致類別倍數增生
  • 注入的物件生命週期應該大於等於被注入的物件,如果有生命週期較短的物件,應該調整架構,將短生命週期的物件分群
偏執狂程式設計(Paranoid programming/coding)
  • 到處是空指標檢查讓測試變得困難
  • 測試房子的油漆功能不需要門,傳入門的空指標,但門的空指標檢查讓測試無法繼續
  • 與其到處用先決條件檢查,不如用許多測試確保程式能跑
  • 如果物件是給外部使用的應用程式介面(API),加入先決條件檢查是合理的
  • 但若給內部使用,一堆先決條件檢查表示對程式的穩定性沒有信心
  • 產品程式碼不應該傳空指標給物件,應該用空集合代替,若希望傳入的集合做事就實作方法,不然就給無操作(NOP a.k.a. NOOP)
  • 測試程式碼可以傳空指標給物件,目的在表明此物件與測試無關
  • 產品程式碼不應該在工廠類別外的類別呼叫new運算元,即業務與建構邏輯混淆
  • 測試程式碼為了實例化小部分的程式做測試可以呼叫new運算元
總結
  • 絕大部分的情況應該避免呼叫new運算元實例化物件,除了最基底的類別或物件圖的葉類別,其餘有與其他類別互動的類別,應該請求物件
  • 將業務類別與建構類別分開,測試會變得容易,因為可以將業務邏輯與建構邏輯分別獨立測試
  • 若將業務與建構邏輯混淆,測試建構邏輯時還不得不跑業務邏輯做的事
Q&A
  • 問:是否建議不需要假(dummy)物件?
  • 答:否,當測試的時候,無關的物件可以傳空物件進行無操作(NOP),空物件是假物件更強的表現形式

2020年4月27日 星期一

"The Clean Code Talks -- Inheritance, Polymorphism, & Testing" 整理


重點整理:
  • 讓程式碼更好懂、更好測試與更好擴充
  • 盡可能用多型代替if
  • 一個類別負責一種事情
  • 若看到switch則用多型代替
  • 若出現重複判斷某個flag的if則用多型替代
  • 把建構子類別(工廠模式)與業務邏輯類別分開,在建構時做if判斷,處理業務邏輯時就不用一直做if判斷
優點:
  • 多型能讓不同的功能分散在不同檔案
  • 程式碼更好被理解與測試
  • 能讓重複的if判斷改成集中在一個地方
  • 不再有重複的程式碼,減少錯誤發生
  • 用類別將負責工作分開,不再需要全域變數做狀況判斷
Q&A重點整理:
  • 確保類別只單一繼承,多重繼承表示類別負責多種事情
  • 多型在運算子實例化時就決定,類別的方法只會給那個類別用,不會一個類別只在某些狀況用到某些方法,另一些方法在別種狀況才用到

練習:
寫一個小專案盡可能用多型代替if

10 Tips For Clean Code 整理

引言重點整理:

  • 為了趕交期寫出骯髒的程式碼,產生更多臭蟲,日後改版會讓維護成本越疊越高
  • 看程式碼與寫程式碼的時間比例最好落在10:1
  • 花時間寫出易讀的程式碼,日後更易寫
十點建議:
  1. 為程式碼的品質負責,別為了交期放棄專業態度與做法
  2. 與其用簡短的變數加註解,不如把變數意義直接寫在變數上,好的程式碼如一篇散文,變數就是散文中的名詞
  3. 函數/方法宣告名稱要表達明確的意圖,不需要前後文、不用先懂他的類別就知道意思
  4. 註解常說謊,盡量做到程式碼即註解(CTO說這個比較有爭議性。其他文章看到頗認同的是,好的註解是寫「為什麼」,後續開發者看註解才能理解並檢討前人的動機)
  5. 童子軍守則(Boy Scout Rule),改善訪視過的程式碼讓他變得更好(實務上最好問一下原作:P)
  6. 單一功能原則(Single-responsibility Principle a.k.a. SRP):一個函數/方法/類別幾乎只做一件事,一個需要太多參數的函數可能做太多事;一個類別某些物件只用到部分性質與方法,另一些物件則用到其他的,這個類別應該拆分成多個類別
  7. 寫測試,分為整合性測試與單元測試,使用測試驅動開發
  8. 疊代式開發,用畫畫比喻,漸進/疊像是把畫中某一塊畫到完美再進行下一塊,反覆/代是先畫整張畫的草稿,再上底色,再做後續直到完美
  9. 軟體架構是獨立的,用來支持使用者案例,框架只是用來達成架構的工具,不是要遵循的架構,講者用WordPress舉例
  10. 練習,練習,再練習,台上一分鐘,台下十年功

2020年4月26日 星期日

The Clean Code Talks - "Global State and Singletons" 整理

重點整理:
  • 全域狀態(Global State)會有幾個問題:測試時每次結果不一樣(Flakiness);測試時必須有順序;某些測試無法平行進行
  • 單例(Singleton)封裝了全域狀態
  • 有全域狀態的程式會變得很難測試
  • 單例直接或間接碰到的變數或狀態,理論上會變成無限多個
  • 測試無法觸及實例化的單例中的私有變數,只能寫專門測試的方法來測
  • 單例(Singleton)容易產生欺騙性的API
  • 欺騙性的API獨立測試會產生許多問題,必須回溯並解決相依類別
  • 相依類別間沒有明顯的順序性,讓最佳化效能或流程增加許多阻礙或負擔
  • 有依賴注入(Dependency Injection),測試目標類別初始化與其相依類別的順序會很清楚,後續開發者看到程式碼很容易了解類別初始化的流程。
  • 依賴注入強制編譯時期初始化的順序
  • 每一個相依類別可以獨立測試
Q&A重點整理:
  • 不要混淆業務邏輯與物件初始化
  • 只有需要用到相依物件的物件才知道相依類別,其他層不會知道
  • 依賴注入讓類別間的相依明確,若一個類別初始化時吃太多參數,也許是這個類別負責太多事情,最好將它拆開成不同類別
  • 某些情況,例如框架強制無參數的建構子,導致無可避免地使用單例,則委派某個單例類別專門處理這些事情,用最少的力氣去處理它
  • 每個物件應該知道它直接合作的物件,間接需要的物件不應該知道
  • 當一個應用程式需要多個實例來執行不同的任務,全域變數會阻礙這樣的可能性
  • 即使一個環境有許多全域變數,能避免使用它可以讓程式更容易測試

2019年8月23日 星期五

Solution for Google Chrome Shortcut Name Changed to LINE after LINE Extension is Installed on Ubuntu

Solution for this issue
https://askubuntu.com/questions/851048/google-chrome-app-name-changed-after-installing-line-from-chrome-web-store/851061

Locate and edit ~/.local/share/applications/_opt_google_chrome_chrome.desktop where content is like below

[Desktop Entry]
Encoding=UTF-8
Version=1.0
Type=Application
Name=LINE
Icon=google-chrome
Exec=/opt/google/chrome/chrome
StartupNotify=false
StartupWMClass=Google-chrome
OnlyShowIn=Unity;
X-BAMFGenerated=true

Change value of Name, restart Chrome and the name shown on desktop should be changed.

If you are using Gnome Display Manager (GDM) and Chrome does not appear while searching on Activities, remove OnlyShownIn=Unity; and search again.

If this does not work, locate another folder where Google Chrome .desktop lies in, such as /usr/share/applications/, where Name is LINE, change the value and restart Chrome.

Good luck.

2017年1月5日 星期四

Fibonacci with print and value cache

Following is simple way to print Fibonacci(n)

long long fibonacci(int n)
{
    if (n <= 2)
        return 1;

    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main(...)
{
    int ret = 0;
    int n = 0;

    /* Enter n ... */

    for (; i <= n; i++)
        printf("%lld ", fibonacci(i));

    return 0;
}

However Fibonacci(n) is recalculated in numerous times, we can cache the result for future usage

Following is fibonacci with cache, much faster

long long fibonacci(int n, int *printed, long long *cache)
{
    long long value = 0;

    if (cache[n] != 0)
        value = cache[n];
    else
    {
        if (n <= 2)
            value = 1;
        else
            value = fibonacci(n - 1, printed, cache) +
                           fibonacci(n - 2, printed, cache);

        cache[n] = value;
    }

    if (!printed[n])
    {
        printf("%lld ", value);
        printed[n] = 1;
    }

    return value;
}

int main(...)
{
    int ret = 0;
    int n = 0;
    int *printed = NULL;
    long long *cache = NULL;
    /* Enter n ... */

    /* Allocate memory for printed and cache by n ... */

    fibonacci(n, printed, cache); 

    return 0;  
}

2016年12月4日 星期日

Using goto safely

Goto statement is two-edged sword, if we use goto under the premise of structured programming, it's safe and more readable for maintenance.

Following are examples for goto:

1. Releasing allocated memory
2. Solution for deeply nested code
3. Breaking the structured code by goto 4. Abuse of goto

1. Releasing allocated memory at the end of function

int function(...) { int ret = ERR_NONE; type *ptr = NULL; ... /* ptr points to an allocated memory block */ ret = function_a(ptr); if (ret != ERR_NONE) { report_error_log(ret); free(ptr); return ret; } ret = function_b(ptr); if (ret != ERR_NONE) { report_error_log(ret); free(ptr); return ret; } ... return ret; }
You have to write duplicate free for ptr We can use goto to release at the end of function once error occurs int function(...) { int ret = ERR_NONE; type *ptr = NULL; ... /* ptr points to to an allocated memory block */
ret = function_a(ptr); if (ret != ERR_NONE) { report_error_log(ret); goto _exit; }
/* implicit else */
ret = function_b(ptr); if (ret != ERR_NONE) { report_error_log(ret); goto _exit; }
/* implicit else */
... report_function_done_log(); _exit: free(ptr); return ret; } It's pretty much the same when there is ptr and ptr2

By the way, if 3 or above pointers are used in a function,
It's time to think about if part of statements can be extracted to sub function.

2. Solution for deeply nested code

int function(...) { int ret = ERR_NONE; type *ptr = NULL; ... /* ptr points to to an allocated memory block */ ret = function_a(ptr); if (ret == ERR_NONE) { ret = function_b(ptr); if (ret == ERR_NONE) { ... /* in some nested block */ report_function_done_log(); } else { report_error_log(ret); free(ptr); return ret; } } else { report_error_log(ret); free(ptr); return ret; } return ret; }
Deeply nested code layout is less readable for maintainability comparing to style in example 1., it's more readable if adopting goto-style of example 1.

3. Breaking the structured code by goto

Using goto to break a for loop is not safe
Following example breaks the one-in-one-out rule
Unexpected hard-to-debug behavior may occur in this style in unfortunate situation
int ret = ERR_NONE; int i = 0; for (; i < N; i++) { ret = function_x(i); if (ret) { report_error_log(ret); goto _exit; } } ... _exit: do_something(); return ret;

Following style keeps one-in-one-out structure
Using break instead of goto to leave loop At exit of loop, checking return code and goto exit if error occurs In this way the for loop can even be extracted to sub function
int ret = ERR_NONE; int i = 0; for (; i < N; i++) { ret = function_x(i); if (ret) { report_err_log(); break; } } if (ret != ERR_NONE) goto _exit; ... _exit: do_something(); return ret;

4. Abuse of goto

Abuse of goto causes redundant behavior
Following code fails to allocate memory for ptr
No need to free null ptr

int function(...) { int ret = ERR_NONE; type *ptr = NULL; ptr = (type *)malloc(sizeof(type)); if (!ptr) { ret = errno; report_error_log(ret); goto _exit; } ... _exit: free(ptr); return ret; }
So goto can be reduced ptr = (type *)malloc(sizeof(type)); if (!ptr) { ret = errno; report_error_log(ret); return ret; } In conclusion goto is two-edged sword, depends on how we use it