數(shù)據(jù)校驗器架構(gòu)模式組

    本文闡述軟件架構(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)圖

image

動態(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)圖

image
    在本例中,我們并不是從整合遺留資產(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)模式》)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市囱晴,隨后出現(xiàn)的幾起案子膏蚓,更是在濱河造成了極大的恐慌,老刑警劉巖畸写,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驮瞧,死亡現(xiàn)場離奇詭異,居然都是意外死亡枯芬,警方通過查閱死者的電腦和手機论笔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來破停,“玉大人翅楼,你說我怎么就攤上這事≌媛” “怎么了毅臊?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長黑界。 經(jīng)常有香客問我管嬉,道長,這世上最難降的妖魔是什么朗鸠? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任蚯撩,我火速辦了婚禮,結(jié)果婚禮上烛占,老公的妹妹穿的比我還像新娘胎挎。我一直安慰自己沟启,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布犹菇。 她就那樣靜靜地躺著德迹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪揭芍。 梳的紋絲不亂的頭發(fā)上胳搞,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音称杨,去河邊找鬼肌毅。 笑死,一個胖子當著我的面吹牛姑原,可吹牛的內(nèi)容都是我干的悬而。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼页衙,長吁一口氣:“原來是場噩夢啊……” “哼摊滔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起店乐,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤艰躺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后眨八,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腺兴,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年廉侧,在試婚紗的時候發(fā)現(xiàn)自己被綠了页响。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡段誊,死狀恐怖闰蚕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情连舍,我是刑警寧澤没陡,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站索赏,受9級特大地震影響盼玄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜潜腻,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一埃儿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧融涣,春花似錦童番、人聲如沸精钮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杂拨。三九已至,卻和暖如春悯衬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背檀夹。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工筋粗, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炸渡。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓娜亿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蚌堵。 傳聞我的和親對象是個殘疾皇子买决,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容

  • JAVA面試題 1、作用域public,private,protected,以及不寫時的區(qū)別答:區(qū)別如下:作用域 ...
    JA尐白閱讀 1,145評論 1 0
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,805評論 0 11
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,090評論 1 32
  • 給定根節(jié)點吼畏,求這個完全二叉樹的節(jié)點個數(shù) [leetcode222]https://leetcode.com/pro...
    futurehau閱讀 2,578評論 0 0
  • 初識李榮亮先生就被他獨到的人生感悟所吸引督赤。更難能可貴的是李先生除了是一位出色的事業(yè)管理者之外,還是一位非常有...
    見微知著的維閱讀 1,568評論 0 0