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);
  }
}