Sunday, July 29, 2018

Thinking in Data (Stuart Sierra)

Thinking in Data

(*) Data Oriented Programming
 
總結: 
(a) 寫程式解真實世界的問題,要用 Data 來對真實世界建模。
(b) 如果要驗証程式的正確性,將程式內部的 Data 加以「視覺化」(visualize) ,常常會比 unit test 還管用。
(c) Make your data visible!


1 用 Data 來對真實的世界建模。
  => 容易除錯。
  => 比物件好太多了

2. 物件 (Object) 適合用來建模的東西,往往是完全在電腦內部的東西。
   stack, queue, GUI, widgit 
   註: Clojure 的 vector, list, set, hash-map ,就是使用 Object 來建模。Why? 因為是電腦內部的東西,一方面效能很重要、另一方面需要 Polymorphism

3. Examine Each Step
   對有使用 reduce 函數來除錯的技巧
   (a) 把 reduce 換成 reductions ,如此就可以看到運算過程的每一個 step
   (b) 使用 pprint

4. Everything is a Map
   一種抽象思考:
   Collections
   This ..  | maps from ... | to ...
   ---------------------------------
   Map     | Keys              | Values
   Set       | Values            | Values
   Vector  | Integers         | Values
   List      | first/rest         | Values/List
 
5. Add Keys with Impunity

(assoc stuart :business/role :developer)
 
用 map 來對真實世界的東西建構,隨時可以加入新的東西。
如果要避免 key 的 collision ,可以對 key 加上 namespace

6. 一個函數的 argument 超過三個的時候
   => 用 map 來把 arugment 包裝起來是更好的作法。

   (defn full-name [person]
     (let [{:keys [first-name last-name]} person]
       (str first-name " " last-name)))

7. defrecord, defprotocol 這些通常是用來處理需要高效率或是 Polymorphism 處理的情況、
   然而,十之八九,用 map 來建模就已經很夠用了。

8. 封裝(Encapulation)實作細節這個技巧,被應用的時候,我們會無法看到 data representation
   然而,將 representation 隱藏起來是有代價的。
   (a) 效能
   用同一個介面去輕易地使用不同的物件,會有不同的效能屬性,有時被誤用,很傷效能。
   (b) 實作的難度
   (c) 「網路傳輸/程式之間的傳輸」仍然依賴於 representation
   也因此,任何我們透過隱藏 data representation 得到的好處,往往會被扺消,一旦我們的程式會接觸到外部世界。
 
   What I suggest, instead of encapsulating your representation, let your representation be visible and think about them. Choose the representation that best fit a particular module, a program, or a component that your are dealing with. Then, you establish the boundary between the components, where representation can change. We change representation all the time. That is what computer can do more than anything else.

 
9. Data Literals feature
   (*) Isolate literal representation from what's in memory
   不同的記憶體格式,可以共用同一種「printed data representation」

   (java.util.Date.)
   ;;=> #inst "2012-03-03T22:50:21.656-00:00"
   (java.util.GregorianCalendar)
   ;;=> #inst "2012-03-03T22:50:21.656-00:00"

(*) 應用 data representation 的技巧來撰寫函數

1. Isolate Components
對一個 state 要經過多次的轉換,但是每一次的轉換,只修改它的一部分狀態,可以用下列的 pattern 來實作。

(defn complex-process [state]
  (-> state
    (update-in [:part-a] subprocess-a)
    (update-in [:part-b] subprocess-b)))


2. Isolate side effects
下列的程式很難測試
;; Bad
(defn complex-process [state]
  (let [result (computation state)]
    (if (condition? result)
      (launch-missile)
      (erase-hard-drive))))


相對比較容易測試的版本
;; Better
(defn complex-process [state]
  (assoc state :analysis (computation state)))

(defn decision [state]
  (assoc state :response
         (if (condition? (:analysis state))
            :launch-missile
            :erase-hard-drive)))

(defn defend-nation [state]
  (case (:response state)
    :launch-missile (launch-missile)
    :erase-hard-drive (erase-hard-drive)))


3. 由 data 思維,推導出 naming convention
A function which...       |  has a name that is...   |  return...
-----------------------------------------------------------------
Computes a value         |  noun: what it returns  |  value
-----------------------------------------------------------------
Create a new thing        |  noun: what it creates  |  value
-----------------------------------------------------------------
Gets input from ouside |  "get-noun"                  |   value
-----------------------------------------------------------------
Affects outside              |  verb: what it does       |  nil
-------------------------------------------------------------------

(*) Summary
(a) Make data visible
(b) Make functions composable
(c) Isolate computation decisions from side effects
(d) Establish boundaries
    - from computation world to side effect world
    - from one data representation to different data representation

Saturday, July 28, 2018

OO design for testability (Miško Hevery)

偶然看到的 youtube 影片--- OO design for testability。對我的幫助滿大的,看完之後,有點讓我豁然開朗,發現:「喔,原來物件導向的程式要這樣子設計!」「原來 dependency injection 的用法是這樣子用!」

影片一開頭不久,就指出了有四大常見的錯誤,一旦犯了,就可以寫出很難測試的程式:
1. Location of the new operator
2. Work in constructor
3. Global State
4. Law of Demeter violation

在明確的去說明四大常見錯誤之前,講者做了一段基本的測試概述:你寫了一個物件,然後你要測試它。測試它,於是就遇上了難題:因為它有許多的「依賴」,而這些「依賴」產生的方式有許多不同的方式:
(a) 依賴可能是 global 物件
(b) 依賴可能是在 class 內 new 出來的物件
(c) 依賴可能是注入的物件
只有 (c) ,不會導致嚴重的測試問題。

錯誤 1 & 錯誤 2 
不容易測試的範例:
class Car {
  Engine engine;
  Car(File file) {
    String model = readEngineModel(file);
    engine = new EngineFactory.create(model);
  }

  // ...
}

=============================================
容易測試的範例:
class Car {
  Engine engine;
  @Inject
  Car(Engine engine) {
    this.egine = engine;
  }
}

@Provides
Engine getEngine(
    EngineFactory engineFactory,
    @EngineModel String model) {
  return engineFactor.create(model);
}
=============================================

由上述的反思可以推論出:
(*) 在 constructor 內部,應該要儘量少做事、唯一應該寫在 constructor 裡的程式碼,就是把 dependencies 指派給 class 內部的 field。 (In constructor, you should do as little work in constructor as possible. You should do nothing else but assign your dependencies to your fields.)

(*) 檢驗物件導向程式有沒有設計好 testability 的方式:
Look at the constructor and make sure there is no work in there. Look at the constructor to make sure that all it's doing is assigning its dependencies to the fields. Check to make sure that the dependencies that your actually save into your fields are actually the dependencies that you truly need. This is where we are going to get into law of Demeter violation.

遵循上述的 Guideline 的話,程式應該長成什麼樣子呢?
(*) 好的測試程式碼 (test code) 應該充滿 new operator 和 null
(*) 好的線上程式碼 (production code) 則應該「沒有」new  和 null

================================================
錯誤 3 (Global State) 的例子

不清不楚的 API 介面。 Deceptive API

testCharge() {
  Database.connect(...);
  OfflineQueue.start(...);
  CreditCardProcessor.init(...);
  CreditCard cc;
  cc = new CreditCar("12..34", ccProc);
  cc.charge(100);
}

================================================
明確說明依賴關系的 API 介面。 Better API

testCharge() {
  db = new Database();
  queue = new OfflineQueue(db);
  ccProc = new CCProcessor(queue);
  CreditCard cc;
  cc = new CreditCar("12..34", ccProc);
  cc.charge(100);
}
================================================
錯誤 4 (Law of Demeter violation) 

不良的例子: 使用超過一個 dot 符號
class LoginPage {
  RPCClient client;
  HttpRequest request;

  LoginPage(RPCClient client,
      HttpServletRequest request) {
    this.client = client;
    this.request = request;
  }

  boolean login() {
    String cookie = request.getCookie();
    return client.getAuthenticator().authenticate(cookie);
  }
}

==========================================
改進的範例
class LoginPage {
  ...
 
  LoginPage(@Cookie String cookie,
            Authenticator authenticaor) {
    this.cookie = cookie;
    this.authenticaor = authenticator;
  }

  boolean login() {
    return authenticator.authenticate(cookie);
  }
}

Wednesday, July 25, 2018

Linux command line 技巧總整理

有一些 linux command line 的技巧,我到了年紀很大才學會,大概比較不流行吧?

(1) autojump
這個是用來取代 cd 。切換資料夾,只需要填入「關鍵字」即可。

(2) ack 或是 git grep
這個是用來取代 grep 。在 nodejs 的開發環境,如果用 grep 來做搜尋,沒有指定複雜的子選項, grep 總是把 node_modules 裡的一堆源碼也做比對。ack 的 default 選項就會忽略 node_modules 這個資料夾。相對省事不費心許多。

git grep 是對已經存進 git repository 的資料做 grep 。速度比 grep 快了十倍左右。而且可以查出所有在 git repo history 裡的資料。

(3) cheat
用來取代 man 。會直接給許多指令的最常見、最好用的用法。

Monday, July 16, 2018

python 依賴宣告、依賴隔離、程式碼排版

開發 python 的程式,要遵循好的開發紀律,避免日後的可移植性問題,最好還是在最初就做好「依賴宣告」和「依賴隔離」、「程式碼排版」

依賴隔離  (dependency isolation) 
cd my_project_folder             // 先進入到 git 管理的 python 開發資料夾
virtualenv venv                      // 建立一個類似 node_modules 的 venv 資料夾
                                               //  virtualenv 也可以在啟動時,用 -p 參數指定要用的 python 版本。
source venv/bin/activate        //  啟動 python 的虛擬環境
pip install autopep8                //  在虛擬環境中安裝套件
deactivate                               //  離開虛擬環境

依賴宣告 (dependency declaration)
啟動虛擬環境之後…
pip install -r requirements.txt          // 根據依賴宣告檔來安裝套件
pip freeze > requirements.txt          // 重新生成依賴宣告

程式碼排版 (linter)
autopep8 --in-place --aggressive

Nodejs 的 unit test 經驗

今年 6 月在荷蘭旅遊時,偶爾看了 clean coder blog 之後,覺得自己還是來認真寫一些測試。畢竟 Uncle Bob 的文字傳達了一種觀點:「寫程式的紀律才是一切的基石。」

在 intowow 開發主要是用 Nodejs 。本來公司就有在用的 library 是 mocha 和 chai 。 BDD 風格的測試,確實比較容易看懂。由於我在 cepave 的工作也有用過 BDD ,我就不假思索地接受了。

最近愈是寫測試,愈是覺得真的很需要用好用的測試 library :
(1) sinon
功能很強,主要的功能是 stub 和 spy 。 stub 可以重新定義函數的行為,讓函數傳回固定的輸出。 spy 可以對函數做出監控,透過 spy ,可以輕易地驗証某個函數是否被呼叫、被怎樣的引數呼叫。

(2) nock
這個很適合來測 web API 。

(3) rewire
這個可以測模組的「私有函數」。

Wednesday, July 11, 2018

使用 localstack 來開發 aws service 的整合測試

要使用這個測試,需要先啟動 localstack 的 docker
docker run -d -p 4567-4583:4567-4583 localstack/localstack

Sunday, July 8, 2018

queue, channel, asynchronous API

以前讀 Rich Hickey 的 Simple Made Easy 時,有一回他講了 If you're not using queues extensively, you should be. You should start right away, like right after this talk. (如果你沒有常常使用 queue 的話,你應該要多用一些。你應該立刻開始…聽完現在的 talk 就開始吧。)

紅色那段英文的上下文, Rich Hickey 的論點是: Queue 可以讓 when 和 where 這兩件事被分開來。這個說法太抽象了,我一直無法理解它。此外,看完那段話之後,也還是想不出來,我寫程式的時候,哪些地方有可能用 queue 去改善?

而公司的同事最近在設計架構時,用了 aws  的 SQS 服務。我隨口問了他,為什麼要用使用 queue ?他回答: producer 和 consumer 的速度根據估計會有很明顯的差距,所以使用 queue 來做解耦,就可以大幅改善效率。

同事的理由,讓我忍不住回頭去看了一下 Rich Hickey 曾經寫過的關於 channel 的設計理由:
There comes a time in all good programs when components or subsystems must stop communicating directly with one another. This is often achieved via the introduction of queues between the producers of data and the consumers/processors of that data. This architectural indirection ensures that important decisions can be made with some degree of independence, and leads to systems that are easier to understand, manage, monitor and change, and make better use of computational resources, etc.

嗯,這一句理由總算是把問題講得清楚了一些,至少也提到了計算資源的消耗問題

那還有什麼情況也是適合用 queue 來處理問題呢? 為什麼 Rich Hickey 說,要多用一些?那如果卯起來用 queue 的話,還可以用到什麼程度?

我在 Martin Trojer 的文章裡,終於也找到了部分的答案: 像 Nodejs ,只要一遇到 I/O 就是用 callback/promise 來處理,以避免 I/O wait 。實際上, callback/promise 也可以用 queue 去取代。(Martin Trojer 的原文內容要旨為:如果要使用 clojure 的 core.async library 的話,正確的使用方式,必須搭配 asynchronous I/O API )

另外,如果不是只考慮計算資源消耗,而是考慮系統的單純性 (simple system) 的話,也有人用 event driven programming 來描述這個概念:

He's really just giving a fuzzy description for the concept of job pools and event driven programming. So you pack a function call into a generic Job object, push that into a queue, and have one or more worker threads work on that queue. The Job then dispatches more Jobs into the queue upon completion.