对于多种策略的选择,用条件语句好,还是用继承好?

kiffa 2008-12-25 01:18:36
比如一个函数Sort(),用来根据客户需要选择不同的排序策略。为了方便描述问题,以下代码做了简化,不必过于追究细节。

// 用条件语句实现:
enum SortStrategy{ BUBBLE_SORT, QUICK_SORT }; // 冒泡排序和快速排序
// 这里用了if,也可以用switch
void Sort(SortStrategy strategy)
{
if (strategy == BUBBLE_SORT)
... // 实现BubbleSort()
if (strategy == QUICK_SORT)
... // 实现QuickSort()
}
// 实际使用
int main()
{
SortStrategy s = BUBBLE_SORT;
Sort(s);
}



// 继承实现:
class Sorter
{
public:
virtual Sort() = 0;
}

class BubbleSorter : public Sorter
{
public:
virtual Sort() { ... }; // 实现BubbleSort()
}

class QuickSroter : public Sorter
{
public:
virtual Sort() { ... }; // 实现QuickSort()
}

void Sort(Sorter &s)
{
s.Sort();
}
// 实际使用
int main()
{
BubbleSoter s;
sort(s);
}



从各方面(复杂度,性能,重用,扩展等等)分析一下,以上两种做法哪一种比较好。
...全文
261 30 打赏 收藏 转发到动态 举报
写回复
用AI写文章
30 条回复
切换为时间正序
请发表友善的回复…
发表回复
jackzhhuang 2009-01-15
  • 打赏
  • 举报
回复
一眨眼看上去应该是继承好。

试想你现在要扩展这些排序类,若是用枚举,要改四处(1、多加一个枚举类型,2,多写一个排序函数,3,修改main里面的定义,4,sort函数多加一个分支)

若使用继承,值修改两处(1、多加一个排序类,2,修改main里面的定义)
damo_xu 2009-01-15
  • 打赏
  • 举报
回复
代码大全这本书应该能解决楼主的疑惑。
上面有个例子说没必要创一个COMMAMD虚函数来代替
case copy
case paste
case cut

既然这两个的好处不是一眼能看出来,那么就让它争论吧。。。
FoxOnWeb 2009-01-15
  • 打赏
  • 举报
回复
对扩充开放,对修改封闭
AlwaysSLH 2009-01-15
  • 打赏
  • 举报
回复
还是好好理解一下开闭原则吧
flydream1980 2009-01-14
  • 打赏
  • 举报
回复
学习一下。
lingling1989r 2009-01-14
  • 打赏
  • 举报
回复
[Quote=引用 6 楼 jcwKyl 的回复:]
同意四楼。你的第二种方法就是典型的策略模式的应用。我自己就是非常喜欢用第二种方法的。你说的那四个方面:性能,复杂度,重用,可扩展。重用和可扩展显然第二种比第一种要容易些。至于性能和复杂度,第一种方法的额外时间花费应该是在那一长串的if-else上面,第二种方法的额外时间花费是在RTTI上,RTTI的实现可以看看MFC中的例子,在《深入浅出MFC》中讲的很详细。另外在《Thinking In C++》第二卷第八章里面也有一些例子。相…
[/Quote]
看看更高效的做法,继承好吧。
毕竟代码大的话,你修改的时候比较好找。用条件语句。。你想增添改点什么都挺头疼的。。
taodm 2008-12-30
  • 打赏
  • 举报
回复
《unix编程艺术》是好书没错,只是人们对软件的需求约束和那本书所在的年代还一致么?
C语言编程技巧大赛现在都叫混沌代码大赛了。
看本年头新点的书对你也许更有用些。只有已经非常非常有经验了,才能回去看《unix编程艺术》
hhyttppd 2008-12-30
  • 打赏
  • 举报
回复
其实很多人是拿着面向对象工具写过程式代码。。

这话不是我说的,是某本书上的。我自己一板一眼的走完OO分析和设计流程的从来都没有过。
楼主所说的"中间层"问题基本是一些流行的[*****设计模式*****]的产物,并不是C++语言本身属性,如果使用同样的模式,较新的java/C#一样也存在这个问题。




lxx155491815 2008-12-30
  • 打赏
  • 举报
回复
都是高手,,学习了..
kiffa 2008-12-30
  • 打赏
  • 举报
回复
嗯,感谢大家的回帖,令我受益良多。我当初主要是从扩展性的角度去考虑的,看了各位的回帖,发现自己还是有些想当然了,由于实际项目经验地匮乏所致。对性能的差异我倒没有特别去探究,受教了。

借此帖说说自己的一些看法吧,主要是近来学习C++以及面向对象所产生的一些疑惑,还望有人能指点一下:

1,关于面向对象:说不出这是我第几次怀疑面向对象了。。。
刚开始接触C++以及OO的时候,很兴奋,发现代码原来还可以这样写,那时感觉前途一片光明;然后啃书写代码,啃啊写啊,啃啊写啊,慢慢地,总觉得有地方不对劲,我学OO是为了更简单更好地去解决问题(其实主要是为了更简单-_-),但我发现,我解决问题的方法好像越来越复杂。。。

《没有银弹》(《No Silver Bulle》,Fred Brooks,1987)我想大家都看过罢,软件开发的复杂度是个很让人沮丧的问题,哪怕只是次要复杂度。

最近翻了翻《unix编程艺术》,作者评论了几种流行语言,他认为C++是一种反紧凑的语言,代码中充斥着大量厚重凝滞的“胶合层”。就我个人的体会,我觉得C++(OO的C++)的确说不上紧凑直接,OO很喜欢抽象,像OO的基本原则之一:依赖倒置原则(DIP)就强调不能让高层策略依赖底层细节,而应该让双方都依赖于抽象。比如A要用到B,在C中就是A直接用B,两者之间紧凑简洁;而C++则会弄一个中间的抽象层出来(一般为接口性质的虚基类),用这个中间层来连接A和B,刚开始我觉得这是很棒的做法,毕竟不直接依赖就意味着很高的灵活性;但是后来我开始怀疑这样做是否值得,毕竟A直接用B给人的感觉很清晰,你非要间接弄个中间层出来,很容易让人头大。最极端的观点甚至建议代码中不要出现实体对象,对所有对象的使用一律通过指针或者引用来进行。与此类似的还有“得墨忒耳”法则。虽然书上说到这些内容的时候都会有句废话:因地制宜,灵活应用。。。

既然软件开发没有万能药,那么各种方法学本质上都是平等的,只需要权衡得失做出抉择,问题是“权衡得失”这四个字说起来容易做起来难,还望经验丰富者提点一下,OO最适合在什么情况下应用?

我C++只能算初学,几个月罢了。目前的我最喜欢的是C++的类封装,也就是用户自定义类型,也就是所谓的基于对象,感觉非常自然,很贴近问题域,在很多时候比“按功能模块来分解问题”更简单清晰,我最喜欢简单的东西。对继承和多态倒没有这么深的感觉,大概还是因为初学的缘故罢。见过不少人说“多态才是OO的核心”。

写的很乱,没什么中心,权当这两个月学C++的体会罢。大家看了后说说看法,提提建议。

我自己觉得我的这些疑惑,主要还是因为没有实践经验的缘故,“纸上得来终觉浅”。
kiffa 2008-12-26
  • 打赏
  • 举报
回复
对于重用:if实现很容易重用各种排序策略,而继承方法需要“打包”基类和所有的继承类,即使你只需要其中一种。

当前项目中的if实现:
// sort.h
enum SortStrategy{ BUBBLE_SORT, QUICK_SORT };
void sort(SortStrategy);

// sort.cc
void sort(SortStrategy strategy)
{
if ( strategy == BUBBLE_SORT )
BubbleSort();
if ( strategy == QUICK_SORT )
QuickSort();
)
// sort_strategy.h
void BubbleSort();
void QuickSort();

// sort_strategy.cc
void BubbleSort() { ... }
void QuickSort() { ... }


如果另外有一个项目需要用到快速排序,那么很容易重用已有的代码:
// another project:
#include "sort_strategy.h"
int main()
{
QuickSort();
}


但是对于继承实现,你必须把基类和其他子类都包括进来,即使你只需要使用QuickSort(),继承的强耦合注定了你需要为你不使用的东西付出代价:
// sorter.h
class sorter{ ... };
class BubbleSorter : public Sorter { ... }
...

// another project:
#include "Sorter.h"
int main()
{
QuickSorter s;
s.sort();
}

xiaoyisnail 2008-12-26
  • 打赏
  • 举报
回复
[Quote=引用 13 楼 kiffa 的回复:]
我正是看书看到strategy模式才来问这个问题的,楼上都说扩展性后者(继承)好,在许多情况下确实如此,我刚开始学C++时也觉得这样做很爽。

但是对于本题,我当初正是觉得这两种方法对于扩展性的区别不大,才有了疑问。看例子:假如现在要增加第三种排序策略:SORT_3

那么对于if实现:
需要修改:

C/C++ code// sort.h
enum SortStrategy{ BUBBLE_SORT, QUICK_SORT,SORT_3 }
// sort.cc
void Sort(SortStrategy…
[/Quote]

困了,明早仔细看后再讨论
jcwKyl 2008-12-26
  • 打赏
  • 举报
回复
嗯。我所理解的扩展性就是目前的代码能很好地工作,然后当你想往当前的代码中加入一些新的功能时,你不需要改变当前的代码,你所需要做的就是为新的功能写实现代码就可以了,这样,当新的功能出现问题时,只需要把那段代码删掉,或者做以修改,而完全不影响已经工作良好的代码。比如LZ的程序中,当想添加一种排序算法时,只需要继承一个新类,然后写新的排序算法的代码,而以前的代码不用做任何改变(当然main函数是可能要做点改变的)。而如果使用if-else,那么就要修改已有的能工作良好的代码,这些修改都是一些琐碎的细节,你需要判断并且记住应该修改哪些地方,并且一旦新的功能出现了问题,还得再改回来,想单独测试新的功能块都非常麻烦的。
hhyttppd 2008-12-26
  • 打赏
  • 举报
回复
我帮楼主总结一点吧:
1 只能使用继承方式的情况(你的代码有可能是某个库,因而客户无法修改)

当排序算法由客户端(使用者)提供时,显然只有使用继承结构可行(我想策略模式本意也是这样吧)

2 客户只需要选择策略,而不需要提供策略
可以使用if-else的方式
xiaoyisnail 2008-12-26
  • 打赏
  • 举报
回复
看了,说一下我的感觉

“继承实现的一个很大优点是对Sort()完全不需要改动,非常非常省心,但在这里,if实现所做的改动也很简单,我觉得两者差别不大,另外我又觉得增加过多的类会让问题更复杂。因此,对于一个比较简单的多策略抉择,也许if更合适一些。另外继承的耦合性很强,继承往往是反重用的 --- 背书而已,没有实际体会过。 ”

针对这段总结,我觉得两者的最大区别就在于一个要改Sort,一个不用改,我觉得这就是strategy模式最大的好处,由于你的例子很简单,所以在这样的上下文下方法一改起来也简单,但是拿到大型的应用中就能比较出好坏了,我觉得设计模式主要就是为了解决大型软件系统中的设计问题的,对于这种小例子不去比较也罢,纯属个人意见

对于“另外我又觉得增加过多的类会让问题更复杂”这句,我觉得用c++这种面向对象的语言,就是把问题域转换为对象来解决,是一种比较清晰的解决方案,此帖中方案一其实就是过程化编程语言解决问题的模型,可能对于这种小例子,确实有种感觉是增加过多的类会使实现复杂一点,但是绝不是让问题更复杂,我觉得问题是更清晰了,还是跟上面一样,如果问题扩大,放入一个复杂的系统中来考虑,那么面向对象的优势是显而易见的
yc406740872 2008-12-26
  • 打赏
  • 举报
回复
同意四、六楼。你的第二种方法就是典型的策略模式的应用。我自己就是非常喜欢用第二种方法的。你说的那四个方面:性能,复杂度,重用,可扩展。重用和可扩展显然第二种比第一种要容易些。至于性能和复杂度,第一种方法的额外时间花费应该是在那一长串的if-else上面,第二种方法的额外时间花费是在RTTI上,RTTI的实现可以看看MFC中的例子,在《深入浅出MFC》中讲的很详细。另外在《Thinking In C++》第二卷第八章里面也有一些例子。相比而言,RTTI是比一长串的if-else开销更大,这是C++抽象的必然结果。它的好处就是让人的工作更容易。正如一本经典书上所说,程序员时间比机器时间更宝贵。

另外,你的第一种方法可以写的更高效些,下面是更高效的写法的伪代码:

C/C++ code
enum SortStrategy{ BUBBLE_SORT = 0, QUICK_SORT }; // ðÅÝÅÅÐòºÍ¿ìËÙÅÅÐò

typedef void (*pSortFunc)(int *, int, int);

void bubble_sort(int *data, int begin, int end) {
//...
}

void quick_sort(int *data, int begin, int end) {
//...
}
static const pSortFunc FuncTable[] = {
bubble_sort,
quick_sort,
//...
};

int main()
{
SortStrategy s = BUBBLE_SORT;
(*FuncTable[s])(data, begin, end);
}

zenny_chen 2008-12-26
  • 打赏
  • 举报
回复
呵呵,使用多态特性的好处是可扩展性以及可维护性,不过你要写比较多的代码。
从开销上来说,空间上,由于使用虚函数,在全局空间上增加了虚函数表。另外,由于类中具有虚函数,因此每个具体实现类必定要有一个默认的构造函数用以为对象初始化虚函数表指针,这个又占用了指令空间。而用if-else或switch-case,在代码上就增加了判断语句。一个32位整型的CMP指令占用5个字节。总的来说,用if-else或switch-case语句来调用的话在空间上的损耗更小。

时间上,使用if-else或switch-case进行调用的话,就是带条件的分支跳转加调用。如果你在比较时使用的是无符号整型的话,那么在基于Core架构上的CPU将可能会有一次宏融合,提高前端译码速度。而使用虚函数的间接调用总是会有执行处罚的。不过如果在一个比较小的循环内进行调用(循环内的指令条数不大于18),那么这个处罚将会消失。

我上面从执行性能上进行了一个对比。我给的建议是,如果调用策略不多的话(比如就是冒泡或快速两种排序),那么使用if-else或switch-case将会有比较好的执行效率。但是如果策略情况很多,(比如加了希尔排序、堆排序、插入排序、选择排序等等),那么这时可以考虑使用多态。因为尽管在空间上,多态方法仍然比if-else要多,但是在执行上可能就会胜过if-else。因为虚函数间接调用就一个过程,而if-else或switch-case可能会遭到多次的分支预测失败。如果在一个小循环内的话问题不大,如果是调用间隔较大的话就会比较大的性能处罚了。

呵呵。尽管策略情况一多可能要写好几个类,从代码量上,使用多态也比用if-else或switch-case要多很多。
kiffa 2008-12-25
  • 打赏
  • 举报
回复
我正是看书看到strategy模式才来问这个问题的,楼上都说扩展性后者(继承)好,在许多情况下确实如此,我刚开始学C++时也觉得这样做很爽。

但是对于本题,我当初正是觉得这两种方法对于扩展性的区别不大,才有了疑问。看例子:假如现在要增加第三种排序策略:SORT_3

那么对于if实现:
需要修改:
// sort.h
enum SortStrategy{ BUBBLE_SORT, QUICK_SORT,SORT_3 }
// sort.cc
void Sort(SortStrategy strategy)
{
if (strategy == BUBBLE_SORT)
...
if (strategy == QUICK_SORT)
...
if (strategy == SORT_3)
...
}

原有的使用了sort函数的代码并不需要修改。只需要添加一个enum,添加一种排序实现即可,虽然不符合开闭原则(OCP,对扩展开放,对修改封闭),但实际上似乎也没有带来多大的麻烦。

再看继承:
需要修改:
// sorter.h,添加一个新的派生类:
class Sorter_3 : public Sorter
{
public:
Sort() { ... }; // 实现可能在sorter.cc中完成。
}




来对比一下两者:
1,if实现添加一个enum;继承实现添加一个Sorter_3,对用户来说,这两种实现都需要你了解所有的排序策略,单从这点来看,似乎两者没什么区别。

2,if修改了Sort()函数,继承不需要修改Sort()。两者都添加了一个新的实现。假如if实现不直接硬编码,而是采用函数或者6楼和11楼提出的函数指针数组(有点机器思维了:) )形如:
if (strategy == BUBBLE_SORT)
BubbleSort(); // BubbleSort()在其他地方实现。
或者:
// 函数指针数组
SortTable[BUBBLE_SORT](); // 相信大家都看得懂,不详细解释了。

那么总体所做的修改也很简单,只需要修改一个地方(Sort()实现)即可。两种实现所做修改的差距也不明显。而一般采用继承代替其他方法的重要原因之一是:由于其他方法往往需要查找修改多处,而继承只需要修改一处。但这个原因似乎对本题不成立。只是前者自己显式做判断(用if选择策略),而后者让编译器(或者说C++运行时支持库)隐式判断(虚函数运行时选择策略)。

3,两者都不改变函数接口,对原有代码都没有影响,增加新策略后,原有的用到了Sort()的代码都不需要修改。例:
// 原有代码:
int main()
{
SortStrategy s = BUBBLE_SORT;
Sort(s);
}

对于上述代码,在你添加了新策略之后,并不需要做任何修改。当然,如果你需要使用新的策略,那么自然要做修改。添加新功能后(OCP中的对“扩展”开放),原有代码不需变动(OCP中的对“修改”封闭),这点也是OO的显著优点之一。但同样,在本题中这两种实现没什么区别。

4,对编译的影响:由于用到排序的客户需要包含它们的头文件(sort.h和sorter.h),而两者的头文件都有了改变(前者增加了enum,后者增加了派生类),因此理论上来说用到了Sort()的模块都需要重新编译。虽然从极端角度来讲,对于未使用新策略的模块,你可以选择不重新编译。总之,从这里来比较,两者似乎也不存在多大的差异。

总而言之:我看书看到这里,然后用以上的思考方法来比较他们的差别,发现并不明显,所以才有所疑惑。而且,从人的直觉来看,实现1 似乎更符合人的正常思维:提供多种策略,判断客户选择哪种策略,调用相应实现。

继承实现的一个很大优点是对Sort()完全不需要改动,非常非常省心,但在这里,if实现所做的改动也很简单,我觉得两者差别不大,另外我又觉得增加过多的类会让问题更复杂。因此,对于一个比较简单的多策略抉择,也许if更合适一些。另外继承的耦合性很强,继承往往是反重用的 --- 背书而已,没有实际体会过。

不过,也有可能是我才开始学C++,思维方式暂时还没转变过来罢。
zambie_ok 2008-12-25
  • 打赏
  • 举报
回复
学习了
sxz2008 2008-12-25
  • 打赏
  • 举报
回复
单单从空间复杂度来说就可以知道是用继承好了
况且采用继承能增加耦合度
加载更多回复(10)

64,639

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧