论坛首页 编程语言技术论坛

朴实的C++设计

浏览 54660 次
该帖已经被评为精华帖
作者 正文
   发表时间:2010-08-17  
oop要注重设计,很多时候是设计得不好,并不是oop不好,至于gp,gp一般会将程序的可读性降像一半。谈到效率,c++中现在谁还碰到虚函数引起的效率问题呢?
0 请登录后投票
   发表时间:2010-08-18  
我的感觉,所有对于OO的批判,对于继承和多态的批判,去追寻炮轰者理论的根源,其实都在于template模式。
原因很简单,只要你遵循C++的语法,大概知道了多态的概念,想着要把这一伟大理论应用于自己的代码,写上个最简单的一个基类,两三个派生类的简单类结构,就自然而然地形成了template模式。
在这里多一句嘴,此template非彼template,是设计模式的概念,而非C++语法中的那个更伟大的关键词。诸位习惯于搞不清虚函数概念,把C++和script混为一谈,搞不清Functional Programming和Procedure Programming概念区别的同学们就不要发表高论了。
不过template模式其实更应该被称作是“template反模式”。因为它的bad smell不是一般地大。所有派生类依赖于基类,它们能够自主的不过是一些可怜的虚函数重载。基类的内部实现逻辑无条件地扩散和统治了所有的派生类。同时由于基类头文件必包含于派生类头文件中,所以所有引用派生类的代码和相关逻辑无一例外会被基类“污染”。基类内部实现的任意一点疑问和错误,对于运行环境的考量,异常处理,等等所有你能想到的问题都会被无限的放大。
而template模式本身对于这些问题的解决大约只有将基类写得尽善尽美,包罗万象。由此产生了一个又臭又长的代码文件。很多的C++类库中都会有这样让人不知所云的文件。
而正确的方法,应该是非常谨慎地限制使用这种模式,非常局部地使用它。

由对template模式的批判,而上升到对继承/多态的批判,甚至对于所有设计模式的批判,我觉得是非常幼稚的。对于“继承树”的建模形式的批判,本身是没有错的。但我想这应该不是我们使用OO的全部。我们不能把孩子和脏水一起倒出去。
我想至少有以下一些理由值得我们使用OO和设计模式,详细的讨论涉及太多细节和代码,所以只写我的论点,没有论述:
1. 依赖倒置原则。代码不应该依赖于具体的实现,而应该依赖于抽象。
  典型的是Java的interface的编程风格。
2. Open-Close原则。
3. 没有上帝原则。我们不能把设计人员当成上帝,他们不能预知未来,所以设计变更是必然的;我们不能要求programmer是上帝,他们不会知晓所有技术和所有代码细节,所以请让他们专注于实现自己的问题;我们不能允许代码模块是上帝,它们绝不能全知全能,应该只包含最少的外部引用,并尽量少地引入逻辑依赖;
4. 最少修改原则。代码需要修改的时候,应该做尽量少的修改,并且修改的地方应该和修改原因明显相关;
5. 不装蛋原则。内涵简单的东西,用简单的形式表示出来,让人一目了然。更多的时候,写的代码让别人看得懂才是最难的。

软件帖的难度在于,对于一个问题的描述,最好使用代码。然而太多时候短代码是很难描述工程中的实际问题的。只好举一个最简单的我称为污染性的问题,说明继承的作用:

class A {
public:
void func();
private:
T1 myFunc(T2 myParam);
private:
T3 _myVariable;
};

这里就有了一个问题,T1/T2/T3是内部实现细节,我并不想把它们引出到外部模块中去。但是C++的语法导致所有引用类A的代码必然知晓T1/T2/T3。
于是我们的代码成为了上帝。我们创造了上帝。不过这种感觉一点儿也不好。

我的解决方法如下:
class A {
public:
virtual void func() = 0;
};

class AImplement : public A {
public:
virtual void func();
private:
T1 myFunc(T2 myParam);
private:
T3 _myVariable;
};

然后可以通过一个Factory或者是Singleton,或是其它创建型模式,可以创建A对象。具体选择依需求而定。比如最简单的函数:
A* Instance() {
return new AImplement();
}

这样外部可以选择只引用A,而不需要引用其内部细节和让人讨厌的内部逻辑。

当然还有另一种方法,比如说把成员函数T1 myFunc(T2)变成一个真正的函数T1 myFunc(A* pThis, T2)不也可以至少去掉讨厌的类型T1, T2么?
然而绝大多数时候由于myFunc内部引用了成员变量_myVariable,导致这样是不可行的。否则我们就要同时把_myVariable变为public才可以。
0 请登录后投票
   发表时间:2010-08-18  

Solstice 写道
总之,继承和虚函数是万恶之源,这条贼船上去就不容易下来。不过还好,在 C++ 里我们有别的办法:
http://blog.csdn.net/Solstice/archive/2008/10/13/3066268.aspx

这篇帖子对伟大的boost做出了无情地吹捧。那么请让我稍微地讨论一下为什么在C++中boost这种怪胎会存在吧!
引用
一直以来,我对面向对象有一种厌恶感,叠床架屋,绕来绕去的,一拳拳打在棉花上,不解决实际问题。面向对象三要素是封装、继承和多态。我认为封装是根本的,继承和多态则是可有可无。用class来表示concept,这是根本的;至于继承和多态,其耦合性太强,往往不划算。

引用
自从找到了boost::function+boost::bind这对神兵利器,不用再考虑类直接的继承关系,只需要基于对象的设计(object-based),拳拳到肉,程序写起来顿时顺手了很多。

boost::function究竟是个什么东东?它究竟有多好?研究一下boost的实现吧,哈哈,发现它已经不是“叠床架屋,绕来绕去的,一拳拳打在棉花上”那么的简单啦,简直是从夸克级开始构造程序呀!
然而它最终实现了什么样的效果呢?
引用

Foo foo;
f1 = boost::bind(&Foo::methodA, &foo);
f1(); // 调用 foo.methodA();
Bar bar;
f1 = boost::bind(&Bar::methodB, &bar);
f1(); // 调用 bar.methodB();
...
如果没有boost::bind,那么boost::function就什么都不是,而有了bind(),“同一个类的不同对象可以delegate给不同的实现,从而实现不同的行为”(myan语),简直就无敌了。

原来就这么屁大点儿功能啊!所谓的成员函数,编译成二进制代码,和普通的函数有个mao的区别。即便是虚函数,假如C++有二进制规范的话,无非也就是个:
A a;
(*((&a)+x) + y)的一个函数地址而已。
无非是C++语言的成员函数的指针声明和类型转换上有些瑕疵,于是boost利用模板语法给它打了个补丁而已。去看看C#的closure实现吧,简直简单到像屁一样。
0 请登录后投票
   发表时间:2010-08-18  
原因在哪里?
C#比C++易学易用的原因很简单,它是很大程度上的一种动态语言。而C++几乎可以称作是彻头彻尾的静态语言。C++模板技术的发展把这种倾向性推向极致。而虚函数技术是C++中唯一可以用来做些运行期的技术手段。都这样了还不让人用,脑袋秀逗了啊你们?
0 请登录后投票
   发表时间:2010-08-18   最后修改:2010-08-18
jimmy_c 写道

软件帖的难度在于,对于一个问题的描述,最好使用代码。然而太多时候短代码是很难描述工程中的实际问题的。只好举一个最简单的我称为污染性的问题,说明继承的作用:

class A {
public:
void func();
private:
T1 myFunc(T2 myParam);
private:
T3 _myVariable;
};

这里就有了一个问题,T1/T2/T3是内部实现细节,我并不想把它们引出到外部模块中去。但是C++的语法导致所有引用类A的代码必然知晓T1/T2/T3。
于是我们的代码成为了上帝。我们创造了上帝。不过这种感觉一点儿也不好。

我的解决方法如下:
class A {
public:
virtual void func() = 0;
};

class AImplement : public A {
public:
virtual void func();
private:
T1 myFunc(T2 myParam);
private:
T3 _myVariable;
};

然后可以通过一个Factory或者是Singleton,或是其它创建型模式,可以创建A对象。具体选择依需求而定。比如最简单的函数:
A* Instance() {
return new AImplement();
}

这样外部可以选择只引用A,而不需要引用其内部细节和让人讨厌的内部逻辑。

当然还有另一种方法,比如说把成员函数T1 myFunc(T2)变成一个真正的函数T1 myFunc(A* pThis, T2)不也可以至少去掉讨厌的类型T1, T2么?
然而绝大多数时候由于myFunc内部引用了成员变量_myVariable,导致这样是不可行的。否则我们就要同时把_myVariable变为public才可以。


为了避免 T1, T2, T3 暴露给客户端,完全不必用继承或Factory,用 pimpl 技法就能很好地解决问题。
// A.h

class AImpl;

class A {
 public:
  A();
  ~A();
  void func();

 private:
  A(const A&);
  void operator=(const A&);
  AImpl* impl_;
};

// A.cc

class AImpl {
public:
	void func();
private:
	T1 myFunc(T2 myParam);
private:
	T3 _myVariable;
};

A::A()
 : impl_(new AImpl)
{
}

A::~A()
{
  delete impl_;
}

void A::func()
{
  impl_->func();
}

pimpl 这种做法还保障了二进制兼容性,让动态库的升级变得更容易。
0 请登录后投票
   发表时间:2010-08-18   最后修改:2010-08-18
打住,打住,boost的二进制兼容性/升级性就别吹了。它毕竟不是C++标准库。
您的实现我实在没看出有什么简化的地方。除了多了一个boost库的引用,代码量只多不少。问题是我要是就不想要boost引用呢?这要求太简单,太常见了吧?
0 请登录后投票
   发表时间:2010-08-18  
jimmy_c 写道
打住,打住,boost的二进制兼容性/升级性就别吹了。它毕竟不是C++标准库。
您的实现我实在没看出有什么简化的地方。除了多了一个boost库的引用,代码量只多不少。问题是我要是就不想要boost引用呢?这要求太简单,太常见了吧?


好,我把 boost 的引用去掉,代码只多一行 delete impl_;

以虚函数作为接口,其直接副作用是降低二进制兼容性。比如,在 A 里边增加一个新的虚函数,尽管客户端没有用到这个虚函数,也要求重新编译客户端的代码,因为虚函数表里的表项的数目与顺序可能变了。同样的,调换两个虚函数声明的顺序也会导致客户端二进制代码实效,需要重新编译。

如果你暴露出一个由 public 虚函数构成的接口给别人用,一旦你想改动你的基类(无论是 bug fix 还是增加新功能),代价会非常大。
0 请登录后投票
   发表时间:2010-08-18   最后修改:2010-08-18
Solstice 写道
jimmy_c 写道
打住,打住,boost的二进制兼容性/升级性就别吹了。它毕竟不是C++标准库。
您的实现我实在没看出有什么简化的地方。除了多了一个boost库的引用,代码量只多不少。问题是我要是就不想要boost引用呢?这要求太简单,太常见了吧?


好,我把 boost 的引用去掉,代码只多一行 delete impl_;

以虚函数作为接口,其直接副作用是降低二进制兼容性。比如,在 A 里边增加一个新的虚函数,尽管客户端没有用到这个虚函数,也要求重新编译客户端的代码,因为虚函数表里的表项的数目与顺序可能变了。同样的,调换两个虚函数声明的顺序也会导致客户端二进制代码实效,需要重新编译。

如果你暴露出一个由 public 虚函数构成的接口给别人用,一旦你想改动你的基类(无论是 bug fix 还是增加新功能),代价会非常大。


哈哈,改了上一个帖。擦,擦,使劲的擦。
不过忘了要做什么了吧,boost是没有,class AImplement又暴露出来啦?回去再改?

如果我一个虚基类的实现还需要很大的改动,那么你这个类呢?
如果我的虚基类的改变会造成很大代价,那么你的这个class A呢?

如果你能说的还是A可能有一大堆派生类之类的老生常谈的话,这不会是我的问题。看清楚我的观点再回话。
真正对程序会有影响的是你这个模块export出去的那个类A本身。请首先解决好这个问题,然后再说可能...如果...之类的话。
0 请登录后投票
   发表时间:2010-08-18  
jimmy_c 写道
Solstice 写道
jimmy_c 写道
打住,打住,boost的二进制兼容性/升级性就别吹了。它毕竟不是C++标准库。
您的实现我实在没看出有什么简化的地方。除了多了一个boost库的引用,代码量只多不少。问题是我要是就不想要boost引用呢?这要求太简单,太常见了吧?


好,我把 boost 的引用去掉,代码只多一行 delete impl_;

以虚函数作为接口,其直接副作用是降低二进制兼容性。比如,在 A 里边增加一个新的虚函数,尽管客户端没有用到这个虚函数,也要求重新编译客户端的代码,因为虚函数表里的表项的数目与顺序可能变了。同样的,调换两个虚函数声明的顺序也会导致客户端二进制代码实效,需要重新编译。

如果你暴露出一个由 public 虚函数构成的接口给别人用,一旦你想改动你的基类(无论是 bug fix 还是增加新功能),代价会非常大。


哈哈,改了上一个帖。擦,擦,使劲的擦。
不过忘了要做什么了吧,boost是没有,class AImplement又暴露出来啦?回去再改?

如果我一个虚基类的实现还需要很大的改动,那么你这个类呢?
如果我的虚基类的改变会造成很大代价,那么你的这个class A呢?

如果你能说的还是A可能有一大堆派生类之类的老生常谈的话,这不会是我的问题。看清楚我的观点再回话。
真正对程序会有影响的是你这个模块export出去的那个类A本身。请首先解决好这个问题,然后再说可能...如果...之类的话。


class AImpl; 只暴露了一个 forward declaration,没有暴露任何成员。客户端只能看到 A.h,不可能直接创建 AImpl 的对象(因为是个 in-complete type),也就不可能使用它,AImpl 的变化不会影响客户端。
如果你不喜欢,可以把 AImpl 作为 class A 的内部类,这样一点都不污染 namespace 。
0 请登录后投票
   发表时间:2010-08-18  
内部类?那T1,T2,T3呢?还是等到C++支持partial class再谈内部类吧。

你没回答我的其他问题呀老大,对于一个外部引用者来说,究竟他是会选择你的实现,还是我的实现呢,你猜?
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics