24 July 2011

程式的變數/方法/物件... 的命名一直是個大學問,這也是一個惱人的問題。不過寫久了,也開始累積了一些方便的 習慣。既然是習慣,自然是很個人偏好的,下面列出我命名 method 時開頭字的 convention (慣例),主要是在 Java 和 ActionScript 開發時使用。歡迎大家提出意見和看法。

查詢類

1. findFoo()

find 開頭字常用於資料庫的查詢的 method,類似的開頭可以是 search 和 query。不過我通常會選 find,因為比較短。find 我特別歸類到 單筆的查詢,而且可以允許回傳 null的查詢上 :

User found = userDao.findByName("xyz");
   if(found == null) {
      //...
   }
   else {
      //...
   }

這程式讀起來就很直覺: 找使用者 -> 回傳時命名成 found -> 用了 found 這個字就會直覺想確認有沒有找到 -> 依不同結果做不同事。

如果單筆查詢的結果不會回傳 null (或者是回傳 null 是不該發生的),這時這種 method 就會用 getFoo(), 例如 userDao.getUserCount(); 而多筆的查詢請見下一個

2. listFoo()

承上,當查詢的結果是多筆時,就會用 list 做為開頭。這個 method 永遠不能回傳 null,最差時也要回傳空的 collection。(見 Effective Java Item 43:Return empty arrays or collections, not nulls)

有了上述的三個慣例之後,有關查詢類的 API 就會很清晰,好學且望文生義,命名時也不會再無所適從。get/find/list 這三個詞是我個人的選擇,但重點在明確區分三種查詢結果:

  • 0~1 筆 (find)
  • 1 筆 (get)
  • 0~n 筆 (list)

格式轉換類

1. toFoo()

物件以不同形式呈現時,可以用 to 開頭的 method,最常見的就是 toString() 和 toArray() 了。用到 to method 時,通常是物件在轉換格式,而且 轉換出去的要跟原物件脫勾,例如:

user.setName("Jonh");
   JSONObject jsonUser = user.toJson();
   jsonUser.put("name", "Neo");  
   //改了由 to 產生的物件,不能影響原來的 user
   assert user.getName() == "Jonh" ;

除了脫勾外,基本上每次呼叫回傳的都是新物件,例如上例的 jsonUser 每次都是不同的 instance。最後,在設計時 to method 有時候會和 javabean get method 搞混,記住 get method 通常只取物件的部份資料,而 to 則是轉換整個物件的內容出去,換句話說,出去的物件通常可以再轉回來。如果轉出去的物件轉不回來,那大概不適合用 to 來命名。下面討論轉換回物件的命名。

2. fromFoo()

意即從 Foo 格式轉換回物件,通常和 to method 成對出現,有時候會用 parseFoo() 這類的名字。不過,如果一個 class 設計了 to method,那就盡可能用 from,這樣會好記許多,以下為範例

JSONObject jsonUser = user.toJson();
   User copy = User.fromJson(jsonUser);
   assert user.equals(copy);
3. asFoo()

as 這個開頭我很少用,這是 effective java 裡建議的。使用時機是回傳的物件的 view 時,可以命名為 asFoo()。所謂的回傳 view 跟上面提到的 toFoo() 相反,它回傳的物件和原物件是相連的,改變 view 時也會改變原來的物件:

user.setName("Jonh");
   JSONObject jsonView = user.asJson(); //as 用在回傳 view
   jsonView.put("name", "Neo");  
   //改了 jsonView,會改變原來的 user
   assert user.getName() == "Neo" ;

java Collection 的 api 有不少是 as 開頭的,例如 Collections.asLifoQueue

變更類 (mutation)

1. setter -- void setFoo(Foo foo)

JavaBean 有規範 setter 該怎麼寫,學 java 的人也很習慣,所以最好是按標準作法 改變單一狀態,沒有回傳值時,使用 set 做為開頭。

2. withFoo()

with 開頭的 method 某些 library 的設計者會使用,通常是用 builder 或是 configuration 上,例如:

UserBuilder builder = new UserBuilder();
   User user = builder.withName("myName")
                      .withNickname("John")
                      .withGender(Gender.MALE)
                      .withBlog("http://......")
                      .build() ;

這樣的 API 使用法看過吧? 每個 with method 除了設定欄位之外,還會再回傳自己回去,讓下一行可以直接呼叫。這 call-chain (沒有正式名稱...) 類型的 API 設計如果設計好的話,使用起來很方便。另一個例子是 Hibernate 的 Query API:

getSession().createQuery(" from Badge where game = :game and code = :badgeCode ")
               .setParameter("game", game)
               .setString("badgeCode", badgeCode)
               .uniqueResult();

當然 Hibernate 是使用 set 當作開頭字。但 setFoo() 在 JavaBean 有特別的規範格式,所以我比較傾向使用 call-chain 時換一個開頭,大部份的人的慣例是用 with。這樣子使用 API 的人一看到 with 就知道可以 call-chain,也不違反 JavaBean style。

3. newFoo()

new 開頭的 method 就是我自創的了,我想很少有人這樣設計。new 開頭的 method 是我為了 immutable 物件而設計的。immutable 物件不是不能改變內容嗎?嗯嗯,先來看一個 mutable 的範例:

class Rect {
      private int x, y, width, height;

      public void setX(int updatedX) {
         x = updatedX;
      }

      public Rect withPosition(int updatedX, int updatedY) {
         x = updatedX;
         y = updatedY;
         return this;
      }

      public Rect withSize(int updatedWidth, int updatedHeight) {
         width = updatedWidth;
         height = updatedHeight;
         return this;
      }
   }

上面這個 Rect 物件是 mutable 的,我們會設計一些 setX() 或是 withmethod 給 Rect 用,使用起來像是:

Rect rect = new Rect();
      rect.setX(12);
      rect.withPosition(1, 3) //改變原有的 x, y
          .withSize(30, 40); //改變原有的 width, height

但如果是 immutable 的 Rect 呢?

final class ImmutableRect {
      private final int x=0, y=0, width=0, height=0;

      public ImmutableRect newPosition(int newX, int newY) {
         return new ImmutableRect(newX, newY, width, height);
      }

      public ImmutableRect newSize(int newWidth, int newHeight) {
         return new ImmutableRect(x, y, newWidth, newHeight);
      }
   }

   //使用範例:
   ImmutableRect rect = new ImmutableRect();
   ImmutableRect rectCopy1 = rect.newPosition(1, 3); 
   ImmutableRect rectCopy2 = rect.newSize(30, 40);

如你所見,ImmutableRect 要改變狀態,每一次變動就是回傳新的物件,這裡使用我的慣例,前面加上 new。這些 new 開頭的 method signature 跟 with method 挺像的,但是如果都共用 with 做開頭,那就糟啦!你的 API 使用者不曉得哪個會改變物件狀態,哪個是回傳新的 copy,這是我們必需極力避免的。雖然冠上 new 之後,英文的文法有時候會怪怪的,但還是比混餚好多了。

mutation 的 method 該不該像我這樣分類,分為 set/with/new 三類呢? 就我見過的 API, set method 的命名最多人用,少部份可以 call-chain。而 with method 類比較少見,然後有部份 with method 是 mutable,有部份是 immutable (不查 Doc 是不會曉得的)。而如果該 library 是傾向 immutable 的設計方向,那麼多半 API 的命名傾向直接使用生動的動詞,例如直接用 locate() / resize() :

final class ImmutableRect {
      private final int x, y, width, height;

      public ImmutableRect locate(int newX, int newY) {
         return new ImmutableRect(newX, newY, width, height);
      }

      public ImmutableRect resize(int newWidth, int newHeight) {
         return new ImmutableRect(x, y, newWidth, newHeight);
      }
   }

這樣設計是相當 ok 的,因為它的 library 內可能大部份的物件都是 immutable,這時冠上 with 或其他前置就顯得多餘了。但我自己經手的專案就不是這樣了,因為是做產品,常常一部份是 mutable (例如 DTO 和 Entity),另一部份 immutable (例如config 和純邏輯),當兩種用法同時出現在專案時,沒有規則分辨就會很困擾。所以我還是選擇加上 new 開頭,區分 set/with/new。

取得類

1. takeFoo()

我們時常會一種需求,就是除了取得物件的資訊外,還會將資訊從物件中移除,這種 method 在 collection 式的物件常會見到,很多人想也不想就直接命名成 getFoo 了。其實這並不是個好習慣,因為這個 method 還有移除的功能,所以我個人偏好換成另一個開頭:用 take。換了個開頭,會讓 API 的使用者警覺 -- 它不是單單取得資訊而已。除了 take 這詞外,還有 acquire/poll...etc 等等詞彙可用。但可想而知選用 take 是最簡單了。java.util.concurrent.BlockingQueue 就是用 take() 這動詞。

2. getter -- Foo getFoo(), boolean isFoo()

get 是最後一個要討論的開頭字,這大概全部的人都會使用吧?就是取得物件的部份資訊時我們會將 method 前面冠 get 或是 is。getter 很容易濫用的,所以我通常都是考慮過上述所有情況後,發現都不適合,最後才會使用 getter。提到 getter 順便插花一下: interface 上不會有 getter ! 如果你設計的 interface 有 getter,你大概設計錯了,有興趣的人可以找找 java.io, java.util, java.lang ...等等裡面所有的 interface,你會發現 getter 很少很少。

結語

除了 naming pattern 這種方式外,在 method 上加 annotation 也是一種解法,而且很適合讓工具 parse,但是 annotation 如果不查 doc 不查 source 可看不到啊。因此程式碼的 method 有一個統一、易分辨功能的命名規則,我認為是必需的,可以增加不少生產力。但可不要做過頭,該用 type safe、該用 annotation 的地方還是要用。(可參見 Effect Java Item:35 Prefer annotations to naming patterns)


回響

可以用 Tag <I>、<B>,程式碼請用 <PRE>