業務邏輯的強類型化(續)

業務邏輯的強類型化(續)
作爲一個好事者,我希望能夠給我周邊的人講解這種技術。他們對C++很不熟悉,但熟悉C#。於是,我打算把這種技術移植到C#中,以便於講解。說做就做。

我建了一個C#項目,把代碼拷貝過去,然後着手修改,這樣可以省些事。我立刻便遇到了問題。C#有泛型,相當於模板,但不支持非類型泛型參數,即int CurrType,只允許用一個類型作爲泛型參數。這樣我們就不能使用C++中耍的手法了(typedef currency<n>)。退而求其次,直接用類實現貨幣類型:

class RMB

{

public double _val;

}

class USD

{

public double _val;

}

這樣太繁瑣了,很多重複。我們可以用一個基類封裝_val,貨幣類從基類上繼承獲得:

class CurrBase

{

public double _val;

}

class RMB : CurrBase

{

}

class USD : CurrBase

{

}

貨幣類都是空的,它們的存在只是爲了創建一個新的類型。

現在處理貨幣轉換問題。C#不能重載operator=,所以只能使用一個helper函數泛型asign代替:

class utility

{

     public static void asign<T1, T2>(T1 c1, T2 c2)

         where T1 : CurrBase

         where T2 : CurrBase

     {

         c1._val = c2._val * utility.e_rate[c2.CurID(),c1.CurID()];

     }

}

這個asign函數是個泛型,泛型參數分別代表了兩個操作數,函數中執行了貨幣轉換。爲了能夠在匯率表中檢索到相應的匯率,我們必須爲基類和貨幣類定義抽象函數:

    public abstract class CurrBase

    {

        public double _val=0;

        public abstract int CurID();

    }

    public class RMB : CurrBase

    {

        public override int CurID()

        {

            return 0;

        }

}

基類中聲明瞭CurID()抽象方法,並在貨幣類中定義。這樣,便可以用統一的方式進行貨幣轉換了:

asign(rmb_, usd_);

還行,儘管不那麼漂亮,但也還算實用。不過,當我多看了幾眼代碼後,便發現這裏根本沒有必要使用泛型。完全可以利用OOP的多態實現同樣的功能:

     public static void asign(CurrBase c1, CurrBase c2)

     {

         c1._val = c2._val * utility.e_rate[c2.CurID(),c1.CurID()];

     }

不過也沒關係,使用方式還沒有變,代碼反而更簡單了。使用泛型畢竟不是我們的根本目的,對吧?

現在輪到運算符了。不過我不知該把泛型運算符定義在哪裏。按C#文檔裏的要求,運算符必須是類的static成員。但我的泛型運算符是針對許多個貨幣類的,定義在任何一箇中,對其他類似乎不太公平。於是,我決定嘗試將其定義在基類裏:

    public abstract class CurrBase

{

   …

        public static CurrBase operator+<T1, T2>(T1 c1, T2 c2)

           where T1 : CurrBase

           where T2 : CurrBase

        {

            …

        }

}

編譯器立刻還我以顏色:操作符根本不能是泛型!好吧,不能就不能吧,繼續退而求其次,用OOP:

    public abstract class CurrBase

{

   …

        public static CurrBase operator+(CurrBase c1, CurrBase c2)

        {

            …

        }

}

不過,這次讓步讓得有點離譜。當我寫下這樣的代碼時,編譯器居然不認賬:

rmb_=rmb_+usd_;

錯誤信息是:錯誤 CS0266: 無法將類型“st_in_cs.CurrBase”隱式轉換爲“st_in_cs.RMB”。存在一個顯式轉換(是否缺少強制轉換?)。

我非得采用強制類型轉換,才能過關:

rmb_=(RMB)(rmb_+usd_);

太誇張了,這樣肯定不行。於是,我被迫在每個貨幣類中定義operator+:

class RMB : CurrBase

{

   public RMB operator+(RMB c1, USD c2)

   {

       …

   }

   public RMB operator+(RMB c1, UKP c2)

   {

       …

   }

}

這可不得了,我必須爲每對貨幣類定義一個+操作符,+操作符的總數將會是貨幣類數量的平方!其他的操作符每個都是貨幣類數的平方。我可受不了!

好在,可愛的OOP爲我們提供了一根稻草,使得每個貨幣類的每個操作符只需定義一個:

class RMB : CurrBase

{

   public RMB operator+(RMB c1, CurrBase c2)

   {

       …

   }

}

這樣,任何貨幣類都可以作爲第二操作數參與運算,而操作符只需定義一個。這樣的工作量,對於一個逆來順受的程序員而言,還是可以接受的。很好,代碼不出錯了:

rmb_=rmb_+usd_;

但當我寫下如下代碼時,編譯器又開始抱怨了:

ukp_ = rmb_ + usd_;

還是要求顯示轉換,除非我們爲UKP定義隱式類型轉換操作符:

class UKP

{

    public static implicit operator UKP(RMB v)

    {

        …

    }

}

光有RMB的不行啊,還得有USD的、JPD…。不過這樣的話,我們必須爲每一個貨幣類定義所有其它貨幣類的類型轉換操作符。又是一個組合爆炸。到這裏,我已經黔驢技窮了。誰讓C#不支持=操作符重載和操作符模板化呢。沒辦法,只能忍着點了。

不過,如果我們能夠降低點要求,事情還是有轉機的。如果我們不通過操作符,而是採用static成員方法,進行貨幣的運算的話,就可以省去很多代碼了:

    public class utility

    {

        public static T1 asign<T1, T2>(T1 c1, T2 c2)

            where T1 : CurrBase, new()

            where T2 : CurrBase

        {

            c1._val = c2._val * utility.curr_rate[c2.CurID(),c1.CurID()];

            return c1;

        }

        public static T1 add<T1, T2>(T1 c1, T2 c2)

            where T1 : CurrBase, new()

            where T2 : CurrBase

        {

            T1 t=new T1();

            t._val=c1._val + c2._val *

               utility.curr_rate[c2.CurID(),c1.CurID()];

            return t;

        }

       …

}

這裏,我還是使用了泛型,因爲這些函數需要返回一個值,只有使用泛型,才能返回一個明確的類型,以避免強制轉換的要求。於是,賦值和計算的代碼就成了:

asign(jpd_, asign(ukp_, add(rmb_, usd_)));//也就是jpd_=ukp_=rmb_+usd_

的確是難看了點,但是爲了能夠少寫點代碼,這也只能將就了。

好了,我盡了最大的努力,試圖在C#中實現強類型、可計算的貨幣系統。儘管最終我可以在C#中開發出一組與C++具有相同效果的貨幣類(除了賦值操作以外),但需要我編寫大量的代碼,實現各種計算操作,以及貨幣類之間的類型轉換操作(組合爆炸)。相比C++中總共200來行代碼,的確複雜、臃腫得多。

我並不希望把這篇文章寫成“C++ vs C#”,(儘管我很高興看到C++比C#強J)。我希望通過對這樣一個代碼優化任務,顯示不同技術運用產生的結果。同時,也可以通過這兩種實現嘗試的對比,瞭解泛型編程的作用,以及泛型編程對語言提出的要求。

毋庸置疑,C++採用了純粹的泛型編程,因此可以對問題進行高度抽象。並利用問題所提供的每一點有助於抽象的信息,以最簡的形式對問題建模。而作爲以OOP爲核心的語言C#,對泛型的支持很弱。更重要的是,C#的泛型對泛型參數的運用嚴重依賴於泛型參數的約束(where)。如果沒有where,C#將泛型參數作爲Object類型處理,此時泛型參數沒有意義(我無法訪問該類型的成員)。如果有了where,C#要求泛型參數必須同where中指定的類型有繼承關係(如asign中的T1必須是CurrBase的繼承類)。而泛型函數中對泛型參數的使用也侷限在約束類型(即CurrBase)上。於是,我們可以直接用以基類(CurrBase)爲參數的asign函數代替泛型版本的asign。由於C#對泛型參數的繼承性要求,使得泛型被困住了手腳,無法發揮應用的作用。正由於這些問題,C++才採用了現在模板的形式,而沒有采用同C#一樣的泛型模式。

或許有人會說,既然OOP能解決問題(asign最初不需要泛型也行,但最終還需要泛型來控制返回值的類型),爲什麼還要GP呢?

對於這個問題,前面也給出了答案。由於C#的泛型不支持非類型泛型參數,因此迫使我們使用傳統OOP的手段:利用基類實現貨幣類的實現,定義貨幣類來創建新類型,使貨幣強類型化,利用虛函數提供貨幣獨有信息。僅這一層面,OOP方式已經大大不如GP方式了,GP僅定義了一個模板,所有的貨幣類型都是通過typedef一句語句產生,無需更多的代碼。而OOP方式要求必須爲每一個貨幣編寫一個類,代碼量明顯多於GP方式。

此後,C++通過重載一組操作符模板,實現貨幣的運算。而貨幣模板以及生成貨幣類型的typedef都無須任何改變。而在C#中,由於不支持泛型操作符,被迫定義大量的特定於類型的操作符。所有運算操作符,在每個貨幣類中都必須重載一次。而轉型操作符,則必須在每個貨幣類中重載n-1次。

換句話說,有n種貨幣,有m個操作符(包括轉型操作符),那麼就需要定義n+1個類(包括基類),n×m+n×(n-1)個操作符。假設n=10,m=10,那麼總共需要11個類定義,190個操作符重載!如果每個類定義需要20行代碼,而每個操作符重載需要5行代碼,那麼總共需要1170行代碼。如果貨幣數量增加,總的代碼數將以幾何級數的方式增長。

上面的計算表明,儘管OOP可以解決問題,實現我們的目標,但所帶來的開發量和維護量卻是難以承受的。而且,OOP的方式擴展非常困難,隨着系統規模的擴大,擴展將越來越困難。所有這些都表明一點,儘管OOP是軟件工程的明星,但在實際情況下,很多地方存在着OOP無法解決或難以解決的問題。這也就是爲什麼業界的先鋒人物不斷拓展和強化泛型編程的原因。
 

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