業務邏輯的強類型化

業務邏輯的強類型化
 
業務邏輯中,很多邏輯上不同類型的東西,到了編程語言中,時常會退化成一種類型。一個最簡單的例子就是貨幣。通常在我們編程時,採用一種類型,如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一書中,對此有詳細的講解。
 

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章