Concept,一个逝去的梦,未来的希望,抽象之毒的解药,...
concept_map PolarComplex<BenComplex>
{
float BenComplex::getMag() {
return sqrt(that
.getReal()*that
.getReal()+that
.getImg()*that
.getImg());
}
float BenComplex::getAngle() {
return arctan(that
.getImg()/that
.getReal());
}
};
这些代码做了两件事。一是将两个复数类和concept绑定;第二是“制造”出concept有,而复数类没有的成员函数。后者是这里的要点。
concept
map俨然成了一个adapter,将一个类型“打扮”成concept所需的样子。关键字that与this相对,this用于对类的内部的访问,而
that则是从外部访问一个对象。但这种concept map在C++0x的是被禁止的。在C++0x中,concept
map的adapter不能作用于成员函数。理由是不能破坏类的封装。但是,如果允许adapter函数,如上面的
BenComplex::getMag(),能够访问类BenComplex的non-public成员,的确会破坏封装。但是如果我们只允许访问
BenComplex的public成员,那么便不会有此问题。这也就是that的意义:this访问类的内部,而that访问类的外部。
这相当于为一个类添加了额外的成员。C#程序员可能会觉得眼熟。没错,类似extension method。但concept
map有它的优势。extension method的作用范围是全局的,会“污染”所有涉及的类型。而concept
map的作用范围仅仅局限在相应的concept之中。一个类型在任何地方都会保持其原本的形象,忠实体现设计者的意图。只有当我们通过concept访
问一个类型时,这些“附加”的成员才会起作用。而这些附加的成员也完全是concept的需求,不会有任何突兀和随意。
但是,此处还有一个问题。+和*的代码中,使用了具体的类型BenComplex和AsslyComplex作为返回类型。这直接导致了两个操作同这两个
具体类型的依赖。我们不希望一个通用性的操作同一个具体类型相关,因为不利于提高抽象度。解决的方法有这样几种:
最简单的,使用在算法中使用类型别名,而非具体类型:
On 9月13日, 下午3时41分, Ian Yang <doit....@gmail.com> wrote:
> 只能继续用traits了。
>
> OO的虚函数有个致命缺点,调用不能inline。
>
> -------
> Sincerely, Ian Yang
>
> 2009/9/13 莫华枫 <longshank...@gmail.com>
>
> > <https://mail.google.com/mail/?ui=2&view=bsp&ver=1qygpcgurkovy>
> > return T(*getReal(lhd)* +*getReal(rhd)* , *getImg(lhd)* +*
> > getImg(rhd)* );
> > };
> > template<typename T>
> > T operator*(T lhd, T rhd) {
> > return T(*getMag(lhd)* **getMag(rhd)* , *getAngle(lhd)* +*
> > getAngle(rhd)* );
> > BenComplex operator+(*IRectComplex* lhd, *IRectComplex* rhd) {
> > return BenComplex(*lhd.getReal()* +*rhd.getReal()* , *
> > lhd.getImg()* +*rhd.getImg()* );
> > }
> > AsslyComplex operator*(IPolarComplex lhd, IPolarComplex rhd) {
> > return AsslyComplex(*lhd.getMag()* **rhd.getMag()* , *
> > lhd.getAngle()* +*rhd.getAngle()* );
> > }
>
> > 两个接口,IRectComplex和IPolarComplex,分表描述直角坐标和极坐标所需的函数。而两个类都实现这两个接口。这样,无论哪个类都可以直接用于+和*操作,而无需一个额外的转换。同时,也减少了那一半没有做任何计算的函数。
> > 但是,OOPer们,别高兴得太早,麻烦接踵而至。首先,笨和爱死理不高兴了。
> > 笨说:"我写的复数类用的是直角坐标,干嘛还要实现一个极坐标的接口,我就是不喜欢极坐标,太恶心人了..."
> > 而爱死理说:"我就是喜欢极坐标,优雅!干嘛还非得实现一个直角坐标接口,我讨厌直角坐标,呆板..."
>
> > 然后,还有件更恐怖的事:一个新来的程序员,用了一个新的复数表示法。这样,便有了三个接口需要每一个类实现。笨和爱死理无论如何不肯再加接口了。于是,三个人吵成一团。
>
> > 很显然,OOP的Interface(抽象类)是侵入式的,必须由相关类型配合,加以实现。如果开发者不配合,或者无法配合,那么事情就混乱了。而且,对于变化的适应性不如前面的转换函数来的强,也不够灵活。
>
> > 小结一下,OOP方式,可以减少很多无意义的代码,但面对变化的灵活性差。转换函数(也可以看作另一种形式的接口)的方式,灵活性高,但需要多写很多没有执行实际转换的函数。两者互有胜负,各有优缺点。
> > 接下来,我打算通过concept/concept map/adapter,实现一匹不吃草的好马儿。
> > 首先,我们接受笨和爱死理最初开发的那两个类,笨的类只考虑直角坐标的东西,而爱死理的类只考虑极坐标的操作。两者不用实现对方的接口,互不搭界。
> > 然后,我们定义两个concept。(请注意,这些事情我们都是在笨和爱死理不知情的情况下做的,以免他们不开心):
> > concept RectComplex<T>
> > {
> > float T::getReal();
> > float T::getImg();
> > };
> > concept PolarComplex<T>
> > {
> > float T::getMag();
> > float T::getAngle();
> > };
> > 接着,我们可以编写+和*操作了:
> > BenComplex operator+(*RectComplex* lhd, *RectComplex* rhd) {
> > return BenComplex(lhd.getReal()+rhd.getReal(),
> > lhd.getImg()+rhd.getImg());
> > }
> > AsslyComplex operator*(*PolarComplex* lhd, *PolarComplex* rhd) {
> > return AsslyComplex(lhd.getMag()*rhd.getMag(),
> > lhd.getAngle()+rhd.getAngle());
> > }
> > 最后,在用之前,我们必须将类型和concept绑定。但是,这不是一般的绑定,绑定的同时,我们还需要弥合各种不同的复数表示法之间的差异:
> > concept_map RectComplex<BenComplex>;
> > concept_map PolarComplex<AsslyComplex>;
>
> > 这两个绑定是顺理成章的,BenComplex本来就是直角坐标表示法,而AsslyComplex本来就是极坐标表示,它们与相应的concept之间完全契合,无须额外修正。接下来,需要面对不同表示法之间的绑定了:
> > concept_map RectComplex<AsslyComplex>
> > {
> > float AsslyComplex::getReal() {
> > return *that* .getMag()*cos(*that* .getAngle());
> > }
> > float AsslyComplex::getImg() {
> > return *that* .getMag()*sin(*that* .getAngle());
> > }
> > };
>
> > concept_map PolarComplex<BenComplex>
> > {
> > float BenComplex::getMag() {
> > return sqrt(*that* .getReal()**that* .getReal()+*that*.getImg()*
> > *that* .getImg());
> > }
> > float BenComplex::getAngle() {
> > return arctan(*that* .getImg()/*that* .getReal());
> > }
> > };
> > 这些代码做了两件事。一是将两个复数类和concept绑定;第二是"制造"出concept有,而复数类没有的成员函数。后者是这里的要点。
> > concept
> > map俨然成了一个adapter,将一个类型"打扮"成concept所需的样子。关键字that与this相对,this用于对类的内部的访问,而
> > that则是从外部访问一个对象。但这种concept map在C++0x的是被禁止的。在C++0x中,concept
>
> ...
>
> 阅读更多 >>
http://blog.csdn.net/longshanks/archive/2009/09/13/4547747.aspx
==============================================================
concept的外快
Concept,一个逝去的梦,未来的希望,抽象之毒的解药,...
Concept!标准委员会叫你回家吃饭!
我们等待的不是C++标准,是寂寞...
就此默哀三分钟...
Concept作为更加完善的抽象体系,消除了OOP等抽象机制的缺陷,将抽象手段提升到一个无忧的境界。与之相关的一个辅助机制,concept map/adapter,则为我们提供了更加优雅的类型扩展之路。这里,我通过一个改编自SICP的案例,来展示其中的奥妙和强劲。必须说明的是,这里所用到的concept map机制,是被前C++0x(1x?)的concept所禁止的。所以在这里我想吼一声:WG21的死脑筋们,看看你们都干了什么!
说有两个程序员,一个叫笨,另一个叫爱死理。他们俩各写了一个表达复数的类型://笨的复数类,使用直角坐标表示复数class BenComplex{public://构造函数BenComplex(float real, float img){...} //用实部和虚部创建一个复数//访问函数float getReal(){...} //提取实部float getImg(){...} //提取虚部...};//爱死理的复数类,使用极坐标表示复数class AsslyComplex{public://构造函数AsslyComplex(float mag, float angle){...} //用复数向量的模和幅角创建一个复数//访问函数float getMag(){...} //提取模float getAngle(){...} ///提取幅角};好,现在我们需要对复数执行计算。先看加法。复数的加法使用直角坐标表示法来得容易,因为只需分别将它们的实部和虚部相加即可:
BenComplex operator+(BenComplex lhd, BenComplex rhd) {return BenComplex(lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());};而乘法则使用极坐标表示法来的容易:AsslyComplex operator*(AsslyComplex lhd, AsslyComplex rhd) {return AsslyComplex(lhd.getMag()*rhd.getMag(), lhd.getAngle()+rhd.getAngle());}
很显然,如果想要让两个极坐标表示的复数(即AsslyComplex的实例)相加,或者两个直角坐标表示的复数(即BenComplex的实例)相乘,要么另行定义新的operator+,要么对复数做转换。通常,从我们会选择后者,因为这么做拥有抽象上的优势:
这相当于为一个类添加了额外的成员。C#程序员可能会觉得眼熟。没错,类似extension method。但concept map有它的优势。extension method的作用范围是全局的,会“污染”所有涉及的类型。而concept map的作用范围仅仅局限在相应的concept之中。一个类型在任何地方都会保持其原本的形象,忠实体现设计者的意图。只有当我们通过concept访问一个类型时,这些“附加”的成员才会起作用。而这些附加的成员也完全是concept的需求,不会有任何突兀和随意。
但是,此处还有一个问题。+和*的代码中,使用了具体的类型BenComplex和AsslyComplex作为返回类型。这直接导致了两个操作同这两个具体类型的依赖。我们不希望一个通用性的操作同一个具体类型相关,因为不利于提高抽象度。解决的方法有这样几种:
最简单的,使用在算法中使用类型别名,而非具体类型:
ComplexPlusRet operator+(RectComplex lhd, RectComplex rhd) {return ComplexPlusRet (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());}
在导入+操作前(include),定义ComplexPlusRet即可。这种方案尽管简单,但只是提供了一个间接,并未彻底消除两者的依赖关系。
稍微复杂些的,就是将这个类型别名的定义放入concept:
concept RectComplex
{
typedef BenComplex RetType;
...
};
RectComplex::RetType operator+(RectComplex lhd, RectComplex rhd) {return RectComplex::RetType (lhd.getReal()+rhd.getReal(), lhd.getImg()+rhd.getImg());}
这种方式更方便些,但同样也没有彻底消除依赖。因为concept是算法的接口,属于算法的一部分,它的某个成分依赖于具体类型,那么也就是这个算法依赖于那个类型了。
于是,我们可以考虑将类型别名定义进一步推迟到concept map的时候:
concept RectComplex
{
typedef RetType; //concept中的声明,占位
};
...
concept_map RectComplex<BenComplex>
{
typedef BenComplex RetType; //实际的定义,绑定
};
concept_map RectComplex<AsslyComplex>
{
typedef BenComplex RetType;
...
};
...
如此,类型的定义同操作的定义彻底分离,两者可以独立开发,互不相关。只有在使用类型和操作的时候,才需要使用者将类型同concept绑定。
但是,这个方案也并非完美的:这里只定义了一个类型别名,但事实上,所需的类型别名还有很多,比如*操作的返回类型就不同于+操作的返回类型,需要 AddRetType和MulRetType。每一个这样的类型别名都需要独立命名和定义。这带来了类型别名的组合爆炸,增加了开发负担,破坏了抽象。
解决此问题的线索蕴藏在SICP这本经典中。SICP谈到了数据的本质。归纳起来,数据的本质就是数据的特征,而不是数据的实现。比如复数,只要一个类型满足以下条件,便可以认为它是一个复数(或者当作复数使用。数学上的复数还有更复杂的定义,这里仅仅从编程的数据类型角度出发):
代码中的粗体部分就是答案。typeof(lhd)提取出参数的类型,比如AsslyComplex,用 RectComplex<AsslyComplex>这样的语法调用adapter构造函数。它的含义是在RectComplex接口的控制下,创建AsslyComplex对象。编译器看到RectComplex<AsslyComplex>的语句,直接到 concept_map中寻找相同的定义,调用相应的构造函数。
此处很明显地体现出concept优于oop interface的地方。除了非侵入外,concept可以描述构造函数,而interface则无此功能。因此,interface无法完整地描述一个类型的特征,而concept拥有更加完善的描述能力。此外,由于oop利用继承作为类型与interface绑定的途径。而继承的原始意图并非于此,担当此任属于“玩票”,因而缺乏进一步扩展的能力。相比之下,concept map则是天生的绑定机制,拥有更大的空间执行adapter之类的任务,具有更大的灵活性和拓展性。这一点在前面的案例中已经充分体现。
至此,我们利用concept,使得抽象算法的同具体类型的开发分离,做到完全无关,并在需要时利用concept map将两者结合。concept的非侵入特性的优势在此处显露无疑。两个Complex类的作者笨和爱死理对于+和*操作的实现一无所知,他们只管按各自喜欢的表达法实现相应的复数类,不需要考虑其他问题。而两个操作的开发也无须考虑复数类的具体实现,所关心的是最方便的算法和接口。两者之间的桥梁是 concept map和adapter,同时提供了灵活和简洁。它具备了转换函数的灵活性和扩展性,同时又具备了OOP接口的简洁和方便。在concept map和adapter的作用下,concept不仅仅成为类型的接口,而且弥合了同一事物的不同实现的差异。
只能继续用traits了。
OO的虚函数有个致命缺点,调用不能inline。
确实很赞,不过如果设计成这样,是不是过度设计呢?一般这种问题,都可以用包装类来实现吧?
> 如果有更多的复数表达类,那么会更加累人,更加浪费。
我理解有时候得允许一个项目里使用多种不同数据结构表示相同的数据,但我绝对不理解的是我们怎么能还都允许把它们
直接混在一起用。我的经验里这种情况下我更习惯这样的三步走:
a) 确定一个内部类。如果我有特殊的方法需求,就写一个包装类,并且定义我需要的那些方法。
b) 如果我需要用到的函数库返回他们自己的复数类,那就用它构造一个我的包装类并返回,并且避免使用它们的接口。
c) 而且这个数据要输出(比如一个framework),那就和项目组统一约定一个类作为外部输出。注意:不一定是自己写的类。
也许复数不是一个好例子,因为常用的复数表示也就这么两种而已。我想更好的例子应该是string。比如我们在Linux下
写东西的时候的时候总也免不了对付各种各样的string,libxml2的XMLString,C++的std::string,GTK+的gchar*,还有古
老的char*。有时候我写一些GTK+的程序,就往往会选择std::string做内部数据,gchar*做外部输出,因为std::string天生
就是一个包装类,而GTK+组件之间通信的时候gchar*是必须保证支持的。
所以在我看来这句话也不成立:
> 至于包装类么,concept adapter所做的就是包装类的工作,而且比包装类更简单方便。
我没觉得它简单。因为上面所有的代码隐藏了一个前提:如果第三个复数类(姑且让我这么说)的函数签名不一样,那么
我还是要为concept编写一堆的包装函数。如果我要把它用在普通函数而不是类上,问题只怕更大,因为普通函数的签名
不像类那样还有一个对象名做前缀,而是采用A_do_something()和B_do_something()的形式,这时候concept仍然别无选
择地要写更多的包装函数。
最后,concept完全没有解决我说的调试便利性问题,而且它变本加厉地让问题变得更严重:我怎么靠读concept上的一堆
类型占位符上读出它现在表示的是什么类?这就跟调试COM程序一样,我只能从代码里看到它的interface,而没法看到实
现。偏偏我的工作里大部分要处理的bug都无法重现,遇到问题的时候首先得读。不厚道地说一句,要是我非得对着一堆
上千行的concept,还不如直接用C呢,何况我工作中要读的代码一般得以十万为单位。
--
《采莲》·江南
为卿采莲兮涉水,为卿夺旗兮长战。为卿遥望兮辞宫阙,为卿白发兮缓缓歌。
另抄自蒜头的评论:http://www.douban.com/review/1573456/:
且祭一束紫琳秋,为一段落花流水的传说
且饮一杯青花酒,为一场几多擦肩的错过
且焚一卷旖旎念,为一腔抛付虚无的惜怜
且歌一曲罢箜篌,为一刻良辰春宵的寂寞
平心而论我不喜欢这样的代码,为了一个语法糖做了太多的事情。
另外我认为这句话不成立:
> 如果有更多的复数表达类,那么会更加累人,更加浪费。
我理解有时候得允许一个项目里使用多种不同数据结构表示相同的数据,但我绝对不理解的是我们怎么能还都允许把它们
直接混在一起用。我的经验里这种情况下我更习惯这样的三步走:
a) 确定一个内部类。如果我有特殊的方法需求,就写一个包装类,并且定义我需要的那些方法。
b) 如果我需要用到的函数库返回他们自己的复数类,那就用它构造一个我的包装类并返回,并且避免使用它们的接口。
c) 而且这个数据要输出(比如一个framework),那就和项目组统一约定一个类作为外部输出。注意:不一定是自己写的类。
也许复数不是一个好例子,因为常用的复数表示也就这么两种而已。我想更好的例子应该是string。比如我们在Linux下
写东西的时候的时候总也免不了对付各种各样的string,libxml2的XMLString,C++的std::string,GTK+的gchar*,还有古
老的char*。有时候我写一些GTK+的程序,就往往会选择std::string做内部数据,gchar*做外部输出,因为std::string天生
就是一个包装类,而GTK+组件之间通信的时候gchar*是必须保证支持的。
所以在我看来这句话也不成立:
> 至于包装类么,concept adapter所做的就是包装类的工作,而且比包装类更简单方便。
我没觉得它简单。因为上面所有的代码隐藏了一个前提:如果第三个复数类(姑且让我这么说)的函数签名不一样,那么
我还是要为concept编写一堆的包装函数。
如果我要把它用在普通函数而不是类上,问题只怕更大,因为普通函数的签名
不像类那样还有一个对象名做前缀,而是采用A_do_something()和B_do_something()的形式,这时候concept仍然别无选
择地要写更多的包装函数。
最后,concept完全没有解决我说的调试便利性问题,而且它变本加厉地让问题变得更严重:我怎么靠读concept上的一堆
类型占位符上读出它现在表示的是什么类?这就跟调试COM程序一样,我只能从代码里看到它的interface,而没法看到实
现。偏偏我的工作里大部分要处理的bug都无法重现,遇到问题的时候首先得读。不厚道地说一句,要是我非得对着一堆
上千行的concept,还不如直接用C呢,何况我工作中要读的代码一般得以十万为单位。
> concept可不是语法糖,是正经的first-class语言特性,就像函数或者类一样。
这里有一个问题:我从未见过正式的定义标准来规定first-class语言特性和语法糖的差异。
在我看来,如果某个语言特性的功能语言本身已经具有,但为了追求形式完善而加入一个
新的表示,那就可以称为语法糖,比如C#中的代理和属性,也包括Python当中的Decorator。
至于这个语言特性是不是first-class,这不重要,***更不是说语法糖就不正经***。顺便说一句,
first-class在我印象里函数式理论家采用的词,而且本身的定义也不清楚。
如果我的定义没错的话,那么莫兄所言的concept就是不折不扣的语法糖:语言中已经可以
用包装类实现的东西,现在为了形式上的一致或者说优雅又发明了一个新概念。我不是泛泛地
说语法糖不好,而仅仅针对莫兄上面的例子:我认为单纯为了一个少写算法代码而引入这个概
念不值得。原因下面详述:
>> 另外我认为这句话不成立:
>>> 如果有更多的复数表达类,那么会更加累人,更加浪费。
> 这句话要看上下文,这是针对那一堆转换函数而言的。SICP中运用Data Direct
> Programming处理这个问题,这边的思路一致,只是尝试用更新的手段而已。
>>
>> 我理解有时候得允许一个项目里使用多种不同数据结构表示相同的数据,但我绝对不理解的是我们怎
>> 么能还都允许把它们直接混在一起用。
>> ……
> 这些话实际上已经说明问题了,我们开发的时候经常会用到外源的,或者无法控制的代码,正如这些字符串。如何针对这些不同的类型写出抽象算法,保持DRY,就是这里的目的。
> 不是所有的东西我们都能够控制的。
莫兄这话没有正面回答我的疑问。我不是质疑concept的功能,更不是在质疑DRY原则,
我所担心的是:这个功能似乎是在鼓励程序员随意在代码中混合使用来自各种函数库的各
种数据结构。而在我的经验里这种行为除了给后来人阅读代码制造困难之外不会带来什么
好处,特别是程序维护的过程中。
我对这个问题十分精神过敏的原因与多少与我的工作有关。我是做代码维护的,平时经常
需要面对的任务就是从一堆我之前从未阅读过的代码里找到一个客户报上来的bug的原因,
然后可能的话给出一个fix。对我来说,最可怕的不是代码规模大(早就习惯上来就是十万行
的代码了),而是数据结构的来源过于庞杂。
我在前面的举COM的例子就属于这种情况。我见过的代码中曾经有人用过多种不同的组件,
有的来自TSF,有的第三方的输入法库,有的来自他们自己写的COM组件,最要命的是,
它们都实现自同一个interface。结果当程序出问题的时候,作为维护人员首先要理解的不是
这些类的功能,而是这些interface分别对应的真实实现分别来自哪里,它是否在我的控制之下,
如果不是再找对应的人去解决。因为这时候我看到的好几千行代码里都是这样的代码:
CComPTR<ISomeCommonInterface> objPtr;
.... // 这里可能就有一大堆的代码,包括各层函数调用
hr = objPtr->DoSomething(); // 如果不用debugger,谁知道这个的真实的类是什么?
所以如果C++接受上面那种concept的用法的话,我最担心的场景是:某程序员写了个巨大的
concept库去整合XMLString,gchar*,std::string,char*,结果在维护的时候我们就可能看到
满屏幕的这样的代码:
auto mystring = DoSomething(string1.Value(), string2.Value());
原先至少我知道我可以到string1和string2的类型定义里寻找Value()的定义——实际上这已经够
困难了,因为string1和string2有时候是一个基类引用,它指向的代码并不总能简单地从上下文
看出来。可现在concept进一步打破了这个规则,光靠读代码我根本不可能知道那两个Value()究
竟是什么,因为他们可能是成员函数,也可能来自程序员自己定义的concept,甚至这个concept
可能来自第三方库。同样的,DoSomething()也有这个问题。如果再加上auto,那就根本不知所
云了。
>>
>> 如果我要把它用在普通函数而不是类上,问题只怕更大,因为普通函数的签名
>> 不像类那样还有一个对象名做前缀,而是采用A_do_something()和B_do_something()的形式,这时候concept仍然别无选
>> 择地要写更多的包装函数。
>
> 同样,作为比较,包装类由此能力否?
这个呢,包装类的确是不能做。但我的意思其实是,这种情况下我会禁止我控制之下的程序员
写包装类,并重新考虑设计。
> 调试倒是没有考虑过。和COM不同的是,concept是源码的一部分,看得见摸得着,自然也是可以调试的,往里单步就是了。当然,这需要调试工具支持。
这里有两个问题:
a) 很多bug是无法调试的,至少在维护期这种情况绝对多于可调试的bug。
我见到的典型场景是用户只发过来一个dump,甚至没有准确的重现步骤。所以我的经验里
维护期测试人员最重要的不是调试能力,而是通过很有限的信息推理出一个重现步骤,甚
至运气更好点,推理出问题的根源。这时候代码可读性好坏往往对这个bug的理解有决定性
的影响。我也曾经见过一些bug由于维护组无法理解而不能解决的情况。
b) 即使代码看得见摸得着,我们也得考虑一个时间问题。我们有没有能力在几天之内从几百兆
的代码文件中定位到我们关心的部分?
所以综上所述,我的观点可以归结为:莫兄所描述的concept的表现能力非常强大,但这种强大
就我个人看来是用后期维护的痛苦换取程序员编写代码时的舒适,而站在一个专门维护别人代码
的人看来,这种功能近乎于有意给我们找麻烦。所以从这个角度上来说,我更乐于看到这个功能
被C++ ban掉。
如果莫兄的看法和我不同,欢迎排砖。
这话说得在理。
我可能习惯于用一种很悲观的态度看问题。因为现实中程序员索隐发微的能力实在
是匪夷所思。比如当初C++的设计者们提出template的时候我相信他们绝对不会想
到后来人们能拿它实现traits,或者Loki里的那些靠模板参数组合指定功能的智能指
针。C++一直以来的迷人之处也在于此,每一个特性都能发掘出一些新奇的用法,
我自己有很长一段时间也沉迷其中。但自从后来工作之后天天要读别人的代码,才
发现这种近乎于疯狂的功能发掘对维护人员来说绝对不是什么愉快的体验,甚至于
读自己早先写的程序有时候不顺畅。
所以现在我更青睐Python和C,把功能集合减少到最少的结果带来的往往就是规范
的设计,无论程序员怎么玩,最后能整出来的花样都是有限的。我最害怕的东西就
是加入一个功能的时候不经深思熟虑,然后后来发现要保持兼容不能不留着它。这
里的经典反面教材就是ActiveX的安全模型。这种情况下与其勉强在已有体系里加入
一个前途未卜的功能,还不如重新做一种语言。
>
> 另外,我想说一下代码难以阅读的问题。静态类型是靠类型系统类型来执行判断的,而动态语言这是靠concept这样的一个东西来推理的。我觉得concept更加符合人类的习惯,比如我们知道行走就是行走,表示在一个平面内移动,我们并不关心怎么样行走,也不关心到底是螃蟹在爬行还是坦克在推进,我们也不关心是现实中的物体在行走,还是游戏中的角色在行走。因此我无法定义一个接口给它,如果硬要指定一个所谓的IMoveable的话,我想这是一个讽刺。
现实是,这种IMoveable的接口才真的是到处都是,比如IComparable。当初说这
玩意儿符合人类习惯的Java信徒可不少。:)
这里有一点我想争论的是:“是否符合人类习惯”在我看来不能算作是代码阅读
难易的标准。也许是常年维护代码让我的视角和自己写代码的看法有了偏差,我
一直认为好读的代码的标准应该是在情况许可的前提下尽量地减少抽象,但实际
上很多框架——顺便说一句我对框架这词没好感——的继承关系高得可怕。想想
DesignPatterns书中提到的代码,实际上GoF并没有真的在书中鼓励大家到处使用
高高的继承树,但这不妨碍Wpf中写出这样的继承关系:
System.Object
System.Windows.Threading.DispatcherObject
System.Windows.DependencyObject
System.Windows.Media.Visual
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.AccessText
呵呵,先得谢谢莫兄如此详细的回复。
> concept可不是语法糖,是正经的first-class语言特性,就像函数或者类一样。
这里有一个问题:我从未见过正式的定义标准来规定first-class语言特性和语法糖的差异。
在我看来,如果某个语言特性的功能语言本身已经具有,但为了追求形式完善而加入一个
新的表示,那就可以称为语法糖,比如C#中的代理和属性,也包括Python当中的Decorator。
至于这个语言特性是不是first-class,这不重要,***更不是说语法糖就不正经***。顺便说一句,
first-class在我印象里函数式理论家采用的词,而且本身的定义也不清楚。
如果我的定义没错的话,那么莫兄所言的concept就是不折不扣的语法糖:语言中已经可以
用包装类实现的东西,现在为了形式上的一致或者说优雅又发明了一个新概念。我不是泛泛地
说语法糖不好,而仅仅针对莫兄上面的例子:我认为单纯为了一个少写算法代码而引入这个概
念不值得。原因下面详述:
>> 另外我认为这句话不成立:
>>> 如果有更多的复数表达类,那么会更加累人,更加浪费。
> 这句话要看上下文,这是针对那一堆转换函数而言的。SICP中运用Data Direct
> Programming处理这个问题,这边的思路一致,只是尝试用更新的手段而已。
>>
>> 我理解有时候得允许一个项目里使用多种不同数据结构表示相同的数据,但我绝对不理解的是我们怎我我觉得在这断代码中,你把concept所在的层次搞错了。觉得在这断代码中,你把concept所在的层次搞错了。
>> 么能还都允许把它们直接混在一起用。
>> ……
> 这些话实际上已经说明问题了,我们开发的时候经常会用到外源的,或者无法控制的代码,正如这些字符串。如何针对这些不同的类型写出抽象算法,保持DRY,就是这里的目的。莫兄这话没有正面回答我的疑问。我不是质疑concept的功能,更不是在质疑DRY原则,
> 不是所有的东西我们都能够控制的。
我所担心的是:这个功能似乎是在鼓励程序员随意在代码中混合使用来自各种函数库的各
种数据结构。而在我的经验里这种行为除了给后来人阅读代码制造困难之外不会带来什么
好处,特别是程序维护的过程中。
我对这个问题十分精神过敏的原因与多少与我的工作有关。我是做代码维护的,平时经常
需要面对的任务就是从一堆我之前从未阅读过的代码里找到一个客户报上来的bug的原因,
然后可能的话给出一个fix。对我来说,最可怕的不是代码规模大(早就习惯上来就是十万行
的代码了),而是数据结构的来源过于庞杂。
我在前面的举COM的例子就属于这种情况。我见过的代码中曾经有人用过多种不同的组件,
有的来自TSF,有的第三方的输入法库,有的来自他们自己写的COM组件,最要命的是,
它们都实现自同一个interface。结果当程序出问题的时候,作为维护人员首先要理解的不是
这些类的功能,而是这些interface分别对应的真实实现分别来自哪里,它是否在我的控制之下,
如果不是再找对应的人去解决。因为这时候我看到的好几千行代码里都是这样的代码:
CComPTR<ISomeCommonInterface> objPtr;
.... // 这里可能就有一大堆的代码,包括各层函数调用
hr = objPtr->DoSomething(); // 如果不用debugger,谁知道这个的真实的类是什么?
所以如果C++接受上面那种concept的用法的话,我最担心的场景是:某程序员写了个巨大的
concept库去整合XMLString,gchar*,std::string,char*,结果在维护的时候我们就可能看到
满屏幕的这样的代码:
auto mystring = DoSomething(string1.Value(), string2.Value());
原先至少我知道我可以到string1和string2的类型定义里寻找Value()的定义——实际上这已经够
困难了,因为string1和string2有时候是一个基类引用,它指向的代码并不总能简单地从上下文
看出来。可现在concept进一步打破了这个规则,光靠读代码我根本不可能知道那两个Value()究
竟是什么,因为他们可能是成员函数,也可能来自程序员自己定义的concept,甚至这个concept
可能来自第三方库。同样的,DoSomething()也有这个问题。如果再加上auto,那就根本不知所
云了。
这个呢,包装类的确是不能做。但我的意思其实是,这种情况下我会禁止我控制之下的程序员
>>
>> 如果我要把它用在普通函数而不是类上,问题只怕更大,因为普通函数的签名
>> 不像类那样还有一个对象名做前缀,而是采用A_do_something()和B_do_something()的形式,这时候concept仍然别无选
>> 择地要写更多的包装函数。
>
> 同样,作为比较,包装类由此能力否?
写包装类,并重新考虑设计。
这里有两个问题:
> 调试倒是没有考虑过。和COM不同的是,concept是源码的一部分,看得见摸得着,自然也是可以调试的,往里单步就是了。当然,这需要调试工具支持。
a) 很多bug是无法调试的,至少在维护期这种情况绝对多于可调试的bug。
我见到的典型场景是用户只发过来一个dump,甚至没有准确的重现步骤。所以我的经验里
维护期测试人员最重要的不是调试能力,而是通过很有限的信息推理出一个重现步骤,甚
至运气更好点,推理出问题的根源。这时候代码可读性好坏往往对这个bug的理解有决定性
的影响。我也曾经见过一些bug由于维护组无法理解而不能解决的情况。
b) 即使代码看得见摸得着,我们也得考虑一个时间问题。我们有没有能力在几天之内从几百兆
的代码文件中定位到我们关心的部分?
所以综上所述,我的观点可以归结为:莫兄所描述的concept的表现能力非常强大,但这种强大
就我个人看来是用后期维护的痛苦换取程序员编写代码时的舒适,而站在一个专门维护别人代码
的人看来,这种功能近乎于有意给我们找麻烦。所以从这个角度上来说,我更乐于看到这个功能
被C++ ban掉。
插一句:既然大家都是理论探讨,那么老兄你为何后面却要反问我试过没有?呵呵
纯属玩笑,决无讽刺的意思。
我并不反对探索,但我不希望在一种已经广为应用的语言中随意增加新特性来实现
这样的探索。如果我们能在某个实验性的语言中实现concept,我倒没有意见。
>> 我对这个问题十分精神过敏的原因与多少与我的工作有关。我是做代码维护的,
>> 平时经常需要面对的任务就是从一堆我之前从未阅读过的代码里找到一个客户
>> 报上来的bug的原因,然后可能的话给出一个fix。对我来说,最可怕的不是代码
>> 规模大(早就习惯上来就是十万行的代码了),而是数据结构的来源过于庞杂。
>
> 你所说的这些麻烦,真的不是concept、oop什么的所能解决的。我们竭力发掘各
> 类抽象技术,也无非是为了使得代码更加简洁人性而已。从这一层面上,或许还
> 能有那么一点点帮助,但是对于既成事实,没有办法。
这一点我同意。正如人类增长的生产力无法解决贪欲一样,技术不可能解决程序员
对过度设计的渴望。我强调的是,语言设计者在增加语言特性的时候应该全面地
考虑这个特性对整个既有的语言元素的影响,特别是在一个特性已然相当复杂的语
言当中加入这个功能尤其需要谨慎。还是那句,如果我们能先在一个小规模的实验语
言上做个实验,或许更稳妥一些。
>
>>
>> 我在前面的举COM的例子就属于这种情况。……
>
> 还是那句话,没有实现,就无法证实你的担心。仅就这句代码而言,string1、
> string2可能是类型,或者concept,不管怎么样,必然有Value()。从gp的角度
> 出发,在这断代码中,我们关心的不是string1、string2是什么类型,而是看上
> 去是什么样子,我调用他们上面的Value(),他们也保证提供这个函数。如果认
> 为Value()的实现有问题,那么应该到这句代码所在的函数之外去找,从而了解
> string1、string2是什么,有没有Value(),是不是被adapt了...对于新的东西,你
> 自然不能用老的方式思考。
> 查一句:pongba曾经建议用concept代替auto,具有更加明确的含义。好像给
> bs他们写过信,但貌似没有后话。
我当然不愿意用老的方式思考。但我面对的还是那个C++,我的调试器和bug强迫我
在新的条件下解决老问题。
我不关注GP或者OOP,事实上我关注的只有一点:在阅读大规模代码的前提下,
这个语言特性能不能帮助我尽快地定位出问题的代码。我说过,我的工作是维护,
显然很容易关心这个问题。
必然有Value()是对的,但我说过的:作为一个靠维护程序吃饭的民工,我不单要求
要能回答“最终我能找到”,而且还要回答“对,我在48小时能找到”。而老实说,莫兄
您的代码让我不觉得我还能这么有底气,而且事实上所有我看过的鼓吹concept的
文章中没有任何人曾经谈到过这个问题。
当然您可以认为我是个不思进取的懦夫,那我认了。我只能说去年对付的那几十万
行ATL/COM+DCOM+.NET的纯面向接口代码(某种角度上来说这种只针对接口的
编程也可以算是纯概念编程,COO?)的确让我丧了胆,虽然最后也确实一一解
决了。
>>
>> > 调试倒是没有考虑过。和COM不同的是,concept是源码的一部分,看得见摸得着,自然也是可以调试的,往里单步就是了。当然,这需要调试工具支持。
>>
>> 这里有两个问题:
>>
>> a) 很多bug是无法调试的,至少在维护期这种情况绝对多于可调试的bug。
>>
>> 我见到的典型场景是用户只发过来一个dump,甚至没有准确的重现步骤。
>> 所以我的经验里维护期测试人员最重要的不是调试能力,而是通过很有限
>> 的信息推理出一个重现步骤,甚至运气更好点,推理出问题的根源。这时
>> 候代码可读性好坏往往对这个bug的理解有决定性的影响。我也曾经见过
>> 一些bug由于维护组无法理解而不能解决的情况。
>>
>> b) 即使代码看得见摸得着,我们也得考虑一个时间问题。我们有没有能力
>> 在几天之内从几百兆的代码文件中定位到我们关心的部分?
>
> 这和concept有关吗?
当然有关。语言特性为代码编写负责,而代码则同时对开发和维护负责。如果
一个语言特性带来的副作用是难于分析和阅读,而仅仅是利于编写——注意,
我说的分析不是按着快捷键单步,那个太基础了,而是包括各种手法,包括
dump分析,fault injection和代码阅读——那么我认为加入这个特性是失败的。
>>
>> 所以综上所述,我的观点可以归结为:莫兄所描述的concept的表现能力非
>> 常强大,但这种强大就我个人看来是用后期维护的痛苦换取程序员编写代码
>> 时的舒适,而站在一个专门维护别人代码的人看来,这种功能近乎于有意给
>> 我们找麻烦。所以从这个角度上来说,我更乐于看到这个功能被C++ ban掉。
>
> 或许这是真的。不过,一定是真的吗?你试过吗?
这话莫兄已经替我回答了。我当然没有,没有实现的东西当然没法试,但是这话
对您也一样成立:您真的肯定concept不会产生我说的问题么?您试过么?显然,
您也没试过。所以这种互相反问的方法辩论不出任何东西来,我们还是罢手吧。
说句不客气的话,如果前面yq chen的说法成立,即concept的主要目的在于确保
template的编译错误能给出一个方便理解的输出,那么莫兄您自己的例子就已经
证明了我的说法:您已经走歪了,或者说,在“重新发现”concept的新用法了。您
那个例子里您是那concept实现了Ruby的Mix-in,而这正是我一直担心的问题,
即过度发掘语言的特性导致巴洛克风格的代码,而写代码的人往往自己不会发现
这个问题,因为他们会陶醉在语法糖所带来的方便和成就感中;除非他们自己在
过了几年之后开始维护别人写的同样的软件,并且被其中无数只能看到concept
(或者interface,随您怎么说)却无法找到实现的代码所困扰,才会意识到追求
语法糖的麻烦。
顺便说一句,您的第一段回答似乎也承认了您在有意无意地发掘语言特性的未知
用法。那么这是不是说明您已经默认了我的担心呢?
最后怨念一句:如果世上真的有一种能实现concept基本特性的语言,我想我们两个
的争论可能就没什么意义了吧。
>> 莫兄这话没有正面回答我的疑问。我不是质疑concept的功能,更不是在质疑
>> DRY原则,我所担心的是:这个功能似乎是在鼓励程序员随意在代码中混合
>> 使用来自各种函数库的各种数据结构。而在我的经验里这种行为除了给后来插一句:既然大家都是理论探讨,那么老兄你为何后面却要反问我试过没有?呵呵
>> 人阅读代码制造困难之外不会带来什么好处,特别是程序维护的过程中。
>
> 这个问题我现在没有办法回答。concept还是很新鲜的东西,并没有实现,而
> 且即便前C++0x中的方案,也仅仅触及了concept的一小部分。我所作的只
> 是在理论上探讨concept的特性,挖掘它的能力。因为没有实现,所以我们无
> 法断定程序员在拥有了这种技术之后的行为。所以,这也是C++中引入concept
> 的重要性,更多地是一种探索,我们可以从中了解其特性,掌握其问题。现
> 在,concept跳票之后,我们所能做的,也只能是理论上探讨了。
纯属玩笑,决无讽刺的意思。
我并不反对探索,但我不希望在一种已经广为应用的语言中随意增加新特性来实现
这样的探索。如果我们能在某个实验性的语言中实现concept,我倒没有意见。
我当然不愿意用老的方式思考。但我面对的还是那个C++,
我的调试器和bug强迫我
在新的条件下解决老问题。
我不关注GP或者OOP,事实上我关注的只有一点:在阅读大规模代码的前提下,
这个语言特性能不能帮助我尽快地定位出问题的代码。我说过,我的工作是维护,
显然很容易关心这个问题。
必然有Value()是对的,但我说过的:作为一个靠维护程序吃饭的民工,我不单要求
要能回答“最终我能找到”,而且还要回答“对,我在48小时能找到”。而老实说,莫兄
您的代码让我不觉得我还能这么有底气,而且事实上所有我看过的鼓吹concept的
文章中没有任何人曾经谈到过这个问题。
当然您可以认为我是个不思进取的懦夫,那我认了。我只能说去年对付的那几十万
行ATL/COM+DCOM+.NET的纯面向接口代码(某种角度上来说这种只针对接口的
编程也可以算是纯概念编程,COO?)的确让我丧了胆,虽然最后也确实一一解
决了。
当然有关。语言特性为代码编写负责,而代码则同时对开发和维护负责。如果
>>
>> > 调试倒是没有考虑过。和COM不同的是,concept是源码的一部分,看得见摸得着,自然也是可以调试的,往里单步就是了。当然,这需要调试工具支持。
>>
>> 这里有两个问题:
>>
>> a) 很多bug是无法调试的,至少在维护期这种情况绝对多于可调试的bug。
>>
>> 我见到的典型场景是用户只发过来一个dump,甚至没有准确的重现步骤。
>> 所以我的经验里维护期测试人员最重要的不是调试能力,而是通过很有限
>> 的信息推理出一个重现步骤,甚至运气更好点,推理出问题的根源。这时
>> 候代码可读性好坏往往对这个bug的理解有决定性的影响。我也曾经见过
>> 一些bug由于维护组无法理解而不能解决的情况。
>>
>> b) 即使代码看得见摸得着,我们也得考虑一个时间问题。我们有没有能力
>> 在几天之内从几百兆的代码文件中定位到我们关心的部分?
>
> 这和concept有关吗?
一个语言特性带来的副作用是难于分析和阅读,而仅仅是利于编写——注意,
我说的分析不是按着快捷键单步,那个太基础了,而是包括各种手法,包括
dump分析,fault injection和代码阅读——那么我认为加入这个特性是失败的。
>> 时的舒适,而站在一个专门维护别人代码的人看来,这种功能近乎于有意给
>>
>> 所以综上所述,我的观点可以归结为:莫兄所描述的concept的表现能力非
>> 常强大,但这种强大就我个人看来是用后期维护的痛苦换取程序员编写代码
>> 我们找麻烦。所以从这个角度上来说,我更乐于看到这个功能被C++ ban掉。这话莫兄已经替我回答了。我当然没有,没有实现的东西当然没法试,但是这话
>
> 或许这是真的。不过,一定是真的吗?你试过吗?
对您也一样成立:您真的肯定concept不会产生我说的问题么?您试过么?显然,
您也没试过。所以这种互相反问的方法辩论不出任何东西来,我们还是罢手吧。
说句不客气的话,如果前面yq chen的说法成立,即concept的主要目的在于确保
template的编译错误能给出一个方便理解的输出,那么莫兄您自己的例子就已经
证明了我的说法:您已经走歪了,或者说,在“重新发现”concept的新用法了。您
那个例子里您是那concept实现了Ruby的Mix-in,而这正是我一直担心的问题,
即过度发掘语言的特性导致巴洛克风格的代码,而写代码的人往往自己不会发现
这个问题,因为他们会陶醉在语法糖所带来的方便和成就感中;除非他们自己在
过了几年之后开始维护别人写的同样的软件,并且被其中无数只能看到concept
(或者interface,随您怎么说)却无法找到实现的代码所困扰,才会意识到追求
语法糖的麻烦。
顺便说一句,您的第一段回答似乎也承认了您在有意无意地发掘语言特性的未知
用法。那么这是不是说明您已经默认了我的担心呢?
最后怨念一句:如果世上真的有一种能实现concept基本特性的语言,我想我们两个
的争论可能就没什么意义了吧。
On Sep 16, 10:41 am, Fuzhou Chen <cppof...@gmail.com> wrote:
> 呵呵,先得谢谢莫兄如此详细的回复。
>
> > concept可不是语法糖,是正经的first-class语言特性,就像函数或者类一样。
>
> 这里有一个问题:我从未见过正式的定义标准来规定first-class语言特性和语法糖的差异。
> 在我看来,如果某个语言特性的功能语言本身已经具有,但为了追求形式完善而加入一个
> 新的表示,那就可以称为语法糖,比如C#中的代理和属性,也包括Python当中的Decorator。
> 至于这个语言特性是不是first-class,这不重要,***更不是说语法糖就不正经***。顺便说一句,
> first-class在我印象里函数式理论家采用的词,而且本身的定义也不清楚。
>
> 如果我的定义没错的话,那么莫兄所言的concept就是不折不扣的语法糖:语言中已经可以
> 用包装类实现的东西,现在为了形式上的一致或者说优雅又发明了一个新概念。我不是泛泛地
> 说语法糖不好,而仅仅针对莫兄上面的例子:我认为单纯为了一个少写算法代码而引入这个概
> 念不值得。原因下面详述:
>
> >> 另外我认为这句话不成立:
> >>> 如果有更多的复数表达类,那么会更加累人,更加浪费。
> > 这句话要看上下文,这是针对那一堆转换函数而言的。SICP中运用Data Direct
> > Programming处理这个问题,这边的思路一致,只是尝试用更新的手段而已。
>
> >> 我理解有时候得允许一个项目里使用多种不同数据结构表示相同的数据,但我绝对不理解的是我们怎
> >> 么能还都允许把它们直接混在一起用。
> >> ......
> > 这些话实际上已经说明问题了,我们开发的时候经常会用到外源的,或者无法控制的代码,正如这些字符串。如何针对这些不同的类型写出抽象算法,保持DRY,就是这里的目的。
> > 不是所有的东西我们都能够控制的。
>
> 莫兄这话没有正面回答我的疑问。我不是质疑concept的功能,更不是在质疑DRY原则,
> 我所担心的是:这个功能似乎是在鼓励程序员随意在代码中混合使用来自各种函数库的各
> 种数据结构。而在我的经验里这种行为除了给后来人阅读代码制造困难之外不会带来什么
> 好处,特别是程序维护的过程中。
>
> 我对这个问题十分精神过敏的原因与多少与我的工作有关。我是做代码维护的,平时经常
> 需要面对的任务就是从一堆我之前从未阅读过的代码里找到一个客户报上来的bug的原因,
> 然后可能的话给出一个fix。对我来说,最可怕的不是代码规模大(早就习惯上来就是十万行
> 的代码了),而是数据结构的来源过于庞杂。
>
> 我在前面的举COM的例子就属于这种情况。我见过的代码中曾经有人用过多种不同的组件,
> 有的来自TSF,有的第三方的输入法库,有的来自他们自己写的COM组件,最要命的是,
> 它们都实现自同一个interface。结果当程序出问题的时候,作为维护人员首先要理解的不是
> 这些类的功能,而是这些interface分别对应的真实实现分别来自哪里,它是否在我的控制之下,
> 如果不是再找对应的人去解决。因为这时候我看到的好几千行代码里都是这样的代码:
>
> CComPTR<ISomeCommonInterface> objPtr;
> .... // 这里可能就有一大堆的代码,包括各层函数调用
> hr = objPtr->DoSomething(); // 如果不用debugger,谁知道这个的真实的类是什么?
>
> 所以如果C++接受上面那种concept的用法的话,我最担心的场景是:某程序员写了个巨大的
> concept库去整合XMLString,gchar*,std::string,char*,结果在维护的时候我们就可能看到
> 满屏幕的这样的代码:
>
> auto mystring = DoSomething(string1.Value(), string2.Value());
>
> 原先至少我知道我可以到string1和string2的类型定义里寻找Value()的定义----实际上这已经够
好吧,我只好说莫兄似乎仍然在回避我的疑问。当我说您文中所说的concept实现
Mix-in不适合调试,您的回答是“单步跟”就行了。我只好解释说我知道的情况里
能够单步跟的bug实在少之又少,我希望说明的是不适合调试不是简单地单步跟踪
就能解决的,添加语言特性的时候多少也要为这个考虑一点。如今您又开始说我是把
讨论引向代码维护和程序员行为。
我从没觉得我偏题过。我一直强调的是:当试图在一门已经广为应用的语言中引入
一个可能影响很大的语言特性时,语言的设计者应当全面地评估它可能对软件开发
的影响,包括编码和维护,也包括阅读和调试,而不是仅仅强调这个功能有多么cool,
多么powerful,程序员未来的日子会有多轻松。我认为这有误导的嫌疑。
莫兄很成功地唱了一出红脸,说了concepts的好,那我就不妨来唱唱黑脸,说说
它的坏。恰好我选取的角度是代码维护和程序员行为,因为在我的经验里,过分强调
抽象带来的负面效果会在这两个地方明显地体现出来。我不认为我在东拉西扯。
>
>>
>> 我不关注GP或者OOP,事实上我关注的只有一点:在阅读大规模代码的前提下,
>> 这个语言特性能不能帮助我尽快地定位出问题的代码。我说过,我的工作是维护,
>> 显然很容易关心这个问题。
>
> 请关注一下吧,为了讨论方便。
>
我想您误会了,我不是说我从没看过GP和OOP。七年前我也曾经疯狂地迷恋STL和Loki,
通读过所有的代码,也做过一些简单的练习,比如要求自己把算法书上所有的数据结构
和算法都写成STL的形式,后来工作了两年,基本上都在和ATL打交道。一直到今年才慢慢
开始远离C++为主的代码,转而参加C为主而C++ COM为辅的项目。
我所强调的是:在代码质量的标准上,我的经验中开发和维护的观点是很不同的。开发人员
喜欢的是“抽象”和“减少代码量”,维护人员喜欢的是“具体”和“易于定位”。显然您
一直站在开发的立场上发言,而我则坚持在维护的立场上说话。而正如我之前说的,开发和
维护都是软件开发的一部分,总得听听维护人员的声音吧。
我从未否认过concepts对进一步改进抽象的作用。但是正如我早先的mail所说,维护人员
不喜欢抽象。是的,过多的抽象会让当时写程序的程序员轻松很多,我也记得当时我第一次
自己实现了一个STL的multi-graph的兴奋劲。但后来在工作中才意识到太多抽象只能让后来
人阅读代码和定位问题变得越发地困难,而且我有之前COM的例子为例。如果您认为我的观
点有问题,那么您至少得证明我的看法有误,比如能给一个例子证明“抽象更多能让维护人
员更轻松”。
顺便说一句:莫兄劝我多关注一些GP和OOP,而我倒认为莫兄似乎对我所一直强调的维护期
的调试问题同样所知不多。以我的经验而言,我的工作中简单地把调试等同于单步跟踪是解不
出几个问题的。
>
>>
>> 必然有Value()是对的,但我说过的:作为一个靠维护程序吃饭的民工,我不单要求
>> 要能回答“最终我能找到”,而且还要回答“对,我在48小时能找到”。而老实说,莫兄
>> 您的代码让我不觉得我还能这么有底气,而且事实上所有我看过的鼓吹concept的
>> 文章中没有任何人曾经谈到过这个问题。
>
> 无能为力,这个问题需要想象力。
>
不同意。我说的场景不是来自臆想,因为我自己就是吃这碗饭的,而且我认识很多公司里做
维护工作的人,华为,阿尔卡特,北电,什么人都有。仅仅在我们组,负责维护的人数就高达
上千人。除非您打算告诉我C++整个社区没有人做过专门维护别人代码的工作,或者更糟糕
的,C++的设计者们一开始就打算对这个问题视而不见——这可不是我想看到的。
>
>>
>> 当然您可以认为我是个不思进取的懦夫,那我认了。我只能说去年对付的那几十万
>> 行ATL/COM+DCOM+.NET的纯面向接口代码(某种角度上来说这种只针对接口的
>> 编程也可以算是纯概念编程,COO?)的确让我丧了胆,虽然最后也确实一一解
>> 决了。
>
> 的确如此。
>
好吧,这个名头其实是我的一个老朋友送的,您也不是第一个这么认为的人,俺笑纳,呵呵。
>
>> > 这和concept有关吗?
>>
>> 当然有关。语言特性为代码编写负责,而代码则同时对开发和维护负责。如果
>> 一个语言特性带来的副作用是难于分析和阅读,而仅仅是利于编写——注意,
>> 我说的分析不是按着快捷键单步,那个太基础了,而是包括各种手法,包括
>> dump分析,fault injection和代码阅读——那么我认为加入这个特性是失败的。
>
> 我在js里用了不少lambda,把一个同事搞晕了。然后,lambda就成了失败的特性,在js里...
> 要说难读,fp代码够劲,同样可以被维护地很好。人适应环境的能力还是很强的。
> 造成代码难读的原因很多了,代码风格、设计、结构、抽象度、对语言的熟悉程度,C++本身的缺陷对代码的阅读和分析产生的巨大副作用等等。
> 反过来说,同样不能排除使用gp后,代码更加简洁,直观,易于分析。一旦代码DRY程度提高,抽象度提高,出bug的机会也会减少。这种因素都不能排除。这就像计算碳排放,需得从全局的角度出发加以分析。
> 我是乐观主义者,主张把bug掐死在摇篮里。这也是各种编程技术被发掘出来的目的。如果只担心维护时的麻烦,那OO也不该用。因为当调用的一个成员函数出错时,我们还需要逆着继承树,挨个追踪成员是属于谁的。
>
事实上我确实在公司强烈反对在JS代码里用lambda,我只在自己写Scheme脚本的时候
坚持使用,所以您不是第一个和我吵架的。:)
需要澄清的是我并不是泛泛地认为OO不该用,而是说不能像我的同事那么用。我之前的mail
也提过,我一直用Python,也并没有认为Python的OO有问题。因为在我看来Python
的一些机制能够有效地限制程序员无理地滥用OO。比如Python当中不允许通过参数表重载
函数,一定程度上限制了程序员大量地为不同的函数和方法起相同的名字从而降低可读性。
就我看来,C++在限制程序员滥用语言机制的问题上做得并不好,而我认为这是语言设计
者的责任——打住,这纯属个人看法,且无关主题。如果有不同意见,我们另开一个thread
拍砖如何?
我认为,一个语言特性在几乎要决定加入语言时我们甚至都还在探讨它可能的适用范围,
那么我认为这个特性的定义是不严格的。我承认严格定义的特性有时候也会被发掘出新的
特性,比如Java 1.3的反射最初就不是设计用来做依赖注入的。但是如果我们急匆匆地把一个
定义尚不严格的特性引入标准,那无异于是敞开大门欢迎误用的到来。所以没错,这一次我赞
赏C++委员会的决定。
对于已经加入语言的东西我们不便褒贬,但既然我们在探讨一个新的语言特性,而且讨论的对象
是C++这种已经广为应用的语言,我们可否更加谨慎一点?何况C++ 98中的所谓陷阱也已经太多
了,少一点又何乐而不为呢?
>> 说句不客气的话,如果前面yq chen的说法成立,即concept的主要目的在于确保
>> template的编译错误能给出一个方便理解的输出,那么莫兄您自己的例子就已经
>> 证明了我的说法:您已经走歪了,或者说,在“重新发现”concept的新用法了。您
>> 那个例子里您是那concept实现了Ruby的Mix-in,而这正是我一直担心的问题,
>> 即过度发掘语言的特性导致巴洛克风格的代码,而写代码的人往往自己不会发现
>> 这个问题,因为他们会陶醉在语法糖所带来的方便和成就感中;除非他们自己在
>> 过了几年之后开始维护别人写的同样的软件,并且被其中无数只能看到concept
>> (或者interface,随您怎么说)却无法找到实现的代码所困扰,才会意识到追求
>> 语法糖的麻烦。
>
> yq chen的说法不成立。在Hasskel、ada等语言中,已经存在类似的东西,concept更加系统全面而已。
> 或许你说的是对的,但你也说过不反对探索,何必这么排斥呢。
看来yq chen的说法确实不全面。我C++ 0x所知不多,也很多年没有跟踪过,所以昨晚专门复习了一下
Bjarne自己的文章:
http://www.research.att.com/~bs/popl06.pdf
>>>
Current work on improving templates
focuses on the notion of concepts (a type system for
templates), which promises significantly improved error diagnostics
and increased expressive power such as conceptbased
overloading and function template partial specialization.
<<<
他确实提到了concept based overloading和function template
partial specification,但他的例子里并没有莫兄的文章中所描述的那种用concept
做mix-in的方法,可见您确实是在“重新发现”哪。
如果我理解有误,请指出。
>
> 发掘语言特性的用法的事很多,几乎每个语言都有,多数都得到了好的结果,别急着下结论。
>
真的“多数都得到了好的结果”么?在我看来template的过度发掘就不是个好结果。相比于
Python的代码,Boost代码的阅读门槛也未免太高了些。但这个问题太个人化了,我们还是就
此打住罢。
深有同感,一开始用抽象、框架飙得很爽,后面要改点什么东西的时候找不着北,恨不得重写一个。
(OO有些看上去很美的原则,比如开放-封闭原则,但这些都只能针对"意料之中"的变化。
遇到意料之外的变化,反而成了累赘。况且人在预测"什么会变"方面似乎并不在行。)
On Sep 17, 6:50 am, Fuzhou Chen <cppof...@gmail.com> wrote:
> 2009/9/16 莫华枫 <longshank...@gmail.com>:
>
>
>
>
>
> > 2009/9/16 Fuzhou Chen <cppof...@gmail.com>
> 的,C++的设计者们一开始就打算对这个问题视而不见----这可不是我想看到的。
>
>
>
> >> 当然您可以认为我是个不思进取的懦夫,那我认了。我只能说去年对付的那几十万
> >> 行ATL/COM+DCOM+.NET的纯面向接口代码(某种角度上来说这种只针对接口的
> >> 编程也可以算是纯概念编程,COO?)的确让我丧了胆,虽然最后也确实一一解
> >> 决了。
>
> > 的确如此。
>
> 好吧,这个名头其实是我的一个老朋友送的,您也不是第一个这么认为的人,俺笑纳,呵呵。
>
>
>
> >> > 这和concept有关吗?
>
> >> 当然有关。语言特性为代码编写负责,而代码则同时对开发和维护负责。如果
> >> 一个语言特性带来的副作用是难于分析和阅读,而仅仅是利于编写----注意,
> >> 我说的分析不是按着快捷键单步,那个太基础了,而是包括各种手法,包括
> >> dump分析,fault injection和代码阅读----那么我认为加入这个特性是失败的。
>
> > 我在js里用了不少lambda,把一个同事搞晕了。然后,lambda就成了失败的特性,在js里...
> > 要说难读,fp代码够劲,同样可以被维护地很好。人适应环境的能力还是很强的。
> > 造成代码难读的原因很多了,代码风格、设计、结构、抽象度、对语言的熟悉程度,C++本身的缺陷对代码的阅读和分析产生的巨大副作用等等。
> > 反过来说,同样不能排除使用gp后,代码更加简洁,直观,易于分析。一旦代码DRY程度提高,抽象度提高,出bug的机会也会减少。这种因素都不能排除。这就像计算碳排放,需得从全局的角度出发加以分析。
> > 我是乐观主义者,主张把bug掐死在摇篮里。这也是各种编程技术被发掘出来的目的。如果只担心维护时的麻烦,那OO也不该用。因为当调用的一个成员函数出错时,我们还需要逆着继承树,挨个追踪成员是属于谁的。
>
> 事实上我确实在公司强烈反对在JS代码里用lambda,我只在自己写Scheme脚本的时候
> 坚持使用,所以您不是第一个和我吵架的。:)
>
> 需要澄清的是我并不是泛泛地认为OO不该用,而是说不能像我的同事那么用。我之前的mail
> 也提过,我一直用Python,也并没有认为Python的OO有问题。因为在我看来Python
> 的一些机制能够有效地限制程序员无理地滥用OO。比如Python当中不允许通过参数表重载
> 函数,一定程度上限制了程序员大量地为不同的函数和方法起相同的名字从而降低可读性。
> 就我看来,C++在限制程序员滥用语言机制的问题上做得并不好,而我认为这是语言设计
> 者的责任----打住,这纯属个人看法,且无关主题。如果有不同意见,我们另开一个thread
> 拍砖如何?
>
> 我认为,一个语言特性在几乎要决定加入语言时我们甚至都还在探讨它可能的适用范围,
> 那么我认为这个特性的定义是不严格的。我承认严格定义的特性有时候也会被发掘出新的
> 特性,比如Java 1.3的反射最初就不是设计用来做依赖注入的。但是如果我们急匆匆地把一个
> 定义尚不严格的特性引入标准,那无异于是敞开大门欢迎误用的到来。所以没错,这一次我赞
> 赏C++委员会的决定。
>
> 对于已经加入语言的东西我们不便褒贬,但既然我们在探讨一个新的语言特性,而且讨论的对象
> 是C++这种已经广为应用的语言,我们可否更加谨慎一点?何况C++ 98中的所谓陷阱也已经太多
> 了,少一点又何乐而不为呢?
>
> >> 说句不客气的话,如果前面yq chen的说法成立,即concept的主要目的在于确保
> >> template的编译错误能给出一个方便理解的输出,那么莫兄您自己的例子就已经
> >> 证明了我的说法:您已经走歪了,或者说,在"重新发现"concept的新用法了。您
> >> 那个例子里您是那concept实现了Ruby的Mix-in,而这正是我一直担心的问题,
> >> 即过度发掘语言的特性导致巴洛克风格的代码,而写代码的人往往自己不会发现
> >> 这个问题,因为他们会陶醉在语法糖所带来的方便和成就感中;除非他们自己在
> >> 过了几年之后开始维护别人写的同样的软件,并且被其中无数只能看到concept
> >> (或者interface,随您怎么说)却无法找到实现的代码所困扰,才会意识到追求
> >> 语法糖的麻烦。
>
> > yq
>
> ...
>
> read more >>
这确实是我想说的。开发时期的调试往往以意料之中的问题为主,而维护的可怕之处在于问题永远
出现在意料之外。这个时候读代码的负担比开发阶段大很多,因为我们不知道该读哪里,也不知道
该读哪些。
我说一个例子,是我去年负责的一个bug。在Tablet PC上我们的程序仅仅会在如下两个情况都具
备的时候崩溃:
a) 系统是XP SP2 + Windows Media Player 11;
b) 系统从XP SP2 在线升级到XP SP3;
开始的报告里用户只是说“升级过程中发生崩溃”,什么都没有讲。解决这个问题我花了整整五
天,最后才发现问题出在Windows XP的欢迎界面代码上。这东西在SP2升级SP3的过程中需要
调Media Player的动态库,却因为不兼容MediaPlayer 11的组件实现而崩溃,而我们的程序是在
欢迎界面的一个框架之下作为动态库实现的插件,所以我们也就殃及池鱼了。
这个例子无法说明抽象过度存在的问题,但它能很好地说明维护与开发对软件分析思路的区别。
我能在一周之内找到问题,有一部分也是得益于那些个基本上用C写成的框架没有什么花巧的抽象。
我也同样见过一些最终不了了之的案例,就是因为我们的developer无法理解那些从头到尾都是
interface和connection points的代码。限于版权问题,我就不能给代码了。抱歉。
这确实是我想说的。开发时期的调试往往以意料之中的问题为主,而维护的可怕之处在于问题永远出现在意料之外。这个时候读代码的负担比开发阶段大很多,因为我们不知道该读哪里,也不知道
该读哪些。
我说一个例子,是我去年负责的一个bug。在Tablet PC上我们的程序仅仅会在如下两个情况都具
备的时候崩溃:
a) 系统是XP SP2 + Windows Media Player 11;
b) 系统从XP SP2 在线升级到XP SP3;
开始的报告里用户只是说“升级过程中发生崩溃”,什么都没有讲。解决这个问题我花了整整五
天,最后才发现问题出在Windows XP的欢迎界面代码上。这东西在SP2升级SP3的过程中需要
调Media Player的动态库,却因为不兼容MediaPlayer 11的组件实现而崩溃,而我们的程序是在
欢迎界面的一个框架之下作为动态库实现的插件,所以我们也就殃及池鱼了。
这个例子无法说明抽象过度存在的问题,但它能很好地说明维护与开发对软件分析思路的区别。
我能在一周之内找到问题,有一部分也是得益于那些个基本上用C写成的框架没有什么花巧的抽象。
我也同样见过一些最终不了了之的案例,就是因为我们的developer无法理解那些从头到尾都是
interface和connection points的代码。限于版权问题,我就不能给代码了。抱歉。
On 9月13日, 下午3时41分, Ian Yang <doit....@gmail.com> wrote:
> 只能继续用traits了。
>
> OO的虚函数有个致命缺点,调用不能inline。
>
> -------
> Sincerely, Ian Yang
>
> 2009/9/13 莫华枫 <longshank...@gmail.com>
>
>
>
> > <https://mail.google.com/mail/?ui=2&view=bsp&ver=1qygpcgurkovy>
> > return T(*getReal(lhd)* +*getReal(rhd)* , *getImg(lhd)* +*
> > getImg(rhd)* );
> > };
> > template<typename T>
> > T operator*(T lhd, T rhd) {
> > return T(*getMag(lhd)* **getMag(rhd)* , *getAngle(lhd)* +*
> > getAngle(rhd)* );
> > }
>
> > 非常简洁,非常抽象,并且充满了了对称之美。但是,非常累人。两个复数类,四种访问,需要写8个函数。而且其中一半仅仅是做了一个简单的调用,实在有些浪费。如-果有更多的复数表达类,那么会更加累人,更加浪费。
> > BenComplex operator+(*IRectComplex* lhd, *IRectComplex* rhd) {
> > return BenComplex(*lhd.getReal()* +*rhd.getReal()* , *
> > lhd.getImg()* +*rhd.getImg()* );
> > }
> > AsslyComplex operator*(IPolarComplex lhd, IPolarComplex rhd) {
> > return AsslyComplex(*lhd.getMag()* **rhd.getMag()* , *
> > lhd.getAngle()* +*rhd.getAngle()* );
> > }
>
> > 两个接口,IRectComplex和IPolarComplex,分表描述直角坐标和极坐标所需的函数。而两个类都实现这两个接口。这样,无论哪个类都可以直-接用于+和*操作,而无需一个额外的转换。同时,也减少了那一半没有做任何计算的函数。
> > 但是,OOPer们,别高兴得太早,麻烦接踵而至。首先,笨和爱死理不高兴了。
> > 笨说:"我写的复数类用的是直角坐标,干嘛还要实现一个极坐标的接口,我就是不喜欢极坐标,太恶心人了..."
> > 而爱死理说:"我就是喜欢极坐标,优雅!干嘛还非得实现一个直角坐标接口,我讨厌直角坐标,呆板..."
>
> > 然后,还有件更恐怖的事:一个新来的程序员,用了一个新的复数表示法。这样,便有了三个接口需要每一个类实现。笨和爱死理无论如何不肯再加接口了。于是,三个人-吵成一团。
>
> > 很显然,OOP的Interface(抽象类)是侵入式的,必须由相关类型配合,加以实现。如果开发者不配合,或者无法配合,那么事情就混乱了。而且,对于变化-的适应性不如前面的转换函数来的强,也不够灵活。
>
> > 小结一下,OOP方式,可以减少很多无意义的代码,但面对变化的灵活性差。转换函数(也可以看作另一种形式的接口)的方式,灵活性高,但需要多写很多没有执行实-际转换的函数。两者互有胜负,各有优缺点。
> > 接下来,我打算通过concept/concept map/adapter,实现一匹不吃草的好马儿。
> > 首先,我们接受笨和爱死理最初开发的那两个类,笨的类只考虑直角坐标的东西,而爱死理的类只考虑极坐标的操作。两者不用实现对方的接口,互不搭界。
> > 然后,我们定义两个concept。(请注意,这些事情我们都是在笨和爱死理不知情的情况下做的,以免他们不开心):
> > concept RectComplex<T>
> > {
> > float T::getReal();
> > float T::getImg();
> > };
> > concept PolarComplex<T>
> > {
> > float T::getMag();
> > float T::getAngle();
> > };
> > 接着,我们可以编写+和*操作了:
> > BenComplex operator+(*RectComplex* lhd, *RectComplex* rhd) {
> > return BenComplex(lhd.getReal()+rhd.getReal(),
> > lhd.getImg()+rhd.getImg());
> > }
> > AsslyComplex operator*(*PolarComplex* lhd, *PolarComplex* rhd) {
> > return AsslyComplex(lhd.getMag()*rhd.getMag(),
> > lhd.getAngle()+rhd.getAngle());
> > }
> > 最后,在用之前,我们必须将类型和concept绑定。但是,这不是一般的绑定,绑定的同时,我们还需要弥合各种不同的复数表示法之间的差异:
> > concept_map RectComplex<BenComplex>;
> > concept_map PolarComplex<AsslyComplex>;
>
> > 这两个绑定是顺理成章的,BenComplex本来就是直角坐标表示法,而AsslyComplex本来就是极坐标表示,它们与相应的concept之间完全契-合,无须额外修正。接下来,需要面对不同表示法之间的绑定了:
> > concept_map RectComplex<AsslyComplex>
> > {
> > float AsslyComplex::getReal() {
> > return *that* .getMag()*cos(*that* .getAngle());
> > }
> > float AsslyComplex::getImg() {
> > return *that* .getMag()*sin(*that* .getAngle());
> > }
> > };
>
> > concept_map PolarComplex<BenComplex>
> > {
> > float BenComplex::getMag() {
> > return sqrt(*that* .getReal()**that* .getReal()+*that*.getImg()*
> > *that* .getImg());
> > }
> > float BenComplex::getAngle() {
> > return arctan(*that* .getImg()/*that* .getReal());
> > }
> > };
> > 这些代码做了两件事。一是将两个复数类和concept绑定;第二是"制造"出concept有,而复数类没有的成员函数。后者是这里的要点。
> > concept
> > map俨然成了一个adapter,将一个类型"打扮"成concept所需的样子。关键字that与this相对,this用于对类的内部的访问,而
> > that则是从外部访问一个对象。但这种concept map在C++0x的是被禁止的。在C++0x中,concept
>
> ...
>
> 阅读更多 >>- 隐藏被引用文字 -
>
> - 显示引用的文字 -
当时我没有单步跟踪的原因是我们的程序分为两部分。第一部分是前台的软键盘,第二部分是
后端的插件,用来监测欢迎界面是否结束,如果结束就关闭软键盘。崩溃的是后端的插件,而
结果是前台的软键盘没有随着用户登录而消失,就这么一直留在桌面上了。
我们的默认测试环境从没想到还要装Media Player 11的,而且用户报告里也没有提,所以开始
我根本找不到重现步骤。我记得第一天就在玩命地跟两个部分的代码,结果只是肯定了我们的
代码问题也没有。直到后来我查到有人报告说Media Player 11和SP2 合用可能导致欢迎界面崩
溃,才试探着问了问用户,结果问了个准,这才把问题找出来。
这就是为什么我说维护人员希望代码能尽量具体的原因。在问题的根源未知的情况下,我们需
要阅读的代码量太大,所以局部的部分越明确,我们排查的速度越快。当然代码阅读能力是主
导,但是如果要看的代码里每一句其实都可以展开为一百行,而且还都给你指向各个不同的地
方让你在各个文件跳来跳去——C++的抽象方式已经够多了,函数、类、模板,现在按莫兄的
希望,还要再加一个可以推导Mix-in的concept——可想而知看起来会有多痛苦。
跑题了,抱歉抱歉。
2009/9/16 Fuzhou Chen <cppo...@gmail.com>:
up兄后面关于抽象理解的评论已经解释了我会“离题”的原因。
我说过开发和维护思路不同。开发阶段程序员的思路是:如果我抽象了,那么这一部分
我就不管了,因为我知道它不会错;程序员也只会在自己确信某部分代码不会错的时候
才会抽象。但是我在排查问题的时候的第一反应是怀疑一切:比如一个缓冲区溢出错误,
我会本能地怀疑一切写入这个缓冲区的操作,然后通过推理一点点地缩小怀疑的范围。
换句话说此时一定范围内巨细靡遗的地毯式搜索是不可避免的。这个时候如果每个操
作都是层层抽象包裹下去的话,别的不说,代码跳转就能折腾得人晕头转向。这时候抽
象对我来说已经不是帮助,而是负担了。
UP兄的话说得是,我确实只是在说我个人的经验而已。不过话说回来,这里谁也不敢说
自己是为广大程序员仗义执言吧,呵呵。
这话冤枉,我相信我已经清楚地表明了我针对的不是concept本身,而是在C++这个本身
已经抽象能力非常丰富的情况下轻率加入一个新的抽象带来的担心。而且么,呵呵,莫兄的
例子进一步加剧了我的担心。即使加入的不是concept而是什么别的,我也得先掂量掂量。
如果这个特性不是加在C++而是加在C里,我反而比较放心一点。
所以我说我宁可希望在加入concepts之前能够在在一些小规模的实验性语言上用几年,看看
它的能力和极限,然后再考虑是不是应该加入C++。对C++这种用户众多历史悠久的语言来
说,走错一步带来的后果可能都是灾难性的,保守一点不能算错,尤其是concept这东西,
严格说来没有任何一门语言在之前完整地实现过这个概念,实在是让人心里没底。
> 不过,这只是皮毛,真正强劲的是:在拥有concept的情况下,一个语言可以完全禁止隐式类型转换,包括基于继承的转换,而不损失灵活性。同时,也可以放弃虚函数。
> 一个本质性的特性,往往可以替代掉诸多不够本质的特性,特别是那些容易招来麻烦的。
禁止隐式转换?放弃虚函数?我几乎可以立即跳出来代表广大程序员宣称这绝对不
可能。我们已经建筑了太多的东西在上面了。更何况我说过的,别小看程序员索隐
发微的能力。
或许这么说吧,莫兄看来是做开发的,比较乐观,也相信程序员能约束自己。可我
这种做维护的永远不可能相信程序员,这是职业本性。当然的,要是程序员都是管
得住自己,我们哪里会有那么多bug呢?
2009/9/16 莫华枫 <longsh...@gmail.com>:
> 咱们就摆代码讲道理。
> ……
>
> 现在看一下concept掺和之下如何。
> 场景二。假设A是一个Concept,B是一个类型:
> void funx(A a) {
> ...
> a.fun(); //错误发生在这里
> ...
> }
> ...
> B b;
> funx(b); //此处有错
> 现在要找fun()。先找到B到A的concept map。方法很简单,用concept_map
> A<B>查找即可,考究点的用grep。找到了,就看一下有没有fun()的adapt。有,就是它了。没有,就到B里找。B里没有,就到B的基类里找,终究会找到的。否则编译会出错的。
> ok,就这些。
这个例子我承认很直观,但太简单。因为这里是一个非此即彼的关系,B是一个类,所以我们明确地
知道问题发生的地方总在一个继承树上。可是考虑这样的情形:
a) 如果B不是类,而是一个COM接口呢?这时候我们可以传任何满足concept的接口过来,
那么原先我只要查对这一个interface下对应的实现,现在我就得预防万一地查验所有满足
这个concept的interface,以及其对应的实现类。考虑到C++可以定义抽象类,即使不是COM,
情形也是一样的。
b) 如果那个程序员是莫兄的fan,学着您的代码用concept给加Mix-in风格的成员函数呢?
那么我不仅得查所有的成员函数,所有的对应的Mix-in实现我也都得查。
c) 当然莫兄可能已经在跳了:concept是静态的!静态!OK,既然有concpet,就不能避开
auto。想象一下一个维护工程师打开个文件看到这样的代码:
auto MyFunction(SomeConcept1 someData1, SomeConcept2 someData2
SomeConcept3 someData3, SomeConcept4 someData4)
{
auto data1 = DoSomething(someData1, someData2);
auto data2 = DoSomething2(someData3, data1);
auto data3 = DoSomething3(someData4, data2);
// 随便,接下来写多长都可以
}
如果所有数据都是明确指定了类型,那么我马上就知道要查什么;如果是interface,
我至少也知道大概该有几个类要看。可现在是concept和auto,好,我得先一个个
静态地推它们究竟是什么类型,且不说上面的代码段还可能是在定义在另一个concept
函数里的,更别说这代码可能我昨天都没看过,而今天老板就堵在门口说你搞不定
就扣你工资。
那么我能不能用这样的代码呢?
MyRealClass data1 = DoSomething(someData1, someData2);
当然可以,但我相信大家都不会这么做,因为这样一来GP的好处将荡然无存。
*********** 介个素华丽滴分割线 ************
话到这里我相信我们在很多问题上都应该了解彼此的观点了。总结一下:
a) Concept的能力我们都不否认:进一步改进抽象,更严格的类型检查,这都是Bjarne的文章提到的。
b) 我认为对C++来说,抽象的手段现在不是太少,而是太多:函数,OOP,GP,如果算上宏,
就是四种,要是再加concept,就是五种,远远超过Python和C的两种(Python是OOP和函数,
C是函数和宏),或者C#和Java的1.5种(OOP是一种,缺胳膊少腿的GP我只能算0.5)。
c) Concepts在加入语言时和原有的四种不是互斥的,换句话说如果程序员愿意就可以混着用。
b) 混用不同的范式在程序员享受好处的同时也会带来副作用,即混用方式不对导致的代码可读
性开销,以及随之而来的bug和维护负担。
e) 混用不同范式带来的语言复杂性会随着基础范式的增多以指数形式上升。
F) 最重要的一条:在所有我看到的文章里,我**从未见过**有人提到过这些副作用以及警告!
至于我拿维护做例子,仅仅是因为我干了几年比较熟悉,当然比较容易发现这些副作用在维护
中的体现。但是羊毛出在羊身上,如果这些问题最终发生,那么开发者其实才是第一群倒霉蛋,
因为很多人添加新功能的时候,也是从别人的代码上开始的。
我这么热衷于吵这个架,也就是为了提醒一句,看到一个东西的好的时候,别忘了看到它潜
在的问题。
> 既然扯到维护,就讲个故事吧,绝对是真的。
> 十年前,我接到一个维护项目。代码非常非常朴实,什么oop、fp、gp都没用,连sp都不用。使用的,是及其强大的copy-paste大法。我刚看了个开头,就被派去开发另一个项目了。另一个同事接手维护。一年后,他倒下了,脑溢血,才二十八岁!好在抢救及时,命保住了,但落下了半身不遂。至今颅骨上还有一个洞。太可惜了。
> 所以说,作维护项目不但埋没人才,而且还有生命危险。劝一句,换工作吧。
> 开个玩笑。:P
Copy & Paste 会死人的。某种角度上说,这才是无敌的范式。
嗯,和我同年哪,确实要注意了,多谢。至于换工作么,也行,倘若您不介意负责说服
所有人放弃掉Win7换个什么别的系统的话。:)
Jeffrey Zhao
Blog: http://www.cnblogs.com/JeffreyZhao
Twitter: http://twitter.com/jeffz_cn
--------------------------------------------------
From: "Fuzhou Chen" <cppo...@gmail.com>
Sent: Thursday, September 17, 2009 1:15 PM
To: <pon...@googlegroups.com>
Subject: [TL] Re: {编程}{泛形}{concept}concept的外快
>> 一个新特性出来,不分青红皂白地加以排斥,是不应该的。
对,所以我不是要求取消抽象。因为我知道抽象对开发人员是必要的。但
我认为抽象这东西在开发和维护者的角度看来是矛盾的。开发倾向于抽象
而维护倾向于具体。所以考虑增加一个语言特性,特别是抽象模式的时候
总得考虑照顾两方面,求得一个平衡。而现在对concept的舆论导向根本就
是一面倒。
从这个角度上说,我很赞赏C++委员会否决concepts的勇气。莫兄看到的
或许是保守,我看到的则是谨慎。这就跟和巴顿同时期的美国名将布莱德
利一样,有人说他谨小慎微不够大胆,有人说他稳健行进没有让盟军遭受
损失。
> 这个bug挺有意思。up兄后面关于抽象理解的评论已经解释了我会“离题”的原因。
> 但是后面说的有点离题。
> 由于“没有什么花巧的抽象”而导致问题容易理解和解决,跟“使用了花巧的抽象【如果我的理解没有错】”导致问题难于理解和解决看起来非常自然,毋庸置疑,但问题就在花巧的抽象因人而异,因时而异,因场景而异,……,所以,实际上只是说了对于“我”来说,容易理解的就容易解决。不知道我说的对否?
> 你觉得C直观,有人觉得asm直观,有人觉得引脚更直观,但是有人觉得组件才直观,更有人觉得架构才直观。所以说,没有办法说存在一个客观的不易的“花巧”的抽象。
> 就我本人而言,我觉得电流电压很直观,可是深入理解电子的运动和电流,我发现其实很抽象,牵扯到电磁波之类的我认为非常不直观的东西。而对于别人来说,这样的东西太自然直观了,因为它已经根植到他的思维模式中作为基础了,不需要想象和思考就能理解。
我说过开发和维护思路不同。开发阶段程序员的思路是:如果我抽象了,那么这一部分
我就不管了,因为我知道它不会错;程序员也只会在自己确信某部分代码不会错的时候
才会抽象。但是我在排查问题的时候的第一反应是怀疑一切:比如一个缓冲区溢出错误,
我会本能地怀疑一切写入这个缓冲区的操作,然后通过推理一点点地缩小怀疑的范围。
换句话说此时一定范围内巨细靡遗的地毯式搜索是不可避免的。这个时候如果每个操
作都是层层抽象包裹下去的话,别的不说,代码跳转就能折腾得人晕头转向。这时候抽
象对我来说已经不是帮助,而是负担了。
UP兄的话说得是,我确实只是在说我个人的经验而已。不过话说回来,这里谁也不敢说
自己是为广大程序员仗义执言吧,呵呵。
>>
> 我习惯于作为开发阶段的人员思考问题。但是,我没有感觉到如果一部分抽象了,我就不用管了,也不是说它就不会错了,只是保证在我有限的注意力上暂时把某些部分忽略了,回过头来还是要考虑的。
这是有误会了。我的意思是不是说一旦程序员一旦抽象就认为这段代码绝对正确,而是说
至少在一段时间里他不会再去担心这里面的细节会不会出问题。我身边的程序员写代码的
方式一般是从下到上的,很少看见有人先实现上层代码(当然,他很可能会先设计),
再实现一个个小块。他们往往是一个个小块地写,然后作单元测试,再一个个地拼。
而维护的看法不同,我们不是在拼积木,而是在切蛋糕。一点一点地把无关的东西剥离
掉,最后定位在一个点上。这个过程中抽象越多,就意味着我我要看的东西越多,其实
很简单,如果一个函数里有50行代码描述一个循环遍历,我看上去一目了然,而如果里面
是连续的五个函数调用,那我就头大了,因为五个函数我都可能得看一遍。
> 偶尔,我也帮助同事查找程序的错误,我也是怀疑一切,但是我跟你不一样,我很少有精力进行暴力搜索,我从尽可能高的抽象级别查看一个操作牵扯到的步骤,当然每个步骤都是抽象的,每个步骤都有可能出错,我一次集中在一个抽象的步骤上,可以先假定最后一个步骤出错,然后看看给一个合理的输入是否能有期待的输出,如果最后步骤不出错,次后的步骤【当然,这儿有人推荐二分法,我很少碰到大的需要二分法的,因为抽象层次多了每一个层次关注的焦点就少了,呵呵】……,如此这般,找到那个出错的步骤,然后再次细化这个步骤……
> 所以,层层抽象对我来说是帮助而不是负担。
合理输入,期待输出。这是我梦寐以求的东西啊。可惜我干的活没这么好。
其实up兄的说法也是我做的。我只是说在某个阶段暴力搜索不可避免,但不可能真的什么
都这么来,俺也是普通人类,呵呵。
实践中我也会假定某个函数没问题,然后先排查别的,这是合理的抽象,也是我们必须要
用的。但是这不能改变一个事实,那就是抽象层次增多就往往意味着维护人员需要过一遍
的总代码量在增大,如果还牵扯到别人负责的代码——在维护过程中这很难避免——就更
麻烦。
顺便说一句,我已经很长时间没有遇上过能通过调整输入简单重现的bug了,我手上的活
里不能重现的东西占绝对多数,比如在一个超过60个线程的core dump里抓一个内存访问
违例的bug,或者我说过的那个问题,即在没有信息的情况下找到正确的重现步骤。这种
情况下一点一点地用unit test的方法排查错误实在不适合我。
扯远了,呵呵。
2009/9/16 up duan <fix...@gmail.com>:
>这是有误会了。我的意思是不是说一旦程序员一旦抽象就认为这段代码绝对正确,而是说
>>
> 我习惯于作为开发阶段的人员思考问题。但是,我没有感觉到如果一部分抽象了,我就不用管了,也不是说它就不会错了,只是保证在我有限的注意力上暂时把某些部分忽略了,回过头来还是要考虑的。
至少在一段时间里他不会再去担心这里面的细节会不会出问题。我身边的程序员写代码的
方式一般是从下到上的,很少看见有人先实现上层代码(当然,他很可能会先设计),
再实现一个个小块。他们往往是一个个小块地写,然后作单元测试,再一个个地拼。
而维护的看法不同,我们不是在拼积木,而是在切蛋糕。一点一点地把无关的东西剥离
掉,最后定位在一个点上。这个过程中抽象越多,就意味着我我要看的东西越多,其实
很简单,如果一个函数里有50行代码描述一个循环遍历,我看上去一目了然,而如果里面
是连续的五个函数调用,那我就头大了,因为五个函数我都可能得看一遍。
> 偶尔,我也帮助同事查找程序的错误,我也是怀疑一切,但是我跟你不一样,我很少有精力进行暴力搜索,我从尽可能高的抽象级别查看一个操作牵扯到的步骤,当然每个步骤都是抽象的,每个步骤都有可能出错,我一次集中在一个抽象的步骤上,可以先假定最后一个步骤出错,然后看看给一个合理的输入是否能有期待的输出,如果最后步骤不出错,次后的步骤【当然,这儿有人推荐二分法,我很少碰到大的需要二分法的,因为抽象层次多了每一个层次关注的焦点就少了,呵呵】……,如此这般,找到那个出错的步骤,然后再次细化这个步骤……合理输入,期待输出。这是我梦寐以求的东西啊。可惜我干的活没这么好。
> 所以,层层抽象对我来说是帮助而不是负担。
其实up兄的说法也是我做的。我只是说在某个阶段暴力搜索不可避免,但不可能真的什么
都这么来,俺也是普通人类,呵呵。
实践中我也会假定某个函数没问题,然后先排查别的,这是合理的抽象,也是我们必须要
用的。但是这不能改变一个事实,那就是抽象层次增多就往往意味着维护人员需要过一遍
的总代码量在增大,如果还牵扯到别人负责的代码——在维护过程中这很难避免——就更
麻烦。
顺便说一句,我已经很长时间没有遇上过能通过调整输入简单重现的bug了,我手上的活
里不能重现的东西占绝对多数,比如在一个超过60个线程的core dump里抓一个内存访问
违例的bug,或者我说过的那个问题,即在没有信息的情况下找到正确的重现步骤。这种
情况下一点一点地用unit test的方法排查错误实在不适合我。
在问题的根源未知的情况下,我们需
要阅读的代码量太大,所以局部的部分越明确,我们排查的速度越快。当然代码阅读能力是主
导,但是如果要看的代码里每一句其实都可以展开为一百行,而且还都给你指向各个不同的地
方让你在各个文件跳来跳去——C++的抽象方式已经够多了,函数、类、模板,现在按莫兄的
希望,还要再加一个可以推导Mix-in的concept——可想而知看起来会有多痛苦。
> 呵呵,你其实逃避了问题。循环中如果有函数调用呢?
逃避这话从何说起?我本来的意思就是在比较一个50行无函数调用的代码(没有抽象)
和五个函数调用(有抽象)之间的差别。注意,我的比较是有大前提的,就是这些代码我
以前都没看过,也没有人帮助我说明(就如莫兄的说法,前任可能早就脑溢血了呵呵)。
显然没有抽象的代码能让我更有底,哪怕稍微长一点,因为当我面对五个函数的时候,
我不知道需要关注的代码究竟有多少,除非我真的看下去。
> 我个人感觉维护要付出跟开发差不多的精力,开发的精力主要用于不断地收敛直至稳
> 定的过程中,而维护的精力主要用于理解这个稳定状态究竟意味着什么,为什么是这
> 样的状态。直白的说,开发是设计并实现一个方案已解决问题的过程,而维护【如果
> 没有文档传承】则是一个理解方案何以能解决问题或者何以不能解决问题的过程。
抱歉,我不想随便评论您的经历,但在我这里我根本不可能去花任何时间理解“稳定状态”
或者它的实现。用一个数字就可以说明问题:去年我曾经在一个星期里接到过五个bug报告,
一个我说过了,来自Windows欢迎屏幕,一个来自我们的产品在Windows里安插的全局
钩子,一个来自显卡驱动,一个来自我们发布的开发包,还有一个,抱歉我忘记了。平均
每个bug的解决时间大约在一周。
也许我的经历太极端了些,但我想要指出的是:对很多公司而言维护这个部门是不赚钱的,
因而也就不愿意投入人手。比如我现在接手的部分在产品组那里对应两个组,测试大约三
十人左右,开发还没有算。实际工作中我和我的同事们根本没有时间悠闲地去理解代码的
细节,只能在一个又一个的bug报告中被动地查阅代码,所以我们的要求就是避免探究架
构而永远直指细节。您可以说这种工作不合理,但这就是现实。
注:所以我说我们已经偏题了,我的工作细节和莫兄的文章其实没什么关系。
>
> 如果抽象的合理,也就是各个部件之间的接口合理,那么,对于维护来说其实是大有好处的,你甚至可以轻易地替换掉一个部件。
> 总体代码量或许会增加,但是这不是问题的关键,你同时需要关注的问题减少了,都在智力控制范围之内,可以保持一个清醒的头脑和自信的心态去面对问题。
我只能说我遇到的bug中还没见过抽象不合理或者接口不合理导致的问题。在我看来这
在开发阶段早该解决了。我遇到的bug从来都是来自具体的问题,缓冲区计算不正确,
关键区保护不正确,通信机制不正确,等等等等。这些问题与抽象其实无关,但抽象过多,
注意,我说的是过多,则强迫维护人员必须去读更多的代码来找到最下层的问题根源。
在前面的mail里我说过,我不是在泛泛地反对抽象,而是反对这种只注重介绍抽象优点
而不讨论潜在问题的做法,因为它的副作用往往就是过度抽象,而过度抽象在我的工作
中则是很多痛苦的代码阅读经历的根源。
另外,轻易替换可不是随便就能做到的。拿我说的那个bug做例子,你觉得我有能力去
写个什么东西替换Windows欢迎屏幕么?就算我有,时间也不允许。
至于清醒和自信的心态之类的,说句实话:这真的偏题了。我们另开一帖再水吧。
>
> 对我来说,并发的bug大都是考虑关键资源是怎么被访问的,都是在头脑中进行演算的,
> 说实话,这时候调试器,log等设施提供的帮助是很小的,如果问题规模确实巨大,需要
> 在草稿纸上画出来关键的资源和关键的路径,以求更直观的整体思考问题。
我看出我们的差别了,UP兄你说的画图也好,头脑演算也好,说的是开发阶段的分析,
而不是维护阶段的。您或者查阅的是自己写的代码,或者总有人能告诉您大概代码是
什么样,又或者您的工作也一般能允许你慢慢读代码。可惜我的印象里,这是开发的
权利,维护部分是没有这么多时间的。
或者这么说吧,可能好理解:开发部门的人也都不是猪头,如果那些bug在开发过程中
通过简单地调整几个参数或者画个图就搞定的,开发过程中早就能发现并解决了。而维
护手中的问题则是产品已经卖出去后在客户的环境里才能发现的。如果这些问题是画个
图就能看出来的话,开发阶段那个时候作为程序的直接设计者和实现者为什么不能发现
呢?或者退一步,如果您作为开发者都没能发现问题,而我这种对代码还不甚了了的维
护人员用相同的简单手段却能发现问题,说句笑话,我是不是可以请老板把开发部门开
掉呢?
我倒也不是说要取消抽象,抽象是要的,不然开发就不是乏味,而是脑溢血了。我还没有
妄自尊大到认为大家要向维护看齐。 :)
>我只能说我遇到的bug中还没见过抽象不合理或者接口不合理导致的问题。在我看来这
> 如果抽象的合理,也就是各个部件之间的接口合理,那么,对于维护来说其实是大有好处的,你甚至可以轻易地替换掉一个部件。
> 总体代码量或许会增加,但是这不是问题的关键,你同时需要关注的问题减少了,都在智力控制范围之内,可以保持一个清醒的头脑和自信的心态去面对问题。
在开发阶段早该解决了。我遇到的bug从来都是来自具体的问题,缓冲区计算不正确,
关键区保护不正确,通信机制不正确,等等等等。这些问题与抽象其实无关,但抽象过多,
注意,我说的是过多,则强迫维护人员必须去读更多的代码来找到最下层的问题根源。
在前面的mail里我说过,我不是在泛泛地反对抽象,而是反对这种只注重介绍抽象优点
而不讨论潜在问题的做法,因为它的副作用往往就是过度抽象,而过度抽象在我的工作
中则是很多痛苦的代码阅读经历的根源。
另外,轻易替换可不是随便就能做到的。拿我说的那个bug做例子,你觉得我有能力去
写个什么东西替换Windows欢迎屏幕么?就算我有,时间也不允许。
至于清醒和自信的心态之类的,说句实话:这真的偏题了。我们另开一帖再水吧。
>我看出我们的差别了,UP兄你说的画图也好,头脑演算也好,说的是开发阶段的分析,
> 对我来说,并发的bug大都是考虑关键资源是怎么被访问的,都是在头脑中进行演算的,
> 说实话,这时候调试器,log等设施提供的帮助是很小的,如果问题规模确实巨大,需要
> 在草稿纸上画出来关键的资源和关键的路径,以求更直观的整体思考问题。
而不是维护阶段的。您或者查阅的是自己写的代码,或者总有人能告诉您大概代码是
什么样,又或者您的工作也一般能允许你慢慢读代码。可惜我的印象里,这是开发的
权利,维护部分是没有这么多时间的。
或者这么说吧,可能好理解:开发部门的人也都不是猪头,如果那些bug在开发过程中
通过简单地调整几个参数或者画个图就搞定的,开发过程中早就能发现并解决了。而维
护手中的问题则是产品已经卖出去后在客户的环境里才能发现的。如果这些问题是画个
图就能看出来的话,开发阶段那个时候作为程序的直接设计者和实现者为什么不能发现
呢?或者退一步,如果您作为开发者都没能发现问题,而我这种对代码还不甚了了的维
护人员用相同的简单手段却能发现问题,说句笑话,我是不是可以请老板把开发部门开
掉呢?
2009/9/16 Dong Feng <middle....@gmail.com>:
>我倒也不是说要取消抽象,抽象是要的,不然开发就不是乏味,而是脑溢血了。我还没有
> 抽象太少,维护是简单,可是开发的时候就乏味了。抽象太多,开发有趣,维护就是恶梦。
>
妄自尊大到认为大家要向维护看齐。 :)
又是OOP与GP之间的恩恩怨怨...
2009/9/17 up duan <fix...@gmail.com>:
>>
> 你不觉得缓冲区计算不正确的问题是通过log就可以轻松解决的?
> 关键区保护不正确显然是没有把关键区保护抽象出来导致的。通信机制不正确也一样。你觉得是抽象无关,我觉得是相关。
> 我承认,过多抽象显然不对,但问题是我怎么觉得很多问题都是抽象过少导致的呢?愿闻其详。
呵呵我可不觉得。当开发者没有写log,而您又没有权限自己编译代码的时候,试问您如何“轻松
解决”?别说这又是我的臆想——我们的动态库在防盗版方面保护做得多,每个binary需要数字
签名,作为维护组成员的我虽然能编译却没有权限申请数字签名,编译出来的动态库装到用户的
系统里程序根本拒绝执行。我总不能说:没有数字签名我不干活吧?
另一个场景是根本没有重现步骤——试问在一个没有重现步骤的情况下,就算我有权限吧,我该在哪
个程序里加log?再说有的程序根本不在我控制之下,还是我说过的那个bug,我怎么往Windows欢迎
屏幕里加log?
另外,log和抽象是两码事。离题的是您而不是我。
>>
>> >
>> > 对我来说,并发的bug大都是考虑关键资源是怎么被访问的,都是在头脑中进行演算的,
> 我承认,维护是很痛苦的,我们公司也有产品需要维护,加一个小小的特性或者修改一个小小的bug都意味着巨大的时间和精力的消耗。以至于没有几个愿意去维护产品,最后只能是自己负责的项目自己去维护。
> 但是为什么?
> 为什么开发阶段加一个小功能也就1、2天的时间而维护阶段却得花1、2周的时间?为什么开发阶段解决一个bug和维护阶段解决一个bug的时间也有同样的差异?
> 因为代码在不断的增加功能和修改bug中逐渐腐烂。最初清晰的思路,设计,想法到后来全都没有了,只是为了完成功能或者解决bug。代码已经是一团粥了。
> 这儿主要的原因是:缺乏重构,缺乏修改接口的勇气,缺乏自我修正的能力。这也是我说的从中层往下层,同时也不断反复的过程。这个过程其实就能保证结构是清晰的,代码是清楚的,维护也因而是简单的。
能自己维护自己的项目——好吧,您要知道这可是我梦寐以求的。可惜不可能,我吃的
这碗饭就是要求我永远维护别人的代码。您可以指责程序员缺乏重构甚至缺乏勇气,但
这还是设计阶段的人操心的,我这样负责维护的人却无法反过来要求开发人员修改设计,
您知道,没.权.限。
仍然说一句,您在谈理想,我在说现实。而编程语言的设计者不能只为理想服务。
>>
>> 或者这么说吧,可能好理解:开发部门的人也都不是猪头,如果那些bug在开发过程中
>> 通过简单地调整几个参数或者画个图就搞定的,开发过程中早就能发现并解决了。而维
>> 护手中的问题则是产品已经卖出去后在客户的环境里才能发现的。如果这些问题是画个
>> 图就能看出来的话,开发阶段那个时候作为程序的直接设计者和实现者为什么不能发现
>> 呢?或者退一步,如果您作为开发者都没能发现问题,而我这种对代码还不甚了了的维
>> 护人员用相同的简单手段却能发现问题,说句笑话,我是不是可以请老板把开发部门开
>> 掉呢?
>>
>
> 你把问题转移了。并发导致的bug并不是那么容易出现的。在大规模使用中出现也是很正常的。
> 并不是因为猪头所以解决不了bug,而是根本不知道有bug所以解决不了bug。人的思维总是有
> 死角的,设计不能发现的问题多了去了。经人提醒或者强制用形式化【相对形式化】的方式去
> 机械的思考,才能突破这些思维的死角。
>
恕我直言,这话让我感觉您显然仅仅是站在您自己的工作环境上说话。并发的bug是否容易出现是
与我们要负责的什么软件有关的,在您的环境里也许不容易出现,在我这里却是家常便饭。我前面
说过一个在60个线程里抓一个访问违例的bug,这属于我接触的bug中相对顶级的,但平时多于二
十个线程的bug还是天天有的。
我感觉UP您是理解偏了我的意思。我上面那一段不是在指责程序员为什么发现不了问题,而是说
作为开发阶段的程序员,你不可能指望我用和你相同的手段发现问题:我对代码没有您了解,我也
没有您那么充分的时间,我甚至没有您那么大的权限。要“突破思维的死角”,我必须借助一些有
别于开发阶段的手法,这其中的一条就是避免**过分**抽象,相对而言,抽象越少的代码对我来说
理解起来越是容易。
我发现我和UP在讨论的时候忽略了一个问题,就是理解框架本身的成本。我反复读了UP的评论,
感觉是您似乎在假设这么一个前提,就是框架本身是大家都了解的,所以合理的框架能帮助理解。
对,我同意这句话,但是别忘了这个前提在我这里不成立。
在一个我们连架构是什么都不甚了了的程序中游历的时候,架构本身和细节一样也是要花时间理
解的。我明白磨刀不误砍柴工,但如果如果架构包含的抽象层次过于繁复,则可能把磨刀的时间
拉长到不可接受的地步。
时间!时间!我好像听见老板在我耳朵边喊了。
>
> 维护的主要成本并不在于修复那个bug本身,而在于找到bug出现的地方。这里面不可避免的要涉及到对于问题和方案的重新理解,推敲其中的可能错误和漏洞,作出推测,实验推测,证实推测,修正。
>
这话我我只能同意一半,因为我们再次忽略了时间问题。每个项目都有实际的时间要求,如果
时间是足够的,那我当然也愿意细细地揣摩,真正理解代码长远上看可以更好地帮助我处理将
来的问题。而维护部门要求代码明确的很大理由就是我们希望在**有限的时间里**消化尽可能
多的**陌生代码**。
再说一遍:是陌生的代码,是有限的时间。
>
> 但是“恰当”的抽象对于开发和维护都有好处,我还是这样认为。唯一的问题就是“恰当”的定义了。
>
OYeah!这说明我们似乎没有矛盾。请看我下面的原文:
>
> 对,所以我不是要求取消抽象。因为我知道抽象对开发人员是必要的。但
> 我认为抽象这东西在开发和维护者的角度看来是矛盾的。开发倾向于抽象
> 而维护倾向于具体。所以考虑增加一个语言特性,特别是抽象模式的时候
> 总得考虑照顾两方面,求得一个平衡。而现在对concept的舆论导向根本就
> 是一面倒。
>