我的工作需要写一个话单转换工具,在写这个工具的过程中,发现整个实现恰恰可以说是策略模式最好的体现。也许用这个例子来说明策略模式的应用,最是合适。该话单转换工具的目的,是将某个服务提供商的话单文本文件,转换为另一个服务提供商的话单文本文件。如将联通的话单格式转换为移动的话单格式。而话单转换工具的要求,是希望能实现多个服务提供商话单文本文件的互相转换。
我们来分析一下任务。首先,各种服务提供商的话单格式,无疑是不相同的。例如,话单采集后,字段的顺序,字段的宽度以及字段间的分隔符,都不相同。但是,从总体上来说,话单的表现形式是大致相同的,这为我们实现话单转换提供了一个技术上可行的前提。
所谓话单转换,就是需要将一个话单文本文件读出,然后对每一行的字符串进行识别后,再转换为符合相对应的服务提供商标准的话单文本文件。操作很简单,就是文本文件的读写而已,不同的就在于转换的方法。根据服务提供商标准的不同,我们应该为每一种转换提供相对应的算法。而所谓策略模式,正是对算法的包装和抽象,将算法的责任和其本身分离。所以,我们现在要做的工作就是将转换话单的算法抽象出来。
根据工具的要求,话单转换应该包括3个方法:
1、将文件读出的一行字符串转换为对应的话单对象;
2、将一种话单对象转换为另一种话单对象;
3、将话单对象转换为字符串,以方便写入话单文本文件;
根据以上的分析,我们还需要为不同的话单格式建立相应的对象。例如:网通、联通和移动的话单格式对象分别为:
接下来就应该实现话单转换的算法了。首先需要将算法进行抽象,而进行抽象的最佳选择莫过于使用接口,例如我们定义一个用于话单转换的接口ICdrConvert:
public interface ICdrConvert
{
}
按照如前的分析,在接口中应该包括三个转换方法。但是现在有个问题,就是转换的话单对象。由于方法在接口中,为一个抽象。而话单对象可能有多种,具体转换为何种话单对象,需要到具体实现时才能决定。因此,在接口方法中,无论返回类型,还是传入参数,涉及到的话单对象只能是抽象的。也许我们可以考虑System.Object来表示话单对象,但更好的办法是为所有的话单对象也提供一个抽象接口。
由于从目前的分析来看,抽象话单对象并没有一个公共的方法,所以这个抽象的话单接口,是一个标识接口:
public interface ICdrRecord
{
}
现在,可以对转换接口的方法进行定义了:
自然,这样的接口定义无法通过编译。为什么呢?是因为第二个方法的签名与第三个方法的签名重复了(方法的签名和返回类型无关)。因此,我们需要为第三个方法修改名字。
但我们仔细想想,第三个方法的转换在转换接口中是必要的吗?该方法的任务是将一个话单对象转换为string类型。实际上这个责任,并不需要专门的转换对象来完成,而应属于话单对象本身的责任。再想想.Net中,所有对象均派生于System.Object,而object类型均提供了ToString()方法。
从设计的角度来看,最好的办法,是在具体的话单对象中override System.Object的ToString()方法,而不是在转换对象中,提供该转换算法。
不过考虑到,在话单处理中,更多地会调用抽象话单接口类型的对象,也许我们将ToString()方法抽象到接口ICdrRecord中会更好。
而具体的话单对象,就应该实现ICdrRecord接口了。因为各话单对象均继承了System.Object,则间接地继承了object对象的ToString()方法,所以,话单对象应该重写该方法:
下面就是关键的实现了。由于我们已经为转换算法进行了抽象,因此根据策略模式来实现具体的转换算法,就是水到渠成的事情了。实现代码之前,先来看看UML类图:
注意看橙色部分,这一部分即为策略模式的主体。接口ICdrConvert为抽象策略角色,类CNCToCUC,CUCToCM为具体策略角色,它们分别实现了将网通话单转换为联通话单,联通话单转换为中国移动话单的算法。根据实际需要,还可以添加多个类似的具体策略角色,并实现ICdrConvert接口:
类CUCToCM的实现相似,不再重复。
那么通过策略模式实现,究竟有什么好处呢?请大家注意上图的CdrOp类。该类是抽象类,它提供了一个构造函数,可以传递ICdrConvert对象:
类CdrFileOp继承了抽象类CdrOp:
这个类,实现了抽象类CdrOp的HandleCdr()方法。但具体的实现细节则是在私有方法Read()和Write()中完成的(根据实际情况,也可以把Read和Write方法作为公共抽象方法或保护方法,放到抽象类CdrOp中,而在抽象类CdrOp中具体提供HandleCdr方法的实现,该方法调用Read和Write方法,这样就使用了模版方法模式)。
注意看Read和Write方法中,没有一个具体类。不管是话单对象,还是话单转换对象,均是抽象接口对象。尤其是在Read()方法中,调用了_Convert的Convert方法:
cdr = _convert.Convert(_convert.Convert(line));
其中,内部的Convert方法,即_convert.Convert(line),是将读出来的字符串转换为ICdrRecord对象,然后通过调用_convert.Convert(ICdrRecord record)方法,再将该对象转换为另一种话单格式对象,但类型仍然属于ICdrRecord。
那么在这些转换过程中,究竟转换成了哪一种话单格式对象呢?这是由_convert字段来决定的。而这个对象则是由构造函数的参数中传递进来的。
同样的道理,在Write()方法中,大家也可以看到为所有话单对象抽象为一个接口ICdrRecord的好处。通过ICdrRecord调用ToString()方法,避免了在CdrFileOp中引入具体对象。要知道,程序一旦引入具体对象,则耦合性就高了。一旦需求发生改变,就需要对编码进行修改。
有了以上的架构,客户端调用就非常方便了:
当然,我们还可以引入工厂模式来创建CdrFileOp对象。或者将ICdrConvert对象设置为CdrFileOp的公共属性,而非通过构造函数来传入。
通过本文的讲解,你会发现策略模式并没有什么神秘的,无非还是一种对象的抽象,唯一不同的是,策略模式是对算法的抽象而已。所以,“抽象才是硬道理”,当我们在作面向对象设计时,需时时刻刻记住这句话啊!