Showing posts with label new. Show all posts
Showing posts with label new. Show all posts

Friday, August 24, 2018

OO design for testability (Miško Hevery) --- 心得(續)

這兩天重看了一篇 OO design for testability (Miško Hevery) 的前 15 分鐘,又有新的領悟。

本來我覺得整個 talk 的重點是在於四個常見的錯誤不要犯,重新讀一次之後,我覺得重點是更抽象的設計原則:

設計 OO 的程式的時候,要把 class 分成兩大類:
(1) 一類是 Object Graph Contruction & Lookup ,比方 Factory ,它會有 new  operator。
(2) 一類是 Business Logic ,它會有 conditionals 和 loop 

依照這種方式設計,如果要測試 Business Logic 的時候,就可以透過 test unit ,重新 wire 一些 fake 的物件去測試 Business Logic 。

而如果要測試 Factory object ,也可以獨立於 business logic 來做測試。



可以測試的程式應該要有性質是:你可以單單只依賴整套軟體組裝起來的方式,就控制軟體的控制流 (You can control the path flow of your software purely by the way the application is wired together.)

於是又提到了一個專有名詞 Seam
A seam is a place where you can alter behavior in your program without editing it in that place.

跟 Seam 相關的另一個詞是 enabling point
Every seam has an enabling point, a place where your can make a decision to use one behavior or another.

Object Seam


附帶一提,我後來針對 seam 去查資料,發現 seam 出自一本書 WELC。在 Working Effectively with Legacy Code 裡,除了作者最建議的 Object seam 之外,作者還介紹了其它兩種 seam:
preprocessing seam 和 link seam 。

object seam 的 enabling point 是「我們生成待測物件的位置

link seam 在 Java 可以透過 classpath 環境變數做為 enabling point 。原文對 link seam 的描述:
The enabling point for a link seam is always outside the program text. Sometimes it is in a build or a deployment script. This makes the use of link seams somewhat hard to notice.

preprocessing seam 的 enabling point 則是 preprocessor define 語法

參考資料:
(1) Testing Effectively With Legacy Code
(2) How to use link seams in CommonJS
(3) Seams in Javascript







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