上篇我們用 TDD 實現了一個簡單的流水號生成器,但模型存在諸多疑點:介面與抽象類職責不清、驗證邏輯異常、調用者需知曉當前號碼、固定代碼卻要呼叫 Next 方法、日期變化時流水號重置邏輯缺失,且整體更像技術模型而非領域模型。本文將重新審視領域知識,構建真正的領域模型:編號規則。
業務編號(如入庫單 RK200901160001)由三部分組成:
RK20090116(YYYYMMDD)0001,日期變化時重置核心概念:編號規則 = 多個連續 編號段 組合。段類型包括:固定代碼、日期時間、數字序列、字母序列、自定義字符序列。
NumberRule (聚合根) ├── Parts: TList(有序段集合) │ ├── TCodeNumberPart (固定代碼) │ ├── TDateTimeNumberPart (日期時間段) │ └── TSequenceNumberPart (序列段:數字/字母/自定義) ├── AddCode / AddDateTime / AddDigits / AddLetters / AddSequence (流式建構介面) └── GetNextNumber(current: string): string (核心業務操作)
IDateTimeProvider = interface
['{...}']
function GetDateTime: TDateTime;
end;
TNumberRule = class
public
function AddCode(const Code: string): TNumberRule;
function AddDateTime(const Format: string; Provider: IDateTimeProvider = nil): TNumberRule;
function AddDigits(const First, Last: string): TNumberRule;
function AddLetters: TNumberRule;
function AddSequence(const Dictionary: string; Len: Integer; const First, Last: string): TNumberRule;
function GetNextNumber(const CurrentNumber: string): string;
end;
procedure TTestNumberRule.TestCompositeNumber;
begin
fRule.AddCode('RK')
.AddLetters
.AddDigits('001', '999');
CheckEquals('RKA002', fRule.GetNextNumber('RKA001'));
CheckEquals('RKA999', fRule.GetNextNumber('RKA998'));
CheckEquals('RKB001', fRule.GetNextNumber('RKA999')); { 字母進位 }
CheckEquals('RKZ999', fRule.GetNextNumber('RKZ998'));
end;
procedure TTestNumberRule.TestDate;
begin
fRule.AddCode('RK')
.AddDateTime('YYYYMM', Self) { 測試用 Mock Provider }
.AddDigits('0001', '9999');
fDateTime := EncodeDate(2009, 1, 1);
CheckEquals('RK2009010002', fRule.GetNextNumber('RK2009010001'));
fDateTime := EncodeDate(2009, 2, 1);
CheckEquals('RK2009020001', fRule.GetNextNumber('RK2009010999')); { 日期變化,流水號重置 }
end;
function TNumberRule.AddCode(const Code: string): TNumberRule;
begin
fParts.Add(TCodeNumberPart.Create(Code));
Result := Self;
end;
function TNumberRule.AddDigits(const First, Last: string): TNumberRule;
begin
Result := AddSequence('0123456789', Length(First), First, Last);
end;
function TNumberRule.AddSequence(const Dictionary: string; Len: Integer; const First, Last: string): TNumberRule;
begin
fParts.Add(TSequenceNumberPart.Create(Dictionary, Len, First, Last));
Result := Self;
end;
function TNumberRule.GetNextNumber(const Number: string): string;
begin
ParseNumber(Number); { 1. 按段長度拆解 }
BuildNumber; { 2. 從低位向高位應用遞增/重置規則 }
Result := GenerateNumber; { 3. 組合輸出 }
end;
procedure TNumberRule.BuildNumber;
var
Part: TNumberPart;
Value: string;
Carried: Boolean;
I: Integer;
begin
Assert(fParts.Count = fList.Count);
Carried := False;
for I := fParts.Count - 1 downto 0 do { 從最低位(最右段)向高位 }
begin
Part := TNumberPart(fParts[I]);
Value := fList[I];
if Part is TSequenceNumberPart then
begin
TSequenceNumberPart(Part).SetValue(Value);
if Carried or (I = fParts.Count - 1) then
TSequenceNumberPart(Part).Next(Carried); { 遞增,輸出進位旗標 }
end
else if (Part is TDateTimeNumberPart) and
not SameText(TDateTimeNumberPart(Part).Value, Value) then
begin
ResetSequenceParts(Part); { 日期段變化,重置所有低位序列段 }
end;
end;
if Carried then
raise ENumberException.Create(SNumberOutOfRange);
end;
序列段核心 委托給 BaseNCalculator(任意進制計算器)實現,支援任意字符字典(如跳過 '4' 的卡號規則)。
INumberGenerator = interface
function NextNumber: string; { 取號,內部處理持久化與鎖 }
end;
INumberCalculator = interface
procedure Validate(const Number: string);
function Compare(const Start, End: string): Integer;
function GetCount(const Start, End: string): Int64;
function GetEndNumber(const Start: string; Count: Int64): string;
end;
TDBNumberGenerator = class(TInterfacedObject, INumberGenerator)
private
fRule: TNumberRule;
fDataSet: TDataSet;
fTypeID: string;
public
function NextNumber: string;
{ 1. SELECT ... FOR UPDATE 鎖定編號表記錄
2. 讀取當前號碼
3. fRule.GetNextNumber(Current)
4. UPDATE 寫回新號碼
5. COMMIT 釋放鎖 }
end;
| 維度 | V1:SerialNumberGenerator | V2:NumberRule |
|---|---|---|
| 核心抽象 | 生成器/組合模式 | 編號規則/段組合 |
| 業務語義 | 弱(NextSerialNumber、Validate) | 強(AddCode、AddDateTime、GetNextNumber) |
| 日期支援 | 無 | 內建日期段+自動重置 |
| 序列靈活性 | 固定數字/字母 | 任意字典、任意進制 |
| 持久化 | 未考慮 | 規則可序列化(XML/JSON) |
| 測試友好 | 需 Mock 生成器 | IDateTimeProvider 可替換 |
領域驅動設計的關鍵在於聆聽領域語言,將技術術語(Generator、Validate、Composite)替換為業務術語(Rule、Part、GetNextNumber、ResetSequence)。新模型不僅功能更完整,更能與業務專家無障礙溝通,支撐複雜規則演進。期待大家批評指正!