10JAVA设计模式第十课数据校验器架构模式组.docx
- 文档编号:8279989
- 上传时间:2023-01-30
- 格式:DOCX
- 页数:17
- 大小:70.47KB
10JAVA设计模式第十课数据校验器架构模式组.docx
《10JAVA设计模式第十课数据校验器架构模式组.docx》由会员分享,可在线阅读,更多相关《10JAVA设计模式第十课数据校验器架构模式组.docx(17页珍藏版)》请在冰豆网上搜索。
10JAVA设计模式第十课数据校验器架构模式组
本文阐述软件架构与设计模式,它为架构师和开发人员提供了一组关于数据校验的架构模式(隔离校验器,可组装校验器,动态策略校验器,动态注册校验器等),数据校验是任何类型的开发中都不可或缺的环节,如果没有统一的架构,可能校验代码会遍布整个应用,如何将数据校验与应用逻辑解耦,如何适应各种粒度的数据和各种复杂程度业务规则,正是本文要探讨的。
在我们各种类型的应用开发中有一个必不可少的环节-数据校验,无论是大型企业应用,还是一个简单的程序。
如果没有统一的架构,可能校验代码会遍布整个应用,一旦校验规则改变就需要修改多处代码,这是一种不好的设计,因为数据校验与应用逻辑耦合得太紧。
数据校验不外乎语法校验和语义校验两类,本文描述了一组架构上的模式来对这两类需求提供解决方案。
该模式组按照待校验数据的粒度大小和业务规则的复杂程度分成多种类型:
隔离校验器,可组装校验器,动态策略校验器,动态注册校验器等。
大家可以针对自己的应用选择合适的架构。
应用这组模式还可以获得一个好处,如果需要的话,我们可以把数据校验器当作一个横切关注点(Crosscutconcern),应用AOP(AspectofProgramming)技术,这样可以彻底分离出数据校验逻辑代码。
问题引出
让我们从几个应用场景(userscenario)开始吧,第一个场景是网站上的注册用户,注册时需要填写很多数据,这些数据都需要校验后才能写进数据库,比如用户名,校验规则可能是:
用户名由a~z的英文字母(不区分大小写)、0~9的数字、点、减号或下划线组成,长度为3~18个字符。
这种关于数据的结构正确性方面的校验我们称之为语法校验。
而身份证号码这种数据,它需要根据出生日期校验身份证号码的正确性,不仅仅是填够了16或19位数字就行。
这种关于数据的内容正确性方面的校验称之为语义校验。
一般情况下语法和语义方面的校验是在一块处理的,比如身份证号码,必然也需要校验数据是否全是数字和必须是16或19位,这是语法校验,同时它需要和出生日期相符,这又是语义校验。
从架构的角度而言,这种情况下区分语法和语义的意义不太大,因为没必要把它分成两个步骤用两个方法来处理。
但是有些应用,比如数据是一段XML的文本串,首先需要校验XML字符串的结构-语法是否符合相应的schema,然后再校验其中某个元素的内容-语义的正确性,这可能就需要分开来处理比较合适,因为语法校验是业务无关的,而后者的语义校验是业务相关的,业务相关就意味着一旦业务规则改变,校验规则就可能改变,所以这种情况最好将语义校验分离出来。
第二个应用场景是一个MDA(ModelDrivenArchitecture)工具开发的例子,我们都用过大名鼎鼎的RationalRose或MicrosoftVisio。
这些工具都提供从UML模型生成代码的功能,这就是MDA,它们将UML模型映射成模型的元数据(meta-data)(称之为元模型meta-model),然后从元模型可以转换成各种支持语言的代码,如Java,C++。
当我们在视图上画一个UML元素(如类Class),然后为其定义了某种Stereotype来标识他的业务语义,比如数据库的表Table,或者自定义的一个用于表示WebService的Service元素。
接下来我们要将该元素生成相应的代码,这时当你选定元素时,运行时系统并不知道该元素是普通的Class,还是Table,因为在运行时环境中都是UML的Class实例对象,这就需要我们提供校验逻辑来处理了,处理Class的校验逻辑和Table的校验逻辑自然不应该放在一起,更何况如果是自定义的扩展元素,根本不可能把校验代码写到已有系统里去。
这就需要我们提供一个统一的校验器接口,不同的校验逻辑封装在单独的类中。
进一步,我们需要对这些独立的校验器进行集中组装和管理,因为我们不必每次都去实例化这些工具类,实例化后将它们缓存起来就可以了。
第三个应用场景是一个银行并购的案例,假如银行A并购了银行B,两家银行都有各自已有的电子银行应用,并购后要将两家应用整合成一个统一的应用,其中有一个余额查询业务,在进行具体的查询操作事务之前,需要校验用户输入的帐号account,两家银行已有的帐号各有不同的创建规则,比如银行A是16位数字作为帐户,首4位是银行代号,第二个4位是地区代号,第三个4位是网点代号,尾4位是用户编号。
而银行B则是19位数字作为帐户,各个区段的含义也和银行A不一样,这就要求用户填写一个帐户的时候,后台必须对应两套数据校验规则,而且应用需要根据一定的规则来选择银行A的校验策略或银行B的校验策略。
而且更复杂的情况是,银行的帐户还可能是升位后的(比如从12位升到16位),这样必须同时兼顾新旧帐户,也就是说有多套校验规则来处理,我们的数据校验器需要支持业务规则的动态切换。
这里面可能有一个有争议的地方,校验帐号时需要有具体的业务规则支持,那么这算不算是业务逻辑呢,当然这个校验逻辑并不那么纯粹,软件设计并不是个黑白的二元世界,各种层次的对象混合在一起很正常,我们也不大可能什么东西都能做个分水岭把它们隔离开来。
另外,这里的校验逻辑还是和银行应用别的业务逻辑不大一样,比如转帐交易,这个动作的触发是一定要在一个高安全可靠的事务中执行的,而我们的校验帐号过程可能不需要运行在事务中,或者只运行在低安全可靠级别的事务中即可。
这是有本质区别的,所以把这种掺有业务规则的校验划分到校验逻辑里而不是业务逻辑中是有理由的。
隔离校验器
针对上述第一类应用场景,我们只需要把数据校验逻辑从其他业务逻辑中剥离出来,将校验逻辑委任到一个单独的校验类中去。
把校验职责分离出来后,第一个好处是:
一旦我们需要更改校验逻辑,只要修改校验类代码即可,而不用修改其他任何业务逻辑类。
第二个好处是:
可以集中管理控制所有的数据校验逻辑,提高了代码的内聚性,而且让代码简洁、清晰。
当然这里说的所有数据集中控制不一定就是全放在一个类中,如果有必要,也可以将数据按照不同的类型分组,每一个组封装在一个校验类中。
第三个好处是可重用性高,校验逻辑封装成了一个工具类,自然可重用性大大提高。
在设计这个隔离校验器类时还有一些需要权衡的地方,在设计某一个数据的校验方法时,比如用户名的校验,如果数据出错了,简单的情况下,我们只需返回一个boolean值,告诉用户数据有误。
而如果是身份证号码这类数据出错了,可能就需要提供更细粒度的错误类型给用户,告诉用户是与出生日期不符还是位数不够。
对这种错误种类较多的情况,我们可以返回错误代号(如int值)来区别各种错误,这是非面向对象语言的一种做法,在面向对象中我们可以用一个异常Exception来返回错误类型,这比返回错误代号更好,因为错误代号需要解析成具体的错误信息,这个解析工作还得由校验器类的API使用者来调,这个使用者是其它的业务逻辑类,这就是说业务逻辑类还是耦合了数据校验错误处理逻辑,显然不如用异常处理来的彻底。
代码如下:
清单1:
UserInfoValidator.java
publicabstractclassUserInfoValidator{
publicstaticbooleanvalidateUserID(Stringuid){
booleanisValid=false;
//校验规则
returnisValid;
}
publicstaticbooleanvalidteEmail(Stringemail){
booleanisValid=false;
//校验规则
returnisValid;
}
publicstaticvoidvalidateSSN(SSNDataObjectssn)
throwsDataValidationException{
if(ssn==null)
thrownewDataValidationException("Nodatafound.");
StringidCard=ssn.getIdCard();
if((idCard==null)||(idCard.equals("")))
thrownewDataValidationException("Noid.carddatafound.");
if(!
((idCard.length()==15)||(idCard.length()==18)))
thrownewDataValidationException(
"ID.cardlengthmustbe15or18.");
DatebirthDay=ssn.getBirthDay();
if(birthDay==null)
thrownewDataValidationException("Nobirthdaydatafound.");
intsex=ssn.getSex();
if(sex==0)
thrownewDataValidationException("Nosexdatafound.");
//生日校验规则
//if(...)
//thrownewDataValidationException("ID.carddidn'tmatchbirthday.");
intidSex=Integer.parseInt(idCard.substring(idCard.length()-1));
if(idSex%sex!
=0)
thrownewDataValidationException("ID.carddidn'tmatchsex.");
}
}
从上面代码可以看出,我们用了静态static方法,因为我们这是个工具类,没有什么状态需要存储,所以不需要实例化类。
而且调用校验方法会很频繁,用静态方法可以提升性能。
另外还有一点值得一提,我们封装了一个身份证数据类,里面包含了三个属性:
身份证号,出生日期,性别。
验证身份证号需要出生日期和性别奇偶码这一点是没有异议的,但为什么不用三个单独的参数呢,这里的封装为以后提供了更大的灵活性,比如将来我们打算将身份证验证逻辑做得更精细,需要判断出生地区的代码是否和身份证的头几位一致,这可能就需要四个参数了,或者我们的出生日期需要换一个类(Date->Calendar)来表示,显然我们只需要修改身份证数据封装类,而不用修改调用接口。
可组装校验器
针对第二类场景,我们对每一个数据类提供一个独立的校验规则类,因为这个数据类本身已经包含了语法和语义逻辑。
语法逻辑是与数据结构相关的,在我们的示例中,是判断对象是否是UML的Class实例。
而语义逻辑是与业务规则相关的,每一个数据类关联的业务规则不尽相同,可能来自不同领域,或不同的业务组件或系统;另外由于业务规则的易变性较强,可扩展性和可配置性要求也较高,所以有必要为每一个数据类设置专属的校验类。
这里我们将每一个校验类称作一条校验规则(Rule)。
校验规则类的接口和实现代码如下:
清单2:
IVRule.java
publicinterfaceIVRule{
/**
*validatevaluebydomainrule.
*/
publicbooleanisValid(Objectvalue);
/**
*validatevaluebydomainrule.
*/
publicvoidvalidate(Objectvalue)throwsDataValidationException;
}
清单3:
ServiceVRule.java
publicclassServiceVRuleimplementsIVRule{
privatestaticStringSTEREOTYPE_NAME="Service";
………..
publicvoidvalidate(Objectvalue)throwsDataValidationException{
if(valueinstanceofClass){
Stereotypest=((Class)value).getStereotype();
Stringname=st.getName();
if(STEREOTYPE_NAME.equals(name)){
Stringwsdl=(String)st.getProperty("WSDL");
if((wsdl==null)||(wsdl.equals("")))
thrownewDataValidationException("NoWSDLfileisdefined.");
}else
thrownewDataValidationException("ItisnotaServiceObject.");
}else
thrownewDataValidationException("ItisnotaUMLClassmodel.");
}}
在这里,我们还是提供了两种校验结果返回机制,boolean值和抛出异常,在具体应用时大家可以选择一个即可。
这里的校验规则实现是关于WebService的,它的语法校验是检查数据是否是UML的Class对象,而语义校验是检查Stereotype是否是Service,并检查Stereotype中是否含有WSDL(Webservicedescriptionlanguage)属性。
接下来,怎么应用这些校验规则rule呢?
一个系统中会有很多校验规则类,那么就需要一个管理机制来管理这些校验类,最简单的我们只需定义一个方法validate(Objectvalue,IVRulerule),提供一层简单的封装,它可以起到一个代理的作用,比如,应用Proxy模式,我们可以对校验规则本身做一些安全性认证方面的工作,然后才决定是否可以用该校验规则。
而更完善的管理机制是提供一个更灵活的环境,让用户可以动态组装,改变,查找校验规则类。
基本思路是:
我们将校验规则类当作一种可重用资源,提供一个组装工厂环境,用户可以将校验规则rule注册到工厂里,工厂会实例化和缓存这些类,并提供查找服务;然后提供一个校验器来从工厂里查找出相应的校验规则rule类,为用户提供校验服务。
这里面用到了Factory,Flyweight,Registry模式。
(本文引用的模式请参考相关模式)
第一步,我们设计一个组装工厂类,它提供实例化、缓存、和查找服务,很显然,实例化类是一个Factory模式的基本职责,将实例缓存cache是一个Flyweight模式的基本职责,查找服务可以很简单,在我们的例子中就是从一个Map中取出校验规则rule实例,也可以复杂化,比如我们的校验规则类是一个远程资源,或者是实例化这个类需要用到其它的远程资源,如数据库,那这个查找功能实现起来可能就复杂些,可以通过JNDI来查找,也可以将远程资源暴露成服务(WebService)并注册到UDDI(UniversalDiscoverDescriptionandIntegration),然后从UDDI中查找服务。
从这里我们可以看到这个组装工厂类具备管理校验规则rule类的整个生命周期的职责,这为校验器应用提供了很大的灵活性和可扩展性,假如我们今后需要实现一个实例池或资源池Pool来管理这些校验规则实例,那么只需要将组装工厂类的功能稍作修改和扩展就可,而不必触及校验器应用的其他类,因为我们已经将实例的管理逻辑从整个校验器应用中剥离出来,管理职责的变化只局限在组装工厂类内,对别的类是封闭的,这正体现面向对象的基本设计原则之一Open-Close原则(对修改封闭,对扩展开放)。
组装工厂类的代码如下:
清单4:
VRuleAssemblerFactory.java
publicclassVRuleAssemblerFactory{
privatestaticMaprules=newHashMap();
/**
*查找校验规则Rule。
*/
publicstaticIVRulelookupVRule(StringruleHandler){
if(ruleHandler==null)
returnnull;
IVRulerule=null;
if(rules.containsKey(ruleHandler))
rule=(IVRule)rules.get(ruleHandler);
returnrule;
}
/**
*注册/加入一个校验规则Rule.
*/
publicstaticvoidaddVRule(StringruleHandler,ClassruleClass){
if((ruleHandler!
=null)&&(ruleClass!
=null)){
try{
rules.put(ruleHandler,ruleClass.newInstance());
}catch(InstantiationExceptione){
e.printStackTrace();
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}
/**
*批量载入校验规则Rule,一般在应用系统初始化时调用。
*/
publicstaticvoidassembleVRules(){
addVRule("Table",TableVRule.class);
addVRule("Service",ServiceVRule.class);
}
}
从上面的代码可以看到,我们用一个Map作为缓存库,注册一个校验规则时以字符串作为关键字,当然也可以用别的自定义类型,在查询时利用了Map的查找功能很简单高效地实现了查找功能,另外我们还定义了一个批量载入校验规则的方法,这个功能是为了用户使用方便,在应用系统初始化时执行一次,而且可以透明地载入校验规则,在本例中只是硬编码了这些校验规则类,需要的话我们可以从别的元数据文件中(XML或CSV文件)导入。
还需注意一点的是,这个组装工厂是一个全局类,在这里是以静态static方式实现的,当然也可以以Singleton方式来实现,还可以以线程安全ThreadLocal的方式来实现。
第二步,我们设计校验器类,校验器类应该作为整个校验器应用的Façade,用户需要校验数据时只需和它打交道,这就很好的把校验规则类隐藏了起来,因此校验器的职责有查找相应的校验规则类和执行校验。
校验器类的接口和实现代码如下:
清单5:
IValidator.java
publicinterfaceIValidator{
/**
*通过关键字ruleHandler查询校验规则来校验数据。
*/
publicbooleanisValid(Objectvalue,StringruleHandler);
/**
*通过关键字ruleHandler查询校验规则来校验数据。
返回异常。
*/
publicvoidvalidate(Objectvalue,StringruleHandler)
throwsDataValidationException;
/**
*直接指定校验规则类来校验数据。
*/
publicbooleanisValid(Objectvalue,IVRulerule);
/**
*直接指定校验规则类来校验数据,返回异常。
*/
publicvoidvalidate(Objectvalue,IVRulerule)throwsException;
}
清单6:
AssemblyValidator.java
publicclassAssemblyValidatorimplementsIValidator{
privatestaticAssemblyValidatorinstance=null;
privateAssemblyValidator(){}
publicstaticsynchronizedAssemblyValidatorgetInstance(){
if(instance==null)
instance=newAssemblyValidator();
returninstance;
}
publicbooleanisValid(Objectvalue,StringruleHandler){
booleanvalid=false;
IVRulerule=VRuleAssemblerFactory.lookupVRule(ruleHandler);
if(rule!
=null)
valid=rule.isValid(value);
returnvalid;
}
publicvoidvalidate(Objectvalue,StringruleHandler)
throwsDataValidationException{
IVRulerule=VRuleAssemblerFactory.lookupVRule(ruleHandler);
if(rule!
=null)
rule.validate(value);
}
publicbooleanisValid(Objectvalue,IVRulerule){
booleanvalid=false;
if(rule!
=null)
valid=rule.isValid(value);
returnvalid;
}
publicvoidvalidate(Objectvalue,IVRulerule)throwsException{
if(rule!
=null)
rule.validate(value);
}
}
从上面代码可以看到,我们使用了Singleton模式,因为没必要每次都实例化校验器类。
另外我们在这里提供了两套返回机制,还有两套取校验规则的方式,这都可以根据实际应用作出取舍。
可组装校验器的架构图如下:
图1:
可组装校验器的架构图
动态策略校验器
针对第三类场景,我们必须支持一种数据对应有多套业务校验规则,我们可以把每种业务规则都建模成一个校验规则类,但这样做灵活性和可扩展性就很差了。
如果我们对一种数据只用一个校验规则类,而将多套业务规则建模成多种策略,在校验规则类中应用这些策略,这样做好处在于:
一可以对用户隐藏业务规
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 10 JAVA 设计 模式 第十 数据 校验 架构
![提示](https://static.bdocx.com/images/bang_tan.gif)