程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 一道考驗你設計能力的C++編程題

一道考驗你設計能力的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, faced等,簡單而高效。


最後我們對這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   www.2cto.com
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打交道就可以了:

 

\
源代碼下載: http://www.BkJia.com/uploadfile/2012/0619/20120619094552671.rar
http://www.BkJia.com/uploadfile/2012/0619/20120619094553222.rar
作者:Richard Wei

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved