【Guava 教學】(2)命名明確的條件檢查



有多少次了呢?你總會對傳入的引數作一些檢查,像是某個管理物件的容器,你也許會有個 add 方法,可將傳入的 List 中元素逐一收納,你不希望傳入 null,或者傳入的 List 是空的…
public void add(List<T> lt) {
    if(lt == null) {
        throw new IllegalArgumentException("不能傳入 null");
    }
    if(lt.isEmpty()) {
        throw new IllegalArgumentException("List 不能是空");
    }
    // 繼續辦事...
}
每次都得為了做這類的檢查而撰寫類似程式碼的話,為什麼不把它封裝起來呢?像是寫個 checkArgument
public static void checkArgument(boolean expression, Object errorMessage) {
    if(expression) {
        throw new IllegalArgumentException(errorMessage.toString());
    }
}
那麼你原本的方法就可以修改為:
public void add(List<T> lt) {
    checkArgument(lt != null, "不能傳入 null");
    checkArgument(!lt.isEmpty(), "List 不能是空");
    // 繼續辦事...
}
看來不錯,那為什麼不用 assert 呢?當然,我們可以用 assert,不過 assert 可以被停用,但這不是不使用 assert 的真正理由,真正的理由是為了「可讀性」,無論如何,使用 checkArgument 這樣的名稱,我們可以一目瞭然地知道,這是在檢查傳入引數,使用 assert 的話,總是要稍微想一下。
在 Guava 中對這類前置檢查的工作,實際上在 com.google.common.base.Preconditions 上提供了一些公用方法可以使用,為了方便,建議你使用 import static com.google.common.base.Preconditions.*,這樣你就可以不用使用 Preconditions 作為前置。事實上,如果你正在使用 Guava,那麼上面的方法最好還可以修改為以下內容:
public void add(List<T> lt) {
    checkNotNull(lt, "不能傳入 null");
    checkArgument(!lt.isEmpty(), "List 不能是空");
    // 繼續辦事...
}
這就是 Bob 大叔在《Clean Code》中一直強調的概念「有意義的命名(Meaningful Names)」,只要有助於可讀性,流程中某個區塊都可以使用函式並「使用具描述能力的名稱(Use Descriptive Names)」來取代。比方說,如果某個方法要檢查物件內部狀態:
public void doSome() {
    if(container.size() > 100) {
        throw new IllegalStateException("超過負載");
    }
    // 繼續辦事...
}
那麼可以直接使用 Guava 的 checkState 方法來修改為:
public void doSome() {
    checkState(container.size() <= 100, "超過負載");
    // 繼續辦事...
}
乍看 checkArgumentcheckState 感覺會有點像,是的!如果你只使用 assert 的話,基本上都是給個判斷條件,然後在不成立時產生錯誤。使用 checkArgumentcheckState 的差別除了一個會丟出 IllegalArgumentException,一個是丟出 IllegalStateException 之外,最主要的是在語義差別,checkArgument 名稱表明這個方法是用於檢查引數,而 checkState 名稱表明,這個方法是用於檢查物件的狀態。
把語義清晰納入考量的話,你會怎麼修改這段程式碼呢?
public T get(int index) {
    if(index < 0) {
        throw new IllegalArgumentException("索引不得小於 0");
    }
    if(index >= container.size()) {
        throw new IllegalArgumentException("索引超出範圍");
    }
    // 繼續辦事...
    return ...;
}
checkArgument
public T get(int index) {
    checkArgument(index >= 0, "索引不得小於 0");
    checkArgument(index < container.size(), "索引超出範圍");
    // 繼續辦事...
    return ...;
}
還不錯!不過如果可以更明確地丟出 IndexOutOfBoundsException 的話,會比拋出 IllegalArgumentException 好些。由於檢查索引是很常見的需求,像是檢查 CollectionString、陣列等等,Guava 提供了 checkElementIndex 方法,你可以告訴它索引,以及要被存取的容器之大小。
public T get(int index) {
    checkElementIndex(index, container.size());
    // 繼續辦事...
    return ...;
}
至於 checkElementIndex 會做什麼事,我想,直接看看它的原始碼就可以瞭解了:
  ...

  public static int checkElementIndex(int index, int size) {
    return checkElementIndex(index, size, "index");
  }

  public static int checkElementIndex(
      int index, int size, @Nullable String desc) {
    // Carefully optimized for execution by hotspot (explanatory comment above)
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException(badElementIndex(index, size, desc));
    }
    return index;
  }

  private static String badElementIndex(int index, int size, String desc) {
    if (index < 0) {
      return format("%s (%s) must not be negative", desc, index);
    } else if (size < 0) {
      throw new IllegalArgumentException("negative size: " + size);
    } else { // index >= size
      return format("%s (%s) must be less than size (%s)", desc, index, size);
    }
  }
checkElementIndex 類似的另一方法是 checkPositionIndex,後者在指定的索引大於 size 時才會丟出例外。那麼如果想檢查一段範圍呢?例如若原本有這樣一段程式碼:
public List<T> slice(int start, int end) {
   if(start < 0 || end < start || end > container.size()) {
        throw new IllegalArgumentException("索引超出範圍");
   }        
   // 繼續辦事...
   return null;
}
那麼就可以使用 Guava 提供的 checkPositionIndexes 改為:
public List<T> slice(int start, int end) {
   checkPositionIndexes(start, end, container.size()); 
   // 繼續辦事...
   return null;
}
有時候,if 中的檢查如果太多,其實就建議用個 isXxx 方法將之封裝起來。當然,你不一定要用 Guava 的 Preconditions,我想 Guava 中存在這玩意的目的,或許也在提醒開發者,對某些檢查情況,或者說對某些功能來說,使用個明確、具描述性的函式,會對程式碼的可讀性有所幫助。