它山之玉可以重構:身份證號碼解析、驗證工具(第三天)
前兩天的進度似乎有些慢,今天加快了一點, 不把每一步說的那么詳細了.
==》地區信息的提取
繼性別和生日之后,最后一個信息塊,只是列出測試如下.

==》有效性
這是一個比較大的問題. 前面,我臨時性的把不同地方的驗證去掉了. 代碼原作者也過來, 暢敘了他關于驗證的看法. 他是對的, 這種完全驗證的方式,根本上說是 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,可以在外部做,不是核心功能。
你,是否能繼續了?
最后,欣賞一下測試結果:

完整代碼:
皓月碧空,漫野如洗,行往卓越的路上

浙公網安備 33010602011771號