它山之玉可以重构:身份证号码解析、验证工具(第三天)
前两天的进度似乎有些慢,今天加快了一点, 不把每一步说的那么详细了.
==》地区信息的提取
继性别和生日之后,最后一个信息块,只是列出测试如下.
==》有效性
这是一个比较大的问题. 前面,我临时性的把不同地方的验证去掉了. 代码原作者也过来, 畅叙了他关于验证的看法. 他是对的, 这种完全验证的方式,根本上说是 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,可以在外部做,不是核心功能。
你,是否能继续了?
最后,欣赏一下测试结果:
完整代码:
皓月碧空,漫野如洗,行往卓越的路上