0. 上篇回顧
在上篇中我們使用測試驅動開發方法(Test-Driven Development)實現了一個簡單的流水號生成器,並獲得了一個初步的軟件模型:
圖1 編號生成器模型(V1)
熟悉設計模式的朋友們一眼就會看出來,這裡運用了組合模式(Composite Pattern),把每個子流水號當做一個流水號來處理。雖然這個模型還能工作,但是我們仔細分析一下就會有很多疑問:
ISerialNumberGenerator接口有什麼用?為什麼不直接使用抽象類(TSerialNumberGeneratorBase)?
客戶需要驗證流水號嗎?即使需要,Validate函數應該返回Boolean嗎?
調用NextSerialNumber函數時需要傳入一個流水號,是不是意味著其調用者需要知道當前的流水號?對於Generator來說這樣合適嗎?
既然TConstantCodeSerialNumberGenerator表示固定代碼,那調用NextSerialNumber方法是不是很奇怪?
如果考慮在流水號中加入日期的話,這個模型需要怎麼修改?
…
除了有這些疑問以外,恐怕這個模型存在最大的問題在於:它看上去更像是一種技術模型——雖然能勉強工作,但是沒有表現任何領域知識。
現在,我們該停下來,回到起點,重新思考一下:
What's the Problem?
1. 領域知識
我們要解決的問題其實很簡單——就是要獲取一個可用的編號(Number)。編號一般是有幾部分(Part)組成的。比如某張入庫單的編號”RK200901160001”就包含下面3個部分:
代碼:“RK”
日期:“20090116”
流水號:“0001”
其中,代碼是固定不變的,流水號會自動遞增,日期一般是當前系統日期(固定格式,比如YYYYMM、YYYYMMDD),另外當日期變化時再重置流水號。寫到這裡,我們終於找到了一個重要概念:編號規則(Number Rule)。編號規則定義了多個連續的段(Number Part),各段組合起來就生成了一個編號。正如下圖所示:
圖2 分析模型
在實際的應用當中,流水號的規則可能很復雜,也許要支持數字(如‘0000’-‘9999’)、英文字母(如‘A' - 'Z'),甚至是一些自定義的字符(如‘0’-‘Z’)的組合。既然這樣,我們可以提取一個抽象概念:序列(Sequence)。如在卡號規則當中規定遇4跳過等等就表示卡號是由除4以外的其他阿拉伯數字組成的序列。{ 序列可以考慮用任意進制的計算器來實現:) }
2. 領域模型
結合上面的領域知識,我設計了新的領域模型:
圖2 編號規則領域模型(V2)
我們來看看客戶是如何使用這個模型的:
Code
1 procedure TTestNumberRule.TestCompositeNumber;
2 begin
3 fRule.AddCode('RK')
4 .AddLetters
5 .AddDigits('001', '999');
6 CheckEquals('RKA002', fRule.GetNextNumber('RKA001'));
7 CheckEquals('RKA999', fRule.GetNextNumber('RKA998'));
8 CheckEquals('RKB001', fRule.GetNextNumber('RKA999'));
9 CheckEquals('RKZ999', fRule.GetNextNumber('RKZ998'));
10 end;
11
12 procedure TTestNumberRule.TestDate;
13 begin
14 fRule.AddCode('RK')
15 .AddDateTime('YYYYMM', Self) // TTestNumberRule類實現了IDateTimeProvider接口,返回fDateTime便於測試
16 .AddDigits('0001', '9999');
17
18 fDateTime := EncodeDate(2009, 1, 1);
19 CheckEquals('RK2009010002', fRule.GetNextNumber('RK2009010001'));
20
21 fDateTime := EncodeDate(2009, 2, 1);
22 CheckEquals('RK2009020001', fRule.GetNextNumber('RK2009010999'));
23 end
(呵呵,現在是不是感覺NumberRule比之前的SerialNumberGenerator貼切多了?)
接下來我們再簡單看看TNumberRule的實現:
1. 設置規則
Code
1 function TNumberRule.AddCode(const code: string): TNumberRule;
2 begin
3 fParts.Add(TCodeNumberPart.Create(code));
4 Result := Self;
5 end;
6
7 function TNumberRule.AddDigits(const first,
8 last: string): TNumberRule;
9 begin
10 Result := AddSequence('0123456789', Length(first), first, last);
11 end;
12
13 function TNumberRule.AddLetters: TNumberRule;
14 begin
15 Result := AddSequence('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 1, 'A', 'Z');
16 end;
17
18 function TNumberRule.AddDateTime(const format: string): TNumberRule;
19 begin
20 Result := AddDateTime(format, TCurrentDateTimeProvider.Create);
21 end;
22
23 function TNumberRule.AddDateTime(const format: string; const provider: IDateTimeProvider): TNumberRule;
24 begin
25 fParts.Add(TDateTimeNumberPart.Create(format, provider));
26 Result := Self;
27 end;
28
29 function TNumberRule.AddSequence(const dictionary: string; len: Integer;
30 const first, last: string): TNumberRule;
31 begin
32 fParts.Add(TSequenceNumberPart.Create(dictionary, len, first, last));
33 Result := Self;
34 end
2. 生成編號
Code
1 function TNumberRule.GetNextNumber(const number: string): string;
2 begin
3 ParseNumber(number);
4 BuildNumber;
5 Result := GenerateNumber;
6 end;
7
8 // ParseNumber負責根據各個NumberPart的Length把編號拆成多個段,BuildNumber中封裝了規則的更新邏輯,GenerateNumber則生成編號。
9
10 procedure TNumberRule.BuildNumber;
11 var
12 part: TNumberPart;
13 value: string; // 拆分的編號
14 carried: Boolean; // 進位標志
15 i: Integer;
16 begin
17 Assert(fParts.Count = fList.Count);
18 carried := False;
19 for i := fParts.Count - 1 downto 0 do // 由低位向高位遍歷
20 begin
21 part := TNumberPart(fParts[i]);
22 value := fList[i];
23 if part is TSequenceNumberPart then
24 begin
25 TSequenceNumberPart(part).SetValue(value);
26 if carried or (i = fParts.Count - 1) then
27 begin
28 TSequenceNumberPart(part).Next(carried);
29 end
30 end
31 else if (part is TDateTimeNumberPart) and
32 not SameText(TDateTimeNumberPart(part).Value, value) then
33 begin
34 ResetSequenceParts(part); // 如果日期不同則重置序列部分編號
35 end;
36 end;
37 if carried then
38 begin
39 raise ENumberException.Create(SNumberOutOfRange);
40 end;
41 end
P.S. 序列部分(TSequenceNumberPart)的核心功能實現委托給任意進制計算器(BaseNCalculator),具體可參考源代碼。
3. 業務應用
為了實現具體的業務應用,我們還需要做兩件事:
1. 編號規則的持久化(一般使用XML,暫省略)
2. 編號的獲取和更新
我們可以在業務層定義了下面兩個接口,方便供客戶使用:
Code
1 INumberGenerator = interface
2 function NextNumber: string;
3 end
Code
1 INumberCalculator = interface
2 procedure Validate(const number: string);
3 function Compare(const startNumber, endNumber: string): Integer;
4 function GetCount(const startNumber, endNumber: string): Int64;
5 function GetEndNumber(const startNumber: string; count: Int64): string;
6 end
我們只需要通過訪問一個全局的Factory/Registry來獲得一個當前Context的INumberGenerator實例,然後調用NextNumber方法就可以獲取編號。其實現可參考:
Code
1 TDBNumberGenerator = class(TInterfacedObject, INumberGenerator)
2 private
3 fRule: TNumberRule;
4 fDataSet: TDataSet;
5 fTypeID: string;
6 public
7 { 訪問數據庫的編號表,根據TypeID進行行鎖定(悲觀鎖),讀取當前可用的編號後,調用fRule的NextNumber,把結果更新回去 }
8 function NextNumber: string;
9 end
INumberCalculator接口主要針對那些手工輸入編號或需要進行統計編號數量的應用(比如,輸入開始卡號和結束卡號,自動計算數量)。
最後,期待大家的批評和指點。