簡介:在使用 演化架構和緊急設計 前幾期描述的技術發現 代碼中的緊急設計之後,下一步您需要一 種獲取和利用這些設計元素的方法。本文介紹了兩種用於獲取慣用模式的方法:將模式作為 APIs 進行捕 捉;使用元程序設計方法。
本 系列 的前幾期主要關注緊急設計中顯而易見的第一步:發現 慣用模式。發現慣用模式之後,您要 用它做什麼?該問題的答案就是本期重點,本文屬於由多個部分組成的系列文章的第二部分。第 1 部分 —代碼與設計的關系探討— 介紹了一種觀點的理論基礎,這種觀點就是軟件中的設計真正是指解決方案 的整個源代碼。一旦轉換角度將所有 代碼當做實際設計,您可以開始考慮在語言級別鞏固設計元素,而 非僅在圖表范圍和其他設計輔助項目中。在這裡我要講一下在發掘出代碼中的可重用設計之後應該做些什 麼,介紹獲取這些模式所用的方法。我首先將它們作為簡單 APIs 獲取,然後描述一種可將這些元素與其 他代碼區分開來的獲取方法。
將模式作為 APIs 予以獲取
捕捉慣用模式最簡單的方式就是將它們作為自身的 API 或框架予以提取。您使用的大多數開源框架都 是與解決特定問題相關的慣用模式集。例如,Web 框架包含您構建 Web 應用程序所需的所有 API 元素, 它們預先從其他運行的 Web 應用程序中獲得。例如,Spring 是用於處理依賴項注入和構建的技術慣用模 式集合,Hibernate 為對象-關系映射封裝模式。
當然,您可以在您的代碼中做同樣的工作。這是目前為止最簡單的方法,因為您改變的僅是代碼的結 構(通常通過在您選擇的 IDE 中重構支持)。這種方法的大量示例參見 第 1 部分 以及 “語言、表達 性與設計:第 2 部分”, 該部分探討了設計模式。
避免結構重復
APIs 偶爾會促進結構重復。使用 APIs 會很煩人,因為您必須頻繁使用主機對象來調用 API。下面來 看一下清單 1 中的示例(其中調用一個與有軌電車相關的 API):
清單 1. 訪問 Car API
Car2 car = new CarImpl();
MarketingDescription desc = new MarketingDescriptionImpl();
desc.setType("Box");
desc.setSubType("Insulated");
desc.setAttribute("length", "50.5");
desc.setAttribute("ladder", "yes");
desc.setAttribute("lining type", "cork");
car.setDescription(desc);
強制用戶輸入主機對象(desc)會給代碼增加不必要的干擾。大部分 APIs 包括主機對象並將其作為 API 的入口點,您必須攜帶它們才能訪問 API。
目前有幾個方法可緩減 APIs 中的這個問題。其中一種方法使用一個鮮為人知的 Java 語法,它允許 您通過一個匿名內部類的作用域界定 “攜帶” 主機對象,如清單 2 所示:
清單 2. 使用一個匿名內部類攜帶主機對象
MarketingDescription desc = new MarketingDescriptionImpl() {{
setType("Box");
setSubType("Insulated");
setAttribute("length", "50.5");
setAttribute("ladder", "yes");
setAttribute("lining type", "cork");
}};
為了便於您理解清單 2,我必須深入探究一個小問題,即 Java 語言如何處理初始化。請看一下清單 3 中的代碼:
清單 3. Java 語言中的初始化設置
public class InitializerDemo {
public InitializerDemo() {
out.println("in constructor");
}
static {
out.println("in static initializer");
}
{
out.println("in instance initializer");
}
public static void main(String[] args) {
out.println("in main() method");
new InitializerDemo();
}
}
清單 3 中的示例展示了 Java 語言中的 4 種不同的初始化方法:
在 main() 方法中
在構造函數中
在一個靜態 初始化塊中,在加載類時執行
在一個初始化塊中,僅在構造函數之前執行
執行順序如圖 1 所示:
圖 1. Java 語言中的初始化順序
加載類之後,靜態初始化器首先運行,緊接著運行的是 main 方法(也是靜態的)。之後,Java 平台 匯集所有實例 初始化塊並在構造函數之前執行它們,最後運行構造函數本身。實例初始化器允許您為一 個匿名內部類執行構造代碼。事實上,它是惟一真實的初始化機制,因為要為一個匿名內部類編寫一個構 造函數是不可能的 — 構造函數必須與類具有相同的名稱,但是匿名內部類下面的類沒有 名稱。
通過使用一種本質上不太智慧的 Java 技巧,您可以避免重用要執行的一系列方法的主機名。但是, 這樣做的代價就是,會有一個奇怪的語法令您的同事備受困擾。
負面效應
將 APIs 作為慣用模式進行提取是一種極其有效的方法,而且可能是利用您所發現的可重用 gems 最 常見的方式。該方法的缺點在於其常態:難以區分您提取的設計元素,因為它們看起來就像您的所有其他 代碼。項目中您的接任人會很難理解您創建的 API 會與其周圍的代碼有所不同,因此您通過探測發現模 式的努力可能會付之一炬。不過,如果您可以將慣用模式從其他代碼中凸顯出來,這樣就可以更容易地看 到它的不同。
使用元程序設計
元程序設計提供一種不錯的方式將模式代碼與實現代碼區分開來,因為您使用關於 代碼的代碼來表達 您的模式。Java 語言提供的一種不錯的方法就是屬性。您可以通過定義屬性來創建聲明性元程序設計標 記。屬性提供一種簡明的方式來表達概念。您可以將大量功能裝入一個小空間,方法就是將其定義為一個 屬性並修飾相關的類。
這裡有一個很好的示例。大多數項目中最常見的技術慣用模式是驗證,它非常適用於聲明性代碼。如 果您將驗證模式作為屬性予以獲取,可以用明確的驗證約束標出您的代碼,這不會影響代碼的主旨。下面 看一下清單 4 中的代碼:
清單 4. MaxLength 屬性
public class Country {
private List<Region> regions = new ArrayList<Region>();
private String name;
public Country(String name){
this.name = name;
}
@MaxLength(length = 10)
public String getName(){
return name;
}
public void addRegion(Region region){
regions.add(region);
}
public List<Region> getRegions(){
return regions;
}
}
使用屬性標記代碼元素的能力揭示了您的意圖,即讓一些外部因素對後面的代碼起作用。這反而更易 於區分模式部分和實現部分。您的驗證代碼很醒目,是因為它看起來 不像周圍的其他代碼。這種通過功 能劃分代碼的方式使我們更易識別特定職責、進行重構和維護工作。
MaxLength 驗證程序規定 Country 名不能超過 10 個字符。屬性聲明本身出現在清單 5 中:
清單 5. MaxLength 屬性聲明
@Retention(RetentionPolicy.RUNTIME)
public @interface MaxLength {
int length() default 0;
}
MaxLength 驗證程序的實際功能存在於兩個類中:名為 Validator 的一個抽象類及其具體實現 MaxLengthValidator。Validator 類出現在清單 6 中:
清單 6. 提取基於屬性的 Validator 類
public abstract class Validator {
public void validate(Object obj) throws ValidationException {
Class clss = obj.getClass();
for(Method method : clss.getMethods())
if (method.isAnnotationPresent(getAnnotationType()))
validateMethod(obj, method, method.getAnnotation(getAnnotationType ()));
}
protected abstract Class getAnnotationType();
protected abstract void validateMethod(
Object obj, Method method, Annotation annotation);
}
該類通過查看 getAnnotationType() 來迭代類中的方法,以確定這些方法是否修飾有特定屬性;當它 找到一個方法時,就執行 validateMethod() 方法。MaxLengthValidator 類的實現見清單 7:
清單 7. MaxLengthValidator 類
public class MaxLengthValidator extends Validator {
protected void validateMethod(Object obj, Method method, Annotation annotation) {
try {
if (method.getName().startsWith("get")) {
MaxLength length = (MaxLength)annotation;
String value = (String)method.invoke(obj, new Object[0]);
if ((value != null) && (length.length() < value.length())) {
String string = method.getName() + " is too long." +
"Its length is " + value.length() +
" but should be no longer than " + length.length();
throw new ValidationException(string);
}
}
} catch (Exception e) {
throw new ValidationException(e.getMessage());
}
}
@Override
protected Class getAnnotationType() {
return MaxLength.class;
}
}
該類從 get 開始檢查方法是否經過潛在驗證,然後獲取注釋中的元數據,最後檢查屬性相對於所聲明 長度的 length 字段值,在出現違規時拋出驗證錯誤。
屬性可以完成很高級的工作。請看下面清單 8 中的例子:
清單 8. 帶惟一性驗證的類
public class Region {
private String name = "";
private Country country = null;
public Region(String name, Country country) {
this.name = name;
this.country = country;
this.country.addRegion(this);
}
public void setName(String name){
this.name = name;
}
@Unique(scope = Country.class)
public String getName(){
return this.name;
}
public Country getCountry(){
return country;
}
}
要聲明 Unique 屬性很簡單,如清單 9 所示:
清單 9. Unique 屬性
@Retention(RetentionPolicy.RUNTIME)
public @interface Unique {
Class scope() default Unique.class;
}
Unique 屬性實現類擴展了 清單 6 中所示的 Validator 抽象類。如清單 10 所示:
清單 10. 惟一驗證程序實現
public class UniqueValidator extends Validator{
@Override
protected void validateMethod(Object obj, Method method, Annotation annotation) {
Unique unique = (Unique) annotation;
try {
Method scopeMethod = obj.getClass().getMethod("get" +
unique.scope().getSimpleName());
Object scopeObj = scopeMethod.invoke(obj, new Object[0]);
Method collectionMethod = scopeObj.getClass().getMethod(
"get" + obj.getClass().getSimpleName() + "s");
List collection = (List)collectionMethod.invoke(scopeObj, new Object [0]);
Object returnValue = method.invoke(obj, new Object[0]);
for(Object otherObj: collection){
Object otherReturnValue = otherObj.getClass().
getMethod(method.getName()).invoke(otherObj, new Object[0]);
if (!otherObj.equals(obj) && otherReturnValue.equals (returnValue))
throw new ValidationException(method.getName() + " on " +
obj.getClass().getSimpleName() + " should be unique but is not since");
}
} catch (Exception e) {
System.out.println(e.getMessage());
throw new ValidationException(e.getMessage());
}
}
@Override
protected Class getAnnotationType() {
return Unique.class;
}
}
該類必須執行相當數量的工作來確保一個國家名的值是惟一的,不過它也展示了屬性在 Java 編程中 的強大功能。
屬性是 Java 語言中備受青睐的一部分。您可以通過它們精確地定義有較廣影響而在目標類中有較少 語法殘留的行為。但是,與 JRuby 等 JVM 上更具表達性的語言相比,它們所做的工作仍然很有限。
使用 JRuby 的 sticky 屬性
Ruby 語言也有屬性(不過它們不像 “屬性” 一樣有特定名稱 — 它們是 Ruby 提供的其中一種元程 序設計方法)。這裡有一個例子。請看清單 11 中的測試類:
清單 11. 測試一個復雜的運算
class TestCalculator < Test::Unit::TestCase
def test_complex_calculation
assert_equal(4, Calculator.new.complex_calculation)
end
end
如果 complex_calculation 方法運行時間較長,您只想在執行驗收測試時運行它,而不想在單元測試 期間運行它。進行該限制的一種方式見清單 12:
清單 12. 限制測試范圍
class TestCalculator < Test::Unit::TestCase
if ENV['BUILD'] == 'ACCEPTANCE'
def test_complex_calculation
assert_equal(4, Calculator.new.complex_calculation)
end
end
end
這是與測試相關的一種技術慣用模式,我可在多個上下文中輕松預見該測試的有用性。在一個 if 塊 中包裝方法聲明為我的代碼增加了復雜度,因為並非所有方法聲明都使用相同的縮進。因此我將使用一個 屬性捕捉該模式,如清單 13 所示:
清單 13. 在 Ruby 中聲明一個屬性
class TestCalculator < Test::Unit::TestCase
extend TestDirectives
acceptance_only
def test_complex_calculation
assert_equal(4, Calculator.new.complex_calculation)
end
end
該版本更清晰且易於讀取。清單 14 中所示的實現無關緊要:
清單 14. 屬性聲明
module TestDirectives
def acceptance_only
@acceptance_build = ENV['BUILD'] == 'ACCEPTANCE'
end
def method_added(method_name)
remove_method(method_name) unless @acceptance_build
@acceptance_build = false
end
end
在 Ruby 中使用如此少的代碼所能完成的工作令人驚歎。清單 14 聲明了一個 module,它是 Ruby 的 混合版本。一個混合版本含有一個您可以包括(include)到類中的功能,從而將該功能添加到類中。您 可以將其作為一種接口,一種可包含代碼的接口。該模塊定義一個名為 acceptance_only 的方法,該方 法檢查 BUILD 環境變量,確定哪個測試階段處於執行中。一旦設置了這個標志,模塊利用一個 hook 方 法。Ruby 中的 Hook 方法在解譯時(而非運行時)執行,且每次向類添加新方法時該 hook 方法都會啟 動。如果設置了 acceptance_build 標志,該方法在執行時會刪除剛才定義的方法。然後將標記設置回 false。(否則,該屬性會影響所有隨後的方法聲明,因為標記仍然為真。)如果您希望它影響包含諸多 方法的代碼塊,您可以刪除標志的重新設置,讓該行為一直保持到有其他因素(比如用戶定義的 unit_test 屬性)改變它時。(這些通俗地講就叫做 sticky 屬性。)
為闡述該機制的功能,Ruby 語言本身使用 sticky 屬性來聲明 private、protected 和 public 類作 用域修飾符。沒錯 — Ruby 中的類作用域界定不是關鍵詞,它們僅僅是 sticky 屬性。
結束語
在本期中,我們展示了如何使用 APIs 和屬性作為獲取慣用模式的方法。如果您能夠設法將獲取的模 式從其他代碼中凸顯出來,那麼就更易於同時讀取兩種代碼,因為它們不相互混雜。
在下一期中,我們將繼續展示如何通過用於構建域特定語言的一系列方法獲取慣用模式。