1. 程式人生 > >一道考驗你設計能力的C++程式設計題

一道考驗你設計能力的C++程式設計題

看到這道題,我們就開始設計這個影象類了,按照面向物件“依賴倒置”的設計原則,我們站在客戶的立場,來考慮我們這個類該提供哪些介面,很快我們設計瞭如下一個類: 

class CSimplePicture
{
public:
    CSimplePicture(char* init[], int nCount); 
    CSimplePicture(CSimplePicture& p1, CSimplePicture& p2, bool bVerCat);

    void Frame();
    void Print(std::ostream& os) const
; protected: std::vector<std::string> m_arData; };

CSimplePicture(char* init[], int nCount);
根據字串陣列構造一幅影象.

CSimplePicture(CSimplePicture& p1, CSimplePicture& p2, bool bVerCat);
根據兩幅影象構造一幅影象,bVerCat表明是縱聯接還是橫聯接.

void Frame();
給影象物件加框

void Print(std::ostream& os) const;
列印輸出影象

std::vector<std::string> m_arData;
儲存影象資料的字串陣列

下面來考慮具體實現,這個對於有一定開發的經驗的人來說還是很容易的,就不具體寫了,
CSimplePicture(char* init[], int nCount)無非是資料的拷貝,CSimplePicture(CSimplePicture& p1, CSimplePicture& p2, bool bVerCat)就是把2幅圖片的資料連線,合在一起,void Frame()修改裡面的資料加上邊框,void Print(std::ostream& os) const遍歷字串陣列輸出。


根據上面的設計和實現,應該已經滿足我們這個題目的要求了。
但是客戶的需求是多變的,現在客戶又有一個新的需求,要求把一幅圖片去掉邊框。
另外客戶覺得我們這個圖片類的效能太差了,每次加框或是合成圖片都要大量的記憶體拷貝。

這時我們傻眼了,該死的客戶,根據我們上面的設計,根本不支援這些新功能,因為我們儲存的是影象的內部的字串資料,根本不知道它是不是加框過的,另外我們的影象資料本身就是不支援共享的。

 

接下來我們就要重新考慮設計了,如何讓我們的影象物件支援UnFrame(去邊框)操作,關鍵是要建立我們的影象型別層次,這樣就可以判斷是否是加框的類物件,於是有了如下的類層次:
//圖象介面基類
class CPic_Base 
{};

//字串影象類
class CPic_String: public CPic_Base
{};

//加框影象類
class CPic_Frame: public CPic_Base
{}

//縱聯接影象類
class CPic_VCat: public CPic_Base
{};

//橫聯接影象類
class CPic_HCat: public CPic_Base
{};

然後我們考慮如何共享影象資料,這就要用到智慧指標了,智慧指標在C++裡一般有2種實現,一種是STL 裡的auto_ptr,還有一種就是基於引用計數。auto_ptr的本質是擁有關係,也就是你擁有了這物件後,別人就不能擁有了,所以這裡不符合我們的要求。引用計數是個好東西,對於共享物件特別有用,COM裡的IUnknow介面就是基於這個技術的,還有很多指令碼語言裡變數自動銷燬,實際上都是基於引用計數的技術。這裡分享一個基於引用計數的智慧指標類。

class CRefCountBase
{
public:
    CRefCountBase()
    {
        m_nRefCount = 0;
    }

    int GetRefCount() const
    {
        return m_nRefCount;
    }

    int AddRefCount()
    {
        return ++m_nRefCount;
    }

    int SubRefCount()
    {
        return --m_nRefCount;
    }

    void ResetRefCount()
    {
        m_nRefCount = 0;
    }

private:
    int    m_nRefCount;
};

template<typename T>
class CRefPtr
{
public:
    T* operator->() const
    {
        return m_pRawObj;
    }

    T& operator()() const
    {
        return *m_pRawObj;
    }

    T& operator*() const
    {
        return *m_pRawObj;
    }

    T* GetPtr() const
    {
        return m_pRawObj;
    }

    bool IsNull() const
    {
        return m_pRawObj == NULL;
    }

    CRefPtr()
    {
        m_pRawObj = NULL;
    }

    CRefPtr(T* p)
    {
        m_pRawObj = p;
        if(p != NULL)
        {
            p->AddRefCount();
        }
    }

    CRefPtr(const CRefPtr& ref)
    {
        m_pRawObj = ref.m_pRawObj;
        if(m_pRawObj != NULL)
        {
            m_pRawObj->AddRefCount();
        }
    }

    ~CRefPtr()
    {
        if(m_pRawObj != NULL && m_pRawObj->SubRefCount() == 0)
        {
            delete m_pRawObj;
        }
    }

    CRefPtr& operator = (const CRefPtr& ref)
    {
        if(this != &ref)
        {
            if(m_pRawObj != NULL
                && m_pRawObj->SubRefCount() == 0)
            {
                delete m_pRawObj;
            }

            m_pRawObj = ref.m_pRawObj;

            if(m_pRawObj != NULL)
            {
                m_pRawObj->AddRefCount();
            }
        }

        return *this;
    }

    bool operator == (const CRefPtr& ref) const
    {
        return m_pRawObj == ref.m_pRawObj;
    }

    CRefPtr<T> Copy()
    {
        if(m_pRawObj != NULL)
        {
            T* p = new T(*m_pRawObj);
            p->ResetRefCount();

            return p;
        }
        else
        {
            return NULL;
        }
    }

private:
    T* m_pRawObj;
};

這樣使用這個類:

class A: public CRefCountBase
{
Public:
    Void fun1();
};

CRefPtr<A> p = new A;
p->fun1();

重新設計我們的CPic_Base,

class CPic_Base: public CRefCountBase
{
public:
    virtual ~CPic_Base() {}

    //列印輸出影象
    void Print(std::ostream& os) const;

    //返回影象寬度
    virtual int GetWidth() const = 0;

    //返回影象高度
    virtual int GetHeight() const = 0;

    //返回某行的影象字串資料
    virtual std::string GetLineData(int nLineIndex) const = 0;

    //返回去掉邊框的物件
    virtual CRefPtr<CPic_Base> GetUnFrame() const { return NULL; }
};

這裡Print方法實現就很簡單了:

void CPic_Base::Print(std::ostream& os) const
{
    for(int i=0; i<GetHeight(); ++i)
    {
        os << GetLineData(i);
        os << "\n";
    } 
}

然後考慮實現CPic_String

class CPic_String: public CPic_Base
{
public:
    CPic_String(char* p[], int nCount);

    virtual int GetWidth() const;
    virtual int GetHeight() const;
    virtual std::string GetLineData(int nLineIndex) const;


protected:
    std::vector<std::string> m_arData;
};

這個類裡儲存真正的字串影象資料,裡面方法的實現也很簡單,和最開始的的第一種實現類似,就不詳寫了。

再考慮實現CPic_Frame

class CPic_Frame: public CPic_Base
{
public:
    CPic_Frame(CRefPtr<CPic_Base>& pic);

    virtual int GetWidth() const;
    virtual int GetHeight() const;
    virtual std::string GetLineData(int nLineIndex) const; 

    virtual CRefPtr<CPic_Base> GetUnFrame() const { return m_pic; }

protected:
    CRefPtr<CPic_Base> m_pic;
};

可以看到這裡我們引用了一個其他的影象資料,而不是真正儲存這些資料,方法實現也很簡單, 主要依賴於m_pic所指向的影象類,同時m_pic是個基於引用計數的智慧指標, 所以賦值時也沒有記憶體拷貝, 注意GetUnFrame這個方法只有這裡返回非NULL,表示只有這種物件支援去邊框。

CPic_Frame::CPic_Frame(CRefPtr<CPic_Base>& pic)
: m_pic(pic)
{
    _ASSERTE(!m_pic.IsNull());
}

int CPic_Frame::GetWidth() const
{
    return m_pic->GetWidth() + 2;
}

int CPic_Frame::GetHeight() const
{
    return m_pic->GetHeight() + 2;
}

string CPic_Frame::GetLineData(int nLineIndex) const
{
    int nWidth = GetWidth();
    int nHeight = GetHeight();

    _ASSERTE(nLineIndex < nHeight && nLineIndex >= 0); 

    if(nLineIndex == 0 //first line and last line
        || nLineIndex == nHeight - 1)
    {
        int nPadding = nWidth - 2;
        return string("+") + string(nPadding, '-') + string("+");
    }
    else
    {
        return string("|") + m_pic->GetLineData(nLineIndex - 1) + string("|");
    }
}

再考慮實現CPic_VCat

class CPic_VCat: public CPic_Base
{
public:
    CPic_VCat(CRefPtr<CPic_Base>& pic1, CRefPtr<CPic_Base>& pic2);

    virtual int GetWidth() const;
    virtual int GetHeight() const;
    virtual std::string GetLineData(int nLineIndex) const;

protected:
    CRefPtr<CPic_Base> m_pic1;
    CRefPtr<CPic_Base> m_pic2;
};

他裡面儲存了上下2個影象物件,方法實現是也不復雜,就不具體寫了。

另外CPic_HCat也是類似:

class CPic_HCat: public CPic_Base
{
public:
    CPic_HCat(CRefPtr<CPic_Base>& pic1, CRefPtr<CPic_Base>& pic2);

    virtual int GetWidth() const;
    virtual int GetHeight() const;
    virtual std::string GetLineData(int nLineIndex) const;

protected:
    CRefPtr<CPic_Base> m_pic1;
    CRefPtr<CPic_Base> m_pic2;
};

有了上面的實現,現在我們可以這麼實現我們需要的功能了:

Int main()
{
    char* init1[] = {"Paris", "in the", "Spring"};
    CRefPtr<CPic_Base> p1 = new CPic_String(init, 3); 

    CRefPtr<CPic_Base> p2 = new CPic_Frame(p1);

    CRefPtr<CPic_Base> p3 = new CPic_VCat(p1, p2);

    P3->Print(cout);
    CRefPtr<CPic_Base> p4 = p2->GetUnFrame();
}

這時我們發現這樣對於客戶呼叫很不友好,因為我們內部實現的類層次都暴露給客戶了,而這些資訊對客戶來說應該都是透明的,我們應該再封裝一個更簡單的介面類給客戶。

於是有了如下的設計,其實介面類似我們的第一種實現。

class CPicture
{
public:
    CPicture(char* p[], int nCount);
    CPicture(CPicture& p1, CPicture& p2, bool bVerCat);

    void Frame();
    bool UnFrame();

    friend std::ostream& operator << (std::ostream& os, const CPicture& pic);

protected:
    CRefPtr<CPic_Base> m_pic;
};

std::ostream& operator << (std::ostream& os, const CPicture& pic);

這樣對客戶來說他們只需要和CPicture打交道,根本不用關心內部的實現。
這個類的實現也很簡單:

CPicture::CPicture(char* p[], int nCount)
{
    m_pic = new CPic_String(p, nCount);
}

CPicture::CPicture(CPicture& pic1, CPicture& pic2, bool bVerCat)
{
    if(!bVerCat)
    {
        m_pic = new CPic_HCat(pic1.m_pic, pic2.m_pic);
    }
    else
    {
        m_pic = new CPic_VCat(pic1.m_pic, pic2.m_pic);
    }
}

void CPicture::Frame()
{
    m_pic = new CPic_Frame(m_pic);
}

bool CPicture::UnFrame()
{
    CRefPtr<CPic_Base> p = m_pic->GetUnFrame();
    if(!p.IsNull())
    {
        m_pic = p;
    }

    return !p.IsNull();
}

std::ostream& operator << (std::ostream& os, const CPicture& pic)
{
    pic.m_pic->Print(os);
    return os;
}

下面是我們使用這個類的程式碼:

char* init1[] = {"Paris", "in the", "Spring"};
char* init2[] = {"Hello world", "every", "thing", "is", "OK!"};

int main(int argc, char* argv[])
{
    CPicture p1(init1, 3);
    CPicture p2(init2, 5);

    //
    std::cout << p1;
    cout <<endl << endl; 

    //
    std::cout << p2;
    cout <<endl << endl; 

    //
    p2.Frame();
    cout << p2;
    cout <<endl << endl; 

    //
    p1.Frame();
    p1.Frame();
    cout << p1;
    cout <<endl << endl;

    //
    CPicture pHorCat(p1, p2, false);
    cout << pHorCat;
    cout <<endl << endl; 

    //
    CPicture pVerCat(p1, pHorCat, true);
    cout << pVerCat;
    cout <<endl << endl; 

    //
    pVerCat.Frame();
    cout << pVerCat;
    cout <<endl << endl; 

    //
    pVerCat.Frame();
    cout << pVerCat;
    cout <<endl << endl; 

    //
    pVerCat.UnFrame();
    pVerCat.UnFrame();
    cout << pVerCat;
    cout <<endl << endl; 

    system("pause");

    return 0;
}

可以看到使用起來非常方便和友好,執行截圖:

可以看到使用第二種實現我們只儲存了一份字串影象資料,同時有保留了影象的層次和結構屬性,實現時包含了很多設計模式,比如Template, Decorate, Composite, Facade等,簡單而高效。


最後我們對這2種實現方式作下比較:

方法1的優勢是資料完整,修改一個物件時不會影響其他物件,因為每個物件都是資料的單獨拷貝。劣勢是低效,不能體現物件的結構屬性,我們不知道這個物件是加邊框的物件還是上下合成的物件。


方法2的優勢是高效,資料共享,同時有保留有物件的結構屬性。劣勢是修改一個對像時會影響其他的物件,因為他們可能是共享同一個物件。實際上,對於基於引用計數的共享物件,還有一種叫做Write Copy(寫入時拷貝)的技術,就是如果你要修改一個物件,就自己拷貝一份。同時引用計數技術還有一個風險就是迴圈引用,比如A引用了B,B也引用了A,這2個物件就永遠沒法釋放了,這也是要謹慎的。

 

上面完美的解決了我們UnFrame(去邊框)的問題,我們正對我們使用基於引用計數的技術來完美的構造字串影象類層次而洋洋得意,但是好景不長。


一個星期後,客戶又找到你提了他的新需求,他想讓你的CPicuture類增加一個功能,能返回一個XML格式的字串來告訴他該物件的構造過程。
比如
+-------+
|Paris   |
|in the |
|Spring |
+-------+
返回的XML串是
< CPic_Frame >
<CPic_String> Paris in the Spring </CPic_String>
</ CPic_Frame >

+-------+Paris
|Paris  |in the
|in the |Spring
|Spring |
+-------+

返回的XML串是:

< CPic_HCat >
< CPic_Frame >
<CPic_String> Paris in the Spring </CPic_String>
</ CPic_Frame >
<CPic_String> Paris in the Spring </CPic_String>
</ CPic_HCat >


+-------+Paris
|Paris  |in the
|in the |Spring
|Spring |
+-------+
Paris
in the 
Spring

返回的XML串是:

<CPic_VCat>
< CPic_HCat >
< CPic_Frame >
<CPic_String> Paris in the Spring </CPic_String>
</ CPic_Frame >
<CPic_String> Paris in the Spring </CPic_String>
</ CPic_HCat >
<CPic_String> Paris in the Spring </CPic_String>
</CPic_VCat>

 

你不禁抱怨道,該死的客戶,上次已經因為要支援UnFrame功能而讓我改變了最初的設計,如果沒有客戶的新需求,開發該是一件多麼美好的事情。

但是抱怨歸抱怨,客戶就是上帝,你還是隻能硬這頭皮把事情做完。
那現在讓我們來考慮如果實現這一功能。

一開始想到的當然是在我們的CPic_Base基類中增加一個介面,比如
String GetStructXMLString();

但是面向對像的設計原則告訴我們,介面不該隨便改動,實際上次CPic_Base裡為UnFrame而增加的CRefPtr<CPic_Base> GetUnFrame()介面已經讓你覺得很不爽,感覺這個介面和我們的影象物件沒有直接關係。

那麼我們是否考慮可以重構CPic_Base介面,讓它能以外掛的形式實現各種功能,也就是說我們的類層次這裡是固定的,但是方法卻可以一直增加而不影響原有的程式碼。

這時我們想到了Visitor模式,它基本上是為我們這類需求而量身定做的。
對於Visitor模式的架構,基本上是固定的,定義個IPic_Visitor

class IPic_Visitor
{
public:
    virtual void VisitPicString(CPic_String& pic) {};
    virtual void VisitPicFrame(CPic_Frame& pic) {} ;
    virtual void VisitPicVCat(CPic_VCat& pic) {};
    virtual void VisitPicHCat(CPic_HCat& pic) {};

    virtual ~IPic_Visitor() {}
};

在我們的CPic_Base基類裡增加一個Accept介面virtual void Accept(IPic_Visitor& visitor) = 0;
這樣影象物件就可以讓各種型別的Visitor訪問了,各個影象類的實現也很簡單:

void CPic_String::Accept(IPic_Visitor& visitor)
{
    visitor.VisitPicString(*this);
}
void CPic_Frame::Accept(IPic_Visitor& visitor)
{
    visitor.VisitPicFrame(*this);
}
void CPic_VCat::Accept(IPic_Visitor& visitor)
{
    visitor.VisitPicVCat(*this);
}
void CPic_HCat::Accept(IPic_Visitor& visitor)
{
    visitor.VisitPicHCat(*this);
}

好了,現在我們用一個新Visitor來改寫我們原來的UnFrame功能,

class CUnFrameVisitor: public IPic_Visitor
{
public:
    virtual void VisitPicFrame(CPic_Frame& pic);

public:
    CRefPtr<CPic_Base> GetUnFrameResult();

protected:
    CRefPtr<CPic_Base> m_picRet;
};

因為Visitor方法都是沒有返回值,引數也是固定的,所以一般都是通過在Visitor裡儲存成員變數和返回介面來實現返回值的。
這樣實現就很簡單了:

void CUnFrameVisitor::VisitPicFrame(CPic_Frame& pic)
{
    m_picRet = pic.m_pic;
}

CRefPtr<CPic_Base> CUnFrameVisitor::GetUnFrameResult()
{
    return m_picRet;
}

可以看到只有訪問 CPic_Frame才有非空的返回值;其他都是用預設的空方法,最終返回的也就空物件。

這樣我們在最終暴露的CPicture裡實現UnFrame也就很簡單了:

bool CPicture::UnFrame()
{
    CUnFrameVisitor vistor;
    m_pic->Accept(vistor);

    CRefPtr<CPic_Base> pRet = vistor.GetUnFrameResult();
    if(!pRet.IsNull())
    {
        m_pic = pRet;
    }

    return !pRet.IsNull();
}

接下來我們考慮如何實現客戶的要求返回XML串的需求,實際上我們前面的Visitor模式已經為我們準備好了條件,我們只需要新增加一個Visitor

class CStructXMLVisitor: public IPic_Visitor
{
public:
    virtual void VisitPicString(CPic_String& pic);
    virtual void VisitPicFrame(CPic_Frame& pic);
    virtual void VisitPicVCat(CPic_VCat& pic);
    virtual void VisitPicHCat(CPic_HCat& pic);

public:
    std::string GetStructXMLString() { return m_strStructXML;}

protected:
    std::string m_strStructXML;
};

實現也不復雜:

void CStructXMLVisitor::VisitPicString(CPic_String& pic)
{
    m_strStructXML = "<CPic_String>";
    int nHeight = pic.GetHeight();
    for(int i=0;i<nHeight; ++i)
    {
        m_strStructXML += pic.GetLineData(i);
    }
    m_strStructXML += "</CPic_String>";
}

void CStructXMLVisitor::VisitPicFrame(CPic_Frame& pic)
{
    CStructXMLVisitor v;
    pic.m_pic->Accept(v);
    m_strStructXML = "<CPic_Frame>";
    m_strStructXML += v.GetStructXMLString();
    m_strStructXML += "</CPic_Frame>";
}

void CStructXMLVisitor::VisitPicVCat(CPic_VCat& pic)
{
    m_strStructXML = "<CPic_VCat>";
    CStructXMLVisitor v1;
    pic.m_pic1->Accept(v1);
    m_strStructXML += v1.GetStructXMLString();

    CStructXMLVisitor v2;
    pic.m_pic2->Accept(v2);
    m_strStructXML += v2.GetStructXMLString();

    m_strStructXML += "</CPic_VCat>";
}

void CStructXMLVisitor::VisitPicHCat(CPic_HCat& pic)
{
    m_strStructXML = "<CPic_HCat>";
    CStructXMLVisitor v1;
    pic.m_pic1->Accept(v1);
    m_strStructXML += v1.GetStructXMLString();

    CStructXMLVisitor v2;
    pic.m_pic2->Accept(v2);
    m_strStructXML += v2.GetStructXMLString();

    m_strStructXML += "</CPic_HCat>";
}

然後我們在我們的CPicture介面裡增加一個GetStructXMLString方法,實現也很簡單:

std::string CPicture::GetStructXMLString()
{
    CStructXMLVisitor v;
    m_pic->Accept(v);
    return v.GetStructXMLString();
}

可以看到,改用新的設計之後,以後我們再有什麼新需求,只要直接增加一個Visitor就好了, 所以說設計不是一層不變的,要根據需求不停的重構。

最後貼一下類圖,外部只要和CPicture打交道就可以了:

原始碼下載:  ConsolePicture_1.rar

                ConsolePicture_2.rar
注:(1)該題引自《C++沉思錄》
      (2)C++11裡已經有基於引用計數的智慧指標share_ptr, 所以以後就不用自己寫了,迴圈引用的問題也可以通過weak_ptr解決.

摘要: http://www.cppblog.com/weiym/archive/2012/06/12/178472.html