前不久与朋友聊到GP和OO,今天搜东西的时候,看到一篇文章,看了之后感慨自己对GP和OO认识的浅薄.
这是那篇文章的地址.
同时也把自己和朋友的对话贴出来,过几年再回过头来看看,自己有没有取得进步:P
下面的内容是我与朋友的对话.我是第一个说话的人 :P
甲
:
开个讨论,我先抛砖引玉。今天看了
C++
设计新思维上实现
Functor
那一节。感觉
GP
更适合类库设计而不适合系统设计。
首先说说 GP 的优势,个人认为 GP 的主要优势在于代码复用,执行效率,和类型安全,易用性。而这正是类库设计需要的特性。作为大型系统来说,代码复用就没有那么重要了。关键是降低设计的复杂性,降低维护的难度,还有就是可扩展性。
个人认为在这方面 GP 做得不好。就设计来说,写出优质的泛型代码很困难,过多的考虑使用 GP 来使代码可复用反而会增加设计的复杂性。泛型代码的可维护性也不如 OO 好,特别是那些比较精巧的泛型代码,看懂要好半天。
其次 GP 不但会增加系统设计的复杂度,就设计类库来说,用 GP 也要更困难一些,个人认为 C++ 类库设计困难的原因之一不是应为 C++ 的多范式,而是因为进行类库设计时过分热衷 GP 。
就可复用性来讲,说 GP 具有更好的复用性也是不完全的。 GP 的可复用性体现在对各种型别只要满足一定的要求都可以简单复用一些类库中的组件上。对那些功能上的复用并没有什么优势。比如说我需要一个 FTP 类进行数据传输,对 FTP 类的复用上, GP 没有优势 .
就执行效率来说,也是类库设计才更关心的,对于系统设计来说,根据二,八定理, GP 所带来的效率提高也不是太重要。
首先说说 GP 的优势,个人认为 GP 的主要优势在于代码复用,执行效率,和类型安全,易用性。而这正是类库设计需要的特性。作为大型系统来说,代码复用就没有那么重要了。关键是降低设计的复杂性,降低维护的难度,还有就是可扩展性。
个人认为在这方面 GP 做得不好。就设计来说,写出优质的泛型代码很困难,过多的考虑使用 GP 来使代码可复用反而会增加设计的复杂性。泛型代码的可维护性也不如 OO 好,特别是那些比较精巧的泛型代码,看懂要好半天。
其次 GP 不但会增加系统设计的复杂度,就设计类库来说,用 GP 也要更困难一些,个人认为 C++ 类库设计困难的原因之一不是应为 C++ 的多范式,而是因为进行类库设计时过分热衷 GP 。
就可复用性来讲,说 GP 具有更好的复用性也是不完全的。 GP 的可复用性体现在对各种型别只要满足一定的要求都可以简单复用一些类库中的组件上。对那些功能上的复用并没有什么优势。比如说我需要一个 FTP 类进行数据传输,对 FTP 类的复用上, GP 没有优势 .
就执行效率来说,也是类库设计才更关心的,对于系统设计来说,根据二,八定理, GP 所带来的效率提高也不是太重要。
乙
:
作为大型系统来说,代码复用就没有那么重要了
---
这个我不同意,如果不复用,产生很多重复的,类似的代码,是很严重的问题,对开发效率,产品质量,维护难度都有很大的不利影响。
写出优质的 GP 代码是很难,但我觉得,任何复用,都要讲一个度,如果一说写 GP 代码,就要能跨平台,效率高,对各种类型都要有良好的行为,等等,这个恐怕很少人能完成。不但 GP 难,就算 OO 也一样,甚至更难。
OO 和 GP 并没有什么冲突,还是看各自适用的地方, GP 就是适合于写库的,写库本来就很难,特别是,效率和设计的优雅往往是矛盾的, GP 能达到一个更好的平衡。
---
这个我不同意,如果不复用,产生很多重复的,类似的代码,是很严重的问题,对开发效率,产品质量,维护难度都有很大的不利影响。
写出优质的 GP 代码是很难,但我觉得,任何复用,都要讲一个度,如果一说写 GP 代码,就要能跨平台,效率高,对各种类型都要有良好的行为,等等,这个恐怕很少人能完成。不但 GP 难,就算 OO 也一样,甚至更难。
OO 和 GP 并没有什么冲突,还是看各自适用的地方, GP 就是适合于写库的,写库本来就很难,特别是,效率和设计的优雅往往是矛盾的, GP 能达到一个更好的平衡。
甲
:
任何复用,都要讲一个度,如果一说写
GP
代码,就要能跨平台,效率高,对各种类型都要有良好的行为,等等,这个恐怕很少人能完成。不但
GP
难,就算
OO
也一样,甚至更难。
------------
同意,我正是想弄清 GP 在实际工作中的应用程度,对我自己而言感觉结合 GP 和 OO 编程有点困难。分别代表了不同的思维模式。个人认为 GP 是对对象和行为之间的关系的一种抽象,而 OO 是对对象与对象之间关系的一种抽象。感觉 OO 很重要一点就是将对象的数据和行为进行封装,而 GP 却是将类型和行为分割开来。虽然这一点并不矛盾,而且自己也并不喜欢纯 OO ,但感觉要将两这结合起来编程感觉很困难(这可能是自己的原因,自己编程经验严重不足:) ),当然 GP 中也大量用到封装,和继承,但个人认为这并不是 OO 和 GP 相结合编程。
话题扯远了 :), 我想问的是 GP 能不能降低大型系统设计的复杂度,虽然软件工程中没有 " 银弹 " ,但感觉 OO 的确在一定程度上降低了系统设计的复杂度,如果在系统设计中考虑 GP ,反到会增加复杂度。
GP 在类库的设计上的强大功能这是有目共睹的,这一点不光体现在 STL, 这样比较低层应用的设计上,像 WTL 这样的 GUI 库,编译出来的程序大小远小于同样功能 MFC 程序,在使用上个人感觉也更容易一些。
个人感觉 GP 更像小型的 framework, 但又有点不贴切,它提供工具和生成工具的基本框架,而 OO 更适合在此基础上进行系统开发。
个人的一点拙见,再次抛砖引玉:)
------------
同意,我正是想弄清 GP 在实际工作中的应用程度,对我自己而言感觉结合 GP 和 OO 编程有点困难。分别代表了不同的思维模式。个人认为 GP 是对对象和行为之间的关系的一种抽象,而 OO 是对对象与对象之间关系的一种抽象。感觉 OO 很重要一点就是将对象的数据和行为进行封装,而 GP 却是将类型和行为分割开来。虽然这一点并不矛盾,而且自己也并不喜欢纯 OO ,但感觉要将两这结合起来编程感觉很困难(这可能是自己的原因,自己编程经验严重不足:) ),当然 GP 中也大量用到封装,和继承,但个人认为这并不是 OO 和 GP 相结合编程。
话题扯远了 :), 我想问的是 GP 能不能降低大型系统设计的复杂度,虽然软件工程中没有 " 银弹 " ,但感觉 OO 的确在一定程度上降低了系统设计的复杂度,如果在系统设计中考虑 GP ,反到会增加复杂度。
GP 在类库的设计上的强大功能这是有目共睹的,这一点不光体现在 STL, 这样比较低层应用的设计上,像 WTL 这样的 GUI 库,编译出来的程序大小远小于同样功能 MFC 程序,在使用上个人感觉也更容易一些。
个人感觉 GP 更像小型的 framework, 但又有点不贴切,它提供工具和生成工具的基本框架,而 OO 更适合在此基础上进行系统开发。
个人的一点拙见,再次抛砖引玉:)
------------------------------------------------------------------------------------------------------------------------------------------
乙
:
我的感觉正好和你相反:)我觉得
OO
会增加复杂度,
GP
能降低复杂度。
类的继承层次多了以后会很复杂,继承关系是一种非常强的耦合,好的设计避免滥用继承, GP 对类型解耦,是比 OO 更高层次的抽象。这个对降低整个系统的复杂度是有很大好处的(当然, GP 技术本身似乎比 OO 要复杂很多)。
你说 GP 象小型的 framework ,我也不太同意,我觉得 GP 更象是非常独立的 “ 组件 ” ,拿来拼装一下就能用的,灵活性非常大。不像所谓的 framework 那么死板。
说到实际工作,恐怕大量使用 GP 的机会不多,没看 CSDN 上讨论的吗,连 STL 都不让用呢,因为怕组里别人看不懂。。。虽然我不同意这个观点,但实际情况确实是,懂 GP 的 C++ 程序员是少数。
你说的 OO 和 GP 思想的结合,是有一定的难度,说到底,还是个度的问题,该分开的时候分开,该封装的时候封装, std::string 的设计不是有大牛说它太胖了吗?里面很多成员函数其实是可以分出来的。但还有很多人说它封的函数太少,不好用,可见这个度的把握的确是很难的。用户不同,层次不同,看法也不同。
类的继承层次多了以后会很复杂,继承关系是一种非常强的耦合,好的设计避免滥用继承, GP 对类型解耦,是比 OO 更高层次的抽象。这个对降低整个系统的复杂度是有很大好处的(当然, GP 技术本身似乎比 OO 要复杂很多)。
你说 GP 象小型的 framework ,我也不太同意,我觉得 GP 更象是非常独立的 “ 组件 ” ,拿来拼装一下就能用的,灵活性非常大。不像所谓的 framework 那么死板。
说到实际工作,恐怕大量使用 GP 的机会不多,没看 CSDN 上讨论的吗,连 STL 都不让用呢,因为怕组里别人看不懂。。。虽然我不同意这个观点,但实际情况确实是,懂 GP 的 C++ 程序员是少数。
你说的 OO 和 GP 思想的结合,是有一定的难度,说到底,还是个度的问题,该分开的时候分开,该封装的时候封装, std::string 的设计不是有大牛说它太胖了吗?里面很多成员函数其实是可以分出来的。但还有很多人说它封的函数太少,不好用,可见这个度的把握的确是很难的。用户不同,层次不同,看法也不同。
乙
:
补充一下,关于
string
的设计,我觉得应该尽量减少
string
类的成员函数,把能对类型解耦的函数都拿出来重用,然后再写一个
fat_string
,组合
string
类,加上所有方便用户使用的成员函数,内部实现使用那些独立出来的重用函数即可。这样就可以满足不同用户的需求了。
甲
:
类的继承层次多了以后会很复杂,继承关系是一种非常强的耦合,好的设计避免滥用继承
--------------------------------------
同意, OO 用的最多的是封装,而不是派生,过多层的继承会造成复杂度的不可管理。但适当控制继承层次,在合理的地方使用继承可以控制复杂度,如果用到了继承那很有可能是需要多态,比如说在处理异质对象的集合上,个人决定 OO 要做的好一些。
#include<iostream>
#include<vector>
#include<boost/shared_ptr.hpp>
using namespace std;
using namespace boost;
class IBASE
{
public:
virtual void doSomething() const = 0;
virtual ~IBASE(){};
};
class DeA: public IBASE
{
public:
void doSomething() const { cout<<"DeA"<<endl; }
};
class DeB: public IBASE
{
public:
void doSomething() const { cout<<"DeB"<<endl; }
};
int main()
{
typedef vector<shared_ptr<IBASE> > IBASEVec;
IBASEVec vec;
vec.push_back(shared_ptr<IBASE>(new DeA));
vec.push_back(shared_ptr<IBASE>(new DeB));
for(IBASEVec::iterator iter = vec.begin();
iter != vec.end();
++ iter)
{
(*iter)->doSomething();
}
}
这是动态多态的例子
--------------------------------------
同意, OO 用的最多的是封装,而不是派生,过多层的继承会造成复杂度的不可管理。但适当控制继承层次,在合理的地方使用继承可以控制复杂度,如果用到了继承那很有可能是需要多态,比如说在处理异质对象的集合上,个人决定 OO 要做的好一些。
#include<iostream>
#include<vector>
#include<boost/shared_ptr.hpp>
using namespace std;
using namespace boost;
class IBASE
{
public:
virtual void doSomething() const = 0;
virtual ~IBASE(){};
};
class DeA: public IBASE
{
public:
void doSomething() const { cout<<"DeA"<<endl; }
};
class DeB: public IBASE
{
public:
void doSomething() const { cout<<"DeB"<<endl; }
};
int main()
{
typedef vector<shared_ptr<IBASE> > IBASEVec;
IBASEVec vec;
vec.push_back(shared_ptr<IBASE>(new DeA));
vec.push_back(shared_ptr<IBASE>(new DeB));
for(IBASEVec::iterator iter = vec.begin();
iter != vec.end();
++ iter)
{
(*iter)->doSomething();
}
}
这是动态多态的例子
#include<iostream>
#include<vector>
using namespace std;
class DeA
{
public:
void doSomething() const { cout<<"DeA"<<endl; }
};
class DeB
{
public:
void doSomething() const { cout<<"DeB"<<endl; }
};
template<typename T>
void DoSomething(vector<T> vec)
{
for(vector<T>::iterator iter = vec.begin();
iter != vec.end();
++iter)
{
iter->doSomething();
}
}
int main()
{
typedef vector<DeA> DEAVEC;
typedef vector<DeB> DEBVEC;
DEAVEC va;
DEBVEC vb;
va.push_back(DeA());
va.push_back(DeA());
vb.push_back(DeB());
vb.push_back(DeB());
DoSomething(va);
DoSomething(vb);
}
这是静态多态的例子
个人觉得,使用动态多态处理异质对象集合上更自然一些。
首先从概念上将,调用 doSomething 职责是 IBASE(IBASE 才知道到底做什么事,而上面的方法打破了这个封装关系 ) ,为了处理不同的对象必须分别为这些对象建立一个 vector 来保存。而处理异质对象集合正是构建系统经常要做的事。
#include<vector>
using namespace std;
class DeA
{
public:
void doSomething() const { cout<<"DeA"<<endl; }
};
class DeB
{
public:
void doSomething() const { cout<<"DeB"<<endl; }
};
template<typename T>
void DoSomething(vector<T> vec)
{
for(vector<T>::iterator iter = vec.begin();
iter != vec.end();
++iter)
{
iter->doSomething();
}
}
int main()
{
typedef vector<DeA> DEAVEC;
typedef vector<DeB> DEBVEC;
DEAVEC va;
DEBVEC vb;
va.push_back(DeA());
va.push_back(DeA());
vb.push_back(DeB());
vb.push_back(DeB());
DoSomething(va);
DoSomething(vb);
}
这是静态多态的例子
个人觉得,使用动态多态处理异质对象集合上更自然一些。
首先从概念上将,调用 doSomething 职责是 IBASE(IBASE 才知道到底做什么事,而上面的方法打破了这个封装关系 ) ,为了处理不同的对象必须分别为这些对象建立一个 vector 来保存。而处理异质对象集合正是构建系统经常要做的事。
呵呵写着,写着发现很难自圆其说了:)
GP 对类型解耦,是比 OO 更高层次的抽象
--------------------------------------------------------
刚才写着代码,渐渐明白了。这是很有道理的。比如说有一个人类,和一个狗类,他们都有吃饭的动作。如果为了处理这个动作就为他们引入一个动物的基类,那的确感觉增加了耦合性,仅仅为了一个行为相同就增加一个基类显然得不尝失。而把这些行为抽象出来也许更好一些。(万一将来要处理食虫草这种生物,
就可以直接重用代码了,不用再引入一个不伦不类的基类:) )
看来自己的信念开始动摇了。
你说 GP 象小型的 framework ,我也不太同意,我觉得 GP 更象是非常独立的 “ 组件
-----------------------------------------------------------------------------------------------------------
这里比如要自己做一个可以使用 STL 算法的容器,那么设计它的迭代器的时候必须提供一些 traits 才行,
感觉这点和 framework 有一点相似之处。但又有很大不太,所以想把 GP 比喻成小型的 framework 感觉又有点不贴切。
std::string 的设计不是有大牛说它太胖了吗?
---------------------------------------------------------------------------------------------------------
好像是 Herb Sutter 说的, Scott Meyer 也说过在适当的时候应该把可重用的函数从成员函数中独立出来。
我也同意这个观点,这也是我不喜欢纯 OO 的原因:)
GP 对类型解耦,是比 OO 更高层次的抽象
--------------------------------------------------------
刚才写着代码,渐渐明白了。这是很有道理的。比如说有一个人类,和一个狗类,他们都有吃饭的动作。如果为了处理这个动作就为他们引入一个动物的基类,那的确感觉增加了耦合性,仅仅为了一个行为相同就增加一个基类显然得不尝失。而把这些行为抽象出来也许更好一些。(万一将来要处理食虫草这种生物,
就可以直接重用代码了,不用再引入一个不伦不类的基类:) )
看来自己的信念开始动摇了。
你说 GP 象小型的 framework ,我也不太同意,我觉得 GP 更象是非常独立的 “ 组件
-----------------------------------------------------------------------------------------------------------
这里比如要自己做一个可以使用 STL 算法的容器,那么设计它的迭代器的时候必须提供一些 traits 才行,
感觉这点和 framework 有一点相似之处。但又有很大不太,所以想把 GP 比喻成小型的 framework 感觉又有点不贴切。
std::string 的设计不是有大牛说它太胖了吗?
---------------------------------------------------------------------------------------------------------
好像是 Herb Sutter 说的, Scott Meyer 也说过在适当的时候应该把可重用的函数从成员函数中独立出来。
我也同意这个观点,这也是我不喜欢纯 OO 的原因:)
丙
:
面向对象和泛型编程都追求
[
复用
]
之美,
[
复用
]
是软件工程的灵魂。
但是,它们 [ 复用 ] 的方式又有所不同:面向对象侧重于二进制级别的 [ 复用 ] ,泛型侧重于源代码级别的 [ 复用 ] 。
所谓优秀的程序库,是为了被工程所用而存在;所谓优秀的工程,是以优美地 [ 复用 ] 程序库而著称。我认为,工程和程序库之间的差别并没有想象中的那么大,而是用 [ 模糊 ] 来形容很是恰当。
设想有很多工程,它们都优雅地运用了某个程序库一个组件来完成一个特定的功能,那么 [ 将这个功能提取出来做成一个组件 ] 是在合理不过的事情了。所以我说,工程与程序库之间并没有严格的界限。
但是,它们 [ 复用 ] 的方式又有所不同:面向对象侧重于二进制级别的 [ 复用 ] ,泛型侧重于源代码级别的 [ 复用 ] 。
所谓优秀的程序库,是为了被工程所用而存在;所谓优秀的工程,是以优美地 [ 复用 ] 程序库而著称。我认为,工程和程序库之间的差别并没有想象中的那么大,而是用 [ 模糊 ] 来形容很是恰当。
设想有很多工程,它们都优雅地运用了某个程序库一个组件来完成一个特定的功能,那么 [ 将这个功能提取出来做成一个组件 ] 是在合理不过的事情了。所以我说,工程与程序库之间并没有严格的界限。
乙
:
其实静态多态可以这样:
#include <iostream>
#include <vector>
using namespace std;
#include <boost/variant.hpp>
#include <boost/foreach.hpp>
using namespace boost;
#define foreach BOOST_FOREACH
class DeA
{
public:
void xxx() const { cout << "DeA" << endl; }
};
class DeB
{
public:
void yyy() const { cout << "DeB" << endl; }
};
class dosomething_visitor : public static_visitor<>
{
public:
void operator()(DeA const & dea) const
{
dea.xxx();
}
void operator()(DeB const & deb) const
{
deb.yyy();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
typedef variant< DeA, DeB > de_t;
typedef vector< de_t > devec_t;
devec_t vec;
vec.push_back( DeA() );
vec.push_back( DeB() );
foreach( de_t const & de, vec )
{
apply_visitor( dosomething_visitor(), de );
}
return 0;
}
很优雅是吧?:)
#include <iostream>
#include <vector>
using namespace std;
#include <boost/variant.hpp>
#include <boost/foreach.hpp>
using namespace boost;
#define foreach BOOST_FOREACH
class DeA
{
public:
void xxx() const { cout << "DeA" << endl; }
};
class DeB
{
public:
void yyy() const { cout << "DeB" << endl; }
};
class dosomething_visitor : public static_visitor<>
{
public:
void operator()(DeA const & dea) const
{
dea.xxx();
}
void operator()(DeB const & deb) const
{
deb.yyy();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
typedef variant< DeA, DeB > de_t;
typedef vector< de_t > devec_t;
devec_t vec;
vec.push_back( DeA() );
vec.push_back( DeB() );
foreach( de_t const & de, vec )
{
apply_visitor( dosomething_visitor(), de );
}
return 0;
}
很优雅是吧?:)