它山之玉可以重构:身份证号码解析、验证工具(第三天)

前两天的进度似乎有些慢,今天加快了一点, 不把每一步说的那么详细了.

==》地区信息的提取

继性别和生日之后,最后一个信息块,只是列出测试如下.

==》有效性

这是一个比较大的问题. 前面,我临时性的把不同地方的验证去掉了. 代码原作者也过来, 畅叙了他关于验证的看法. 他是对的, 这种完全验证的方式,根本上说是 DDD的设计思想。不过,想我所说,我知识临时性的去掉,保证测试的单元性。验证的功能,由验证的测试来驱动。而第二点考虑,我的验证打算放在构造器中,也就是说,如果,有任何错误的输入,连第一道门都进不来。

这里,测试和实现都很简单,看起来很多,只是一些罗列,不同的错误场景而已。

 1 [Subject("身份证,有效性")]
 2     public class when_create_social_id_with_valid_format {
 3         private Because of = () => subject = new SocialID("430103123456780020");
 4 
 5         private It should_create_social_properly =
 6             () => subject.getCardNumber().ShouldEqual("430103123456780020");
 7         private static SocialID subject;
 8     }
 9     [Subject("身份证,有效性")]
10     public class when_create_social_id_with_null_string {
11         private Because of = () =>exception= Catch.Exception(()=>new SocialID(null));
12 
13         private It should_not_allow_to_create =
14             () =>exception.ShouldNotBeNull();
15         private static SocialID subject;
16         private static Exception exception;
17     }
18 
19     [Subject("身份证,有效性")]
20     public class when_create_social_id_with_empty_string {
21         private Because of = () => exception = Catch.Exception(() => new SocialID(string.Empty));
22 
23         private It should_not_allow_to_create =
24             () => exception.ShouldNotBeNull();
25         private static SocialID subject;
26         private static Exception exception;
27     }
28 
29     [Subject("身份证,有效性")]
30     public class when_create_social_id_with_2_length_string {
31         private Because of = () => exception = Catch.Exception(() => new SocialID("12"));
32 
33         private It should_not_allow_to_create =
34             () => exception.ShouldNotBeNull();
35         private static SocialID subject;
36         private static Exception exception;
37     }
38     [Subject("身份证,有效性")]
39     public class when_create_social_id_with_20_length_string {
40         private Because of = () => exception = Catch.Exception(() => new SocialID("12345678901234567890"));
41 
42         private It should_not_allow_to_create =
43             () => exception.ShouldNotBeNull();
44         private static SocialID subject;
45         private static Exception exception;
46     }
47     [Subject("身份证,有效性")]
48     public class when_create_social_id_alphet_length_string {
49         private Because of = () => exception = Catch.Exception(() => new SocialID("A23456789012345678"));
50 
51         private It should_not_allow_to_create =
52             () => exception.ShouldNotBeNull();
53         private static SocialID subject;
54         private static Exception exception;
55     }

实现

1 public SocialID(String cardNumber)
2         {
3             if (string.IsNullOrEmpty(cardNumber))
4                 throw new ApplicationException("Card Number is empty");
5             if (cardNumber.Length != CARD_NUMBER_LENGTH)
6                 throw new ApplicationException("Card Number Length is wrong.");
7             if (!SOCIAL_NUMBER_PATTERN.IsMatch(cardNumber))
8                 throw new ApplicationException("Card Number has wrong charactor(s).");          
9         }

 

==》验证码

验证码是个特殊的有效性检查,较为复杂,我这里,把这部分逻辑代码提炼出来成为一个验证器。

测试极其简单,和实现几乎原封不动。

测试:

1 public class when_verify_soical_number:Specification<Verifier>
2     {
3         Because of = () => { code = subject.verify("43010319791211453"); };
4 
5         private It verify_code_should_match =
6             () => code.ShouldEqual('4');
7         private static char code;
8     }

实现

 1 namespace Skight.eLiteWeb.Domain.Specs.Properties
 2 {
 3     public class Verifier
 4     {
 5         private static char[] VERIFY_CODE =
 6             {
 7                 '1', '0', 'X', '9', '8', '7',
 8                 '6', '5', '4', '3', '2'
 9             };
10 
11         /**
12          * 18位身份证中,各个数字的生成校验码时的权值
13          */
14 
15         private static int[] VERIFY_CODE_WEIGHT =
16             {
17                 7, 9, 10, 5, 8, 4, 2, 1,
18                 6, 3, 7, 9, 10, 5, 8, 4, 2
19             };
20         private static int CARD_NUMBER_LENGTH = 18;
21 
22         public char verify(string source)
23         {
24             /**
25              * <li>校验码(第十八位数):<br/>
26              * <ul>
27              * <li>十七位数字本体码加权求和公式 S = Sum(Ai * Wi), i = 0...16 ,先对前17位数字的权求和;
28              * Ai:表示第i位置上的身份证号码数字值 Wi:表示第i位置上的加权因子 Wi: 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4
29              * 2;</li>
30              * <li>计算模 Y = mod(S, 11)</li>
31              * <li>通过模得到对应的校验码 Y: 0 1 2 3 4 5 6 7 8 9 10 校验码: 1 0 X 9 8 7 6 5 4 3 2</li>
32              * </ul>
33              * 
34              * @param cardNumber
35              * @return
36              */
37 
38             int sum = 0;
39             for (int i = 0; i < CARD_NUMBER_LENGTH - 1; i++)
40             {
41                 char ch = source[i];
42                 sum += ((int) (ch - '0'))*VERIFY_CODE_WEIGHT[i];
43             }
44             return VERIFY_CODE[sum%11];
45         }
46 
47     }
48 }

 

这时候,身份证构造器的完整实现就变成了

 1 public SocialID(String cardNumber)
 2         {
 3             if (string.IsNullOrEmpty(cardNumber))
 4                 throw new ApplicationException("Card Number is empty");
 5             if (cardNumber.Length != CARD_NUMBER_LENGTH)
 6                 throw new ApplicationException("Card Number Length is wrong.");
 7             if (!SOCIAL_NUMBER_PATTERN.IsMatch(cardNumber))
 8                 throw new ApplicationException("Card Number has wrong charactor(s).");
 9 
10             if (cardNumber[CARD_NUMBER_LENGTH - 1] != verifier.verify(cardNumber))
11                 throw new ApplicationException("Card Number verified code is not match.");
12             this.cardNumber = cardNumber;
13         }

至此,代码已经很干净了。 是的,还有进一步的改进,如,3个元素(地区,生日,性别)的提炼应该移到构造器中,各个提取的功能就变成了,简单的数据读取。Social 的类型,不是class而是struct,因为这是典型的 Value Object。 另外,我把15转18位的部分也去掉了,这可以看作一个Utilit,可以在外部做,不是核心功能。

你,是否能继续了?

最后,欣赏一下测试结果:


完整代码:

SocialID.cs

Verifier.cs


posted @ 2013-01-05 10:40  予沁安  阅读(...)  评论(... 编辑 收藏