领域驱动设计实践——流水号生成器(上)

时间:2021-12-13 18:41:05

今天在CSDN上逛的时候,我突然看到一个提问帖子

问一个大家一个问题
一个字符串 加1 谁做过
例如KA001A001
下一个就是 KA001A002
到 KA001Z999
就 KA002A001

这顿时引起了我的兴趣。流水号(Serial Number)在程序中应用很普遍,生成规则也各不相同。(比如,我们公司的会员卡卡号规则里面就有一个“卡号遇4跳过”的选项。)我上Google简单搜了一下,发现都是硬编码的函数,虽然它们能解决具体的问题,但不够通用灵活,换个应用场景又需要重写代码。那有没有一种简单、通用又灵活的流水号生成器呢?今天就让我们一起来试试。

流水号一般都是固定长度,由几部分组合而成:

  • 日期(如:20090101)
  • 常量代码(如:KA)
  • 数字序列(如:0001-9999)
  • 字母序列(如:A-Z)
  • 特殊字符(如:-)

简单分析之后,我们先定义一个接口(Delphi):

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 ISerialNumberGenerator = interface
2   function NextSerialNumber(const serialNumber: string): string;
3   function Validate(const serialNumber: string
): Boolean;
4 end 

ISerialNumberGenerator接口主要有两个作用(职责):

  1. 生成下一个可用的流水号(NextSerialNumber)
  2. 验证某个流水号是否合法(Validate)

OK,接下来我们先列个简单的任务列表:

任务列表

  1. 支持可循环的数字序列('001’-'999’)
  2. 支持可循环的字母序列('A’-'Z’)
  3. 支持常量代码('KA’)
  4. 支持字母序列和数字序列组合(KA001A001)

 再写一个简单的测试用例(Test Case):

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 procedure TTestNumbericSerialNumberGenerator.SetUp;
2 begin
3   fGenerator := TNumbericSerialNumberGenerator.Create('001', '999');
4 end;
5 
6 procedure TTestNumbericSerialNumberGenerator.TestNextSerialNumber;
7 begin
9     CheckEquals('002', fGenerator.NextSerialNumber('001'));
10   CheckEquals('010', fGenerator.NextSerialNumber('009'));
11   CheckEquals('011', fGenerator.NextSerialNumber('010'));
12   CheckEquals('100', fGenerator.NextSerialNumber('099'));
13   CheckEquals('999', fGenerator.NextSerialNumber('998'));
14   CheckEquals('001', fGenerator.NextSerialNumber('999'));

 运行Test Case,编译器提示TNumbericSerialNumberGenerator没有定义,我们一起来实现它:

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TNumbericSerialNumberGenerator Class Interface

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TNumbericSerialNumberGenerator

再运行,Test Case通过!第一个任务完成了。注意上面代码中的repeated,当流水号到了结束值时,应递进更高位。

任务列表

  1. 支持可循环的数字序列('001’-'999’)
  2. 支持可循环的字母序列('A’-'Z’)
  3. 支持常量代码('KA’)
  4. 支持字母序列和数字序列组合(KA001A001)

接下来我们分别实现字母序列和常量代码流水号:

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 procedure TTestLetterSerialNumberGenerator.TestNextSerialNumber;
2 var
3   letter: Char;
4 begin
5   for letter := 'A' to 'Y' do
6   begin
7     CheckEquals(Chr(Ord(letter) + 1), fGenerator.NextSerialNumber(letter));
8   end
9   CheckEquals('A', fGenerator.NextSerialNumber('Z'));
10 end

 

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TLetterSerialNumberGenerator

任务列表

  1. 支持可循环的数字序列('001’-'999’)
  2. 支持可循环的字母序列('A’-'Z’)
  3. 支持常量代码('KA’)
  4. 支持字母序列和数字序列组合(KA001A001)

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 procedure TTestConstantCodeSerialNumberGenerator.TestNextSerialNumber;
2 begin
3   CheckEquals('KA', fGenerator.NextSerialNumber('KA'));
4 end

 

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TConstantCodeSerialNumberGenerator

任务列表

  1. 支持可循环的数字序列('001’-'999’)
  2. 支持可循环的字母序列('A’-'Z’)
  3. 支持常量代码('KA’)
  4. 支持字母序列和数字序列组合(KA001A001)

呵呵,到了高潮部分了,我们先写一段测试案例来测试组合流水号:

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 procedure TTestCompositeSerialNumberGenerator.SetUp;
2 begin
3   inherited;
4   fGenerator := TSerialNumberGenerator.Create([
5     TConstantCodeSerialNumberGenerator.Create('KA'),
6     TNumbericSerialNumberGenerator.Create('001', '999'),
7     TLetterSerialNumberGenerator.Create,
8     TNumbericSerialNumberGenerator.Create('001', '999')
9   ]);
10 end;
11 
12 procedure TTestCompositeSerialNumberGenerator.TestNextSerialNumber;
13 begin
14   CheckEquals('KA001A002', fGenerator.NextSerialNumber('KA001A001'));
15   CheckEquals('KA001B001', fGenerator.NextSerialNumber('KA001A999'));
16   CheckEquals('KA001Z002', fGenerator.NextSerialNumber('KA001Z001'));
17   CheckEquals('KA002A001', fGenerator.NextSerialNumber('KA001Z999'));
18 end

再实现TSerialNumberGenerator:

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TSerialNumberGenerator

 我们来想想TSerialNumberGenerator的这两个方法应该如何实现。我们只要运用组合模式(Composite Pattern),把serialNumber拆分开来,并委托给相应的Generator实例处理就好了。我们需要再调整一下ISerialNumberGenerator 接口:

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)
1 ISerialNumberGenerator = interface
2   function NextSerialNumber(const serialNumber: string): string; overload;
3   function NextSerialNumber(const serialNumber: string; var repeated: Boolean): string; overload;
4   function Validate(const serialNumber: string): Boolean;
5   function GetTotalLength: Integer;
6   property TotalLength: Integer read GetTotalLength;
7 end

 

领域驱动设计实践——流水号生成器(上)领域驱动设计实践——流水号生成器(上)TSerialNumberGenerator

 再运行Test Case,呵呵,绿色进度条: )

领域驱动设计实践——流水号生成器(上)

写到这里,我们已经成功了一半了。接下来,希望大家提出批评意见,我将继续重构代码。下集将更加精彩,敬请关注:)

领域驱动设计实践——流水号生成器(上)