源於 CSDN 一個「字符串加 1」的提問:KA001A001 → KA001A002 → ... → KA001Z999 → KA002A001。本文展示如何用測試驅動開發(TDD)逐步構建一個通用、靈活的流水號生成器,並引入組合模式處理多段組合。
ISerialNumberGenerator = interface
function NextSerialNumber(const SerialNumber: string): string;
function Validate(const SerialNumber: string): Boolean;
end;
職責:生成下一個可用流水號、驗證合法性。
'001'-'999')'A'-'Z')'KA')KA001A001)procedure TTestNumericSerialNumberGenerator.SetUp;
begin
fGenerator := TNumericSerialNumberGenerator.Create('001', '999');
end;
procedure TTestNumericSerialNumberGenerator.TestNextSerialNumber;
begin
CheckEquals('002', fGenerator.NextSerialNumber('001'));
CheckEquals('010', fGenerator.NextSerialNumber('009'));
CheckEquals('100', fGenerator.NextSerialNumber('099'));
CheckEquals('999', fGenerator.NextSerialNumber('998'));
CheckEquals('001', fGenerator.NextSerialNumber('999')); { 循環 }
end;
type
TNumericSerialNumberGenerator = class(TInterfacedObject, ISerialNumberGenerator)
private
fBegin, fEnd: string;
fLength: Integer;
public
constructor Create(const BeginSN, EndSN: string);
function NextSerialNumber(const SN: string): string;
function Validate(const SN: string): Boolean;
end;
constructor TNumericSerialNumberGenerator.Create(const BeginSN, EndSN: string);
begin
inherited Create;
fBegin := BeginSN; fEnd := EndSN; fLength := Length(BeginSN);
end;
function TNumericSerialNumberGenerator.NextSerialNumber(const SN: string): string;
var
Val: Int64; Repeated: Boolean;
begin
CheckSerialNumber(SN);
Repeated := (SN = fEnd);
if Repeated then
Result := fBegin
else
begin
Val := StrToInt64(SN);
Inc(Val);
Result := IntToStr(Val);
Result := StringOfChar('0', fLength - Length(Result)) + Result;
end;
end;
function TNumericSerialNumberGenerator.Validate(const SN: string): Boolean;
var I: Integer;
begin
Result := False;
if Length(SN) = fLength then
begin
for I := 1 to fLength do
if not (SN[I] in ['0'..'9']) then Exit;
Result := True;
end;
end;
{ 字母序列 }
TLetterSerialNumberGenerator = class(TInterfacedObject, ISerialNumberGenerator)
function NextSerialNumber(const SN: string): string;
function Validate(const SN: string): Boolean;
end;
function TLetterSerialNumberGenerator.NextSerialNumber(const SN: string): string;
var C: Char;
begin
C := SN[1];
if C = 'Z' then Result := 'A' else Result := Chr(Ord(C) + 1);
end;
{ 常量代碼 }
TConstantCodeSerialNumberGenerator = class(TInterfacedObject, ISerialNumberGenerator)
function NextSerialNumber(const SN: string): string;
function Validate(const SN: string): Boolean;
end;
function TConstantCodeSerialNumberGenerator.NextSerialNumber(const SN: string): string;
begin
Result := SN; { 常量不變 }
end;
procedure TTestCompositeSerialNumberGenerator.SetUp;
begin
fGenerator := TSerialNumberGenerator.Create([
TConstantCodeSerialNumberGenerator.Create('KA'),
TNumericSerialNumberGenerator.Create('001', '999'),
TLetterSerialNumberGenerator.Create,
TNumericSerialNumberGenerator.Create('001', '999')
]);
end;
procedure TTestCompositeSerialNumberGenerator.TestNextSerialNumber;
begin
CheckEquals('KA001A002', fGenerator.NextSerialNumber('KA001A001'));
CheckEquals('KA001B001', fGenerator.NextSerialNumber('KA001A999'));
CheckEquals('KA001Z002', fGenerator.NextSerialNumber('KA001Z001'));
CheckEquals('KA002A001', fGenerator.NextSerialNumber('KA001Z999')); { 多級進位 }
end;
ISerialNumberGenerator = interface
function NextSerialNumber(const SN: string): string; overload;
function NextSerialNumber(const SN: string; var Repeated: Boolean): string; overload;
function Validate(const SN: string): Boolean;
function GetTotalLength: Integer;
property TotalLength: Integer read GetTotalLength;
end;
type
TSerialNumberGenerator = class(TInterfacedObject, ISerialNumberGenerator)
private
fGenerators: TInterfaceList;
fTotalLength: Integer;
public
constructor Create(const Generators: array of ISerialNumberGenerator);
destructor Destroy; override;
function NextSerialNumber(const SN: string): string; overload;
function NextSerialNumber(const SN: string; var Repeated: Boolean): string; overload;
function Validate(const SN: string): Boolean;
function GetTotalLength: Integer;
end;
function TSerialNumberGenerator.NextSerialNumber(const SN: string; var Repeated: Boolean): string;
var
Gen: ISerialNumberGenerator;
Part: string;
Pos: Integer;
I: Integer;
List: TStringList;
begin
if Length(SN) <> fTotalLength then
raise ESerialNumberException.CreateFmt(SIllegalSerialNumber, [SN]);
Pos := 1;
List := TStringList.Create;
try
{ 1. 拆分 }
for I := 0 to fGenerators.Count - 1 do
begin
Gen := ISerialNumberGenerator(fGenerators[I]);
Part := Copy(SN, Pos, Gen.TotalLength);
List.Add(Part);
Inc(Pos, Gen.TotalLength);
end;
{ 2. 從低位向高位遞增 }
Repeated := False;
for I := List.Count - 1 downto 0 do
begin
Gen := ISerialNumberGenerator(fGenerators[I]);
List[I] := Gen.NextSerialNumber(List[I], Repeated);
if not Repeated then Break; { 無進位則停止 }
end;
{ 3. 組合 }
Result := '';
for I := 0 to List.Count - 1 do
Result := Result + List[I];
finally
List.Free;
end;
end;
function TSerialNumberGenerator.Validate(const SN: string): Boolean;
var
Gen: ISerialNumberGenerator;
Part: string;
Pos, I: Integer;
begin
if Length(SN) <> fTotalLength then
raise ESerialNumberException.CreateFmt(SIllegalSerialNumber, [SN]);
Result := True;
Pos := 1;
for I := 0 to fGenerators.Count - 1 do
begin
Gen := ISerialNumberGenerator(fGenerators[I]);
Part := Copy(SN, Pos, Gen.TotalLength);
Inc(Pos, Gen.TotalLength);
Result := Gen.Validate(Part);
if not Result then Break;
end;
end;

雖然 V1 模型已能工作,但存在疑問:
ISerialNumberGenerator 與抽象基類職責重疊?Validate 返回 Boolean 是否足夠?NextSerialNumber 語義怪異?下篇將回到領域知識,重構為真正的領域模型:NumberRule。敬請期待!