如何設計接口的參數以減少對接口的修改

或許說這個東西的時候,最好能依託一個背景。


在稍大型一點的項目中,總會有一個base層,我們認爲它封裝了最最底層和基礎的一些列功能,因爲底層的東西追求穩定和運行效率,所以90%是用C/C++寫的,一般以頭文件+DLL的方式給上層使用(不考慮它是基於COM的,如果是COM,VARIANT的參數類型就不在討論範圍內了)。頭文件中定義了一些列導出函數或者導出類,這些導出函數或類的成員函數,都會有一些列參數,由於C/C++是強類型語言,所有強類型語言對類型轉換都是極其嚴格的,不能像javascript裏那樣用var j = ... 的形式搞定一切。所以,底層接口中函數的參數如何設計是非常重要的。

假設有一個接口void A(int a),就只能接受一個int類型作爲參數。

1、當需求變了,需要處理string類型時,就要修改A或者增加新的接口A1;

     void A(int a);

     void A(const string& str);

或void A1(const string& str);

2、當參數個數變了,需要接受兩個int時,仍然需要修改;

   void A(int a, int b);

   如果兩天後上層又提需求,說需要三個int參數...好吧,再改

   void A(int a, int b, int c);

如果確實修改了這個接口A的參數類型或者參數個數或者增加新的接口A1,則必然導致至少兩中問題:

1、所有引用這個頭文件的cpp文件重編,而一個底層模塊在整個項目中使用的普遍性是非常高的,那麼最嚴重的情況就是,修改了一個頭文件,造成整個項目重編;

2、所有調用過這個接口的上層代碼都需要重新修改,更悲劇的是,還需要重新測試。


所以,如何才能涉及出具有很強適應力和擴展性的接口及參數類型,對於底層的接口是很重要的,也是必須的。


大概總結了一些,目測有這麼幾種方案,有些是坑爹的,有些在某些場合特定場合比較使用,有些比較通用。

1. void*

void*做參數在純C語言寫的代碼裏還是挺常見的,比如一個接口void A(void* p);

那如果你在A裏對p進行某些類型轉換,比如double *pd = (double*)p; 而傳入的p原先是int* pn,那就慘了,多半*pd 不是原來的*pn,這就是用void*做參數的悲劇之處,它不攜帶原來的類型信息,對於使用者來說不知道應該怎麼轉,而且轉了就有風險。這種void*參數現在幾乎是絕對不允許使用的。

2.聯合體類型

struct param
{
      int id;
      union BaseArg
    {
        struct CommonEventArg
        {
            RECT         rcItem;
        }CommonEventArgs;
        struct RightMenuArg
        {
            BOOL bShowDesk;
            BOOL bWndMoved;
            int  nIconSize;
        }RightMenuArgs;
        struct ItemDragArg
        {
            RECT rcBegin;
            RECT rcEnd;
        }ItemDragArgs;
        struct ItemSelectArg
        {
            BOOL isSelected;
        }ItemSelectArgs;
        struct BoxItemUpdateArg
        {
            RECT rcBegin;
        }BoxItemUpdateArgs;
        struct BoxRenameArg
        {
            wchar_t *pszName;
        }BoxRenameArgs;
        struct FileChangeArg
        {
            LPITEMIDLIST  pItem;
            LPITEMIDLIST  pAdditionItem;
        }FileChangeArgs;
        struct RightMenuResponseArg
        {
            int nX;
            int nY;
        }MenuResponse;
        struct StringArg
        {
            const wchar_t *pszName;
        }StringArgs;
    }Data;
};
比如這樣一個結構體參數param,它包了一個聯合體,這樣做的思路也很清晰,接口這麼定義:void A(const param& p);當需要變時,就去改param裏面的結構就好,外頭不用動。裏面增加了聯合體的包裝,其實是把這種思路優化了一把,因爲如果param使用場合很多,用到N多種結構體,那麼一個param對象就佔用很多內存,而我們知道聯合體並不會給它的每一個成員分配內存,而是用它內存需要最多的那個成員的內存長度作爲整個聯合體的內存長度,這樣,就着實省了一把內存。

這樣的涉及,比較常見的應用場合貌似是消息的響應,似乎MFC裏的消息響應就是這麼涉及的,Mouse消息、LBtn消息等等各自有不同的子struct包在聯合體內。

3.json做參數

json做爲一種小巧輕便易解析,最重要的強大的可修改性和可擴展性(這點有上面第二點struct+union的意思,但更強大)的玩意,不做參數實在是有點可惜,貌似我知道的的比較早的使用在網絡傳輸,以及客戶端和web方通信上比較多,其實網絡傳輸也可以看成是一次函數調用嘛,那json就可以理解成這個函數調用的參數了。

4.模版

模版生來就是爲了泛化的,經典的 int Add(int a, int b)經過模版化後就可以處理所有數值類型的加法操作了,但問題是什麼呢?問題就是模版函數或模板類不適合作爲模塊接口,如果是在模塊內用模版那是完美的設計,但如果在模塊接口一級用模版,那就悲催了。因爲上面說了,模塊一般是以頭文件+DLL的方式提供,而模版的一個特點就是不支持分離編譯(這個不清楚的自行google),就是說,模版的實例化是要在編譯時才決定的,你把模版函數的聲明和實現分別放在頭文件和cpp中,那是不行滴,必須都放在頭文件中,那這樣也就不叫模塊話了,直接全給頭文件就行了,boost大部分是這麼搞的。

5.boost::any

這個東西是boost提供的又一牛逼東西,實現了類似於var j = ...的傻瓜式參數類型,相當於把本身強類型的C/C++中的參數封裝成javascript中的弱類型。

void my_func(boost::any a)
{
    if(a.type() == typeid(int))
    {
        //int類型
    }
    else if(a.type() == typeid(string))
    {
        //string類型
    }
    //...
}
my_func(10);
my_func("123");
class custom
{
    int b;
    double d;
};
custom cus;
my_func(cus);


typedef std::list<boost::any> list_any;
list_any.push_back(10);
list_any.push_back("123");
class custom
{
    int b;
    double c;
    char *p;
};
custom cus;
list_any.push_back(cus);

一個python中的列表就誕生了。
貌似設計模式裏的開放封閉原則套到函數參數的設計上也是適用的吧,提高參數擴展性,儘量不修改。

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