2010年11月24日 星期三

泛型與設計範式(Design Patterns)缺少了什麼

我最近在看候捷/於春景翻譯Andrei Alexandrescu的Modern C++ Design。從事程式工作這些年來,對於template的使用,我還只是停留在T容器的概念,雖然還在研讀中,不過已讓我對template的設計有了新的體會。對於許多以前想不通的ATL設計方式,覺得慢慢能掌握了。

以一個程式開發者角度來使用與開發template是完全不同的等級(使用與開發class也是不同等級)。使用template相對簡單的多,但為何template始終無法推廣成為設計的主流模式?Template缺少了什麼關鍵東西?

首先,要程式設計師能把行為抽象化來思考本身就是很具有難度的一件事。光是物件虛擬化就可以難倒眾多的"程式設計師",當把行為也抽象化後,每個物件所需具有的行為特性就更難以理解。而目前的STL又無法提供出唯一的模型,許多模板都有各種效能、安全性、擴充性的考量,造成許多的變化與組合,當套用的模板不合宜時,結果往往出乎意料,而template的除錯,則是極為艱困的一件工作。

更別提不同的工具對template語法上的差異,我還記得最早在VC 6.0上學習template時,為了一個分行的問題,耗去我一整天的時間。而且每次Visual Studio改版、移植到Mac平台,或多或少都要調整template的語法對其做修正。

其次,template與繼承不同,我的感覺是template class更像是interface。在實作一個template class時像是在玩數獨遊戲一樣,你必需把所有的空格(function)都填寫完畢,且填寫的答案符合他的遊戲規則時才能正常的運作。但困難點在於,當我們在使用還不熟悉的模板時,我如何能得知我要填入多少個空格?MSDN Help只會告訴我們這個模板有什麼功用,卻不會列出套用這個模板的class T必需要具有什麼樣的功能,看著那一大串有看沒有懂的編譯錯誤,許多許用者寧可回到原始的模式一行一行的打著自已能夠看的懂的code也不願再接觸那傳說很方便的template。

對於一個使用template的開發人員,我們要的是什麼。我認為我們真正要的是一個好的精靈(wizard)工具。如果ATL沒有了ATL wizard,不知道有多少人能夠正確的做出一個ATL元件,奇怪的是,這麼多年來,怎麼都沒有一個像樣的STL wizard來協助我們使用template?

這個STL wizard至少應提供2個功能,一個是提供我們"組合"上的選擇測試,另一個則是必需能夠引導我們一步一步的完成這個模板所需的所有函式(至少編譯無錯)。在這個工具出現前,我相信設計範式的模板只是少數人才玩的起的工匠技藝。

2010年11月11日 星期四

CMyCommandLineInfo

在遠古的DOS時代,我們常會在可執行檔後面加上一大堆的command來指定特殊的功能,進到了視窗模式後command已經很少有人在使用了。今天就來談談要始何在透過MFC實現這種原始人才會想用的特殊技藝。

不論產生的是MDISDI或是dialog baseapp,都可以在自動產生的CWinAppInitInstance中找到這段code

CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);

if (!ProcessShellCommand(cmdInfo))
    return FALSE;

ParseCommandLine會將執行的指令進行剪貼轉換為CCommandLineInfo物件的格式,而ProcessShellCommand則依傳入的cmdInfo所擁有的屬性進行相對應的處理。

ParseCommandLine內的作用方式如下,如果我們想做的指令不在預設的列表中時,我們必需改寫ParseParam來對應我們想要的功能。

void CWinApp::ParseCommandLine(CCommandLineInfo& rCmdInfo)
{
    for (int i = 1; i < __argc; i++)
    {
        LPCTSTR pszParam = __targv[i];
        BOOL bFlag = FALSE;
        BOOL bLast = ((i + 1) == __argc);
        if (pszParam[0] == '-' || pszParam[0] == '/')
        {
            // remove flag specifier
            bFlag = TRUE;
            ++pszParam;
        }
        rCmdInfo.ParseParam(pszParam, bFlag, bLast);
    }
}

極簡單的範例:

我在開發一些演算法時常需要反覆測試一些已知的影像比較程式修改前修改後的差異,我懶得每次開啟程式時都要去按File選單來選擇我上一次測試的影像,這時我就會在我的debug條件中加入”/default”command

繼承自CcommandLineInfo產生CMyCommandLineLifo類別,並覆載(override) ParseParam函式。小心,UnicodeMulti-Byte有一點點的不同,若想讓這類別二者通吃必需多覆載針對UnicodeParseParam

完成後記得把InitInstanceCCommandLineInfo cmdInfo改為CMyCommandLineInfo cmdInfo

class CMyCommandLineInfo :  public CCommandLineInfo
{
public:
    virtual void ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast);
#ifdef _UNICODE
    virtual void ParseParam(const char* pszParam, BOOL bFlag, BOOL bLast);
#endif
};

void CMyCommandLineInfo::ParseParam(const TCHAR* pszParam, BOOL bFlag, BOOL bLast)
{
    if (bFlag)
    {
        const CString strParam(pszParam);
        if(strParam.CompareNoCase(_T("default")) == 0)
        {
            m_nShellCommand = CCommandLineInfo::FileOpen;
            m_strFileName   = AfxGetApp()->GetProfileString(_T("Recent File List"),_T("File1"));

            ParseLast(bLast);

            return;
        }
    }

    CCommandLineInfo::ParseParam(pszParam, bFlag, bLast);
}

#ifdef _UNICODE
void CMyCommandLineInfo::ParseParam(const char* pszParam, BOOL bFlag, BOOL bLast)
{
    const CString strParam(pszParam);

    ParseParam(strParam, bFlag, bLast);
}
#endif

只為了擴充單一功能而必需繼承產生一個新類別,每次遇到這種例子我就有點懷念Object-C

2010年11月4日 星期四

Button Array

我想建一個如下圖所示的對話盒界面。


當我在Resource Edit中double click [Method 1]的check box,Visual Studio 會自動幫我們加入相對應的click message函式

afx_msg void OnBnClickedCheckMethod1();

與其Message Map

ON_BN_CLICKED(IDC_CHECK_METHOD_1, &CSetCompareMethodDlg:: OnBnClickedCheckMethod1)

我程式中的功能會將此時的相對應Method元件設為enable或disable

void CSetCompareMethodDlg::OnBnClickedCheckMethod1()
{
    if(CButton* pCheck = (CButton*)GetDlgItem(IDC_CHECK_METHOD_1))
    {
        m_pObj[0]->Enable((pCheck->GetCheck() == 0) ? false : true);
    }
}

以我的例子而言,我要做8個非常類似的函式,其中只有ID與index不同。這並不會造成我們什麼困擾只是程式的一點怪味而已。

那,該如何讓這個動作變得優雅呢。

關鍵在於能否讓UI的動作包含ID。我們可以用ON_COMMAND_EX取代ON_BN_CLICKED來改寫。

1. 在class宣告中加入

afx_msg BOOL OnBnClickedCheckMethodEX(UINT uid);

2. 把

ON_BN_CLICKED(IDC_CHECK_METHOD_1, &CSetCompareMethodDlg:: OnBnClickedCheckMethod1)

改寫為

ON_COMMAND_EX(IDC_CHECK_METHOD_1, &CSetCompareMethodDlg::OnBnClickedCheckMethodEX)

同樣的動作把IDC_CHECK_METHOD_2 ~IDC_CHECK_METHOD_8都用ON_COMMAND_EX取代

3. 實作OnBnClickedCheckMethodEX

BOOL CSetCompareMethodDlg:: OnBnClickedCheckMethodEX(UINT uid)
{
    if(CButton* pCheck = (CButton*)GetDlgItem(uid))
    {
        // m_pObj[index]->Enable((pCheck->GetCheck() == 0) ? false : true);
    }

    return TRUE;
}

4. 現在的問題是如何把uid和index對應起來,有2種方法可行。如果你無法更動Resource.h的話。

BOOL CSetCompareMethodDlg:: OnBnClickedCheckMethodEX(UINT uid)
{
    const UINT nCheckID[8] = {
        IDC_CHECK_METHOD_1, IDC_CHECK_METHOD_2, IDC_CHECK_METHOD_3, IDC_CHECK_METHOD_4,
        IDC_CHECK_METHOD_4, IDC_CHECK_METHOD_6, IDC_CHECK_METHOD_7, IDC_CHECK_METHOD_8
    };

    for(int index=0 ; index<8 ; index++)
    {
        if(uid == nCheckID[index])
        {
            if(CButton* pCheck = (CButton*)GetDlgItem(uid))
            {
                pObj[index]->Enable((pCheck->GetCheck() == 0) ? false : true);
            }

            return TRUE;
        }
    }
    return FALSE;
}

如果你可以更動Resource.h的話,我會先到Resource.h中把ID做排序

#define IDC_CHECK_METHOD_1 1004
#define IDC_CHECK_METHOD_2 1005
#define IDC_CHECK_METHOD_3 1006
#define IDC_CHECK_METHOD_4 1007
#define IDC_CHECK_METHOD_5 1008
#define IDC_CHECK_METHOD_6 1009
#define IDC_CHECK_METHOD_7 1010
#define IDC_CHECK_METHOD_8 1011

然後函式能寫的更簡捷

BOOL CSetCompareMethodDlg::OnBnClickedCheckMethodEX(UINT uid)
{
    int index = uid - IDC_CHECK_METHOD_1;

    if(CButton* pCheck = (CButton*)GetDlgItem(uid))
    {
        pObj[index]->Enable((pCheck->GetCheck() == 0) ? false : true);

        return TRUE;
    }

    return FALSE;
}

當然要改ID你必需要很小心很小心。