一道考验你设计能力的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
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解决.
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值