易码技术论坛

 找回密码
 加入易码
搜索
查看: 437649|回复: 17

[教程] [C++]Get与Set--关于property的操作

[复制链接]
发表于 2007-1-23 01:44:22 | 显示全部楼层
久违的F流教程,沙发抢了~
发表于 2007-1-23 13:02:09 | 显示全部楼层
板凳
收回去仔细看^_^
发表于 2007-1-23 19:14:02 | 显示全部楼层
受教了~
 楼主| 发表于 2007-1-23 21:39:45 | 显示全部楼层
补完了……
前一半能当教程讲,后一半是自己YY的。可以略过=.=
发表于 2007-1-23 22:42:34 | 显示全部楼层
关于那个Sequence,既然不是Image的属性,那么可以考虑再外面再封装一层:
  1. public class RenderFrame
  2. {
  3. private:
  4.    Imgae *m_image;
  5.    int m_sequence;
  6. public:
  7.    int GetSequence();
  8.    void SetSequence(int value);
  9.    Image *GetImage();
  10.    void SetImage(Image *newImage);
  11. }
复制代码
这样会不会好一点~
--------------------------
以上约等于YY=v=
 楼主| 发表于 2007-1-24 00:53:03 | 显示全部楼层
如果是“包涵”的方式,那么就应该是你这种设计方式。
但是一开始我们希望是“继承”的方式……也就是说,写好一些基本元素,然后靠c++的多继承来拼出新的东西。

=v=
使用只能单继承的语言就不会出这种问题了……
发表于 2007-1-24 02:29:40 | 显示全部楼层
我也不是什么“包涵”的意思,把Image类(及其子类)考虑成一种“材料”级元素。然后我们再使用的时候重新封装它。
例如上面的RenderFrame需要一个渲染的图像和渲染顺序。
我们还可以在别的类似Render地方地方定义一个类FooClass,然后也可以在里面封装Image,做一些foo函数。
而Image类只用管好自己的工作就好。
 楼主| 发表于 2007-1-24 12:36:40 | 显示全部楼层
ls说的不就是包含么=_=
重新封装,那么RenderFrame不就拥有了一个Image类型的成员么?这便是包含啊……
Image亦可以作为base class而被RenderFrame继承,这时RenderFrame可以实现同样的逻辑。

如果用Image和RenderFrame来说明问题,可能继承方式有点牵强。但除去它们本身的字面意义,从结构上说,包含和继承就是这样啊……

假如Java或者其他OO的语言,譬如c#或者VB.NET,无法多继承,因此多使用包含方式来组合几个元件。而c++提供了多继承,所以通过继承方式来组合在c++中很常见。
发表于 2007-1-24 15:30:18 | 显示全部楼层
哦,了解……
 楼主| 发表于 2007-1-24 22:49:03 | 显示全部楼层
大概我应该说“组合”

貌似某本书上号召“组合优于继承”。在我所知的语言种,除了c++彪悍的可以多继承之外,其他只能单继承的地方也只好"组合"了=.=

但是越来越倾向于这种观点了,"组合"的时候多写点,以后改起来少受罪=v=
发表于 2007-1-24 23:11:07 | 显示全部楼层
Java ,c#都有接口(Interface)来实现类似与C++中的多继承语法结构的~
(依稀记得好像有篇文章证明了Java中的inner class&interface与c++的多继承在语法功能上等价)


另外,象这种类的设计不应该只从语法上来考虑用继承还是用组合吧?
我现在对OO的理解就是尽可能的用对象来模拟现实中的模型,所以如果A既是A1又是A2,我就用A extends A1,A2;如果A1,A2是A的两个部分,就用组合。
当然,有时这种关系不是很明显,就尽可能用组合模式了,组合模式比用继承要考虑的事情简单多了,而且修改的时候也方便。
 楼主| 发表于 2007-1-25 00:34:45 | 显示全部楼层
说到继承接口的事情……即使语法上等价,写起来感觉不一样的。c++的多继承就是把子类写好了拼起来就OK,有点代码复用的味道,而接口本身是不能带有任何实现的。所以肯定不爽……

我不是在鼓吹多继承,只是感觉这个东西在c++里面用起来比较爽=v=其他形似的手法都不太爽,与其去模仿不如老老实实的组合。

还有,问题往往不是出现在设计初期,即使刚开始的设计合理,日后需求还是会发生变化。所以还是遵循一条比较普适的原则比较好……考虑问题还可以简单化。我现在比较倾向于组合......
发表于 2007-1-25 00:56:15 | 显示全部楼层
....
个人不喜欢在公共接口中带有具体的实现代码,觉得这样会增加类之间的藕合性.
我现在有点OO控了,一些代码首先就想怎么设计几个接口(interface)把程序框架建立起来....
但是总很难一下子把问题完全抽象的很合理.
不知道学些对象建模方面的知识是不是会好点。

还是用面向过程思考容易,从这边写到那边,只是维护起来麻烦。
 楼主| 发表于 2007-1-25 14:00:38 | 显示全部楼层
昨天我明明发了这么一贴啊=v=
有本叫做《设计模式》的好书,可以让OO更愉快~
发表于 2007-1-26 04:17:14 | 显示全部楼层
~OO~
好像有个姓阎的人写过这么一本书,在图书馆扫过一眼,当时感觉写的挺玄的,没有细看.
 楼主| 发表于 2007-1-26 11:43:51 | 显示全部楼层
不是中国人写的,可以考虑看中文译版,或者英文原版:
作者是GoF:EG、RH、RJ、JV,四个老外博士……
《Design Patterns - Elements of Reusable Object-Oriented Software》
《设计模式——可复用面向对象软件的基础》
发表于 2007-1-26 12:47:06 | 显示全部楼层
哦,那个四人帮滴书啊~
 楼主| 发表于 2007-1-23 00:42:51 | 显示全部楼层 |阅读模式
在Blog中的文章链接:第一部分§第二部分

对于某个object(对象)的property(属性),究竟该怎样编写它的操作方法?
于是有了下面粗浅的想法,个人见解,有待高人批评指教=.=

这里说的property和data members(成员变量)不同,关于后者在《Effective C++》的条款22里面有详细介绍。大师说:“应该将data members统统声明为private”,详细理由翻书便得。
于是我们先有了下面的代码:
  1. template <class T>
  2. class Object
  3. {
  4. public:
  5.    T GetData(void) {return m_Data};
  6.    void SetData(const T& data) {m_Data = data};
  7. private:
  8.    T m_Data;
  9. };
复制代码
这段代码定义了一个Object类,它有一个data member是m_Data。Get/Set方法用于m_Data的读/写操作。因为c++拥有template、inline,世界变得非常美好。但是GetData和SetData可以更加丰富多彩,不仅仅是return以及赋值。并且鉴于template让代码变得难看,造成诸多额外考虑因素,我们干脆简化代码到如下形式:
  1. class Object
  2. {
  3. public:
  4.    int GetData(void);
  5.    void SetData(int data);
  6. private:
  7.    int m_Data;
  8. };
复制代码
标题所说的property可以用data members来表示,但是我们不关心这个问题。在讨论property的时候,我们只关心它的操作方式,Get(读操作)和Set(写操作)就是它的全部。因此代码进一步转化成这个样子:
  1. class Object
  2. {
  3. public:
  4.    int GetProperty(void);
  5.    void SetProperty(int value);
  6. };
复制代码
我们可以想象这个property牵连到class的很多内部实现,获取它需要经过某种运算,因此Get不会是写一个return这么简单。如果property的改变也会影响很多方面,那么Set的实现保证每个受到影响的地方都被正确的处理。单看这个class的话,只要写好Get和Set,这个property的操作就不会出问题。

可惜事情还没有结束。考虑这种情况:
  1. class Derived:public Object
  2. {
  3. public:
  4.    int GetProperty(void);  // 和Object::GetProperty实现方式不同
  5.    void SetProperty(int value); // 和Object::SetProperty实现方式不同
  6. };
复制代码
这个derived class(派生类)也会拥有具有同样的property。假如Derived对property的操作等同与Object,那么没有问题。假如Derived需要额外增加一些操作才能得到正确的值,这样写就不行了。因为就这样简单的在Derived中将Object的Get/Set方法override(覆盖),在某些情况下会出问题。
譬如将一个Derived实例赋值给Object&或者Object*,然后调用Get/Set。真正调用到是Object的方法而不是Derived中被Override的方法。
  1. void foo(void)
  2. {
  3.    Derived de;
  4.    Object& ob=de;
  5.    ob.GetProperty(); // 会调用Object::GetProperty
  6. }
复制代码
详细的说明请翻阅《Effective C++》的条款36:“绝不要重新定义继承来的non-virtual方法”。因此解决这个问题的办法就是将Object中的Get/Set声明为virtual。代码如下:
  1. class Object
  2. {
  3. public:
  4.    virtual int GetProperty(void);
  5.    virtual void SetProperty(int value);
  6. };
  7. class Derived:public Object
  8. {
  9. public:
  10.    virtual int GetProperty(void);  // 和Object::GetProperty实现方式不同
  11.   virtual void SetProperty(int value); // 和Object::SetProperty实现方式不同
  12. };
  13. void foo(void)
  14. {
  15.    Derived de;
  16.    Object& ob=de;
  17.    ob.GetProperty(); // 会调用Derived::GetProperty
  18. }
复制代码

接下来考虑一个比较抽象的设计。为了避免抽得太厉害,就假设这个object是用来表示一张2D图像资源吧。譬如它可以是一个从bmp文件里面载入的图像,或者是从屏幕上截取的图像,或者是渲染过程中生成的图像。作为一个2D的图像,我们希望能获取它的宽度和高度信息,因此这两个数值是它的property。
但是在不同的图像系统中,图像资源的储存方式是不同的。譬如DirectDraw中图像存放在surface(表面)里,D3D的图像资源可以存放在texture(材质)里,OpenGL的图像资源既可以保存在texture里,也可以直接按照象素点颜色值储存在数组里。从这些来源获取图像宽高的方式显然也是各不相同的,为了解决这个问题,我们先定义这个一个东西:
  1. class Image
  2. {
  3. public:
  4.    virtual int GetWidth(void) = 0;
  5.    virtual int GetHeight(void) = 0;
  6. };
复制代码
它是一个抽象类,拥有两个纯虚函数(宽和高各一个)。对于不同的API中的图像,可以让它们都继承自Image,然后各自实现这两个方法。对于使用者来说,他只对顶层的Image进行操作,不关心具体的实现细节。虚函数的机制保证它的Get方法都能得到正确的调用,当然也根本没有出错的可能,因为Image不包含任何实现,它只定义了接口。可以说Image只是一个规范,它强制Image的derived class必须实现这两个方法,否则作为抽象类无法被实例化……
说了这么多,可以用代码表示如下(Image的声明就不重复了):
  1. class ImageD3D:public Image
  2. {
  3. public:
  4.    virtual int GetWidth(void); // 里面实现D3D中的Get方法
  5.   virtual int GetHeight(void); // 里面实现D3D中的Get方法
  6. };
  7. class ImageOGL:public Image
  8. {
  9. public:
  10.    virtual int GetWidth(void); // 里面实现OpenGL中的Get方法
  11.   virtual int GetHeight(void); // 里面实现OpenGL中的Get方法
  12. };
  13. void foo(void)
  14. {
  15.    ImageD3D  d;
  16.    ImageOGL  o;
  17.    Image*  i;
  18.    i = &d;
  19.    i->GetWidth(); // 调用D3D的方法
  20.   i = &o;
  21.   i->GetWidth(); // 调用OpenGL的方法
  22. };
复制代码
这是多态的一个运用,Image的宽和高这两个property的读取方法就这样解决了。我们注意到宽和高是没有Set操作的,因为对于一个已经存在的图像资源来说,这两个property是固定的,它来自于object自身。

以上为铺垫……接下来的内容属于个人参考意见,有待时间的检验=_=

接下来我们扩展Image的定义,假如Image会被放入一个渲染的队列中去,对于若干副图片系统需要知道谁该被先画出来,因此它还应该有一个表示顺序的property。依照前面的方式扩展后的定义如下:
  1. class Image
  2. {
  3. public:
  4.    virtual int GetWidth(void) = 0;
  5.    virtual int GetHeight(void) = 0;
  6.    virtual int GetSequence(void) = 0; // 增加了一个获取渲染顺序的方法
  7. };
复制代码
然后derived class分别实现GetSequence就可以了………是这样么?我们不妨多问一句:GetSequence是必须的么?
如果按照上面的定义,都用纯虚函数来表现,那么所有的derived class都必须实现GetSequence方法。假如适应于某个系统的derived class不需要这个值,也得写一份出来,否则无法实例化对象。偷懒的方式提供一份默认的实现方法:
  1. class Image
  2. {
  3. public:
  4.    virtual int GetWidth(void) = 0;
  5.    virtual int GetHeight(void) = 0;
  6.    virtual int GetSequence(void) {return 0;}; // 默认返回0值
  7. };
复制代码
这样derived class就不必被强迫去实现一个不必要的方法了。但是,仍然有问题:
渲染顺序是图像本身的property么?

分析一下这个property的作用我们不难发现,它更应该是一个由外界来决定的东西。譬如这副图像被另外一副图像遮挡了,那么渲染的时候它的顺序就该靠前一些。显然两副图像不可能知道对方的存在,这需要有一个更高层的协调系统来控制它们的渲染顺序,因此Set方法是必要的。于是代码变成了这样:
  1. class Image
  2. {
  3. public:
  4.    virtual int GetWidth(void) = 0;
  5.    virtual int GetHeight(void) = 0;
  6.    virtual int GetSequence(void){return 0;}; // 默认返回0
  7.   virtual void SetSequence(int value) = 0; // 设置渲染顺序
  8. };
复制代码
这下就出现混乱了。显然上述代码的GetSequence和SetSequence不配套了,后者也得提供默认的行为。否则derived class被强迫实现一份Set,却忘记实现对应的Get,那么Set的值将永远不起作用。引入一个data member可以解决这个问题(我就不写关于宽和高的操作了):
  1. class Image
  2. {
  3. public:
  4.    virtual int GetSequence(void){return m_Sequence;};  //获取渲染顺序
  5.    virtual void SetSequence(int value) {m_Sequence = value;}; // 设置渲染顺序
  6. private:
  7.    int m_Sequence; // 记得在构造函数里面初始化
  8. };
复制代码
但是并没有完全解决这个问题。假如derived class中Set被override,但是它无法操作m_Sequence,因为m_Sequence被声明为private。它需要显示的调用Image::SetSequence来做这件事情。Get方法也一样有这个问题。如果它们没有这样做……那么二者仍然是不配套的。问题出在override上面,去掉virtual告诉后来人不要做任何override尝试,这是一种解决方法。

可惜这样会丧失灵活性,derived class需要附加一些操作上去就变得不方便了。我们可以尝试去掉一个virtual,譬如相对于Get来说,可能Set更有可能需要额外的操作。所以去掉Get的virtual,变成这样:
  1. class Image
  2. {
  3. public:
  4.    int GetSequence(void){return m_Sequence;};  //获取渲染顺序
  5.   virtual void SetSequence(int value) {m_Sequence = value;}; // 设置渲染顺序
  6. private:
  7.    int m_Sequence; // 记得在构造函数里面初始化
  8. };
复制代码
derived class看到这样的架势……或许会迷惑?因为Get/Set是成对的操作,所以在无法override其中之一的情况下,它应该知道最好保证原来的方法被调用。所以Set被override的时候,Image::SetSequence()也会被调用,以保证Get的正确。并且由于少了一个virtual,所以出错的几率减少了一半[Belial]

上诉假设的前提是derived class的作者对于这种东西保持足够的敏感……当然也有可能是我自己的奇怪念头,有待进一步讨论了。在RTG的Graphic模块设计的时候,就遇到很多这样的问题。头大啊头大……留给时间做检验吧。
您需要登录后才可以回帖 登录 | 加入易码

本版积分规则

Archiver|手机版|小黑屋|EMAX Studio

GMT+8, 2024-4-25 04:21 , Processed in 0.014916 second(s), 18 queries .

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表