Java和Ceylon物件的構造和驗證

2016-05-11    分類:JAVA開發、程式設計開發、首頁精華0人評論發表於2016-05-11

一些壞程式碼
考慮下面的Java類。(夥計,不要在家裡寫這樣的程式碼)
public class Period {
    private final Date startDate;
    private final Date endDate;
    //returns null if the given String
    //does not represent a valid Date
    private Date parseDate(String date) {
       ...
    }
    public Period(String start, String end) {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }
    public boolean isValid() {
        return startDate!=null && endDate!=null;
    }
    public Date getStartDate() {
        if (startDate==null) 
            throw new IllegalStateException();
        return startDate;
    }
    public Date getEndDate() {
        if (endDate==null)
            throw new IllegalStateException();
        return endDate;
    }
}
嘿,我之前已經警告過,它是人為的。但是,在實際Java程式碼中找個像這樣的東西實際上並非不常見。

這裡的問題在於,即使輸入引數(在隱藏的parseDate()方法中)的驗證失敗了,我們還是會獲得一個Period的例項。但是我們獲取的那個Period不是一個“有效的”狀態。嚴格地說,我的意思是什麼呢?

好吧,假如一個物件不能有意義地響應公用操作時,我會說它處於一個非有效狀態。在這個例子裡,getStartDate() 和getEndDate()會丟擲一個IllegalStateException異常,這就是我認為不是“有意義的”一種情況。

從另外一方面來看這個例子,在設計Period時,我們這兒出現了型別安全的失敗。未檢查的異常代表了型別系統中的一個“漏洞”。因此,一個更好的Period的型別安全的設計,會是一個不使用未檢查的異常—在這個例子中意味著不丟擲IllegalStateException異常。

(實際上,在真實程式碼中,我更有可能遇到一個getStartDate() 方法它不檢查null ,在這個程式碼行之後就會導致一個NullPointerException異常,這就更加糟糕了。)

我們能夠很容易地轉換上面的Period類成為Ceylon形式的類:
shared class Period(String start, String end) {
    //returns null if the given String
    //does not represent a valid Date
    Date? parseDate(String date) => ... ;
    value maybeStartDate = parseDate(start);
    value maybeEndDate = parseDate(end);
    shared Boolean valid
        => maybeStartDate exists 
        && maybeEndDate exists;
    shared Date startDate {
        assert (exists maybeStartDate);
        return maybeStartDate;
    }
    shared Date endDate {
        assert (exists maybeEndDate);
        return maybeEndDate;
    }
}
當然了,這段程式碼也會遇到與原始Java程式碼同樣的問題。兩個assert符號衝著我們大喊,在程式碼的型別安全中有一個問題。

使Java程式碼變得更好

Java裡我們怎麼改進這段程式碼呢?好吧,這兒就是一個例子關於Java飽受詬病的已檢查異常會是一個非常合理的解決方法!我們可以稍微修改下Period來從它的構造器中丟擲一個已檢查的異常:
public class Period {
    private final Date startDate;
    private final Date endDate;
    //throws if the given String
    //does not represent a valid Date
    private Date parseDate(String date)
            throws DateFormatException {
       ...
    }
    public Period(String start, String end) 
            throws DateFormatException {
        startDate = parseDate(start);
        endDate = parseDate(end);
    }
    public Date getStartDate() {
        return startDate;
    }
    public Date getEndDate() {
        return endDate;
    }
}
現在,使用這個解決方案,我們就不會獲取一個處於非有效狀態的Period,例項化Period的程式碼會由編譯器負責去處理無效輸入的情形,它會捕獲一個DateFormatException異常。
try {
    Period p = new Period(start, end);
    ...
}
catch (DateFormatException dfe) {
    ...
}
這是一個對已檢查異常不錯的、完美的、正確的使用,不幸的是我幾乎很少看到Java程式碼像上面這樣使用已檢查異常。

使Ceylon程式碼變得更好

那麼Ceylon怎麼樣呢?Ceylon沒有已檢查異常,因而我們需要尋找一個不同的解決方式。典型地,在Java呼叫一個函式會丟擲一個已檢查異常的情形中,Ceylon會呼叫函式返回一個聯合型別。因為,一個類的初始化不返回除了類自己外的任何型別,我們需要提取一些混合的初始化/驗證的邏輯來使其成為一個工廠函式。
//returns DateFormatError if the given 
//String does not represent a valid Date
Date|DateFormatError parseDate(String date) => ... ;
shared Period|DateFormatError parsePeriod
        (String start, String end) {
    value startDate = parseDate(start);
    if (is DateFormatError startDate) {
        return startDate;
    }
    value endDate = parseDate(end);
    if (is DateFormatError endDate)  {
        return endDate;
    }
    return Period(startDate, endDate);
}
shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
}
根據型別系統,呼叫者有義務去處理DateFormatError:
value p = parsePeriod(start, end);
if (is DateFormatError p) {
    ...
}
else {
    ...
}
或者,如果我們不關心給定日期格式的實際問題(這是有可能的,假定我們工作的初始化程式碼丟失了那個資訊),我們可以使用Null而不是DateFormatError:
//returns null if the given String 
//does not represent a valid Date
Date? parseDate(String date) => ... ;
shared Period? parsePeriod(String start, String end)
    => if (exists startDate = parseDate(start), 
           exists endDate = parseDate(end))
       then Period(startDate, endDate)
       else null;
shared class Period(startDate, endDate) {
    shared Date startDate;
    shared Date endDate;
}
至少可以說,使用工廠函式的方法是優秀的,因為通常來說在驗證邏輯和物件初始化之間它具有更好的隔離。這點在Ceylon中特別有用,在Ceylon中,編譯器在物件初始化邏輯中新增了一些非常嚴厲的限制,以保證物件的所有領域僅被賦值一次。
評論(0)

相關文章