业务逻辑中,很多逻辑上不同类型的东西,到了编程语言中,时常会退化成一种类型。一个最简单的例子就是货币。通常在我们编程时,采用一种类型,如double(有些系统中有专门的Currency类型,为了简便起见,这里使用double),来表示货币。
但是,随之而来的就是币种的问题。不同的币种间存在换算,也就是汇率的问题。比如我们有RMB和USD两种货币,分别表示人民币和美元。尽管都是货币(在代码中有相同的类型),我们却不能对他们直接赋值:
double rmb_;
double usd_=100;
rmb_=usd_;
//绝对不行,100美元可相当于768元人民币,尽管人民币在升值
必须进行汇率换算:
rmb_=usd_*e_rate;
e_rate就是汇率。这个谁都清楚。在逻辑上,100美元和768元人民币是等价的(假设今天的汇率是7.68),是可以兑换的。但在软件中,我们不能简单的赋值了事,必须做换算。
现在我们希望用代码直接表现逻辑上的意义,也就是用赋值操作:=,实现货币间的换算,该怎么做呢?啊对,没错,操作符重载。
我们可以重载operator=操作符,使其具备汇率换算的功能。(或许有人会提出异议,改变一个操作符已有的语义,是否违背大师们的教诲。但我个人认为,语义应当遵从业务逻辑,既然按照逻辑含义进行重载,不应该引发什么纠纷。否则还需要重载干吗?)但问题是,重载依赖于不同的类型,double operator=(double)的操作符定义是默认的,已经存在,无法以相同形式重载。再说,即便是可以,复制对象和被赋值对象的类型相同,如何区分两种类型的转换呢?
很明显,我们需要新的类型。typedef肯定是没指望的,因为它仅仅为一个类型起别名,并没有产生新的类型。所以,我们只能求助于类。我们可以以如下方式定义各种不同的货币类:
class RMB
{
public:
double _val;
};
class USD
{
public:
double _val;
};
…
这样,便可以针对不同货币重载operator=:
class RMB
{
public:
RMB operator=(const RMB& v) {
_val=v._val;
}
RMB operator=(const USD& v) {
_val=v._val*e_rate; //货币换算
}
public:
double _val;
};
class USD
{
public:
USD operator=(const USD& v) {
_val=v._val;
}
USD operator=(const RMB & v) {
_val=v._val/e_rate; //货币换算
}
public:
double _val;
};
这样,我们便可以对两种货币赋值了:
RMB
rmb_;
USD
usd_;
rmb_=usd_;
//带货币换算的赋值操作
根据这个方法,我们一直往下推,可以构造出各种各样的货币,并且定义它们之间的转换:
class UKP //英镑
{…}
class JPD //日元
{…}
…
不过有个问题,如果有10中货币,我们必须定义100个operator=的重载,而且都是些重复代码。这样太蠢了。得采用更好的方法才能实现我们的理想。
注意观察,每个货币类的代码都符合同一种模式,有很强的规律性。看出来了吧,这种情况非常适合使用C++的超级武器——模板。没错,说做就做:
template<int CurrType>
class Currency
{
public:
double _val;
};
注意看,这里非常规地使用了模板的一个特性:非类型模板参数,就是那个int CurrType。模板参数通常都是一个类型,比如int什么的。但也可以是一个非类型的模板参数,就象这里的CurrType。传统上,非类型模板参数用于传递一个静态的值,用来构造模板类。但在这里,这个模板参数并没有被模板使用,也永远不会被使用。这个模板参数的作用就是“制造类型”:
typedef
Currency<0> RMB; //人民币
typedef
Currency<1> USD; //美元
typedef
Currency<2> UKP; //英镑
typedef
Currency<3> JPD; //日元
…
typedef本身不会产生新的类型,但是这里Currency<
n>已经是完全不同的类型了。当一个模板被实例化成一个类的时候,只要模板参数的实参有所不同,便是一个不同的类型。我们利用了模板的这个特性,凭空制造出任意多个结构完全相同,但却是完全独立的类型。
好,下一步,便是重载operator=操作符。当然不能再做为每一对货币类型重载operator=的蠢事了。用一个成员函数模板就可以解决问题:
double e_rate[10][10];
//汇率表
template<int CurrType>
class Currency
{
public:
template<int ct2>
Currency<CurrType>& operator=(count Currency<ct2>& v) {
_val=v._val * e_rate[ct2][CurrType]; //找出汇率表中相应的汇率,
// 计算并赋值
}
public:
double _val;
};
操作符operator=的代码中,赋值对象v的值乘上一个汇率,这个汇率存放在汇率表中,通过模板参数CurrType和ct2检索(当然汇率表得足够大)。
这样,我们便可以随意地赋值,而无须关心货币转换的问题了:
///初始化汇率表
e_rate[0][0]=1;
e_rate[1][0]=7.68;
…
//使用货币
USD
usd_;
UKP
ukp_;
JPD
jpd_;
jpd_=usd_=ukp=rmb_;
//成功!一切顺心。
需要说明的是,汇率表并没有在声明时就初始化,是考虑到汇率经常变动,不应当作为常量写死在代码中。更进一步可以使用一个类封装成可变大小的汇率表,甚至可以用某个文件或数据库对其初始化。
问题当然还有,货币是要参与运算的,否则没有什么用处。所以,我们还得使这些货币具备基本的计算能力。货币的计算,根据业务逻辑大致应具备以下能力:
1. +、-:两种货币的加法和减法,允许不同种货币参与计算,必须考虑转换操作,返回做操作数类型;
2. *、/:货币乘上或除以一个标量值,这里设定为double。但两种货币不能相乘或相除。
3. ==、!=:比较两种货币,允许不同种货币参与比较,但必须考虑转换操作。
还有其他的操作,暂不做考虑,毕竟这里的目的不是开发一个完整的货币系统。为了编码上的方便,这里同时还定义了四则运算的赋值版本:+=、-=、*=、/=。为了节省篇幅,这里只展示+、*和==的代码,其他代码类推:
template<int ty, int tp>
inline bool operator==(currency<ty>& c1, const currency<tp>& c2) {
return c1._val==c2._val*curr_rate[tp][ty];
}
template<int ty, int tp>
inline currency<ty>& operator+=(currency<ty>& c1, const currency<tp>& c2) {
c1._val+=c2._val*curr_rate[tp][ty];
return c1;
}
template<int ty, int tp>
inline currency<ty> operator+(currency<ty>& c1, const currency<tp>& c2) {
currency<ty> t(c1);
t+=c2;
return t;
}
请注意==和+操作符中的的货币转换运算,每次都是将第二操作数货币转换成第一操作数货币后再进行运算操作。第一参数和第二参数的类型不同,因此允许不同货币进行计算。这可以进一步简化代码,完全以逻辑的方式编程。
template<int ty>
inline currency<ty>& operator*=(currency<ty>& c1, const double q) {
c1._val*=q;
return c1;
}
template<int ty>
inline currency<ty> operator*(currency<ty>& c1, const double q) {
currency<T, ty> t(c1);
t*=q;
return t;
}
template<int ty>
inline currency<ty>& operator*=(const double q,currency<ty>& c1) {
return operator*=(c1, q);
}
template<int ty>
inline currency<ty> operator*(const double q,currency<ty>& c1) {
return operator*(c1, q);
}
…
*操作符的参数只有一个是货币类型,另一个是double类型,表示数量。只有货币乘上数量才有意义,不是吗?*操作符包括两个版本,一个货币在前,数量在后;另一个数量在前,货币在后。为的是适应rmb_*1.4和1.4*rmb_两种不同的写法,算法是完全一样的。
现在,货币可以运算了:
usd_=usd_*3;
//同种货币运算
ukp_=rmb_*2.5;
///计算後直接赋值给另一种货币
jpd_=ukp_=rmb_+usd_;
///同上,但有四种货币参与运算
现在,货币运算非常方便了,不需要考虑货币种类,货币的转换是自动的,无需额外代码。
在简化代码的同时,也提供了操作上的约束,比如:
ukp_=rmb_*usd_;
///编译错误。货币乘上另一种货币无意义!!!
这句代码会引发编译错误,因为我们没有为两种货币相乘提供*的重载。很明显,一种货币与另一种货币相乘是根本没有意义的。这里通过静态的重载类型检查,对施加在货币上的运算做出约束。促使违背逻辑的代码在第一时间被拦截,避免出现运行时错误。要知道,两种货币相乘,赋给另一个货币的错误是非常隐蔽的,只有盘库或结账的时候才会发现。
很好,这里我们利用了C++模板的一些特殊机制,以及操作符模板、操作符重载等技术,开发一个货币系统。这个系统可以用最简洁的语句实现各种货币的计算和转换功能。同时,还利用重载机制的强类型特性,提供了符合业务逻辑的操作约束。
货币运算只是一个简单的案例,但相关的技术可以进一步推广到更复杂的领域中。而且业务越复杂,所得到的收益越多。因此,充分理解并运用C++所带来的泛型编程功能,可以大大简化软件的开发、减少代码的错误,降低开发的成本。
这种技术适合用在一些逻辑上存在差异,但在物理上具备相同特征的实体上。一方面使这些实体在代码中强类型化,以获得重载和类型检测能力。由于代码中逻辑实体的对应类型强类型化,是我们可以通过重载和静态类型检测等技术手段,实现仅使用语言提供的要素,在代码中直接构造业务模型的能力。但手工对每一个逻辑实体进行强类型化,是费力的和琐碎的,并且存在着大量的重复劳动。此时,我们可以利用模板的抽象能力,反过来利用逻辑实体在物理上的共同特性,一次性构建抽象的模板,并利用模板实例化的一些特性,很方便地构造新的类型(仅仅一个typedef)。
这种技术进一步扩展后,可以有更高级的应用。一个经典的范例就是实现编译期的量纲分析。在
Template Meta-programming
一书中,对此有详细的讲解。