2013年3月25日 星期一

打造穩固程式碼-實作篇



Spring是一個底層framework,幫我們處理 IoC 的事情,為支撐這個framework,裡面也開發了很多非常好用的 Utils,研究他的source過程中,無意中發現有些好用的Components,再對照自己先前寫好的一堆 Utils 中,發現原來自己遇到的問題,其實人家早就遇過,而且也早已開發了更多、更豐富、功能更強大的元件,這些元件很多都已切開並可獨立引用的,於是我就挑了其中一個 Assert 小元件來幫忙處理資料驗證。不過有很多還是中冠特有的需求,所以我繼寫了支class繼承它,再加入符合中冠需求之功能,完成了這支 com.icsc.dpms.de.dejcAssert資料驗證公用元件。

介紹這個元件前,可先看看iThome上的這篇文章-打造穩固程式碼,先從思路方向著手,文中提出了Assertion 的觀念,我用比較白話一點的比喻來說明好了,自從911事件之後,航空安檢就變得很嚴格,他們必須保證上飛機的客人都是「安全的客人」,所以他們都會針對上飛機的客人做足各式各樣的安全測試-有沒有帶小刀、液體…等,通過才能上飛機。而我們的程式就像一台飛機,送進來的參數就是乘客,要確保你的程式正常運行,必須對進來的參數作嚴格檢查。在「軟體預先架構之美學」一書中所提到「別讓冷空氣進來」,就是類似這種「保護自己」的觀念。

理論大家都知道,但實作上有時就顯得很累贅,以下面程式為例,為確保qty 數量必須要大於0,所以寫了藍色字體檢查 :
    /**
     * 庫存總金額處理邏輯
     * @param inventoryType 品別,qty 數量
     */
public boolean countAmt(dsjccom dsCom, String inventoryType, BigDecimal qty) {
    if ( qty.compareTo( new BigDecimal("0"))==0 ) {
       throw new Exception(“使用平均金額交易,交易數量不可能為0”) ;
    }
}
寫起來很囉嗦吧,若想讓你的程式更穩固,還要再檢查另兩個參數,看他們是否恐佈份子。
public boolean countAmt(dsjccom dsCom, String inventoryType, BigDecimal qty) {
    if ( dsCom==null ) {
       throw new Exception(“dsCom 不可為 null”) ;
    }
    if ( inventoryType==null ) {
       throw new Exception(“庫別不可為null”) ;
    }
    if ( qty.compareTo( new BigDecimal("0"))==0 ) {
       throw new Exception(“使用平均金額交易,交易數量不可能為0”) ;
    }
}

! 寫一堆,還沒真正進入核心邏輯,寫得這麼煩,真得很難提高大家檢查參數的意願。

有鑑於此,這個dejcAssert元件的任務,就是讓大家在做上述檢查邏輯時變得輕鬆易用又簡單,我們看看怎麼用,上面的需求,其實3行就搞定 !
public boolean countAmt(dsjccom dsCom, String inventoryType, BigDecimal qty) {
    dejcAssert.notNull( dsCom, “dsCom 不可為 null”);
    dejcAssert.notNull( inventoryType, 庫別不可為 null”);
    dejcAssert.notZero( qty, 使用平均金額交易,交易數量不可能為0);
}

為何變這麼簡單? 因為程式將異常部份用 Exception 拋出去了,所以壓根兒不需用 if 一個一個檢查,尤其在跟帳務、庫存有關的系統中,為確保帳務沒問題,通常會做很多的數字檢查,諸如大於0、小於0、不等於0,或A數量必須大於B數量之類的比較,其中尤其大家都用BigDecimal來處理數量,而BigDecimal要比大小又不像一般數字比較式如: a>b 這麼簡單,
例如我要檢查 if ( qty < 0 ),就要寫成 if ( qty.compareTo(new BigDecimal(“0”))== -1 ) ,這種寫法超級不直覺。而很不幸,這類跟數字有關的判斷充斥在ERP的每個角落,為讓程式碼更好讀,更容易寫,dejcAssert 專責提供一些常用的判斷,寫來更直覺,更easy !

範例 1 : 不可等於0
使用前
if ( qty.compareTo( new BigDecimal(“0”) )==0 ) {
   throw new Exception(“庫存量不得等於0”) ;
}
使用後
dejcAssert.notZero( qty , “庫存量不得等於0”) ;
說明
若發生錯誤會拋出 IllegalArgumentException,此 exception runtime exception,不會強制 catch 的,用的人不會有 catch exception 壓力及負擔

範例 2 : 數字A必須大於數字B
使用前
if ( numA.compareTo( numB ) !=1 ) {
   throw new Exception(“A庫存量[“+numA+”]不得少於B庫存量[“+numB+”]”) ;
}
使用後
dejcAssert.greater( numA, numB, “A庫存量{0}不得少於B庫存量{1}”) ;
說明
方法的取名都是第1個參數相對第2個參數。
其中錯誤訊息都應該顯示出實際錯誤數字,以利用戶判斷錯誤,但要組合這種訊息,寫起來很麻煩,要把變數加來加去,雙引號括來括去,不好寫也不好讀,所以這元件會順便提供將送入之參數自動放到{n}之中,
其中 n=0..參數數量-1

下面是目前有提供的各類方法
after(java.lang.String date1, java.lang.String date2, java.lang.String msg)
          date1
必須在 date2 之後
afterOrEquals(java.lang.String date1, java.lang.String date2, java.lang.String msg)
          date1
必須在 date2 之後,但2個日期可相同
before(java.lang.String date1, java.lang.String date2, java.lang.String msg)
          date1
必須在 date2 之前
beforeOrEquals(java.lang.String date1, java.lang.String date2, java.lang.String msg)
          date1
必須在 date2 之前,但2個日期可相同
betweenPeriod(java.lang.String fromDate, java.lang.String fromTime, java.lang.String toDate, java.lang.String toTime, java.lang.String date, java.lang.String time, java.lang.String msg)
          
驗證 date,time 必須界於 fromDate,fromTime toDate,toTime 之間 (含邊界)
equals(java.math.BigDecimal num1, java.math.BigDecimal num2, java.lang.String msg)
          
驗證 num1 必須等於 num2
greater(java.math.BigDecimal num1, java.math.BigDecimal num2, java.lang.String msg)
          
驗證 num1 必須大於 num2
less(java.math.BigDecimal num1, java.math.BigDecimal num2, java.lang.String msg)
          
驗證 num1 必須小於 num2
negative(java.math.BigDecimal num, java.lang.String msg)
          
驗證必須為負數
notZero(java.math.BigDecimal num, java.lang.String msg)
          
驗證不可等於0
positive(java.math.BigDecimal num, java.lang.String msg)
          
驗證必須為正數
todayAfter(java.lang.String date, java.lang.String msg)
          
驗證 date 必須為今天以後的日期 (不含今天)
todayBefore(java.lang.String date, java.lang.String msg)
          
驗證 date 必須為今天以前的日期 (不含今天)
todayOrAfter(java.lang.String date, java.lang.String msg)
          
驗證 date 必須為今天或今天以後的日期
todayOrBefore(java.lang.String date, java.lang.String msg)
          
驗證 date 必須為今天或今天以前的日期
zero(java.math.BigDecimal num, java.lang.String msg)
          
數字num必須為0

同時因為本 class 是繼承 Spring org.springframework.util.Assert ,所以 dejcAssert 也會提供下列 Spring Assert 本來就有的功能:
doesNotContain(String textToSearch, String substring)
          Assert that the given text does not contain the given substring.
doesNotContain(String textToSearch, String substring, String message)
          Assert that the given text does not contain the given substring.
hasLength(String text)
          Assert that the given String is not empty; that is, it must not be
null and not the empty String.
hasLength(String text, String message)
          Assert that the given String is not empty; that is, it must not be
null and not the empty String.
hasText(String text)
          Assert that the given String has valid text content; that is, it must not be
null and must contain at least one non-whitespace character.
hasText(String text, String message)
          Assert that the given String has valid text content; that is, it must not be
null and must contain at least one non-whitespace character.
isAssignable(Class superType, Class subType)
          Assert that
superType.isAssignableFrom(subType) is true.
isAssignable(Class superType, Class subType, String message)
          Assert that
superType.isAssignableFrom(subType) is true.
isInstanceOf(Class clazz, Object obj)
          Assert that the provided object is an instance of the provided class.
isInstanceOf(Class type, Object obj, String message)
          Assert that the provided object is an instance of the provided class.
isNull(Object object)
          Assert that an object is
null .
isNull(Object object, String message)
          Assert that an object is
null .
isTrue(boolean expression)
          Assert a boolean expression, throwing
IllegalArgumentException if the test result is false.
isTrue(boolean expression, String message)
          Assert a boolean expression, throwing
IllegalArgumentException if the test result is false.
notEmpty(Collection collection)
          Assert that a collection has elements; that is, it must not be
null and must have at least one element.
notEmpty(Collection collection, String message)
          Assert that a collection has elements; that is, it must not be
null and must have at least one element.
notEmpty(Map map)
          Assert that a Map has entries; that is, it must not be
null and must have at least one entry.
notEmpty(Map map, String message)
          Assert that a Map has entries; that is, it must not be
null and must have at least one entry.
notEmpty(Object[] array)
          Assert that an array has elements; that is, it must not be
null and must have at least one element.
notEmpty(Object[] array, String message)
          Assert that an array has elements; that is, it must not be
null and must have at least one element.
notNull(Object object)
          Assert that an object is not
null .
notNull(Object object, String message)
          Assert that an object is not
null .
state(boolean expression)
          Assert a boolean expression, throwing IllegalStateException if the test result is
false.
state(boolean expression, String message)
          Assert a boolean expression, throwing
IllegalStateException if the test result is false.

有了這麼輕便的驗證資料方法,希望大家以後都要有驗證參數的習慣,擴大來應用的話,不僅要在執行程式邏輯之前要驗證,執行之後也最好再驗證一次。例如庫存一般在領料後會再做一次檢查,以確保結餘的庫存量是安全的,只要每人都養成重要邏輯的「前」與「後」驗證資料的習慣,「中冠品質、堅若盤石」的境界,相信指日可待。

附註:
資料必須驗證之場合
一、外來系統傳入之變數 (如財會收集各AP拋入之帳務資料…)
二、由通訊傳入之資料 ( DI …)
三、舉凡開放給別人呼叫的API而傳入之參數
四、設計成獨立 Component 所傳入之參數,雖然有時是被自己呼叫,但因設計時已將之視為獨立運作之 Component,所以不管是自己或其它系統使用它,對Component而言,傳入之參數都是「陌生人」,都需被檢查。
五、分析時已找出有明確之「後置條件」者,就需針對「後置條件」作檢驗,例如【領料】的後置條件為「庫存量必須大於0」,因此在【領料】後就需檢查庫存量。

不必驗證之場合:
一、很清楚所傳入之參數來源,例如private方法之參數,顯然只有自己才能使用,而參數也是自己製造並傳入的,此時就沒必要又做一堆檢查了,畢竟檢查,還是多少會花點系統資源的。 

沒有留言: