0. 上篇回顾

在上篇中我们使用测试驱动开发方法(Test-Driven Development)实现了一个简单的流水号生成器,并获得了一个初步的软件模型:

java生成流水号自增工具类_ide


图1 编号生成器模型(V1)

熟悉设计模式的朋友们一眼就会看出来,这里运用了组合模式(Composite Pattern),把每个子流水号当做一个流水号来处理。虽然这个模型还能工作,但是我们仔细分析一下就会有很多疑问:

  1. ISerialNumberGenerator接口有什么用?为什么不直接使用抽象类(TSerialNumberGeneratorBase)?
  2. 客户需要验证流水号吗?即使需要,Validate函数应该返回Boolean吗?
  3. 调用NextSerialNumber函数时需要传入一个流水号,是不是意味着其调用者需要知道当前的流水号?对于Generator来说这样合适吗?
  4. 既然TConstantCodeSerialNumberGenerator表示固定代码,那调用NextSerialNumber方法是不是很奇怪?
  5. 如果考虑在流水号中加入日期的话,这个模型需要怎么修改?

除了有这些疑问以外,恐怕这个模型存在最大的问题在于:它看上去更像是一种技术模型——虽然能勉强工作,但是没有表现任何领域知识

现在,我们该停下来,回到起点,重新思考一下:

What's the Problem?

 

1. 领域知识

我们要解决的问题其实很简单——就是要获取一个可用的编号(Number)。编号一般是有几部分(Part)组成的。比如某张入库单的编号”RK200901160001”就包含下面3个部分:

  1. 代码:“RK”
  2. 日期:“20090116”
  3. 流水号:“0001”

其中,代码是固定不变的,流水号会自动递增,日期一般是当前系统日期(固定格式,比如YYYYMM、YYYYMMDD),另外当日期变化时再重置流水号。写到这里,我们终于找到了一个重要概念:编号规则(Number Rule)。编号规则定义了多个连续的段(Number Part),各段组合起来就生成了一个编号。正如下图所示:

java生成流水号自增工具类_java生成流水号自增工具类_02


图2 分析模型

在实际的应用当中,流水号的规则可能很复杂,也许要支持数字(如‘0000’-‘9999’)、英文字母(如‘A' -  'Z'),甚至是一些自定义的字符(如‘0’-‘Z’)的组合。既然这样,我们可以提取一个抽象概念:序列Sequence)。如在卡号规则当中规定遇4跳过等等就表示卡号是由除4以外的其他阿拉伯数字组成的序列。{ 序列可以考虑用任意进制的计算器来实现:) }


2. 领域模型

结合上面的领域知识,我设计了新的领域模型:

java生成流水号自增工具类_流水号_03


图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接口主要针对那些手工输入编号或需要进行统计编号数量的应用(比如,输入开始卡号和结束卡号,自动计算数量)。


最后,期待大家的批评和指点。


P.S. NumberGenerator项目及源代码(Delphi 2009,含Test Case)下载地址:

 

作者:左保权 (Zuo Baoquan)