構造方法必須無代碼

在一個構造方法裏應該完成多少工作?在構造方法裏進行一些運算然後封裝結果似乎有些道理。那樣的話,當對象方法需要結果時,我們已經準備好了。聽起來像是一個好方法。不,不是。這是一個壞主意的原因之一是:它阻止了對象組合並且讓它們無法擴展。
現在我們做一個展現一個人名字的接口:

interface Name {
  String first();
}

相當簡單,對吧?現在,我們試着實現它:

public final class EnglishName implements Name {
  private final String name;
  public EnglishName(final CharSequence text) {
    this.name= text.toString().split(" ", 2)[0];
  }
  @Override
  public String first() {
    return this.name;
  }

這樣做有什麼問題嗎?它更快,對吧?它一次就把name分割成多個部分並封裝了它們。然後,不管我們調用first()方法多少次,它都會返回相同的值並且我們不需要再次進行分割。然後,這個想法有缺陷。讓我向你展示正確的方式和解釋:

public final class EnglishName implements Name {
  private final CharSequence text;
  public EnglishName(final CharSequence txt) {
    this.text = txt;
  }
  @Override
  public String first() {
    return this.text.toString().split("", 2)[0];
  }
}

這是正確的設計。我能看見正在微笑,所以讓我來證明我的觀點。

在我開始證明之前,請先閱讀這編文章:組合修飾符 vs. 必要實用方法
。文章解釋了靜態方法與可組合修飾符的區別。上面的第一段代碼非常接近一個必要實用方法,即使它看起來像一個對象。第二個例子纔是一個真正的對象。

在第一個例子裏面,我們濫用了new操作符並且把它轉換成了一個靜態方法,所有的運算都在裏面立刻進行。這就是命令式編程。在命令式編程中,我們立即進行所有的運算並返回完全準備好的結果。相反,在聲明式編程中,我們儘可能的推遲計算。
讓我們嘗試使用EnglishName類:

final Name name = new EnglishName(
  new NameInPostgreSQL(/*...*/)
);
if (/* something goes wrong */) {
  throw new IllegalStateException(
    String.format(
      "Hi, %s, we can't proceed with your application",
      name.first()
    )
  );
}

代碼片段的第一行,我們只是創建了一個對象的實例並命名爲name。我們還不想去數據庫,把從那裏獲取的全名,分割成部分,並且把它們包裝在name裏面。我們只是想創建一個對象的實例。這樣的一個解析行爲會有副作用,那樣的話,會拖慢現有應用程序。正如你所看到的,我們只需要name.first(),如果出了問題,我們需要構造一個異常對象。

我的觀點是在構造方法裏進行任何運算是一個不好的實踐並且必須避免,因爲有副作用並且不是對象擁有者所需要的。

你可能會問,在name的重用期間性能怎麼樣呢?如果我們創建一個EnglishName的實例並且調用name.fist()方法5次,我們最終會調用5次String.split()方法。

爲了解決這個問題,我們創建另一個類,一個組合修飾符,會幫助我們解決這個重用問題。

public final class CachedName implements Name {
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  @Cacheable(forever = true)
  public String first() {
    return this.origin.first();
  }
}

我使用的是http://aspects.jcabi.com/
裏面的Cacheable註解,但是你能使用任何其他的Java緩存工具。

public final class CachedName implements Name {
  private final Cache<Long, String> cache =
    CacheBuilder.newBuilder().build();
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  public String first() {
    return this.cache.get(
      1L,
      new Callable<String>() {
        @Override
        public String call() {
          return CachedName.this.origin.first();
        }
      }
    );
  }
}

但是請別讓CachedName可變和延遲加載—它是一個反面模式,這個之前已經在 對象應該不變 中討論過。
我們的代碼現在像這樣:

final Name name = new CachedName(
  new EnglishName(
    new NameInPostgreSQL(/*...*/)
  )
);

這是一個非常原始的例子,但我希望你能明白。

在這個設計中,我們基本上把對象分割成了兩部分。第一部分知道怎樣在英文名字中拿到first name.第二知道怎樣在內存中緩存運算結果。我決定我是否需要緩存。這就是對象組合。

讓我重申在構造方法裏唯一允許的聲明就是賦值。如果你需要在構造方法裏放其他東西,開始思考重構吧—–你的類肯定需要重新設計。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章