« 修复了留言本的 Bug ,翻出几篇旧文 | 返回首页 | 魔兽世界的影响力 »

看到一句话,心有戚戚

语法层面的(nontrivial的)错误往往预示着语意层面的错误。例如,循环依赖导致的语法错误往往暗示抽象设计存在问题。

-- 转至 http://fanfou.com/statuses/uiT9IzmuonQ

我一直想好好写写循环依赖的问题,但不知如何下笔。只是在前段时间的 Blog 末尾提了一下。 (见 良好的模块设计 的最后两段。)

这些是我最近用 C 写了很多代码后悟到的。

C 往往允许程序员省略函数原型声明,只需要你把被依赖的函数实现列在前面即可。

刚学写 C 程序的时候,我老喜欢把各种函数的声明列在一起放在一个头文件中。后来明白了应该尽可能隐藏实现细节这个道理,转而把内部函数的声明放在独立的内部使用的头文件中。

今天,我发现,如果能保证没有循环依赖,所有非接口的函数都不必另外写一个声明;这一点可以反过来促成更好的设计。

在用 C++ 的日子里,我对这些一直没能有太清晰的认识。想来是因为 C++ 提倡每个类都预先写好类声明有关。

当然,间接递归并不等同于循环依赖。递归正是利用将实现和接口分离的方法,从而消除循环依赖。但解决掉眼前的问题就够了吗?我有点理解为什么当年 Fortran 不支持递归了。

当人们手中的工具越有威力,就越不把跟前的问题看在眼里。能力越大,越难 KISS 。

Comments

@刘未鹏
语法层面的(nontrivial的)错误往往预示着语意层面的错误。例如,循环依赖导致的语法错误往往暗示抽象设计存在问题。

这句话的确很好呀,心有戚戚焉...感觉同 希尔伯特的形式化数学的理论有点像的

前几天跟公司的一个牛人讨论到这个技术问题:我觉得此类依赖问题一般是将该模块分离出抽象(接口)的部分和具象(实现)的部分,一个抽象模块对于具体模块的调用,应该采用一个间接层来表达(vtbl或函数指针表都好);我们的争论集中在应该采用函数指针表还是使用switch语句,他认为设计和实现的偏重是不同的,实现层面应该侧重可推演性(因此赞成用switch)。
请问云风怎么看待这个问题的?

能力越大,越难 KISS 。

多好的话,但是谁在开发过程中不想要更大的能力呢?大能力(机制、技术)和 KISS 到底应该取哪个?

觉得 rockcarry 很有思想很务实态度很好,提到的问题我觉得都蛮好的。不过后面的讨论貌似跟本篇的关系不大了。Atry 的回答言简意赅,云风一直没说啥哈。

网上质疑 c++ 和 oo 的文章很多,我觉得 rockcarry 可以去看看这方面的文章了。

TAOUP 这本书应该适合 rockcarry 的心境的,不知道看过了没。

我也时时在c的“简洁、浅平透”的“基本抽象”和 oo 的各种抽象之间纠结啊~如果 rockcarry真的对oo没有多少研究而能得出来这些结论的话,我觉得真的是一个很有思想的人了。Atry的空间找到了,找不到你的链接,挺想找你讨论一下,联系方式是什么呢?我的 qixinkui@qq.com

c++水更深,稍有不慎更易翻船,所以也不适合大规模协作。云风说的层面自然不同,如果你能用好,就用吧。

来点伪哲学的论点:程序设计语言,开发模式的选则要适应个人能力和项目规模的需要,匹配的会促进生产效律

这里的讨论很少融洽过
呵呵,不知现实中云风身边的人讨论是怎样的呢?会不会像这里一样,一大篇一大篇有深度而有点尖锐呢?
个人觉得一大群人激烈地争论是一个好气氛,很是向往面红耳赤过后友谊更加深厚的情景。
国内学术界应该很少这样吧,太多“学霸”了,一人讲后众人鼓掌的更常见。

"本来以为来这里讨论会有一个融洽的气氛"

哈哈,我这里似乎很少融洽过。

习惯就好 :D

写了3年多程序,在这里还是找不到自信,看来是废了 :(

据我的观察,写程序1-2年时大多很虚心,3-4年时会突然有一个自信尖峰,5-8年这段又重新变得低调并两极分化(很多人的能力不再随着经验增长从而拉开了差距),10年左右能力停滞不前的人基本都转行或转业,而甩开他人一截的精英又会出现一个自信小高峰,但自信的起落会比较平滑然后逐步转为正常的缓慢上升过程。

呵呵,多谢关心

去书店看书要非常小心。只有极少的书看了会有正面的效果

其实,我个人是很少在网上的论坛等地方发表言论的。因为网络毕竟不是一个适合讨论的地方,更多的讨论只是争吵。

本来以为来这里讨论会有一个融洽的气氛,看来更多的想法我也不再发表了,否则一些人又要叫我先回去读书了。

好吧,周末到书店逛逛。

to Alpha:

我还是认为,人与人之间的交流应当是平等的。不知道你是否同意我的这个观点?如果你同意,那么我很高兴,因为至少在这一点上,我们达到了一个共同的认识。如果你同意这一点,那么请往下看,我们继续进行交流,如果不同意,那么,请“无视”我以下的文字。

在你和我之间没有相互深入的理解之前,你如何知道我对自己所评价的事物没有进行深入的理解。因此我认为,在相互交流时,单方面的假定对方对某一事物的认知程度,至少是不平等的,也是不礼貌的。如果你有发现对方在认识上游错误,最好的做法是合理的指出,以引发对方的思考,从而让对方发现,并改正自己的错误。讨论的目的是为了解决问题,是为了消除认识上的错误,使讨论的参与者达到一个共同的正确的认识,不知道你对我这个观点是否赞同。

我承认,我读书是很少,因此我会记住教诲,学而不倦,不骄不躁。但是,为了达到以上我所说的交流和讨论的目的,我希望大家在评论中,尽量讨论一些具体的,能引发人思考的问题。而不是简单的叫某个人去读某些书,这样的做法是不明智的,也是无法解决问题的。如果你有发现我的错误,并且有热情想纠正我的错误,请合理的指出。毕竟,在这里是讨论时间,而不是读书时间。

另外,我希望你所说的把C++说得一无是处的人不是我,因为我认为,我目前为止发表的评论中,有关C++的其实很少,更多的是在哲学层面上讨论软件设计的问题。即使有对C++的批评,我也在前面的评论中承认过,我的看法不一定正确。另外,即使是有,也达不到一无是处的程度啊,而更多的是在阐述,使用C也能合理的实现对象模型。另外,可能有些人有误解,我所指的繁琐的封装,不是特指使用C++的对象进行封装,封装的含义是很广的,当你将一个功能做成一个模块,就是封装。我更多的是强调,对逻辑全局的事物,在逻辑上不用进行繁琐的封装,而这种封装,与到底使用何种语言,何种方法无关。减少封装,是希望降低模块之间的层次,以达到一个扁平的层次结构,而不是说不封装。至于封装用何种语言的何种技术,就看你自己选择了。

另外,我也承认,我哲学学得不多,可能前面的许多评论是在滥用哲学。但是由于个人认识上的局限性,我一时意识不到自己观点上的错误。因此,良好的交流与沟通,是合理的指出对方的错误,并引起对方的思考。而你的评论,并没有实质的内容,可以启发我思考。

我也从来没有奢望自己能一下就超越前人,在交流中,这种单方面的假设对方的想法,是不平等的,只能引发战争,阻碍相互的交流与沟通。就算我全部的观点都是错误的,但是我还是希望大家相互的交流是有意义的,而不是无意义的争吵。不知道你是否赞同我的观点。


勇于挑战权威,敢于发明创造,都是很值得敬佩的精神。
但是在口水四溅的时候也应该想想自己对所要评价的事物是否有足够充分的了解
否则怎么才能够给于足够准确的评价呢?
建议把c++说的一无是处的,可以看看《C++ 语言的设计和演化》
Bjarne Stroustrup计算机科学和哲学造诣不是一般人能够企及的。

喜欢唯物主义,建议多看看哲学史,了解一下近百年来哲学的发展。
喜欢方法论,希望看看它是如何出现的,具体的含义是什么,到如今的发展又如何。
喜欢简洁之美,多看看数学,看看数学的发展史,了解一下人类理性的局限性。

钱钟书曾说:“一个人二十不狂没志气,三十犹狂是无识妄人。”

不要轻易奢望自己在前辈们研究了n年的领域中能够一朝一夕之间超越。
就像前面有人所说,einstein也要补习数学,牛顿也要补习几何。
当你知道的越多,越能发现自己的无知。
有激情,喜欢求知和表达都非常好,但是有的放矢,厚积薄发不更好么?

我觉得rockcarry 只不过是说出了自己的隐喻而已,没有人会觉得他要跟爱因斯坦自比。无论如何,他很清楚自己的隐喻,而且至少看起来现阶段工作良好,不是吗?

另外,我的基本观点都始终在努力做到以唯物主义的方法论为指导,因此我个人认为我的观点并没有颠覆常理,也没有对已有的技术进行“无视”。相反,我是努力在尝试用更加理性的,唯物的方法去分析现有的技术,比如,我对COM技术的分析,我对对象模型的分析。

我承认,我对C++的类的评价不一定正确,因为,我个人对C++的研究的确不深入。但是,我的评价也是在努力希望引起大家的一些思考。学习方面,我会记取教导,学而不倦,不骄不躁。我到目前为止发表的评论,也并没有掺入太多的感情因素,有的感情因素只是为了表达生动,以及抑制不住自己表达的欲望和兴奋。

不简单的做技术的追随者,而要争取做技术的发明创造者,和技术的应用者。

技术日新月异,然而我们不应当拼命追求技术外在的、形式上的东西,这只会让人在技术面前疲于奔命、迷失方向。技术无论怎样变化,其内在的一些基本原理总是相对稳定不变的。因此,我个人相信总有一些稳定不变的分析和解决问题的方法。

事物的发展总是稳定的向前,从简单到复杂,从量变到质变,而正是这些基本的原理和规律,支撑着整个世界的稳定性,否则整个世界将失去自己的重心,轰然倒塌。我所强调的分析和设计方法,都是倾向于一种唯物主义的方法论,即软件设计的出发点应当是你所要解决的实际问题,在计算机世界里面用合理的技术方法反映和刻画现实世界中客观存在的事物。而不应当拘泥于某些形式上的要求,用一些繁琐的技术,去刻意的回避一些问题,比如全局变量的使用。另外,我崇尚简洁的设计方法,也许有些人对此表示不满,但我个人认为简单没有什么不好啊。

另一方面,技术的最终目的,永远都是为了解决问题,技术是人类改造自然界的有力工具,是人们在认知上的强大武器,技术应当为人类服务。因此技术在于应用,而不在于拘泥其形式。我们应当做技术的主人,而不应当成为技术的奴隶。

看得出rockCarry是认真思考过这些问题的,表达得很清晰,而且态度和热情也值得称赞和学习。我自己是从中学到不少东西。

也希望dayn9这样的前辈高人不要点到即止,能将自己的经验体会与大家分享一下。

许多时候,人们太过于重视技术的形式,而忽视了技术的内涵,也就是技术内在的哲理。

其实我个人感觉,我的许多看法,更多时候是在哲学层面上思考问题,而非技术层面上。

不过我希望大家的评论最好能引起我的思考,帮助我发现自己的错误。

我个人读书不多,所以某些错误在你们看来很明显,但是我个人却意识不到。如果仅仅告诉我去读书,也要指明下读哪方面的啊。

所以我也说了,只有行动才能证明一切,真理需要实践来检验。前人所总结出来的经验也要好好学习。

其实大家不必这么严肃了,我也只是最近公司实在是没有什么事情干,所以到这来随便说说。

无论怎样的批评,只要有利于改正我认识上的错误,我都是乐于接受的。

当然,我的观点到底对不对,我自己都不知道,所以发表出来和大家讨论。有的时候说的兴奋了,可能言辞比较夸张。

看rockcarry的留言总能引发我自省

看到rockcarry这么多产,实在忍不住多嘴两句,我真心希望我所说的能对rockcarry有所帮助,而不是伤害到他的自尊或影响到他的热情。
rockcarry的部分观点有点惊世骇俗,不是颠覆了许多基本常识,就是对近10年的技术发展完全视而不见。大概爱因斯坦当初也这么干的,如果你有足够证据证明你也是那样的人,那么请坚持。

另一些建议:
在这个日新月异的行业里,不要抱有那么多“永恒”的想法,多少存点怀疑,能让你思考。
如果想批评C++,至少要掌握相当的程度,否则无的放矢,比如云风对C++的任何批评,都会引领我们深入思考。
如果不怕被前人的框架所束缚,最好多读点书,就算是爱因斯坦也补习过数学,牛顿也补习过几何,头脑中适当多些“个人愚见”之外的内容。当然,道可学,也可悟,我赞成悟比学好,但先客观评价自己的“悟性”。
做技术需要理性,不能掺杂太多个人情感。关于这一点,上一代C++程序员包括云风,几乎都是反面典型,应该记取。


对于一个大的物体的操作,都可以分解为对这个大物体的组成部分,也就是相对来说的一系列的小的物体的,一系列的小的操作。小物体相互组合成为大物体,小操作也是相互组合成为大操作。这也许就是分析与综合的哲学原理。这也许就是逐层的模块划分基本原理和出发点。

一个实例化了的模块,就是一份内部数据,以及一组对内部数据的操作。操作是不变的,数据却是独立的。实例化实际上只是实例化了数据,因为操作永远不变,除非你改写了操作。就是因为这样的操作的不变性,使得重用成为可能,因为许多现实的问题本质上都是对数据的处理,数据是独立的,并且是大量的,然而对数据的处理方法却永恒不变。就如同1+1=2,做的始终是加法,而处理的对象却永远不仅仅是1和1。因此更多情况下,我们重用的是操作,就像当你书写了2+2=4,实际上是你对2+2两个数据重用了加法这个操作。从这里也可以看出,许多操作可能会是多个数据体参与,而且数据体之间并没有严格的主从关系。这也是我为什么说C++中的类方法的表达,在语意上存在缺陷的原因。

个人认为,当人做到“无欲”时,人发展的希望也许就应该寄托在对“物”的斗争和对真理的追求上吧。因为当人失去了欲望,或多或少的都会失去一些发展的动力。这时候,动力的来源,就更多的在于对真理执著的追求和对“物”的斗争上。

人在改造世界的生产活动中,不断的对客观存在的事物进行着认识。人们对客观事物的认识,都是对客观存在的一种反映。然而,作为个体的人与人之间,不可避免的存在差异性,而这种差异性,将会导致不同的人对相同的客观事物产生不同的认识。而人作为社会性的人,必然会相互交流各自的认识。当人与人之间的认识不相符时,就产生了矛盾。人总是渴望自己的想法被别人所认同,因此消除认识上的差异,使人类社会对客观事物的认识达到一个共同的真理,就需要作为个体的人不断思考,不断与他人沟通,不断的行动(以实践去证明真理,因此需要行动)。

因此,在认识问题上,我希望大家都要有正确的态度和方法。我们都渴望被认同,但是也要勤于思考,善于沟通,勇于承认错误,并且付诸行动。当人们达成了共同的正确的认识,这时候,认识就能指导生产活动,从而创造财富,推动社会的进步。

如果只停留于公式化的生产方式,就失去了创新精神,失去了发展的动力。

流行与大众化,是客观存在的一种现象,正如你所说是因为某件实物有其合理性,并且这种合理性已经达到了社会多数人所能认可和接受的范围。然而,如果只满足于已有的对客观事物的认识,则丧失了创造力和创新精神。

可以证明我的观点的例子很多,爱因斯坦的相对论,在提出之时,是多么的让人感觉荒诞。我个人认为,只要是正确的,合理的事物,终究会被人们所接受,并且发挥其价值。

而人们对真理的追求,和对真理的阐述与表达,都是需要勇气的,也是让人赞赏的。当然,在追求真理的过程中,难免会有一些错误的认识。因此我希望大家能够对这种错误予以理解。

忍不住插一句嘴。

曲高和寡,重点词汇“深刻理解”,“优雅”,“美感”。

喝西湖龙井的人很难理解那么多俗人喝可乐。其实道理很简单,公式化的生产方式,能让更多的人加入生产,也能让更多的人接受。

语言亦然。

另外个人认为,C++的类在描述对象上也会有缺陷,因为它太过于强调类在对象相互操作中的主体性,并且更多的是体现一种主从的二元关系。比如说,Screen.Display(...); 在语义上讲,就是在屏幕上显示某个东西,Screen 是操作中的主体,屏幕与某个东西是个二元的关系。在C++的类的这种形式上讲,其类方法就有一种隐含的语义上的暗示,这使得其在描述一些非二元的、从关系不明显的对象模型时(虽然这种情况不多见),显得有点力不从心。而如果按照我所说的那种C语言的对象模型来描述问题时,似乎不会存在这个力不从心。

另外再讲一讲COM组件对象模型,从这个模型中,可以明显的看出模块的复用实际上是对操作的复用。因为COM最大限度的隐藏了内部实现的细节,而只是暴露出了对对象进行操作的方法,这个方法就是COM的接口。COM中连数据类型的暴露都给隐藏了,一个具体的数据类型被定义为了一个GUID,只有COM对象的内部才知道,这个 GUID是对应着如何的数据类型,外部看到的始终就是一个个GUID和一组COM的接口。这样的设计在我看来堪称完美,不过不够简洁,毕竟它是一个标准,所以要考虑的东西更加多一些。因此虽然COM很完美,但是要实现一个COM仍然是很复杂的,所以一般人,一般情况下都只是复用COM,而不会去实现COM。每一个事物都有一个适用范围,COM也是如此,我想没有人会用COM去实现一个可复用的线性表组件吧,这无论在实现上还是在使用上都太过繁琐。因此说对于简单的问题,我们还是希望用最简洁有效的办法去实现。一切事物都有一个适用的范围,而简洁的设计原则却是永久不变,掌握并使用它,将会终生受益。

还要谈谈接口不变性的价值,我个人认为,要做到模块的可复用性,只需要暴露接口就可以了。而COM中的接口不变原则,则是为了实现被修改的模块的可维护性,以及与被修改的模块相关联的所有模块的可复用性。保证接口不变,模块内部就可以随意修改,从而保证模块的可维护性,同时,尽管被复用的模块内部作了修改,但是接口没有改变,那些与之相关联的模块也就不用改变,从而达到了对这些模块的复用。也就是说维护的代价被最小化到了需要修改模块的内部。这样就体现了接口不变性在可复用性和可维护性上的双重价值。整个软件系统的可复用性和可维护性也会因此得到提升。

不知道以上观点大家是否赞同。

我认为是否使用全局变量,与模块的复用性之间的关系不大。例如,COM组件里面接口的引用计数的实现就可以理解为逻辑上的一种全局变量。因为在COM的内部要为每一个接口都维护一个引用计数的变量,在COM模块的内部,这个变量应该说是一个相对全局的东西,至少AddRef和Release两个方法要访问这个变量。而COM设计的目的就是为了复用,这一点大家应该毫无疑问。

个人认为,理解清楚全局和局部的相对关系还是关键。不知道这个例子能否证明我自己的想法。

其实我多数时候是这样看待C++中的一个类的,就是类的public方法等同于C模块所暴露的外部函数,private方法等同于C模块中的内部函数,而private成员等同于C模块中的内部全局变量。唯一的不同是class是一个模板,可以实例化成许多的对象,而C模块的这种实现方法其实只能代表一个已经实例化了的对象,因为对象的方法不变的,而不同对象的内部数据却是独立的。C模块中,由于内部的全局变量只有一份,因此这样的模块实质上是一个已经被实例化了的对象。

如果不考虑多态继承等特性,C++中的类其实就可以通过C中的一个模块来实现,对这个模块的要求是,暴露一组外部函数,内部定义了模块内部使用的全局变量,用作一个对象的数据存储。

如果想实现可以多次实例化的对象,C也可以简单的做到。面向对象的核心就是对象内部数据的独立性和封装性,以及对对象内部数据的可操作性(即需要向外部暴露用于操作数据的方法)。这里的方法,是唯一的,就是说方法的代码只有一份拷贝(这也是复用的基础,见后面的分析),而数据是独立的,每个对象都要有自己独立的数据。我所说的C对象简单的实现办法就是定义一个结构体类型,然后实现一组对这个结构体类型变量进行操作的函数。将这个结构体类型和这组函数都暴露给用户。这是一个古老,但是却非常简单实用的实现方法。例如:
typedef struct
{
...
}MyClass, *PMyClass;
BOOL CreateLinkList(PMyClass pmc);
void DestroyLinkList(PMyClass pmc);
BOOL MyClassDoSomething(PMyClass pmc, int arg1, int arg2);
如果不考虑C++的继承和多态所带来的优势,个人认为这样的C形式的实现更为优美和简洁。

而所谓模块的复用,个人认为更多的是复用的模块中的方法,也就是说对象模型中的方法,即对对象数据的操作。因为操作的实现,需要的预算法相关的代码,而这样的代码的编写相对来说更加困难。而数据的复用,实质上是复用的一种数据结构,也就是数据类型。就如前面的MyClass,我只要知道MyClass的类型定义,就可以定义出无数个MyClass类型的变量,这样就已经做到了数据的复用。因此数据的复用更加容易。而光有数据的复用是毫无疑义的,只有当某种操作,作用到了对象的数据上,对象模型才会变得真正的有意义。因此对操作的复用才是复用的关键。为什么C++里面不太容易看清楚这个本质,就是因为C++的类,将数据和方法搞到了一起,当然是用起来更加方便了,然而却更加的蒙蔽了一些人的眼睛。

对象模型,毫无疑问,是在计算机世界里深刻刻画现实事物的最直观最简洁最有效的一种模型,但是要深刻的理解他却是多么的不容易。对象模型,只有在当操作作用到具体的数据上时,才能真正发挥它的威力。

我的观点就是,模块的复用,在于对方法的复用,而所有的方法都是要依赖于一个操作对象(就连最简单的printf函数都不例外,他操作的是屏幕和字符串)。另外,我个人真正的认为,采用何种语言,何种实现,真的不是解决问题的关键,关键在于你对你需要解决的现实问题的理解,深刻的理解,才能简洁而优美的表达。而如果大家能够承认,模块复用的关键在于对操作的复用,那么大家就不会认为全局变量的使用会对复用带来困难,也不会再对全局变量抱有偏见。

大家有没有试着这样理解printf函数,也就是printf和你的显示器还有要显示的字符串,就组成了一个具体的对象模型。其实对象无处不在,只是你是否深刻的理解他们。printf函数其实是一个操作,我们复用的永远都是操作,而数据,则是作为操作的载体。而操作,始终都是交互的,也就是一个实体与另一个(或多个)实体的交互性行为。在这里的两个主要实体就是屏幕和字符串。真正面向对象的思维方法就是如此,C标准库没有什么不好,他的设计是我们成功的复用了许多的东西,简化了开发的难度。


我觉得rockcarry说的很好啊,而且态度很值得称赞!

没有没有,你继续说嘛。反正大家都是来过过嘴瘾。观点不同更热闹嘛

以一种正确的态度和方式,去表达自己的想法,和相互沟通,这种表达和沟通本身就是没有错误的,哪怕我个人的想法可能存在错误。

并且,大家相互交流时,地位应当是平等的。如果我有言辞不得体,向各位道歉。

另外需要指出,误会,应当是指大家在相互交流是没能正确理解对方所表达的意思吧。因此,误会不是某个人单方面进行纠正的,而是通过相互的平等而深入的交流,来消除的。


算了,不用纠正了。
我也是有自知之明的,这两天吧克劳德大哥的BLOG当作BBS来用了,先说声抱歉。我不会再在这里发表言论了,免得更多的人生厌。
我只是想表达自己的想法而已,不过是想得到大家的认同。

我想要强调的是,软件设计应该是以你索要解决的实际问题为出发点,来进行设计。在计算机世界里最直接而简洁的刻画你要所模拟的事物,而没有必要使用繁琐的方法去回避一些无意义的问题(比如说全局变量),并且现实事物逻辑上存在的东西是没法回避的,不应该回避,而是应该考虑如何用最好的方法去表达他。

或者说一个球场,你就在程序里将他用一个二维数组表示。这是物体的映射,还有行为的映射。比如踢球,可以写一个函数来实现。
我说的映射是这个意思。

有问题吗?软件设计的目的无非是为了解决实际问题,要解决实际问题首先需要建立模型。

在计算机世界里,总是需要用计算计的方法去建立一些模型,以刻画现实世界中的事物。

任何学科里面,先建立起模型,再进行研究,这应该是毫无疑问的吧。

计算机科学也应该如此啊,要解决现实问题,实现世界中的事物,就必须要在计算机世界里面有对应的模型。

而我所说的映射过程,就是这些模型的建立过程。比如书你实现了一个足球的游戏,实际上你是在计算机中建立起了与现实世界种对应的足球比赛的模型。当然,这个模型的刻画和实现是相当复杂的。

也许我的想法不对,但仅是个人预见。

to rockcarry: "软件系统的设计与实现过程,实质上是现实世界中的事物,到计算机世界中的事物,的一种映射的过程" 对你这句话我感到非常震惊。。我已经很少管人家的闲事了,这世上的误会数绝对是个天文数字根本不是一两个人能纠正得过来的。

“同一层级的模块应该是不需要相互依赖的吧”
前面这句话说错了。

相互依赖应该是不可避免的,只是在设计和实现时需要尽量保证独立性和无环有向的依赖。

简单的讲,一个模块总是要有输入与输出的(指的是调用与被调用),也就是说它必须暴露出一些相对全局的接口,否则这个模块是毫无意义的。在同一层级的内部,每个模块向外暴露出的方法或数据在这个范围内都是相对全局的。对于在多个子模块组成的大模块里,每个子模块处于大模块的内部,因此这个时候来看,子模块所暴露的全局接口都是局限于大模块内部的,是一个局部的概念。大模块就是对内部子模块的再封装,然后再暴露出新的接口。在这里,全局和局部,大模块和小模块都始终是一个相对的概念,理解这种相对的概念很关键。如果有做过硬件IC设计的,应该对这个理解得更加深刻吧。多个器件,在同一层级是可以随意互联的,这个时候各个器件之间的相互接口都是公开的。但是当你将多个器件互联,引出必要的输入输出管脚,再将整个系统封装入一个黑盒子里,那些原本可见的器件的管脚都变得不可见了。可见的只是这个黑盒子所暴露出来的新的管脚。在这里,外部于内部、全局与局部,大与小都始终是一个相对的概念。

正是那些过于繁琐的封装,导致了整个系统的复杂性,使得整个系统过于臃肿,使得整个系统难于被理解。如果能保证简洁的设计,和扁平的层次架构,那么整个系统的将更加易于理解和维护。个人认为,一个系统易于维护的基础就是,首先它要易于被维护者所理解。

当然隐藏内部细节和暴露简洁的接口是必须的。另外整个系统的层次还要尽量保持扁平。

以上只是我个人的愚见,希望得到大家的认同。

to cloud:我丝毫没有低估前人智慧的意思,反而很崇拜。只是认为事物发展都有一个循序渐进的过程,计算机科学也应该是这样。

另外,库的设计者努力地隐藏细节不想调用者知道,而我的习惯觉得这样子隐藏得太多,总喜欢一层层剥开去看。因此,有时在看一段比较高层的很短的代码都得花很长的时间,不知这样地花时间值不值得?来这里讨论的很多都是高手啊,经常上来看看还是学到了不少东西。

有一种说法是,软件系统的设计与实现过程,实质上是现实世界中的事物,到计算机世界中的事物,的一种映射的过程。这也就决定了软件开发的一些基本原理,在现实问题中,客观存在的事物,就必须在计算机这一层面找到一种对应的存在。还是那句话,对于一个具体的要实现的系统,在逻辑上相对全局的一些事物,在程序里面,就必须有相对全局的变量和方法与之对应。不管你是使用何种语言,不管你对你要描述的事物进行了和何种的封装,仍然回避不了其全局性。因此,总要找一种合适的方式,来描述这些事物。对于我来说,我更加喜欢全局变量,不喜欢进行繁琐的封装,易于理解,易于维护。

另一方面,还需要指出的是,全局与局部,都始终是一个相对的概念。一个函数内部的局部变量,相对于这个函数的内部来说,他就是全局的。一个模块内部的全局变量,相对于模块外部来说,它又是局部的。一个进程内部的数据,相对于另外一个进程来说,是不可见的,因此也可以理解为是局部。如果使用了内存映射文件的共享内存技术,多个进程又可以通过共享内存交换数据,因此共享内存又可以理解为是全局的。因此对全局的理解不应该太过片面和狭隘。

在处理多线程多访问时,模块里的共享数据,在逻辑上就是一个临界资源,毫无疑问,对临界资源的处理,都是必须使用互斥和同步。在实现上无论你采用什么方法,什么技术,在逻辑上,要解决的问题都是很简单的一个,就是互斥和同步。然而,在外部看来,模块内部的共享数据是不可见的,因此只要在实现时保证安全的临界资源处理,就能保证你所暴露出的接口是安全的。但是要分清楚,设计是设计,实现是实现,因此安全的设计,没有安全的实现,导致的也是不安全的系统。

设计的时候,都是用抽象的方法,在逻辑上考虑问题,而不会涉及到实现的细节。有好的设计,当然也需要有好地实现,如果选择了C语言,在有了良好设计的前提下,个人认为大家所争论的问题已经不是问题。如果还要像大学生一样去讨论,使用全局变量到底好不好,使用类到底好不好,是根本毫无疑义的。

既然到了实现,就必然要选择一门语言,既然选择了一门语言,就应当进行优雅的实现。实现的最终目的是为了解决问题,但实现不光要解决问题,而是要优雅的解决问题,要考虑可复用性、可读性、可维护性(其实这些本身也是一个问题)。为了达到以上目的,我个人比较崇尚的设计原则就是简洁的设计原则。我不喜欢使用繁琐的方法,对需要描述的事物进行封装。我也写过很多代码,用 C++ 也写过,也重构过很多代码。每次重构的时候,都会发现,当初的设计是多么的不合理,一些繁琐的封装完全没有必要,本来根简单的问题,因为自己的封装变得复杂了。一些简单的系统,用C语言就足以非常简洁的进行描述。当然,我也绝对不会参与什么语言之争,因为那只是大学时代的争论而已。C++也有自己的优点,只是需要好好的利用。还是那句话,语言就是一门艺术,语言的每一个组成部分都有其存在的价值,就看你是否真正的懂得如何使用他们。

以上仅是我的个人愚见。


to david , 用 TLS 就好了。这也是 C 库实现中常见的手段。

如果模块里面有static变量,那么多线程访问的时候就的加锁,这让我很不舒服, 但是也想到什么好的方法。。。

同一层级的模块应该是不需要相互依赖的吧,也就是相互独立的。这是否就是低耦合高内聚。不同层级之间,应当保证无环有向地依赖关系。

简单的讲,也就是一个库内部的函数都是独立的平行的。而一个库,向下调用其底层库提供的接口,向上提供被调用的接口。如果能够保证层级式的设计,应该可以避免有环的依赖,最生动的例子就是协议栈的实现。

或者说被调用的接口就处于下层,调用者处于上层,调用与被调用的关系可以组成一个有向图。我们设计的目的就是为了保证这个图是无环有向的。并且,这个图中,许多节点都是平行的,相互独立的,因此在这个图上可以看出明显的层次性。合理的将一些节点划分入一个集合,这个集合就成为一个大的模块。你所需要做的,就是划分出所有的模块。然后就会发现,每个模块都有输入和输出。所谓最理想的设计,就是最终的大模块们的输入和输出,从头到尾连成了一条直线。在看看大模块的内部,他也是一个图,是整个大图的一个子图。同样,需要对这个子图进行划分。如果,每一层的划分,都能保证连成直线,并且这些直线相互平行,就能保证这个图的无环有向性,也就证明了其架构是良好的。

在设计时,也是这样,设计出一系列依赖关系成直线性的大模块。
然后再对每个大模块进行划分。每个大模块内部最好的设计就是多条相互平行的直线,如果不行至少要保证无环。
这样一直细化,直到每个节点都清晰可实现。

平行的函数,就类似于我所讲的平行线了。
个人认为,这就是最基本的设计方法。

类继承、类的层次结构什么的,这些让你一想到就感觉很恐怖的东西是C++的糟粕。通常,只有极少的情况下会继承一个具有实现的基类。

theApp正是一个全局变量使用的反面教材。

如果一个模快要复用,就不能用全局变量。因为任何一个模块都有其运行的上下文。在一个层次上看起来是唯一的,在用户看来就不一定是。这仅仅是美感方面的问题,别的问题还有多线程的线程安全问题。C标准库在这方面未必是正确的。

当然,代码的复用并不只有库的复用这一种方式,还可以是进程的复用,然后用管道或者shell编程把进程串起来。这种情况下一个进程最外层的代码就可以用全局变量。

对于目前的我来说,C语言所提供的东西,已经足够我简洁而优雅的描述现实中的事物。

C++中的许多特性,我目前都用不上,所以没有深入的学C++。感觉还是很繁琐,比如类继承、类的层次结构什么的,一想到就感觉很恐怖。

以后遇到更加复杂的系统,还是有可能使用某些C++的特性的。


MFC中的theApp就是一个全局变量,个人感觉他没有什么不好的。

全局变量的问题在于,许多人非正确的利用它,在不同的模块之间传递参数,这样将导致模块之间的相互依赖关系,而无法实施低耦合高内聚的设计原则,以及保证模块间的无患有向依赖关系。

但是合理的使用为什么就不可以呢?语言就是一门艺术,语言的每一个组成部分都有其存在的价值,就看你是否真正的懂得如何使用他们。

现实问题中,有很多事物都是全局的。C语言中的全局变量,天生就是为了描述这些全局的事物而设计了。为什么不用呢,为什么就说不好呢?

不管你在C++中,对一个全局的变量进行如何的繁琐的封装,在逻辑上,相对于局部来说,他始终是一个全局的事物。你需要描述的客观事物的逻辑就是如此。比如说Windows程序,有一个全局的实例句柄,不管你怎么封装,他始终是全局的,比如说MFC的封装吧。

我个人不喜欢繁琐的封装。在架构上,最好还是保证一种扁平的层次结构。这样便于理解和维护。

就这样,C天生就提供了最适合于描述全局事物的方法,就是全局变量,但是许多人却弃之不用。不过,无论采用何种语言,逻辑上全局的事物,是弃不掉的,就如同MFC中的全局对象theApp。

不喜欢C++,就是因为它太过于繁琐。

static 修饰的全局变量,只是在模块内部使用,并不会暴露给用户。

我不懂,为什么大家都认为全局变量就不好。如果在模块内部定义全局变量,有利于模块的实现,为什么不用呢?。

另外C里面的函数都是全局的,这样也很好啊。调用起来简单方便。

如果一个.c是一个模块,那么其暴露出的C函数接口,就类似于C++中的public方法。而在.c内部使用的全局变量,则类似于C++中的private类成员变量。这样的设计也C++很类似,也很好用,而且相比C++的类,这样的方法会暴露更加少的接口。只需要暴露一组函数接口而已。再结合C的结构体变量,一样可以做出优秀的设计。

至于多态的实现,可以借助于函数指针,同样可以做得很好。

最关键的不是何种语言,而是要保证设计。设计原则就是单向依赖的可复用的模块化设计。

拿private和static比较是不公平的。因为static的全局变量,以及需要依赖于全局变量的全局函数不是好东西。而如果是一个结构,结构的一部分字段需要暴露,另一部分字段不需要暴露,C一样没有好的办法。而解决这个依赖问题C和C++的共同办法都是抽象,但是正如楼主所说的,抽象只是隐藏了问题而已。

C++ 中等同于 C 的 static 的做法通常是在 .h 中声明一个private 的子类, 把其具体定义放在 .cpp 或是内部引用的头文件中。同样可以达到隐藏细节的目的。

个人认为架构的设计的最终目的一方面是为了更加易于实现,另一方面是为了更加易于维护。

设计的基本原则也很简单,就是逐级的模块化。我目前只懂这个,对于什么面向对象的方法,没有什么研究。
也许是在驱动程序开发这一层,问题都比较简单吧,接口比较规范,而且都是由操作系统定义好了。

不过感觉 C++ 的类还是没有 C 的函数好用,函数只需要知道原型,就可以调用。实现也很简单。而要使用或实现一个类,都真的好麻烦。如果再搞一个什么类层次,用上虚函数,多态什么的,头都大了。

在设计上,对模块逐步的细分,不就可以了吗。小模块之间的设计,也尽量保证模块之间的无环有向依赖,设计上至少应该这样考虑。

C中也有static,用于修饰内部符号,感觉比C++的private好用,C++中的类声明或多或少都要暴露类的一些内部细节,除非采用COM的那种做法。否则,就算private修改了某个内部方法,但是在class的声明中任然能够看到这个方法的声明,只是用户无法调用而已。

而C的static就很好用,内部符号放在.c里面声明,并以static修改,以保证编译出来的.obj中,编译器不将其当作外部符号输出,这样link的时候就不会有这个符号。而在.h中,根据就不会出现被static所修饰的符号,因此给用户暴露的内部实现细节应该更少。

我其实不怎么懂C++,我写代码一直都是用C的,所以有一些观点可能不正确。

to rockcarry, 问题总是出现在未察觉时。写程序,我们要避免问题;做设计,我们要预知问题。可以这样做和应该这样做是有区别的。IMHO

例如,我们可以把一个大模块拆分到许多 .c 里去实现。可以看成大模块被细分成了许多小模块。那么大模块内部的小模块之间的交互用的接口该如何表述就是一个设计问题,但它并没有实现上的困难。

C++ 中提倡用 private 或是 namespace 来隐藏细节,这的确提供了一个很优秀的工具。但是话说回来,工具的便利使我们更难发现深层次的问题了。

to longtrue, 我个人认为,还是不要低估前人的智慧的好 :)

.h天生的设计就是为了声明,就是为了被有需要.c所include

.c天生的设计就是为了实现,就是为了生成.obj

.h中有了声明,就能保证compile的pass

.c中有了实现,就能保证link的pass

如果做成库,也只需要简单的将.c编译出来的.obj打包到一个.lib中,这样小的模块就组成一个大模块。

向用户就只需要提供一个.lib和若干的.h就可以了。用户根据需要include进相应的.h,然后complie自己的.c,将编译出来的.obj和.lib连接就OK了。

我不赞成只向用户提供一个单一的.h,将所有的外部声明都放在一个.h里面,虽然用户使用起来会更加方便。这样会多维护一个.h,而且用户对整个lib的架构(指的是外特性,也就是接口)就不会很清晰。

每个.c对应一个.h应该是毫无疑问的做法,最终做成lib的时候,向用户提供最原始的若干个.h也是毫无疑问的做法。不用再额外的去维护别的.h文件。

.h中只做外部声明是毫无疑问的啊,只声明外部常量和外部符号。
.h中只做实现也是毫无疑问的啊,当然为了实现还需要定义内部的常量和符号。

.h天生的设计就是为了声明,就是为了被有需要.c所include

.c天生的设计就是为了实现,就是为了生成.obj

这些应该都不用再讨论了吧,至少在c语言里面就是这样的简单

我个人是这样理解得,不知道是否正确

不知道我的做法是否合理,不知道云风大哥是否有更好的办法。

我也想戚戚,但是还没有足够的能力大戚而特戚,所以想听一听云风对这个问题更详细一点的阐述。

如果是做底层或驱动,还是用C最合适,C++会显得繁琐和臃肿。
至于什么循环依赖的问题,我想不用再讨论了吧,只要保证模块间的依赖关系是一个无环有向图就可以了啊。
代码的组织上,肯定都是一个.c对应一个.h,.h中只放外部声明,以保证编译的时候不报错。.c中可以放内部的常量定义和内部声明,并且要实现.h中声明的外部符号,以保证链接的时候不报错。
我对这个问题的认识也就如此简单了,没有什么大的问题啊,一直都用得好好的。
可能是我做的项目还不够大吧,没有遇到云风大哥的问题。

我一直以为fortran不支持递归是由于当时人们对程序认知的局限所造成的。或者并没能把递归的处理方法做出来。

"能力越大,越难 KISS 。"
这话说得太好了。

以前有个写了接近20年程序的老鸟说,他写的C程序永远是被调用模块在前而且从不在文件内声明。以前我只觉得是习惯,现在才明白这样做的好处。

但是如果纯用模板的话就没办法在编译的时候引入这个间接层来脱耦合。很想知道如果用模板怎么实现回调事件。或者说回调本身就是循环依赖,不应该有回调,每一个模块都应该是被动查询的?

我也心有戚戚一下……

抽象出一个纯虚函数的接口很容易在编译问题上脱耦合。但是那些事件Handler之类的东西其实还是有依赖关系的。

Post a comment

非这个主题相关的留言请到:留言本