应用 Command 模式进行流水号管理的最佳实践

转自
http://www.ibm.com/developerworks/cn/java/j-lo-serialNo/


    本文介绍了一种基于自定义格式字符串, 应用 Command 模式的流水号管理功能,可以让用户灵活设定流水号的格式,具备非常好的灵活性和可扩展性。

摘要

    流水号管理作为很多信息系统的一种基础功能,对于一些具有业务含义的流水号,一般由多个具有不同意义的组成部分组成,其格式会相对比较复杂,如何快速灵活的支持不同的流水号格式,十分有现实意义。本文介绍了一种基于自定义格式字符串,应用 Command 模式的流水号管理方案。在本方案中,对复杂的的流水号进行分析分解,拆分为更细小的子序列,再分别调用相应的生成接口生成,最终生成的流水号。使用本方案,流水号的生成规则定义在格式字符串中,如要生成不同格式的流水号,只需要修改格式字符串即可,生成和扩展都十分方便。

背景

    在信息系统中流水号作为一项必不可少的基础功能,流水号的生成、控制和管理十分重要。一般一个信息系统中,不同的流水号序列有不同的生成规则。如果我们对这些流水号进行进一步分析,这些不同的流水号序列又可细分为多个规则固定的子序列。基于这种分析,对于这种实际应用需求,如何来高效灵活的生成所需的流水号,就显得十分有意义。


场景分析

    首先看几个我们身边常见的流水号:身份证号、税务票、银行的业务流水号、排队号、国务院办公厅发文号(国办发〔2011〕48 号)等。如果对这些流水号进一步分析,我们可以得出,流水号一般由多个规则固定的子序列组成,常见的子序列规则如下:
    1. 数字序列:1,2,3 … 或者 00001,00002,00003
    2. 字母序列:如 ‘A-Z’
    3. 固定字符串:如固定的字符前缀或后缀
    4. 时间戳:如 yyyyMMdd 格式对应的为 20130201
    5. 其他序列:如罗马字符 ‘I-X’
    从另一个角度我们可以说,一个流水号就是上面子序列的排列组合。
基于这样的分析,我们可以创建一个流水号生成引擎,这个引擎可以读取和解析给定的流水号规则,将流水号规则拆分成多个子序列规则,并根据子序列规则生成子序列串,拼接返回最终的流水号。有了这个流水号生成引擎,在系统实际应用的时候,只需要对所需的流水号配置相应的生成规则即可。并且本方案还预留扩展接口,如果实际中遇到个性化的序列生成规则,开发者也可以根据实际需要进行扩展开发,这样也能有效覆盖个性化的需求。
说了这么多,不如图形展示来得直观,整个解决方案的流程见下图:
图 1. 流水号生成流程图


     1. 流水号规则读取
     2. 流水号规则解析、分割、获取子序列规则
     3. 生成子序列任务的派发
     4. 子序列汇总和合并
     5. 返回最终的生成流水号实例


图 2. 流水号生成过程实例


    在上图中,这里需要说明如下几点:
    1. 流水号规则用格式字符串表示为:Str@ 国办发〔# DateTime@yyyy # Str@〕# NumSeq@0C0 # Str@ 号,其中: “#” 为子序列间的分隔符,“@” 为子序列内部的分隔符。当然实际应用时,读者也可以按照自己的规则创建格式字符串。
    2. 在每个子序列规则定义的内部,用 “@” 分为两部分,前面一部分(Str/DateTime/NumSeq)表明该子序列的类型,用于确定调用哪个类型的 Generator 来生成该子序列串,后面一部分是用于生成子序列串所需的信息。举例来讲,如 “Str@ 国办发〔”,“Str” 表示为固定字符串类型,固定字符串为 “国办发〔”;类似的 “DateTime@yyyy”,“DateTime” 表示时间日期类型,格式为四位的年代号,根据当前时间生成。
    3. 在各个 Generator 生成完毕之后,自动拼接并返回最终生成的流水号。
我们可以认为,对于每一个流水号生成规则,似乎可以看做是一个命令队列;对于流水号生成引擎,一方面按照顺序接收生成子序列规则的命令,逐个读取并解析子序列规则,再根据类型分发给各个 Generator 来生成子序列传,最后合并返回最终需要的流水号;每个子序列的 Generator,即为最终的命令的执行者。
    经过如上的分析并结合应用场景,我们很容易想到,可以使用 Command 模式来实现。Command 模式,一个典型的应用场景就是处理命令队列,减少行为请求者和动作执行者的耦合,提高灵活性和降低代码冗余。


Command 模式简介

    命令(Command)模式属于对象的行为模式【GOF95】。命令模式又称为行动(Action)模式或事务(Transaction)模式。命令模式把一个请求或者操作封装到一个对象中。命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
    适用性:在软件系统中,行为请求者与行为实现者之间通常呈现一种紧耦合的关系。但在某些场合,比如要对行为进行记录撤销重做事务等处理,这种无法抵御变化的紧耦合是不合适的。这种情况下,使用 command 模式将行为请求者与行为实现者进行解耦。
说到这里,我们来看看 Command 模式 UML 类图。
图 3. Command 模式的 UML 类图


    这里对上图做一个简单的解释,命令模式涉及到五个角色,分别为:
    客户(Client)角色:创建一个具体命令(ConcreteCommand)对象,并设置命令的接收者。
    命令(Command)角色:定义一个给所有命令类的抽象接口,定义了统一的 execute() 接口方法。
    具体命令(ConcreteCommand)角色:定义一个接受者和行为之间的弱耦合;实现 Command 接口,并实现 execute() 方法,负责调用接收者的相应操作。
    请求者(Invoker)角色:负责调用由 Client 下达的对象执行请求。
    接收者(Receiver)角色:负责具体实施和执行一个请求。任何一个类都可以成为接收者,实施和执行请求的方法为行动方法。
    【注意】:在某些简单业务场景下,可在 ConcreteCommand 类的 execute 方法中直接编写实现代码,而省去接收者(Receiver)类以简化代码。如在本应用场景中省去 Receiver 类的创建。
    在本应用场景中,正是利用了 Command 模式将行为请求者与行为实现者进行解耦,用于处理命令队列,实现了原子操作上的复用。Command 是对行为进行封装的典型模式,这样做有利于代码的健壮性,可维护性,还有复用性。


实现代码
    对应 Command 模式,简单介绍一下本方案中使用到的核心的类和方法:
    1. Client - SNGenerateApp.java:
    具体的命令请求者,通过调用 addBuildInGenerator() 方法创建内置 generator,暴露 addGenerator() 方法供进一步扩展。

    2. Invoker - SNGeneratorEngine.java:
    为命令的直接接收者和分派者。

    3. ICommand - IGenerator.java:
    定义生成接口,子类通过实现 generate() 方法来生成具体的子序列串。
 
    4. ConcreteCommand - XXXXGenerator.java:
    各个具体的子序列生成类,实现 generate() 方法,负责最终的的子序列生成,为 Command 的最终执行者。这里简单实现了四个 generator。
    a). 字符序列生成器:CharacterSequenceGenerator.java
    b). 日期时间生成器:DateTimeGenerator.java
    c). 数字序列生成器:NumberSequenceGenerator.java
    d). 固定字符串生成器:StringGenerator.java

    5. Receiver:
    在本例中由于业务场景简单,为简化代码而直接将实现代码写在各 Generator 的 generate() 方法中,无独立的 Receiver 类。
另外还有两个辅助类简单提一下,MockDB.java 用于模拟数据库,存取自增序列;ParamConsts.java 为常量变量类,GeneratorTypeSet.java 定义子序列类型常量。

    图 4. 包结构及类清单图


    下面是具体的代码展示和介绍。
    Generator 接口
    定义 Generator,这里只有两个方法,一个是 getGeneratorType() 用于返回 Generator 类型,另一个是 generate() 方法用于生成子序列串。
    清单 1. Generator 接口
public interface IGenerator {
	/**
	 * 获取子序列类型
	 * @return
	 */
	public String getGeneratorType();
	/**
	 * 生成子序列串
	 * @param formatStr
	 * @return
	 */
	public String generate(String formatStr,Map paraMap);
}


     数字序列 Generator
    Generator 接口的实现类,用于处理数字序列的生成。下面的代码考虑到变长字符串和定长字符串两种不同情形。在实际中,序列号的生成还需要考虑按照一定的周期重置的情况(如每年都从 1 重新开始计数),可以在扩展实现的时候加以考虑。
    清单 2. 数字序列 Generator
public class NumberSequenceGenerator implements IGenerator{
	private static final String type = GeneratorTypeSet.NUMBER_SEQUENCE;
	private String splitC = "C";
	@Override
	public String getGeneratorType() {
		// TODO Auto-generated method stub
		return type;
	}
	@Override
	public String generate(String formatStr,Map paraMap) {
		String seqId = (String)paraMap.get(ParamConsts.PARAM_SEQ_ID);
		//从数据库中获取当前数值,并自动加 1
		int seqNum = MockDB.getSeqNumAndIncrease(seqId);		
		String[] charArray = formatStr.split(splitC);
		int seqNumLength = Integer.parseInt(charArray[0]);
		char prefixChar = charArray[1].charAt(0);
		if(seqNumLength == 0)
			return String.valueOf(seqNum);
		else
			return appendPrefixChar(seqNum,seqNumLength,prefixChar);
	}
	/**
	 * 补足前缀以保证序列定长,如 0001, 保持 4 位,不足 4 位用'0'补齐
	 * @param seqNum 当前数值
	 * @param seqNumLength 需要返回的字符串长度
	 * @param prefixChar 用于补齐的前置字符串
	 * @return
	 */
	private String appendPrefixChar(int seqNum,int seqNumLength,char prefixChar)
	{
		String seqNumStr = String.valueOf(seqNum);
		for(int i = seqNumStr.length();i<seqNumLength;i++)
		{
			seqNumStr = prefixChar + seqNumStr;
		}
		return seqNumStr;
	}
}


     字符序列 Generator
     Generator 接口的实现类,用于处理字符序列。这里只是一个简单的例子,比如车牌号的生成就可以使用到字符序列。
     清单 3. 字符序列 Generator
public class CharacterSequenceGenerator implements IGenerator{
	private static final String type = GeneratorTypeSet.CHARATER_SEQUENCE;
	//从’A’开始,自动增长
	private char c = 'A'; 
	@Override
	public String getGeneratorType() {
		// TODO Auto-generated method stub
		return type;
	}
	@Override
	public String generate(String formatStr,Map paraMap) {
		return String.valueOf((char)c++);
	}
}


     日期序列 Generator
     Generator 接口的实现类,用于生成时间日期戳。下面的代码基于 java.text.SimpleDateFormat 实现。
     清单 4. 日期序列 Generator
public class DateTimeGenerator implements IGenerator{
	private static final String type = GeneratorTypeSet.DATE_TIME;
	@Override
	public String getGeneratorType() {
		// TODO Auto-generated method stub
		return type;
	}
	@Override
	public String generate(String formatStr,Map paraMap) {
		SimpleDateFormat sdf = new SimpleDateFormat();
		sdf.applyPattern(formatStr);
		return sdf.format(new Date());
	}
}


    固定字符 Generator
    Generator 接口的实现类,用于生成固定字符。固定字符 Generator 比较简单,即直接返回配置的字符串。
    清单 5. 固定字符 Generator
public class StringGenerator implements IGenerator{
	private static final String type = GeneratorTypeSet.STRING;
	@Override
	public String getGeneratorType() {
		// TODO Auto-generated method stub
		return type;
	}
	@Override
	public String generate(String format,Map paraMap) {
		return format;
	}	
}


    流水号生成引擎 Engine
    流水号引擎需要负责接收流水号生成规则,根据类型调用对应的各个 Generator 生成子序列串,最后将子序列串并拼接并返回最终的流水号。
    清单 6. 流水号生成引擎 Engine
public class SNGeneratorEngine {
	private static SNGeneratorEngine snGeneratorEngine = new SNGeneratorEngine();
	private static Map<String,IGenerator> generatorMap = new HashMap<String,IGenerator>();
	/**流水号生成规则*/
	private String formatStr; 
	/**子序列间分隔符*/
	private String splitChar = "#";
	/**子序列内部分隔符*/
	private String innerChar = "@";
	/**流水号子序列生成规则*/
	private String[] subFormatStr;
	/**
	 * 按照类型和 Generator 实例存放
	 * @param Generator
	 */
	public void addGenerator(IGenerator Generator)
	{
		generatorMap.put(Generator.getGeneratorType(),Generator);
	}
	private SNGeneratorEngine()
	{
	}
	public static SNGeneratorEngine getInstance()
	{
		return snGeneratorEngine;
	}
	/**
	 * 接收流水号格式字符串,并分割成子序列
	 * @param formatStr
	 */
	public void setFormatStr(String formatStr)
	{
		this.formatStr = formatStr;
		subFormatStr = this.formatStr.split(splitChar);
	}
	/**
	 * 生成流水号:分发给各个子序列 Generator 生成
	 * @param parameterMap
	 * @return
	 */
	public String generate(Map parameterMap)
	{
		StringBuffer seriableNumber = new StringBuffer();
		for(String format:subFormatStr)
		{
			seriableNumber.append(generateSubSN(format,parameterMap));
		}
		return seriableNumber.toString();
	}
	/**
	 * 根据类型调用子序列 Generator 生成
	 * @param generateSubSN
	 * @param parameterMap
	 * @return
	 */
	private String generateSubSN(String subFormatStr,Map parameterMap)
	{
		String[] innerSubStr = subFormatStr.split(innerChar);
		IGenerator Generator = this.getGenerator(innerSubStr[0]);
		return Generator.generate(innerSubStr[1],parameterMap);
	}
	/**根据 GeneratorType 获取 Generator 实例*/
	private IGenerator getGenerator(String generatorType)
	{
		return generatorMap.get(generatorType);
	}
}


    流水号生成应用 App
    流水号应用 SNGenerateApp.java 类,声明了 SNGeneratorEngine 对象的实例,并通过调用 addBuildInGenerator() 创建内置的 Generator 类并添加到 SNGeneratorEngine 中,通过 generateSN() 方法来发送生成流水号的请求。
    清单 7. 流水号生成应用 App
public class SNGenerateApp {
	private SNGeneratorEngine snGenEngine = SNGeneratorEngine.getInstance();
	/**
	 * 设置内置的生成器
	 */
	private void addBuildInGenerator(){
		snGenEngine.addGenerator(new CharacterSequenceGenerator());
		snGenEngine.addGenerator(new DateTimeGenerator());
		snGenEngine.addGenerator(new NumberSequenceGenerator());
		snGenEngine.addGenerator(new StringGenerator());
	}
	/**
	 * 添加 Generator,提供扩展功能
	 * @param generator
	 */
	public void addGenerator(IGenerator generator)
	{
		snGenEngine.addGenerator(generator);
	}
	/**
	 * 生成序列号
	 * @param snFormatStr 流水号格式字符串
	 * @param parameterMap 参数列表
	 * @return
	 */
	public String generateSN(String snFormatStr,Map parameterMap)
	{
		snGenEngine.setFormatStr(snFormatStr);
		return snGenEngine.generate(parameterMap);
	}
}


    应用举例
    现在,我们就通过创建 SNGenerateApp 实例并执行相应方法来模拟流水号的生成。
这里仍然以前文提到的国务院办公厅发文号(国办发〔2011〕48 号)为例来说明,其格式字符串为:“Str@ 国办发〔#DateTime@yyyy#Str@〕#NumSeq@0C0#Str@ 号”,使用 MockDB.SEQ_ID_1 的 sequence 获取自增数值,示例代码如下:
    清单 8. 应用示例代码 1
SNGenerateApp appication = new SNGenerateApp();//创建 App 实例
appication.addBuildInGenerator(); //设置内置 Generator
//1. 设定流水号生成规则 国务院办公厅发文号(国办发〔2014〕48 号)
System.out.println("1. 生成 国务院办公厅发文号(国办发〔2014〕48 号)");
//设定流水号生成规则
String snFormatStr = "Str@ 国办发〔#DateTime@yyyy#Str@〕#NumSeq@0C0#Str@ 号"; 
Map parameterMap1 = new HashMap(); //设定参数
parameterMap1.put(ParamConsts.PARAM_SEQ_ID, MockDB.SEQ_ID_1); //使用 sequence id 1 进行流水自增
for(int i=1;i<=5;i++)//生成 5 个流水号
{
 System.out.println("流水号"+i+":"+appication.generateSN(snFormatStr,parameterMap1));
}


    输出结果:
    1. 生成 国务院办公厅发文号(国办发〔2014〕48 号)
引用
流水号 1:国办发〔2014〕48 号
流水号 2:国办发〔2014〕49 号
流水号 3:国办发〔2014〕50 号
流水号 4:国办发〔2014〕51 号
流水号 5:国办发〔2014〕52 号

    如果我们要模拟生成常见的 ICP 备案号((沪 ICP 备 05172190 号))呢?很简单,只需要规则重新配置流水号的格式字符串,在这里对应的为“Str@ 沪 ICP 备 #NumSeq@8C0#Str@ 号”,无需修改任何代码。注意,一般来讲不同的流水号会用到不同的 sequence 数值,我们也做了模拟,这里传入 MockDB.SEQ_ID_2 的 sequence ID 来获取自增数值。
    清单 9. 应用示例代码 2
//2. 例如生成 :ICP 备案号(沪 ICP 备 05172190 号)
System.out.println("2. 生成 ICP 备案号(沪 ICP 备 05172190 号)");
//设定规则
snFormatStr = "Str@ 沪 ICP 备 #NumSeq@8C0#Str@ 号";
Map parameterMap2 = new HashMap();//设定参数
parameterMap2.put(ParamConsts.PARAM_SEQ_ID, MockDB.SEQ_ID_2);//使用 sequence id 2 进行流水自增
for(int i=1;i<=5;i++)//生成 5 个流水号
{
	System.out.println("流水号"+i+":"+ appication.generateSN(snFormatStr,parameterMap2));
}


    输出结果:
    2. 生成 ICP 备案号(沪 ICP 备 05172190 号)
引用
流水号 1:沪 ICP 备 05172190 号
流水号 2:沪 ICP 备 05172191 号
流水号 3:沪 ICP 备 05172192 号
流水号 4:沪 ICP 备 05172193 号
流水号 5:沪 ICP 备 05172194 号


    想必有读者会问,如果我想生成的流水号还有一些其他特殊的生成规则怎么办?也很简单,只需要编写符合你业务要求的 Generator,然后将这个 Generator 通过 SNGenerateApp.addGenerator() 添加进去,最后再配置相应的流水号格式字符串即可。这样我们就会发现,只要新添加了一个 Generator,那么这个应用就具备相应的子序列生成功能,能够为以后生成含有该子序列的任意流水号提供支持。换句话说,我们可以生成出任意流水号,只要其生成规则是基于当前已有的 Generator 的排列组合,所需要做的只是配置一下流水号格式字符串,是不是非常方便和灵活呢?这也正是应用 Command 模式给我们带来的巨大好处。

总结
    本文简单介绍了应用 Command 模式及其优点,并通过一个基于自定义的格式字符串的流水号生成的方案,进一步让读者了解应用场景。采用 Command 模式,我们将原本复杂的流水号生成过程化整为零,分解成各个可以重用的子序列,根据各子序列的类型派发给对应的 Generator,最终在拼接返回流水号。该实现方案适用于各种复杂规则流水号的生成,并且十分易于扩展和维护,在实际应用中也取得了不错的效果。通过本例,希望读者能够进一步了解 Command 模式,应用到适合的场景中。

参考资料
学习
    阅读 GoF(“四人帮”)《设计模式》书籍,原名《Design Patterns: Elements of Reusable Object-Oriented Software》深入了解更多设计模式。
    参考百度百科中关于 Command 模式的词条,可点击 command 模式。
    访问 “developerWorks Java 设计模式与建模专题”,了解更多设计模式相关的专题讨论与实践。
    developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。

猜你喜欢

转载自jacky-dai.iteye.com/blog/2310332
今日推荐