程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 領域驅動設計實踐——流水號生成器(下)

領域驅動設計實踐——流水號生成器(下)

編輯:Delphi

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接口主要針對那些手工輸入編號或需要進行統計編號數量的應用(比如,輸入開始卡號和結束卡號,自動計算數量)。

最後,期待大家的批評和指點。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved