首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> Delphi

領域驅動設計實踐:流水號生成器(下)——從技術模型到領域模型的重構

     編輯:Delphi

上篇我們用 TDD 實現了一個簡單的流水號生成器,但模型存在諸多疑點:介面與抽象類職責不清、驗證邏輯異常、調用者需知曉當前號碼、固定代碼卻要呼叫 Next 方法、日期變化時流水號重置邏輯缺失,且整體更像技術模型而非領域模型。本文將重新審視領域知識,構建真正的領域模型:編號規則。

1. 領域知識提煉

業務編號(如入庫單 RK200901160001)由三部分組成:

  • 代碼:固定前綴 RK
  • 日期:格式化日期 20090116(YYYYMMDD)
  • 流水號:自動遞增 0001,日期變化時重置

核心概念:編號規則 = 多個連續 編號段 組合。段類型包括:固定代碼、日期時間、數字序列、字母序列、自定義字符序列。

2. 領域模型(V2)

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;

3. 客戶端使用範例(測試代碼)

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;

4. 核心實現細節

流式建構段

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' 的卡號規則)。

5. 業務應用層介面

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;

6. 模型對比:V1 vs V2

維度V1:SerialNumberGeneratorV2:NumberRule
核心抽象生成器/組合模式編號規則/段組合
業務語義弱(NextSerialNumber、Validate)強(AddCode、AddDateTime、GetNextNumber)
日期支援無內建日期段+自動重置
序列靈活性固定數字/字母任意字典、任意進制
持久化未考慮規則可序列化(XML/JSON)
測試友好需 Mock 生成器IDateTimeProvider 可替換

總結

領域驅動設計的關鍵在於聆聽領域語言,將技術術語(Generator、Validate、Composite)替換為業務術語(Rule、Part、GetNextNumber、ResetSequence)。新模型不僅功能更完整,更能與業務專家無障礙溝通,支撐複雜規則演進。期待大家批評指正!

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved