注册 登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

祝灾区人民早日度过难关

努力工作

 
 
 

日志

 
 

设计模式感悟之一到四  

2006-09-07 17:03:47|  分类: 默认分类 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

最近接触SeSe大牛的Wukong源代码,发现设计极为精妙。于是开始看《设计模式:可复用面向对象软件的基础》。我们应该努力成为软件架构者,而不能永远是蓝领工人。SeSe是我的榜样。

 

设计模式感悟之一:

    最近看<设计模式-可复用面向对象软件基础>,颇有些感悟,写出来供大家分享,有错误之处也请大家指正.

    为什么要尽量从抽象类继承?

    我曾经写过三个版本的坦克大战,然而一直不满意自己的设计,往往一开始设计得很完美,然而随着代码的增加,原来的类的封装一再被破坏.这是软件开发中常见的现象"熵递增原理",也就是软件结构趋于混乱.那么好吧,干脆再设计一遍.

    我是这么设计的:

    游戏中有动的物体如坦克和子弹,静的物体如障碍物.他们有不同的行为.于是有了以下设计:

    MyObject是基类;

    StaticObject和MovingObject继承MyObject

    动的物体有坦克,子弹和奖励(如加生命力等),于是,很自然地,为MovingObject增加三个子类Tank,Bullet,Bonus,它们都继承MovingObject

    接下去有个问题,哪些应该是抽象类?

    MyObject毫无疑问应该是抽象类,但MovingObject呢?我原来想把MovingObject设为非抽象类,原因是我认为Tank,Bullet和Bonus的运动规律差不多,如果MovingObject添加move()方法,Tank,Bullet和Bonus的move()方法就可以直接利用父类的mov()方法了.C++的出现不就是为了更好的实现代码重用么?OOP万岁!

    后来发现我错了,Tank,Bullet,Bonus的move()的实现是不一样的.而且我原来的想法很荒唐,父类怎么可能知道子类是如何运动的呢?如果一个父亲规定了所有儿子的所有行为都要和父亲一模一样,那么这个世界永远不会进步了.

    那么move()不设为虚函数可不可以呢?

    显然不可以,因为面向对象的一条重要原则就是"不要去重写父类的非虚函数",所以move()只能设为虚函数.

    那么move()不设为纯虚函数可不可以呢?

   我还是不死心,我发现Tank,Bullet,Bonus有部分属性是一样的.比如,他们的方位都可以用两个变量x,y(横坐标和纵坐标)表示,大小可以用两个变量w,h(长和宽)表示.干嘛不把这些相同的东西移到父类中去?

    这次,我似乎成功了,但是,留下了祸根:如果以后游戏升级了,增加了大型飞碟,它的形状不能用x,y,w,h表示(因为太不精确),而应该用x,y,r(半径)来表示,那么,我将发现,我在MovingObject类中定义的成员变量不再适用.虽然x,y可以凑合着用,通过增加成员变量r,这个类可以正常工作.但是,w,h就成了冗余的成员变量,怎么看怎么别扭.

    所以,把move()设成纯虚函数,把MovingObject设成抽象类就顺理成章了.

 

 

设计模式感悟之二:

为什么说要优先使用组合而不是继承?

继承的本意是为了更好地实现代码的复用。如果恰当地使用,继承是强有力的工具。然而,就像不恰当使用C的宏会带来很多安全隐患,不恰当使用继承也会带来很多问题。

首先,继承一定程序上破坏了类的封装性。因为子类不仅可以调用,而且可以重写父类的publicprotected方法。此外,子类还可以修改父类的public protected成员变量。

其次,编译器允许子类重写父类的非纯虚函数。而这是应该极力避免的。

比如:

#include <iostream>

class Base

{

public:

       void f(){std::cout<<"Base\n";};

};

class Derive

{

public:

       void f(){std::cout<<"Derive\n";};

};

 

int main()

{

       Base * d=(Base *)new Derive;

       d->f();

       return 0;

}

与虚函数相反,最后对象指针d执行的函数不是Derive::f()而是Base::f(),这带来了混乱。

再次,继承关系是在编译时确定的,无法在运行时改变。

最后,不恰当使用继承会带来类爆炸。例如,如果把编辑器的编辑区看作一个窗口Win,那么窗口周围的边框Border,滚动条Scroller也是一个窗口Win,那么,怎么表示窗口+边框+滚动条呢?一种办法是用组合,有四种情况,SingleWin,BorderWin,ScrollerWin,BorderScrollerWin,如果窗口边上的修饰组件有n种,可能的组合将有2^n种。不仅增加了类的数量,而且增加了维护的难度。

而组合就可以很好地解决这些问题。

首先,组合不会破坏类的封装。

其次,组合可以动态地改变类之间的关系,如窗口类Win有一个Shape指针指向Rect对象,如果要使运行时窗口的形状变成圆形,只要把指针指向Circle对象就行了。

最后,组合不会带来类爆炸。把所有修饰的类定义为MonoWinMonoWinWin继承。MonoWin包含指向Win(除了该修饰组件以外的所有Win),就可以表示所有可能的组合。

 

设计模式感悟之三:

为什么说要针对接口而不是针对实现编程?

首先要区分一下接口继承和实现继承。接口是一个抽象类,所有的函数只有声明没有定义。而可实例化的类则不仅定义(即实现)了函数,而且定义了数据成员。

接口继承的好处在于不用担心破坏类的封装性。本来无一物,何处染尘埃?因为接口本来就没有实现,所以就谈不上破坏封装性。

接口继承的第二个好处是隐藏了类的实现细节。由于接口不声明数据成员,因此用户无法知道类的具体实现。

接口继承的第三个好处是只要保持接口不变,那么类库升级时就客户就不用把软件重新编译一遍(试想一下如果QQ通过编译源代码的方式升级,那是多么糟糕的一件事。何况大部分情况下你没有源代码,想编译也不能编译)。例如,第二版的类A比第一版多一个成员变量n.如果A是用实现继承开发的,并且软件B用到类A,那么你不得不把软件B重新编译一遍。因为实现继承把成员变量也暴露给了客户。而用接口继承,客户只要更新一下类A的新版本的DLL就行。这也是COM的思想之一,通过接口继承实现代码的二二进制层面上的兼容。

由于标准C++并没有规定C++在二进制层面上如何实现,导致各个C++厂商编译器的实现各不相同。如果软件的各个组件的二进制代码由不同编译器生成,那他们无法在因为最后暴露给客户的只是接口,

 

 

设计模式感悟之四

前几天忙着搬家,今天终于有空了。^_^

Strategy模式的应用

一般的面向对象的书上都会说,结构化程序设计和面向对象程序设计的区别在于前者是结构+算法,后者是把结构和算法封装在一起,作为一个类的成员变量和成员。这样的好处是在类内部耦合度很高,而类与类之间的耦合度很低。但是,这样做也有很多不足:算法是在编译期间确定的,无法动态修改。而且,算法与结构的耦合度很高,一旦算法改变,结构也得作相应改变,反之亦然。例如一个文本编辑软件需要动态地选择格式化算法:牺牲速度换取质量或者牺牲质量换取速度。

这时候可以把算法独立出来,作为一个单独的虚基类Serialise。把文档中的数据作为一个虚基类Doc。在Doc中包含Serialise类的指针。这样就可以在运行中动态地改变格式化算法。

  评论这张
 
阅读(100)| 评论(2)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018