技术开发 频道

数据校验器架构模式组

  【IT168 技术文章】本文阐述软件架构与设计模式,它为架构师和开发人员提供了一组关于数据校验的架构模式(隔离校验器,可组装校验器,动态策略校验器,动态注册校验器等),数据校验是任何类型的开发中都不可或缺的环节,如果没有统一的架构,可能校验代码会遍布整个应用,如何将数据校验与应用逻辑解耦,如何适应各种粒度的数据和各种复杂程度业务规则,正是本文要探讨的。

  在我们各种类型的应用开发中有一个必不可少的环节-数据校验,无论是大型企业应用,还是一个简单的程序。如果没有统一的架构,可能校验代码会遍布整个应用,一旦校验规则改变就需要修改多处代码,这是一种不好的设计,因为数据校验与应用逻辑耦合得太紧。数据校验不外乎语法校验和语义校验两类,本文描述了一组架构上的模式来对这两类需求提供解决方案。该模式组按照待校验数据的粒度大小和业务规则的复杂程度分成多种类型:隔离校验器,可组装校验器,动态策略校验器,动态注册校验器等。大家可以针对自己的应用选择合适的架构。应用这组模式还可以获得一个好处,如果需要的话,我们可以把数据校验器当作一个横切关注点(Crosscut concern),应用 AOP(Aspect of Programming)技术,这样可以彻底分离出数据校验逻辑代码。

  问题引出

  让我们从几个应用场景(user scenario)开始吧,第一个场景是网站上的注册用户,注册时需要填写很多数据,这些数据都需要校验后才能写进数据库,比如用户名,校验规则可能是:用户名由 a~z 的英文字母(不区分大小写)、0~9 的数字、点、减号或下划线组成,长度为 3~18 个字符。这种关于数据的结构正确性方面的校验我们称之为语法校验。而身份证号码这种数据,它需要根据出生日期校验身份证号码的正确性,不仅仅是填够了 16 或 19 位数字就行。这种关于数据的内容正确性方面的校验称之为语义校验。一般情况下语法和语义方面的校验是在一块处理的,比如身份证号码,必然也需要校验数据是否全是数字和必须是 16 或 19 位,这是语法校验,同时它需要和出生日期相符,这又是语义校验。从架构的角度而言,这种情况下区分语法和语义的意义不太大,因为没必要把它分成两个步骤用两个方法来处理。但是有些应用,比如数据是一段 XML 的文本串,首先需要校验 XML 字符串的结构-语法是否符合相应的 schema,然后再校验其中某个元素的内容-语义的正确性,这可能就需要分开来处理比较合适,因为语法校验是业务无关的,而后者的语义校验是业务相关的,业务相关就意味着一旦业务规则改变,校验规则就可能改变,所以这种情况最好将语义校验分离出来。

  第二个应用场景是一个 MDA(Model Driven Architecture)工具开发的例子,我们都用过大名鼎鼎的 Rational Rose 或 Microsoft Visio。这些工具都提供从 UML 模型生成代码的功能,这就是 MDA,它们将 UML 模型映射成模型的元数据(meta-data)(称之为元模型 meta-model),然后从元模型可以转换成各种支持语言的代码,如 Java, C++。当我们在视图上画一个 UML元素(如类 Class),然后为其定义了某种 Stereotype 来标识他的业务语义,比如数据库的表 Table,或者自定义的一个用于表示 Web Service 的 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

1  public abstract class UserInfoValidator {
2
3   public static boolean validateUserID(String uid) {
4
5   boolean isValid = false;
6
7   //校验规则
8
9   return isValid;
10
11   }
12
13   public static boolean validteEmail(String email) {
14
15   boolean isValid = false;
16
17   //校验规则
18
19   return isValid;
20
21   }
22
23   public static void validateSSN(SSNDataObject ssn)
24
25   throws DataValidationException {
26
27   if (ssn == null)
28
29   throw new DataValidationException("No data found.");
30
31   String idCard = ssn.getIdCard();
32
33   if ((idCard == null) || (idCard.equals("")))
34
35   throw new DataValidationException("No id.card data found.");
36
37   if (!((idCard.length() == 15) || (idCard.length() == 18)))
38
39   throw new DataValidationException(
40
41   "ID.card length must be 15 or 18.");
42
43   Date birthDay = ssn.getBirthDay();
44
45   if (birthDay == null)
46
47   throw new DataValidationException("No birthday data found.");
48
49   int sex = ssn.getSex();
50
51   if (sex == 0)
52
53   throw new DataValidationException("No sex data found.");
54
55   //生日校验规则
56
57   // if (...)
58
59   // throw new DataValidationException("ID.card didn't match birthday.");
60
61   |-------10--------20--------30--------40--------50--------60--------70--------80--------9|
62
63   |-------- XML error: The previous line is longer than the max of 90 characters ---------|
64
65   int idSex = Integer.parseInt(idCard.substring(idCard.length() - 1));
66
67   if (idSex % sex != 0)
68
69   throw new DataValidationException("ID.card didn't match sex.");
70
71   }
72
73   }
74
75

  从上面代码可以看出,我们用了静态 static 方法,因为我们这是个工具类,没有什么状态需要存储,所以不需要实例化类。而且调用校验方法会很频繁,用静态方法可以提升性能。

  另外还有一点值得一提,我们封装了一个身份证数据类,里面包含了三个属性:身份证号,出生日期,性别。验证身份证号需要出生日期和性别奇偶码这一点是没有异议的,但为什么不用三个单独的参数呢,这里的封装为以后提供了更大的灵活性,比如将来我们打算将身份证验证逻辑做得更精细,需要判断出生地区的代码是否和身份证的头几位一致,这可能就需要四个参数了,或者我们的出生日期需要换一个类(Date->Calendar)来表示,显然我们只需要修改身份证数据封装类,而不用修改调用接口。

0
相关文章