本文闡述軟件架構(gòu)與設(shè)計模式锌仅,它為架構(gòu)師和開發(fā)人員提供了一組關(guān)于數(shù)據(jù)校驗的架構(gòu)模式(隔離校驗器瘩蚪,可組裝校驗器瀑志,動態(tài)策略校驗器敷鸦,動態(tài)注冊校驗器等),數(shù)據(jù)校驗是任何類型的開發(fā)中都不可或缺的環(huán)節(jié)浩销,如果沒有統(tǒng)一的架構(gòu)合武,可能校驗代碼會遍布整個應(yīng)用,如何將數(shù)據(jù)校驗與應(yīng)用邏輯解耦宇挫,如何適應(yīng)各種粒度的數(shù)據(jù)和各種復(fù)雜程度業(yè)務(wù)規(guī)則,正是本文要探討的酪术。
在我們各種類型的應(yīng)用開發(fā)中有一個必不可少的環(huán)節(jié)-數(shù)據(jù)校驗器瘪,無論是大型企業(yè)應(yīng)用,還是一個簡單的程序绘雁。如果沒有統(tǒng)一的架構(gòu)橡疼,可能校驗代碼會遍布整個應(yīng)用,一旦校驗規(guī)則改變就需要修改多處代碼庐舟,這是一種不好的設(shè)計欣除,因為數(shù)據(jù)校驗與應(yīng)用邏輯耦合得太緊。數(shù)據(jù)校驗不外乎語法校驗和語義校驗兩類挪略,本文描述了一組架構(gòu)上的模式來對這兩類需求提供解決方案历帚。該模式組按照待校驗數(shù)據(jù)的粒度大小和業(yè)務(wù)規(guī)則的復(fù)雜程度分成多種類型:隔離校驗器,可組裝校驗器杠娱,動態(tài)策略校驗器挽牢,動態(tài)注冊校驗器等。大家可以針對自己的應(yīng)用選擇合適的架構(gòu)摊求。應(yīng)用這組模式還可以獲得一個好處禽拔,如果需要的話,我們可以把數(shù)據(jù)校驗器當作一個橫切關(guān)注點(Crosscut concern),應(yīng)用 AOP(Aspect of Programming)技術(shù)奏赘,這樣可以徹底分離出數(shù)據(jù)校驗邏輯代碼寥闪。
問題引出
讓我們從幾個應(yīng)用場景(user scenario)開始吧,第一個場景是網(wǎng)站上的注冊用戶磨淌,注冊時需要填寫很多數(shù)據(jù),這些數(shù)據(jù)都需要校驗后才能寫進數(shù)據(jù)庫凿渊,比如用戶名梁只,校驗規(guī)則可能是:用戶名由 a ~ z 的英文字母 ( 不區(qū)分大小寫 )、0 ~ 9 的數(shù)字埃脏、點搪锣、減號或下劃線組成,長度為 3 ~ 18 個字符彩掐。這種關(guān)于數(shù)據(jù)的結(jié)構(gòu)正確性方面的校驗我們稱之為語法校驗构舟。而身份證號碼這種數(shù)據(jù),它需要根據(jù)出生日期校驗身份證號碼的正確性堵幽,不僅僅是填夠了 16 或 19 位數(shù)字就行狗超。這種關(guān)于數(shù)據(jù)的內(nèi)容正確性方面的校驗稱之為語義校驗。一般情況下語法和語義方面的校驗是在一塊處理的朴下,比如身份證號碼评疗,必然也需要校驗數(shù)據(jù)是否全是數(shù)字和必須是 16 或 19 位棍矛,這是語法校驗,同時它需要和出生日期相符,這又是語義校驗纵东。從架構(gòu)的角度而言,這種情況下區(qū)分語法和語義的意義不太大颓影,因為沒必要把它分成兩個步驟用兩個方法來處理枫虏。但是有些應(yīng)用,比如數(shù)據(jù)是一段 XML 的文本串灸姊,首先需要校驗 XML 字符串的結(jié)構(gòu)-語法是否符合相應(yīng)的 schema拱燃,然后再校驗其中某個元素的內(nèi)容-語義的正確性,這可能就需要分開來處理比較合適厨钻,因為語法校驗是業(yè)務(wù)無關(guān)的扼雏,而后者的語義校驗是業(yè)務(wù)相關(guān)的,業(yè)務(wù)相關(guān)就意味著一旦業(yè)務(wù)規(guī)則改變夯膀,校驗規(guī)則就可能改變诗充,所以這種情況最好將語義校驗分離出來。
第二個應(yīng)用場景是一個 MDA(Model Driven Architecture)工具開發(fā)的例子诱建,我們都用過大名鼎鼎的 Rational Rose 或 Microsoft Visio蝴蜓。這些工具都提供從 UML 模型生成代碼的功能,這就是 MDA,它們將 UML 模型映射成模型的元數(shù)據(jù) (meta-data)(稱之為元模型 meta-model)茎匠,然后從元模型可以轉(zhuǎn)換成各種支持語言的代碼格仲,如 Java, C++。當我們在視圖上畫一個 UML 元素(如類 Class)诵冒,然后為其定義了某種 Stereotype 來標識他的業(yè)務(wù)語義凯肋,比如數(shù)據(jù)庫的表 Table,或者自定義的一個用于表示 Web Service 的 Service 元素汽馋。接下來我們要將該元素生成相應(yīng)的代碼侮东,這時當你選定元素時,運行時系統(tǒng)并不知道該元素是普通的 Class豹芯,還是 Table悄雅,因為在運行時環(huán)境中都是 UML 的 Class 實例對象,這就需要我們提供校驗邏輯來處理了铁蹈,處理 Class 的校驗邏輯和 Table 的校驗邏輯自然不應(yīng)該放在一起宽闲,更何況如果是自定義的擴展元素,根本不可能把校驗代碼寫到已有系統(tǒng)里去握牧。這就需要我們提供一個統(tǒng)一的校驗器接口容诬,不同的校驗邏輯封裝在單獨的類中。進一步我碟,我們需要對這些獨立的校驗器進行集中組裝和管理放案,因為我們不必每次都去實例化這些工具類,實例化后將它們緩存起來就可以了矫俺。
第三個應(yīng)用場景是一個銀行并購的案例吱殉,假如銀行 A 并購了銀行 B,兩家銀行都有各自已有的電子銀行應(yīng)用厘托,并購后要將兩家應(yīng)用整合成一個統(tǒng)一的應(yīng)用友雳,其中有一個余額查詢業(yè)務(wù),在進行具體的查詢操作事務(wù)之前铅匹,需要校驗用戶輸入的帳號 account押赊,兩家銀行已有的帳號各有不同的創(chuàng)建規(guī)則,比如銀行 A 是 16 位數(shù)字作為帳戶包斑,首 4 位是銀行代號流礁,第二個 4 位是地區(qū)代號,第三個 4 位是網(wǎng)點代號罗丰,尾 4 位是用戶編號神帅。而銀行 B 則是 19 位數(shù)字作為帳戶,各個區(qū)段的含義也和銀行 A 不一樣萌抵,這就要求用戶填寫一個帳戶的時候找御,后臺必須對應(yīng)兩套數(shù)據(jù)校驗規(guī)則元镀,而且應(yīng)用需要根據(jù)一定的規(guī)則來選擇銀行 A 的校驗策略或銀行 B 的校驗策略。而且更復(fù)雜的情況是霎桅,銀行的帳戶還可能是升位后的(比如從 12 位升到 16 位)栖疑,這樣必須同時兼顧新舊帳戶,也就是說有多套校驗規(guī)則來處理滔驶,我們的數(shù)據(jù)校驗器需要支持業(yè)務(wù)規(guī)則的動態(tài)切換遇革。 這里面可能有一個有爭議的地方,校驗帳號時需要有具體的業(yè)務(wù)規(guī)則支持瓜浸,那么這算不算是業(yè)務(wù)邏輯呢澳淑,當然這個校驗邏輯并不那么純粹,***軟件設(shè)計并不是個黑白的二元世界插佛,各種層次的對象混合在一起很正常,我們也不大可能什么東西都能做個分水嶺把它們隔離開來量窘。***另外雇寇,這里的校驗邏輯還是和銀行應(yīng)用別的業(yè)務(wù)邏輯不大一樣,比如轉(zhuǎn)帳交易蚌铜,這個動作的觸發(fā)是一定要在一個高安全可靠的事務(wù)中執(zhí)行的锨侯,而我們的校驗帳號過程可能不需要運行在事務(wù)中,或者只運行在低安全可靠級別的事務(wù)中即可冬殃。這是有本質(zhì)區(qū)別的囚痴,所以把這種摻有業(yè)務(wù)規(guī)則的校驗劃分到校驗邏輯里而不是業(yè)務(wù)邏輯中是有理由的。
隔離校驗器
針對上述第一類應(yīng)用場景审葬,我們只需要把數(shù)據(jù)校驗邏輯從其他業(yè)務(wù)邏輯中剝離出來深滚,將校驗邏輯委任到一個單獨的校驗類中去。把校驗職責(zé)分離出來后涣觉,第一個好處是:一旦我們需要更改校驗邏輯痴荐,只要修改校驗類代碼即可,而不用修改其他任何業(yè)務(wù)邏輯類官册。第二個好處是:可以集中管理控制所有的數(shù)據(jù)校驗邏輯生兆,提高了代碼的內(nèi)聚性,而且讓代碼簡潔膝宁、清晰鸦难。當然這里說的所有數(shù)據(jù)集中控制不一定就是全放在一個類中,如果有必要员淫,也可以將數(shù)據(jù)按照不同的類型分組合蔽,每一個組封裝在一個校驗類中。第三個好處是可重用性高满粗,校驗邏輯封裝成了一個工具類辈末,自然可重用性大大提高。
在設(shè)計這個隔離校驗器類時還有一些需要權(quán)衡的地方,在設(shè)計某一個數(shù)據(jù)的校驗方法時挤聘,比如用戶名的校驗轰枝,如果數(shù)據(jù)出錯了,簡單的情況下组去,我們只需返回一個 boolean 值鞍陨,告訴用戶數(shù)據(jù)有誤。而如果是身份證號碼這類數(shù)據(jù)出錯了从隆,可能就需要提供更細粒度的錯誤類型給用戶诚撵,告訴用戶是與出生日期不符還是位數(shù)不夠。對這種錯誤種類較多的情況键闺,我們可以返回錯誤代號(如 int 值)來區(qū)別各種錯誤寿烟,這是非面向?qū)ο笳Z言的一種做法,在面向?qū)ο笾形覀兛梢杂靡粋€異常 Exception 來返回錯誤類型辛燥,這比返回錯誤代號更好筛武,因為錯誤代號需要解析成具體的錯誤信息,這個解析工作還得由校驗器類的 API 使用者來調(diào)挎塌,這個使用者是其它的業(yè)務(wù)邏輯類徘六,這就是說業(yè)務(wù)邏輯類還是耦合了數(shù)據(jù)校驗錯誤處理邏輯,顯然不如用異常處理來的徹底榴都。
代碼如下:
清單 1: UserInfoValidator.java
public abstract class UserInfoValidator { public static boolean validateUserID(String uid) { boolean isValid = false; // 校驗規(guī)則 return isValid; } public static boolean validteEmail(String email) { boolean isValid = false; // 校驗規(guī)則 return isValid; } public static void validateSSN(SSNDataObject ssn) throws DataValidationException { if (ssn == null) throw new DataValidationException("No data found."); String idCard = ssn.getIdCard(); if ((idCard == null) || (idCard.equals(""))) throw new DataValidationException("No id.card data found."); if (!((idCard.length() == 15) || (idCard.length() == 18))) throw new DataValidationException( "ID.card length must be 15 or 18."); Date birthDay = ssn.getBirthDay(); if (birthDay == null) throw new DataValidationException("No birthday data found."); int sex = ssn.getSex(); if (sex == 0) throw new DataValidationException("No sex data found."); // 生日校驗規(guī)則 // if (...) // throw new DataValidationException("ID.card didn't match birthday."); int idSex = Integer.parseInt(idCard.substring(idCard.length() - 1)); if (idSex % sex != 0) throw new DataValidationException("ID.card didn't match sex."); } }
從上面代碼可以看出待锈,我們用了靜態(tài) static 方法,因為我們這是個工具類嘴高,沒有什么狀態(tài)需要存儲竿音,所以不需要實例化類。而且調(diào)用校驗方法會很頻繁阳惹,用靜態(tài)方法可以提升性能谍失。
另外還有一點值得一提,我們封裝了一個身份證數(shù)據(jù)類莹汤,里面包含了三個屬性:身份證號快鱼,出生日期,性別纲岭。驗證身份證號需要出生日期和性別奇偶碼這一點是沒有異議的抹竹,但為什么不用三個單獨的參數(shù)呢,這里的封裝為以后提供了更大的靈活性止潮,比如將來我們打算將身份證驗證邏輯做得更精細窃判,需要判斷出生地區(qū)的代碼是否和身份證的頭幾位一致,這可能就需要四個參數(shù)了喇闸,或者我們的出生日期需要換一個類(Date->Calendar)來表示袄琳,顯然我們只需要修改身份證數(shù)據(jù)封裝類询件,而不用修改調(diào)用接口。
可組裝校驗器
針對第二類場景唆樊,我們對每一個數(shù)據(jù)類提供一個獨立的校驗規(guī)則類宛琅,因為這個數(shù)據(jù)類本身已經(jīng)包含了語法和語義邏輯。語法邏輯是與數(shù)據(jù)結(jié)構(gòu)相關(guān)的逗旁,在我們的示例中嘿辟,是判斷對象是否是 UML 的 Class 實例。而語義邏輯是與業(yè)務(wù)規(guī)則相關(guān)的片效,每一個數(shù)據(jù)類關(guān)聯(lián)的業(yè)務(wù)規(guī)則不盡相同红伦,可能來自不同領(lǐng)域,或不同的業(yè)務(wù)組件或系統(tǒng)淀衣;另外由于業(yè)務(wù)規(guī)則的易變性較強昙读,可擴展性和可配置性要求也較高,所以有必要為每一個數(shù)據(jù)類設(shè)置專屬的校驗類膨桥。這里我們將每一個校驗類稱作一條校驗規(guī)則 (Rule)箕戳。校驗規(guī)則類的接口和實現(xiàn)代碼如下:
清單 2: IVRule.java
public interface IVRule { /** * validate value by domain rule. */ public boolean isValid(Object value); /** * validate value by domain rule. */ public void validate(Object value) throws DataValidationException; }
清單 3: ServiceVRule.java
public class ServiceVRule implements IVRule { private static String STEREOTYPE_NAME = "Service"; ……… .. public void validate(Object value) throws DataValidationException { if (value instanceof Class) { Stereotype st = ((Class) value).getStereotype(); String name = st.getName(); if (STEREOTYPE_NAME.equals(name)) { String wsdl = (String) st.getProperty("WSDL"); if ((wsdl == null) || (wsdl.equals(""))) throw new DataValidationException("No WSDL file is defined."); } else throw new DataValidationException("It is not a Service Object."); } else throw new DataValidationException("It is not a UML Class model."); }}
在這里,我們還是提供了兩種校驗結(jié)果返回機制国撵,boolean 值和拋出異常,在具體應(yīng)用時大家可以選擇一個即可玻墅。這里的校驗規(guī)則實現(xiàn)是關(guān)于 Web Service 的介牙,它的語法校驗是檢查數(shù)據(jù)是否是 UML 的 Class 對象,而語義校驗是檢查 Stereotype 是否是 Service澳厢,并檢查 Stereotype 中是否含有 WSDL(Web service description language) 屬性环础。
接下來,怎么應(yīng)用這些校驗規(guī)則 rule 呢剩拢?一個系統(tǒng)中會有很多校驗規(guī)則類线得,那么就需要一個管理機制來管理這些校驗類,最簡單的我們只需定義一個方法 validate(Object value, IVRule rule)徐伐,提供一層簡單的封裝贯钩,它可以起到一個代理的作用,比如办素,應(yīng)用 Proxy 模式角雷,我們可以對校驗規(guī)則本身做一些安全性認證方面的工作,然后才決定是否可以用該校驗規(guī)則性穿。而更完善的管理機制是提供一個更靈活的環(huán)境勺三,讓用戶可以動態(tài)組裝,改變需曾,查找校驗規(guī)則類吗坚∑碓叮基本思路是:我們將校驗規(guī)則類當作一種可重用資源,提供一個組裝工廠環(huán)境商源,用戶可以將校驗規(guī)則 rule 注冊到工廠里车份,工廠會實例化和緩存這些類,并提供查找服務(wù)炊汹;然后提供一個校驗器來從工廠里查找出相應(yīng)的校驗規(guī)則 rule 類躬充,為用戶提供校驗服務(wù)。這里面用到了 Factory讨便,F(xiàn)lyweight充甚,Registry 模式。(本文引用的模式請參考相關(guān)模式)
第一步霸褒,我們設(shè)計一個組裝工廠類伴找,它提供實例化、緩存废菱、和查找服務(wù)技矮,很顯然,實例化類是一個 Factory 模式的基本職責(zé)殊轴,將實例緩存 cache 是一個 Flyweight 模式的基本職責(zé)衰倦,查找服務(wù)可以很簡單,在我們的例子中就是從一個 Map 中取出校驗規(guī)則 rule 實例旁理,也可以復(fù)雜化樊零,比如我們的校驗規(guī)則類是一個遠程資源,或者是實例化這個類需要用到其它的遠程資源孽文,如數(shù)據(jù)庫驻襟,那這個查找功能實現(xiàn)起來可能就復(fù)雜些,可以通過 JNDI 來查找芋哭,也可以將遠程資源暴露成服務(wù)(Web Service)并注冊到 UDDI (Universal Discover Description and Integration)沉衣,然后從 UDDI 中查找服務(wù)。從這里我們可以看到這個組裝工廠類具備管理校驗規(guī)則 rule 類的整個生命周期的職責(zé)减牺,這為校驗器應(yīng)用提供了很大的靈活性和可擴展性豌习,假如我們今后需要實現(xiàn)一個實例池或資源池 Pool 來管理這些校驗規(guī)則實例,那么只需要將組裝工廠類的功能稍作修改和擴展就可烹植,而不必觸及校驗器應(yīng)用的其他類斑鸦,因為我們已經(jīng)將實例的管理邏輯從整個校驗器應(yīng)用中剝離出來,管理職責(zé)的變化只局限在組裝工廠類內(nèi)草雕,對別的類是封閉的巷屿,這正體現(xiàn)面向?qū)ο蟮幕驹O(shè)計原則之一 Open-Close 原則(對修改封閉,對擴展開放)墩虹。組裝工廠類的代碼如下:
清單 4: VRuleAssemblerFactory.java
public class VRuleAssemblerFactory { private static Map rules = new HashMap(); /** * 查找校驗規(guī)則 Rule嘱巾。 */ public static IVRule lookupVRule(String ruleHandler) { if (ruleHandler == null) return null; IVRule rule = null; if (rules.containsKey(ruleHandler)) rule = (IVRule) rules.get(ruleHandler); return rule; } /** * 注冊 / 加入一個校驗規(guī)則 Rule. */ public static void addVRule(String ruleHandler, Class ruleClass) { if ((ruleHandler != null) && (ruleClass != null)) { try { rules.put(ruleHandler, ruleClass.newInstance()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } /** * 批量載入校驗規(guī)則 Rule, 一般在應(yīng)用系統(tǒng)初始化時調(diào)用憨琳。 */ public static void assembleVRules() { addVRule("Table", TableVRule.class); addVRule("Service", ServiceVRule.class); } }
從上面的代碼可以看到,我們用一個 Map 作為緩存庫旬昭,注冊一個校驗規(guī)則時以字符串作為關(guān)鍵字篙螟,當然也可以用別的自定義類型,在查詢時利用了 Map 的查找功能很簡單高效地實現(xiàn)了查找功能问拘,另外我們還定義了一個批量載入校驗規(guī)則的方法遍略,這個功能是為了用戶使用方便,在應(yīng)用系統(tǒng)初始化時執(zhí)行一次骤坐,而且可以透明地載入校驗規(guī)則绪杏,在本例中只是硬編碼了這些校驗規(guī)則類,需要的話我們可以從別的元數(shù)據(jù)文件中 (XML 或 CSV 文件 ) 導(dǎo)入纽绍。
還需注意一點的是蕾久,這個組裝工廠是一個全局類,在這里是以靜態(tài) static 方式實現(xiàn)的拌夏,當然也可以以 Singleton 方式來實現(xiàn)僧著,還可以以線程安全 ThreadLocal 的方式來實現(xiàn)。
第二步障簿,我們設(shè)計校驗器類盹愚,校驗器類應(yīng)該作為整個校驗器應(yīng)用的 Fa?ade,用戶需要校驗數(shù)據(jù)時只需和它打交道站故,這就很好的把校驗規(guī)則類隱藏了起來杯拐,因此校驗器的職責(zé)有查找相應(yīng)的校驗規(guī)則類和執(zhí)行校驗。校驗器類的接口和實現(xiàn)代碼如下:
清單 5: IValidator.java
public interface IValidator { /** * 通過關(guān)鍵字 ruleHandler 查詢校驗規(guī)則來校驗數(shù)據(jù)世蔗。 */ public boolean isValid(Object value, String ruleHandler); /** * 通過關(guān)鍵字 ruleHandler 查詢校驗規(guī)則來校驗數(shù)據(jù)。返回異常朗兵。 */ public void validate(Object value, String ruleHandler) throws DataValidationException; /** * 直接指定校驗規(guī)則類來校驗數(shù)據(jù)污淋。 */ public boolean isValid(Object value, IVRule rule); /** * 直接指定校驗規(guī)則類來校驗數(shù)據(jù),返回異常余掖。 */ public void validate(Object value, IVRule rule) throws Exception; }
清單 6: AssemblyValidator.java
public class AssemblyValidator implements IValidator { private static AssemblyValidator instance = null; private AssemblyValidator() { } public static synchronized AssemblyValidator getInstance() { if (instance == null) instance = new AssemblyValidator(); return instance; } public boolean isValid(Object value, String ruleHandler) { boolean valid = false; IVRule rule = VRuleAssemblerFactory.lookupVRule(ruleHandler); if (rule != null) valid = rule.isValid(value); return valid; } public void validate(Object value, String ruleHandler) throws DataValidationException { IVRule rule = VRuleAssemblerFactory.lookupVRule(ruleHandler); if (rule != null) rule.validate(value); } public boolean isValid(Object value, IVRule rule) { boolean valid = false; if (rule != null) valid = rule.isValid(value); return valid; } public void validate(Object value, IVRule rule) throws Exception { if (rule != null) rule.validate(value); } }
從上面代碼可以看到寸爆,我們使用了 Singleton 模式,因為沒必要每次都實例化校驗器類盐欺。另外我們在這里提供了兩套返回機制赁豆,還有兩套取校驗規(guī)則的方式,這都可以根據(jù)實際應(yīng)用作出取舍冗美。
可組裝校驗器的架構(gòu)圖如下:
圖 1:可組裝校驗器的架構(gòu)圖
動態(tài)策略校驗器
針對第三類場景魔种,我們必須支持一種數(shù)據(jù)對應(yīng)有多套業(yè)務(wù)校驗規(guī)則,我們可以把每種業(yè)務(wù)規(guī)則都建模成一個校驗規(guī)則類粉洼,但這樣做靈活性和可擴展性就很差了节预。如果我們對一種數(shù)據(jù)只用一個校驗規(guī)則類叶摄,而將多套業(yè)務(wù)規(guī)則建模成多種策略,在校驗規(guī)則類中應(yīng)用這些策略安拟,這樣做好處在于:一可以對用戶隱藏業(yè)務(wù)規(guī)則蛤吓,二是將來對策略進行修改或增加新的策略都不需要更改用戶的調(diào)用接口,三是我們可以在運行時動態(tài)地改變業(yè)務(wù)規(guī)則-策略糠赦。要實現(xiàn)上述需求会傲,我們只需要在可組裝校驗器架構(gòu)的基礎(chǔ)上,對校驗規(guī)則 rule 類引入 Strategy 模式拙泽。首先我們設(shè)計策略類淌山,對于銀行并購這個例子,對帳號的校驗可以分成以下幾步:校驗銀行代號奔滑,地區(qū)代號艾岂,網(wǎng)點代號,用戶代號朋其,因此在這里我們先根據(jù)各個銀行的業(yè)務(wù)策略對帳號進行分割王浴,比如 19 位的帳號分成 4 位銀行代號,4 位地區(qū)代號梅猿,4 位網(wǎng)點代號氓辣,和 7 位用戶代號,然后再執(zhí)行上述幾步校驗袱蚓。業(yè)務(wù)策略類的接口和實現(xiàn)代碼如下:
清單 7:IAccountStrategy.java
public interface IAccountStrategy { /** * 獲得字符串分割規(guī)則钞啸,比如 19 位的帳號分成 {4,4,4,7}。 */ public int[] getSeperatedNumbers(); /** * 校驗銀行代號喇潘。 */ public boolean validateBankCode(String bankCode); /** * 校驗地區(qū)代號体斩。 */ public boolean validateDistrictCode(String districtCode); /* 校驗網(wǎng)點代號。*/ public boolean validateSiteCode(String siteCode); /* 校驗用戶代號颖低。*/ public boolean validateUserCode(String userCode); }
清單 8:BankAAccountStrategy.java
public class BankAAccountStrategy implements IAccountStrategy { private static int[] seperatedNumbers = { 4, 4, 4, 4 }; public int[] getSeperatedNumbers() { return seperatedNumbers; } public boolean validateBankCode(String bankCode) { // query bank id from local database or meta-data file. String bankID = "9880"; if (bankID.equals(bankCode)) return true; return false; } public boolean validateDistrictCode(String districtCode) { if ((districtCode != null) && (districtCode.length() == seperatedNumbers[1])) if (districtCode.startsWith("8")) return true; return false; } public boolean validateSiteCode(String siteCode) { if ((siteCode != null) && (siteCode.length() == seperatedNumbers[2])) if (siteCode.startsWith("1")) return true; return false; } public boolean validateUserCode(String userCode) { if ((userCode != null) && (userCode.length() == seperatedNumbers[3])) return true; return false; } }
接下來我們設(shè)計校驗規(guī)則類絮吵,該類主要有選擇策略和使用策略來校驗兩種職責(zé),代碼如下:
清單 9:
public class AccountVRule implements IVRule { private static Map strategies = new HashMap(); static { // 注冊策略類 strategies.put(new Integer(16), new BankAAccountStrategy()); strategies.put(new Integer(19), new BankBAccountStrategy()); } public void validate(Object value) throws DataValidationException { if (value == null) throw new DataValidationException("Account can't be empty."); if (!(value instanceof String)) throw new DataValidationException("Can't cast Object to String."); String val = (String) value; if (strategies.containsKey(new Integer(val.length()))) { IAccountStrategy strat = (IAccountStrategy) strategies .get(new Integer(val.length())); int[] sepNum = strat.getSeperatedNumbers(); //validate bank code String bankCode = val.substring(0, sepNum[0]); if (!strat.validateBankCode(bankCode)) throw new DataValidationException("Bank code " + bankCode + " doesn't match account rule"); //validate district code. String disCode = val.substring(sepNum[0], sepNum[0] + sepNum[1]); if (!strat.validateDistrictCode(disCode)) throw new DataValidationException("District code " + disCode + " doesn't match account rule"); //validate site code String siteCode = val.substring(sepNum[0] + sepNum[1], sepNum[0] + sepNum[1] + sepNum[2]); if (!strat.validateSiteCode(siteCode)) throw new DataValidationException("Site code " + siteCode + " doesn't match account rule"); //validate user code String userCode = val.substring(val.length() - sepNum[3]); if (!strat.validateUserCode(userCode)) throw new DataValidationException("User code " + userCode + " doesn't match account rule"); } else throw new DataValidationException("The length of input account NO " + val.length() + " doesn't match account rule."); } }
在這里我們再一次用到了 Registry 模式忱屑,可以看到這個校驗規(guī)則類具有管理和緩存策略類的職責(zé)蹬敲。當然我們還可以在該類中增加一個方法 regiesterStrategy() 用來在運行時動態(tài)地增加業(yè)務(wù)規(guī)則策略,但目前我們的應(yīng)用沒有這么復(fù)雜的需求莺戒,就算將來有也很容易重構(gòu)目前的架構(gòu)伴嗡,所以這個設(shè)計活動應(yīng)該點到為止。這也是設(shè)計中的一個權(quán)衡點从铲,***設(shè)計是沒有絕對完美的瘪校,人們在追逐絕對完美設(shè)計的過程中經(jīng)常把對未來的種種揣測當作真正的需求,結(jié)果只能是危及整個設(shè)計名段,導(dǎo)致代碼臃腫渣淤,難以維護赏寇,僵化,靈活性差价认。適度的設(shè)計才是完美的嗅定。***
動態(tài)策略校驗器的架構(gòu)圖如下:
圖 2:可組裝校驗器的架構(gòu)圖
在本例中,我們并不是從整合遺留資產(chǎn)的角度出發(fā)的用踩,在實際的例子中渠退,銀行 A 和銀行 B 可能都已存在各自的校驗類,這些類的接口不會是一致的脐彩,而且返回類型可能是 boolean 也可能是異常碎乃,甚至銀行 A 是 Java 應(yīng)用而銀行 B 是 C 應(yīng)用,這樣的話我們必須將這些遺留應(yīng)用中已有的校驗類適配成現(xiàn)在的接口惠奸,這里可以對具體的 Strategy 實現(xiàn)類應(yīng)用 Adapter 模式梅誓。
模式與價值觀
模式的三要素-問題,語境佛南,解決方案我們在前面已經(jīng)論述過了梗掰,每個模式都有它自己獨特的價值觀,那么這組架構(gòu)模式給我們帶來了什么嗅回?
首先及穗,它將校驗邏輯從應(yīng)用邏輯中解耦出來,使得應(yīng)用和校驗器可以獨立變化绵载。第二埂陆,它促進了代碼重用,校驗器可以用到任何應(yīng)用邏輯中去娃豹,不必局限于一處焚虱。如果需要的話,我們甚至可以將整個校驗器當作一個橫切關(guān)注點 (Crosscut concern)懂版,應(yīng)用 AOP(Aspect of Programming)技術(shù)著摔,將待校驗數(shù)據(jù)當作 Pointcut,這樣在應(yīng)用的代碼中會看不到任何校驗代碼的痕跡定续,這就徹底分離出了數(shù)據(jù)校驗邏輯代碼。第三禾锤,從應(yīng)用場合來看私股,隔離校驗器主要用在那些數(shù)據(jù)類型簡單而且校驗規(guī)則簡單的數(shù)據(jù)校驗中,可組裝校驗器用在那些數(shù)據(jù)類型復(fù)雜或校驗規(guī)則復(fù)雜恩掷、多變的數(shù)據(jù)校驗中倡鲸,而動態(tài)策略校驗器則用在同一個數(shù)據(jù)的校驗就有多種校驗規(guī)則策略的數(shù)據(jù)校驗中』颇铮可以看到峭状,這幾種模式是根據(jù)待校驗數(shù)據(jù)的粒度大小和業(yè)務(wù)規(guī)則的復(fù)雜程度來劃分的克滴。
接下來,我們研究一下模式的變體优床∪芭猓可組裝校驗器是這組模式的核心,它有很多變體胆敞,其實動態(tài)策略校驗器就是它的一種變體着帽,其他變體還有復(fù)合規(guī)則檢驗器,鏈式檢驗器移层,動態(tài)注冊檢驗器等仍翰。比如在 XML 校驗器 SAX 的實現(xiàn)中,用戶可以動態(tài)地插入校驗 handle观话,或者我們需要對一個數(shù)據(jù)依次執(zhí)行多套校驗規(guī)則予借,而不像之前一次只有一個校驗規(guī)則會被執(zhí)行。對于這種需求频蛔,我們有三種方案可選灵迫,第一種是復(fù)合規(guī)則檢驗器,利用 Composite 模式來實現(xiàn)校驗規(guī)則 IVRule 接口帽驯,復(fù)合的校驗規(guī)則類中包含一組簡單的校驗規(guī)則類 VRule龟再,當調(diào)用復(fù)合類的 validate() 方法時,復(fù)合類會依次調(diào)用所有的簡單校驗規(guī)則類尼变。第二種是鏈式校驗器利凑,利用 Chain of responsibility 模式來實現(xiàn)校驗規(guī)則 IVRule 接口,前一個校驗規(guī)則類執(zhí)行校驗后傳遞到下一個校驗規(guī)則類嫌术,一層層按固定順序傳遞下去哀澈,每一個校驗規(guī)則類關(guān)注的校驗點不一樣,這適合于順序固定的情況度气。第三種是動態(tài)注冊校驗器割按,利用 Registry 模式,將校驗規(guī)則類 VRule 動態(tài)地注冊到校驗器類 Validator 中去磷籍,比如注冊到一個 List 或 Map 中适荣,在校驗器類的 validate() 方法中可以按某種算法來實現(xiàn)調(diào)用校驗規(guī)則類的順序或更復(fù)雜的調(diào)用邏輯。很顯然動態(tài)注冊校驗器很靈活院领,可擴展性也很強弛矛,但同時對校驗器使用者來說,它復(fù)雜了比然,校驗規(guī)則也不透明了丈氓。所以并非靈活性和可擴展性越強就越好,一切應(yīng)該取決于需求,如果你一次只有一個校驗規(guī)則執(zhí)行就沒必要再引入復(fù)雜性了万俗。
另外還可以對校驗方法的返回類型作一下擴展湾笛,boolean 值和異常作為返回一般來說足夠了,但如果我們的返回結(jié)果比較復(fù)雜闰歪,比如前面講到的一個數(shù)據(jù)需要執(zhí)行多個校驗規(guī)則的情況嚎研,返回的結(jié)果可能需要將多個校驗規(guī)則的返回結(jié)果匯總,也可能需要更細級別的結(jié)果课竣。這就需要一種工業(yè)級的返回機制嘉赎,在 Eclipse 中就有這么一個返回類型,稱為狀況對象 (Status object)于樟,它可以對返回類型進行分級:OK吗冤、 Warning竟贯、 Error赊窥,甚至還封裝了更低級的異常佳遂。而且針對返回狀態(tài)比較復(fù)雜的情況,還應(yīng)用了 Composite 模式實現(xiàn)了一個 MultiStatus 類來組合多個錯誤狀態(tài)路捧。這在校驗結(jié)果不僅僅是 true 或 false 兩種狀態(tài)的場合下非常有用关霸,而且可以記錄跟蹤校驗規(guī)則信息。不過這是一種重量級的返回機制杰扫。下面展示了狀況對象的接口代碼:
清單 10:Status.java
<strong>public interface IStatus { public static final int OK = 0; public static final int INFO = 0x01; public static final int WARNING = 0x02; public static final int ERROR = 0x04; // 返回更低級的異常队寇。 public Throwable getException(); // 返回消息,如錯誤信息章姓。 public String getMessage(); // 返回狀況類型佳遣,OK,INFO, WARNING,ERROR public int getSeverity(); public boolean isOK(); // 是否復(fù)合狀況。 public boolean isMultiStatus(); // 如果是復(fù)合狀況凡伊,返回子狀況類零渐。 public IStatus[] getChildren();</strong>
結(jié)束語
這組架構(gòu)模式不局限于某種語言、應(yīng)用系忙,可以應(yīng)用到任何場合诵盼。如果我們將數(shù)據(jù)校驗當作一項業(yè)務(wù)操作的話,可以將它擴展到其他領(lǐng)域银还。模式可以促進好的架構(gòu)风宁,也可能導(dǎo)致萬劫不復(fù),關(guān)鍵取決于設(shè)計者的把握蛹疯。所以戒财,***我們在選擇模式的時候,一定要考量模式的何種特性對你最有價值苍苞,模式所提供的價值觀與您的需求期望是否吻合。***
相關(guān)模式
- Factory Method:工廠模式用于產(chǎn)生類的實例。
- Singleton:單例模式用于保證一個類只產(chǎn)生一個實例羹呵。
- Flyweight:享元模式用于緩存和維護一組實例骂际。
- Composite:復(fù)合模式用于組合一組類,而這些類和復(fù)合類具有同一接口冈欢。
- Facade:門面模式用于為客戶提供統(tǒng)一的外觀歉铝,隱藏復(fù)雜的實現(xiàn)細節(jié)。
- Proxy:代理模式用于代理一個對象凑耻,控制其他對象對該對象的訪問太示。
- Strategy:策略模式用于提供一組可互換的算法 / 策略,它們遵循同一接口香浩。
- Adapter:適配器模式用于將類的接口適配為另一種接口类缤。
- Chain of responsibility:職責(zé)鏈模式用于為鏈式傳遞處理請求。
以上模式來自《 GOF 設(shè)計模式》邻吭。
- Registry:注冊器模式用于注冊和管理一個或多個對象餐弱。(來自《企業(yè)架構(gòu)模式》)