为什么GP更自然

131 views
Skip to first unread message

pongba

unread,
Sep 16, 2007, 2:29:44 AM9/16/07
to pon...@googlegroups.com
OOSC里面提到一个ADT的例子,用stack来讲的。

我做了一个stack类,用数组实现。我另有一个函数foo,要用到这个stack:

void foo(stack);

按照面向对象的思路,要使得这个foo可以复用与所有的stack,比如可能我因为效率原因要替换stack类为另一个链表实现。那我就得抽出一个IStack接口来,也就说,一个具体类还没写出来,乃至于刚刚写出来,我就得考虑抽它的象,这就是过早抽象我觉得:

void foo(IStack);

何必呢?

另一方面,GP的方法是把形参的类型给虚化掉,当成无类型语言来用:

void foo<Stack>(Stack s);

这样的foo不依赖于任何具体的interface,天生就自然依赖于stack这个概念本身的抽象语意。

ruby的火星人类型系统就是这回事,不过由于ruby不是静态类型的,所以不用C++ 模板这种workaround。直接做到了真正的GP。

--
刘未鹏(pongba)|C++的罗浮宫
http://blog.csdn.net/pongba
TopLanguage
http://groups.google.com/group/pongba

lijie

unread,
Sep 16, 2007, 3:03:54 AM9/16/07
to pon...@googlegroups.com
你这个例子的IStack用最时髦concept来做和OO的接口效果也没什么差别,不同的是OO是二进制复用的,GP是源代码级的,OO接口是可以动态绑定的,GP就只能是静态的。

软件开发过程中我认为最有用的还是OO,接口协议在实现以前就可以确定,看看GP,实例化时才能检测出一些低级错误。目前一些项目中我就倾向于使用接口,先把接口抽象出来,编写过程中都是使用接口,使用哪个实现可以在最后阶段确定,实际上目前正在打算做一个类似spring的动态配置框架,把这种初始化的东西交给框架和配置文件来做。GP的各种实例化类型之间互不相认,可做不出这种功能来。

在07-9-16,pongba <pon...@gmail.com> 写道:

red...@gmail.com

unread,
Sep 16, 2007, 3:15:10 AM9/16/07
to pon...@googlegroups.com
lijie 写道:
> 你这个例子的IStack用最时髦concept来做和OO的接口效果也没什么差别,不同
> 的是OO是二进制复用的,GP是源代码级的,OO接口是可以动态绑定的,GP就只能
> 是静态的。
>
> 软件开发过程中我认为最有用的还是OO,接口协议在实现以前就可以确定,看看
> GP,实例化时才能检测出一些低级错误。目前一些项目中我就倾向于使用接口,
> 先把接口抽象出来,编写过程中都是使用接口,使用哪个实现可以在最后阶段确
> 定,实际上目前正在打算做一个类似spring的动态配置框架,把这种初始化的东
> 西交给框架和配置文件来做。GP的各种实例化类型之间互不相认,可做不出这种
> 功能来。
>
lijie 说得也很有道理.

我自己的感觉, 大概也和我自己的工作习惯有关系, 一旦一个东西怎么做比较有把
握了, 我就会指导手下, 怎么怎么分对象, 每个对象做什么什么 --- 直接到类的
时候较多, 我这边的项目多数是要求性能高, 总体代码量未必是非常大,
interface 用得不多.

对架构设计不是非常有把握的东西, 我会自己来做, 这个时候, 我自己动用的东
西, 主要是 gp 为多了.

red...@gmail.com

unread,
Sep 16, 2007, 4:15:00 AM9/16/07
to pon...@googlegroups.com
lijie 写道:

> 软件开发过程中我认为最有用的还是OO,接口协议在实现以前就可以确定,看看
> GP,实例化时才能检测出一些低级错误。
unittest 代码中, 可以实例化这些模板类进行测试的啊.
我自己的底层模板类的单元测试, 会提供一些 stub 类作为模板的参数, 然后写测
试代码, 你说的这个问题不会存在.

Jian Wang

unread,
Sep 16, 2007, 4:16:12 AM9/16/07
to pon...@googlegroups.com
我不同一lijie的说法,在这个例子里,OO和GP的差别非常大.OO是二进制复用的会引入更多的细节,因此interface相对难以稳定.

比如说这里的IStack接口必然会依赖于Stack里的元素类型。而元素类型和Stack的概念没什么关系,foo也未必关心该元素的类型。这就是GP大显身手的地方了。

在 07-9-16,red...@gmail.com<red...@gmail.com> 写道:

longshanksmo

unread,
Sep 16, 2007, 5:50:50 AM9/16/07
to TopLanguage
差别还是很大嘀。想想看,天下并不是所有的stack都实现了IStack吧。如果没有实现怎么办呢?做个adapter,实现IStack?太折腾了
吧。而且好些不是stack,但行为上却又是stack的东西到底是实现IStack好呢,还是不实现的好?如果这个东西恰巧又具备list的行为,那
是不是还应该实现IList呢?所以,它们的灵活性不是在同一个层次上的。
OO现在是给过度使用的东西。在开发软件时,应优先使用GP,以此得到性能、灵活性、扩展性、简洁性、容错性等等方面的优势。只有在确实需要动多态的情
况下,比如著名的绘图对象案例,OOP才是最强大的。一般的手法可以是,用OO设计运行时模块,用GP优化代码实现。

Googol Lee

unread,
Sep 16, 2007, 5:51:59 AM9/16/07
to TopLanguage
补充一个看法。oo在多继承时简直就是要命的难受。想要将一个interface从另一个庞大的interface里抽离出来也是一样。不光是新写个类
的问题,而是整个继承结构都要跟着改变。

而concept感觉在这方面就会灵活很多,至少不用动整个的继承体系。

lijie

unread,
Sep 16, 2007, 7:04:55 AM9/16/07
to pon...@googlegroups.com
GP倾向于产生不同的类型,OO倾向于提取出同一个类型。C++里面泛型接口(类)有没有什么坏处还不清楚,不过目前我有些地方使用了这种方式来把元素类型做进去,比如实现一个IQueue<T>接口,实现类包括有一个基于内存的Queue<T>,还有一个使用BDB实现的BdbQueue<T>,这么做的原因就是不想为了一些系统的微小差异编出不出的代码,根据配置去选择实现就行了。

所以我还想讨论一下泛型接口的问题,纯OO和纯GP似乎都有缺点,如何把它更好的统一起来?这在动态语言里根本不是什么问题。。。

在07-9-16,Jian Wang <oxygen.j...@gmail.com> 写道:

lijie

unread,
Sep 16, 2007, 7:21:44 AM9/16/07
to pon...@googlegroups.com
每一种库都要提供一整套的容器,GP也并不是为了要兼容其它库,很少有库在设计时把其它库也考虑进去。如果是为了使用其它库并且提供统一接口,OO的做法就是使用适配器模式处理函数名或是实现上的一些差异。GP呢?concept可以解决吗?它只能解决名字的问题。

至于GP和OO哪个应该更优化使用,我认为目前大家都是偏向于OO吧,我还真没看到大量使用GP构造的大系统,编译效率就不是一个级别的,少量使用还可以接受,用来构造整个系统就过了些。另外OO对于性能的降低是很少的,"灵活性、扩展性、简洁性、容错性" 这些从来都没成为OO的弱点啊。

在07-9-16,longshanksmo <m...@seaskysh.com> 写道:

red...@gmail.com

unread,
Sep 16, 2007, 7:29:41 AM9/16/07
to pon...@googlegroups.com
OO 程序, 很基本的类就要开始提取共性, 组织基类和 interface, 在这些基本,
老早的东西里面就开始耦合了. 整个架构的类耦合度比较高, 如果类设计合适, 同
时也不需要调整, 架构还是很清楚的, 反之就比较糟糕了.

GP 程序, 只有到将各个类, 模板类最终组装的时候, 才会开始耦合, 耦合度低得多了.

这两年我设计的程序慢慢转向这个风格:

1. 模块之间的接口, 主要用 free function, delegate, 少用类, 如果用到类,
只用简单的单个类, 不将类之间的关系弄到接口中.

2. 一个模块内部的实现, GP + OB, 基本很少用到 interface 和 继承, 对象组合
用得较多.

lijie 写道:

yq chen

unread,
Sep 16, 2007, 7:35:20 AM9/16/07
to pon...@googlegroups.com
to lijie cpu...@gmail.com:
OO对于性能的降低是很少的,"灵活性、扩展性、简洁性、容错性" 这些从来都没成为OO的弱点啊。
 
首先,OO对于性能的降低再少,它还总是有降低的,有时候这一点降低就是也是不可接受的。
其次,使用OO的主要问题不是性能的降低,更加值得关注的是OO容易造成系统的耦合性的严重问题。一个系统中有无数的类,每个类都不知道在干什么?而且互相都有关联,就像一张蜘蛛网,造成严重的系统功能扩展合维护的问题。OO的复杂性很难把握,所以很多人都会回到C的老路上去,因为用C进行设计,首先整个系统架构是一目了然的。
 
至于,完全以GP实现的系统(系统的主要脉络),一个样板就是STL,然后ATL和WTL里边用了大量的GP技术。
 

pongba

unread,
Sep 16, 2007, 7:35:55 AM9/16/07
to pon...@googlegroups.com
我认为redsea的这种方式很中肯!:-)

过早抽象是魔鬼:-)

By the way, 伟大的鸭子!

pongba

unread,
Sep 16, 2007, 7:40:47 AM9/16/07
to pon...@googlegroups.com
我也觉得耦合是最大的问题。设计得好加上抽象体系本身就具有内在固有的稳定性(如GUI)的话,OO固然能够搭建出好的框架来。然而如云风所说,在实际当中的一个典型系统,你敢保证两年后这个抽象体系还适用吗?到那个时候类已经叠床架屋了,怎么办?前面有人说java的重构在这方面很牛?召唤有识之士指教哈:-)

云风目前已经回归C了,至少在模块间,是肯定要回归C的,因为模块间的设计重点就在于低耦合,越低越好。就算C++有了ABI,支持跨二进制传递接口指针,可能这也并不是个好主意。模块内,避免过早抽象同时又能恰当避免依赖具体实现的折衷办法我觉得就是duck typing了。

Huafeng Mo

unread,
Sep 16, 2007, 8:03:26 AM9/16/07
to pon...@googlegroups.com
库不仅仅包含容器或者类什么的。库还可以包含算法。实际上算法是核心,是某种具体功能的体现。当一个算法要"一次编写,到处使用"的话,GP是唯一的类型安全的出路。
我曾经在csdn论坛上发过一个帖子,便是利用GP实现对代码优化:http://community.csdn.net/Expert/TopicView3.asp?id=5626229。 这类优化做法是OO没有办法实现的。如果你觉得C++的编译效率低的话,那么,据说D的编译效率就很高了。编译效率低并非GP造成的。至于"扩展性"之类的修辞,可以尝试着全面地用一下GP,到时候就有体会了。

pierric

unread,
Sep 16, 2007, 8:08:03 AM9/16/07
to pon...@googlegroups.com
在 07-9-16,lijie<cpu...@gmail.com> 写道:
>
> 所以我还想讨论一下泛型接口的问题,纯OO和纯GP似乎都有缺点,如何把它更好的统一起来?这在动态语言里根本不是什么问题。。。
>

haskell中的type class是否就可以看做是interface + concept呢?

red...@gmail.com

unread,
Sep 16, 2007, 8:16:36 AM9/16/07
to pon...@googlegroups.com
OO 的缺点应该还有一个, 我刚才才想起, 以前我看 java log4j 的复杂性的时候,
想过这个问题.

(我很少在 OO, GP 这么高的层次对自己的经验做总结, 可能三不五时想起以前碰
到过的一些问题).

用 OO 设计系统, 扩展灵活性是需要事先准备的, GP 则不同, 这点带来的影响其
实很大.

拿 log4j, log4cpp 之类的东西来说, 一边是前端可能有多个实例可以提交 log,
一边可能是console, file, syslog 等多种 appender, 要支持这种灵活的体系,
需要事先将这些interface/基类, 以及之间的关系, 具体的log 类, append 类之
前就准备好, 不然的话, 以后就需要复杂的重构了.

如果是 GP, 反正是最后组装, 那我最初设计的类, log 知道后端有一个接收函数
即可, 刚开始可以直接用 console append 做后端, 需要实例化的时候, 做一个粘
合层, 将一个 log 和 console appender 结合起来即可, log 其实不管后端是什
么类型, appender 也不管前端是什么; 万一以后要支持 多log 前端对多
appender, 基本上只需要写一个加强的粘合层就好, 原来写好的代码可以做到不改
都可以.

可能 OO 支持者说, OO 方法, 先抽象一个 ILogFront, 一个 ILogAppender 接口
也可以, 然后在体系架构中增加一个多路器.

但是

1. 如果这里多一个 interface, 那里多一个 interface, 复杂度就白白多了很多
了, 广东话说, 多个香炉多个鬼, 东西多了, 危险总是多了

2. 传说, 人同时能够把握的维度数目是有限的; 我自己知道同一个时候, 上下文
context 太复杂, 我会晕的,


3. 这整个体系还是需要事先设计好, 如果我比较笨, 可能就只有等到所有代码写
完了, 才想起还要加一个类到里面去, 就要调整很多代码了.


这里只是多一个间接层的问题, 某些时候, 一个类自己有几个相关的间接层, 或者
某个间接层相连的, 还是间接层, 都不奇怪.

-------------------

OO 的方式, 多半会需要为各种灵活性准备很多中间层, 各种基类多半都需要知道
这些中间层, 架构会比较复杂, 而实际应用用没有必要用到这么多灵活性的时候,
白白带来很多阅读理解和维护和运行的开销.


GP 的方式会好很多, 基本部件做好了, 缺省的粘合部件不合适, 可以另外换一个
粘合部件, 复杂性没有影响到基本的部件. 而且, 的粘合层, 无论 OO, GP 都少不
了, 耦合度放在这边, 也比较自然.


pongba 写道:

pongba

unread,
Sep 16, 2007, 8:20:53 AM9/16/07
to pon...@googlegroups.com
redsea能否就"粘合层"稍加详述?代码样例那就更好了:-)

On 9/16/07, red...@gmail.com <red...@gmail.com > wrote:

redsea

unread,
Sep 16, 2007, 8:24:01 AM9/16/07
to TopLanguage
突然想到, 其实用 free function , function pointer, delegate 作为接口, 扩展灵活性也是可以不需要
事先设计好的.

redsea

unread,
Sep 16, 2007, 8:32:28 AM9/16/07
to TopLanguage
所谓的粘合层, 就是这么一种玩意:

这东西没有实际业务领域对应的对象, 只是负责将另外两种对象之间连接起来.

象 log4j 这种东西, 给你的用来输出 warn, error 各种信息的那个对象(忘了叫什么名字了), 是有业务领域意义的; 具体对应
到 file, console, syslog 的 appender 对象, 也是有业务意义的, 但是, 将这两个东西组装起来, 使得前端一个
或者多个对象的输出, 可以到后端一个多个 appender 中去的这个玩意, 是没有业务领域意义的, 只是为了让事情可以转起来, 需要它的工
作.

还有, 一个 IM 软件, 网络收包解析部分的代码, 和界面显示的代码, 也需要一个东西串起来, 这里可能还需要并行-->串行的转换, 这玩意
其实也没有业务领域的意义, 也可以认为是一个粘合层.

很多时候, 软件结构, 为了增加灵活性, 无非就是增加中间层, 这些中间层, 应该也可以认为是粘合层.

<<unix 编程艺术>> 一书中, 认为 OOP 带来最大的问题, 就是粘合层过厚, 传统 C 编码, 粘合层是很薄的, 你可以找那本书看
看.

> --
> 刘未鹏(pongba)|C++的罗浮宫http://blog.csdn.net/pongba
> TopLanguagehttp://groups.google.com/group/pongba- 隐藏被引用文字 -
>
> - 显示引用的文字 -

Huafeng Mo

unread,
Sep 16, 2007, 8:38:31 AM9/16/07
to pon...@googlegroups.com
这让我想起了《modren C++ design》里的policy模型中的主模板

redsea

unread,
Sep 16, 2007, 8:39:32 AM9/16/07
to TopLanguage
http://www.javaresearch.org/article/6815.htm

胶合层
当自顶向下和自底向上的汽车撞在一起的时候,情形通常是一片混乱。顶层的应用逻辑和底层的元操作必须由胶合层来阻隔。

几十年来,Unix程序员明白了一个道理,胶合层是令人厌恶的东西,应该让粘结层越薄越好,此乃性命攸关之大事!胶合层应该用来把东西粘在一
起,而不是用来掩盖层与层之间的冲突和裂痕。

拿上面那个浏览器的例子来说,粘结层包括:把由HTML解析而来的文档对象应设为显示缓冲区里的位图。这部分代码是声名狼藉的难写,错误百
出。HTML解析和GUI库的错误和缺陷都会在这层里表现出来。

浏览器的胶合层不仅要在规范和元操作之间充当中介者,还要在若干不同的外部规范中间充当中介者----HTTP网络协议的行为,HTML文档结构,
不同的图形和多媒体格式,以及来自GUI的用户行为。

一层胶合层已经很容易出错了,但这还不是最糟糕的。如果一个设计者意识到胶合层的存在,并且试图去用自己的一套数据结构或者对象把这个胶合层组
织到一个中间层中,那么结果就会是多出两个胶合层----一个在那个中间层之上,一个在其下。那些聪明但却欠缺历练的程序员经常积极地跳到这个陷阱里去。他
们把基本的类(应用逻辑,中层和元操作)做得像课本上的例子那样漂亮,最后却为了把这些漂亮的代码粘合到一起而在很多个越来越厚的胶合层中忙得团团转,
直到困死。

C语言本身被认为是薄胶合层的良好范例。


Unix和面向对象语言
自1980年代中期开始,新的语言纷纷宣称自己对面向对象编程提供直接支持。

OO设计的概念首先在图形系统,GUI系统和仿真系统里被证明是很有价值的。然而历史证明,在这些领域之外,OO并没有带来明显的益处,这令很
多人感到吃惊,感到幻灭。应该试图去理解其中的道理,这将会是很有意义的事情。

在Unix传统的模块化技术与围绕OO语言发展起来的模式之间,存在着一些冲突和张力。Unix程序员较之其他人对于OO抱有更大的怀疑态度。
原因之一是多样性法则。OO被说成是软件复杂性问题唯一正确的解决之道,这未免令人生疑。不过,还有更深层的原因。

我们刚才提到,Unix的模块化传统中,薄胶合层是一个重要原则,从顶层程序对象到下层硬件之间的抽象层越少越好。

这部分是因为C的影响。在C中间模拟真正的对象是件很费力的工作。因此,叠置一大堆抽象层简直是要人老命的事情。因此,在C中的对象层次倾向于
平坦和透明。长此以往,Unix程序员使用其他语言也习惯于薄粘接/浅层次。

OO语言使得抽象变得容易了----也许是太容易了。它鼓励整个架构具有厚厚的、精致的胶合层。如果问题域确实复杂,确实需要大量的抽象,这可能是
好事。但是这也是很糟糕的事,因为程序员最后会把很简单的事情用很复杂的办法来做,仅仅因为他们可以这么做。

所有的OO语言都有有一些倾向,吸引程序员跳进"过度分层"的陷阱里。对象框架和对象浏览器并不能取代好的设计和文档,但是却经常被看成一回
事。太多的层次破坏了透明性----我们很难看穿下面的东西,很难在思想上对于代码的功能建立清晰的模型。简单性、明晰性和透明性一口气全被破坏了,结果代
码充满了晦涩的错误,带来严重的维护性问题。

这种情况还在继续恶化,很多培训班把厚厚的软件分层当成好东西传授----你拥有那一大堆类被认为是数据中所潜藏的知识的体现。问题在于,在胶合层
中的"smart data"经常与程序所操作的自然实体无关,而仅仅只是胶合本身。(一个确切的标志就是抽象子类的不断增值,以及所谓
的"minxins"。)

Unix程序员对这些问题有本能的直觉。Unix中OO语言没有能够替代非OO语言如C,Perl(虽然支持OO,但很少有人用
到),Shell等,这大概是原因之一。Unix世界里对于OO的批评比别的领域中要尖刻得多。Unix程序员知道什么时候不应该用OO,就算是要用
OO,他们也尽可能的保持对象设计的简洁。正如Michael Padlipsky所说:"如果你知道你在干什么,三层足够;如果你不知道你在干什么,
十七层也没用。"

OO在GUI、仿真和图形领域里取得成功的原因,可能是因为在这些领域中,相对而言,比较容易解决"类型存在与否"的问题。例如,在GUI和图
形系统中,类和可视对象之间存在着自然的映射关系。如果你发现自己所增加的类并不直接映射可视对象,则你也可能就会发现胶合层已经变得很厚。

On 9月16日, 下午8时20分, pongba <pon...@gmail.com> wrote:

Huafeng Mo

unread,
Sep 16, 2007, 8:45:37 AM9/16/07
to pon...@googlegroups.com
这是否意味着,应该将业务实体做成尽可能小的对象,然后用形同算法的自由函数,或类函数的东西操作、组合这些对象。因为自由函数之类可以泛化,并且很容易地替换,以此大幅提高灵活性,简化开发。
我的理解没错吧。:)

On 9/16/07, redsea < red...@gmail.com> wrote:

redsea

unread,
Sep 16, 2007, 8:46:50 AM9/16/07
to TopLanguage
我将这一章其他部分也贴出来

Eric Raymond谈模块化原则,胶合层和面向对象的缺陷

模块化 ---- Keep it clean, keep it simple

程序员所面对的复杂性日益增大,而划分代码的方法也有一个自然的发展过程。一开始,软件不过是一大块机器代码。最早的过程化语言带来了"依据子
例程划分代码"的观念,接下来我们发明了程序库,为不同程序提供公共服务。再下来我们发明了独立地址空间和进程间通信。如今,我们已经惯常于把程序系统
分布在彼此相隔万里的互联主机上。

Unix的早期开发者同时也是软件模块化思想的先锋。在他们之前,模块化只是计算机科学的思想,还不是工程实践。其弘旨是:要开发可正确工作的
复杂软件的唯一途径,就是用定义良好的界面把诸多简单模块连结起来,形成整个系统。只有如此,大部分局部问题才可能在局部得到修正或者优化,而不至于破
坏整体。

今天所有的程序员都被教育要在子程序的层次上进行模块化。有些幸运者掌握了在抽象数据类型(ADT)层次上的模块化能力,就已经被认为是好的设
计者了。如今的设计模式运动,就是希望把这个层次再提高一步,发现那些有助于对程序大型结构进行良好组织的成功的设计抽象。

封装和最佳模块体积

模块代码的第一重要特质乃封装。封装良好的模块决不互相暴露内部信息。它们不去窥测其他模块的实现,不胡乱地共享全局数据,而是通过API互相
通信。

模块之间的API有双重身份。在实现层次,API函数扼守模块之间的连接点,阻止内部信息外泄。在设计层次,API实际上定义了你的系统架
构。

模块分解越细致,模块越小,API的定义越重要。整体的复杂度、错误的可能性也随之降低。

然而,如果分解过度,导致过小的模块,会得到意想不到的情况。下面的图来自Hatton 1997年的统计数据。可见到图形是U形的。

Hatton的数据是在不同语言和不同平台上经过广泛统计的道德,故具有可信性。可见,代码量在200到400逻辑行之间的模块效果最佳。

紧凑性和正交性

代码并不是唯一具有所谓"最佳单块体积"的软件要素。语言和APIs同样受到人类认识能力的限制而逃不出Hatton的U曲线。

因此,Unix程序员在设计APIs、命令集、协议以及其他东西的艰苦思索过程中发现了模块化的两个要素:紧密性和正交性。

紧凑性
紧凑性是指设计对于人脑而言"易于理解和接受的程度"。

紧凑的软件工具跟顺手的日常手工工具一样拥有很多优点。它让人们乐于使用,用起来方便自然,大大提高你的效率,而且安全可靠,不像那些复杂的工
具,动不动就弄伤你。

紧凑并不意味着功能弱,如果一个设计建筑在容易理解的抽象基础之上,那么它可以非常强大和灵活,同时又保持紧凑。紧凑也不意味着容易学习,你可
能必须先理解抽象之下精致的概念模型,之后才能感到容易。

软件很少有绝对紧凑的,但是很多软件是相对紧凑的,它们有一个紧凑的工作集,一个功能子集,可以满足80%甚至更多的专家级用户的日常需求。

举例来说,Unix系统调用API就是紧凑的,而C标准库则不是。Unix工具中make(1)是紧凑的,autoconf(1)和
automake(1)则不是。标记语言中,HTML是紧凑的,而DocBook不是。Man(7)是紧凑的,troff(1)不是。通用语言中,C和
Python是紧凑的,Perl, Java, Emacs Lisp和Shell不是。C++则是"反紧凑"的----该语言的设计者自己都承认,他不指
望有人能够完全理解C++。

不过也不是说不紧凑的设计就是邪恶的或者糟糕的。有些问题域太复杂,紧凑的设计无法实现。这里强调紧凑性,其目的并不是希望读者把它看成是一个
绝对要求,而是像Unix程序员那样,合理对待,尽力实践,决不轻易放弃。


正交性

正交性有助于你将复杂设计紧凑化,在这一点上,它的重要性非常突出。在纯正交的设计中,操作没有副作用,每一个行动只改变一件事情,不影响其它
东西。对于系统中的每一个属性,有且只有一条途径去改变它。

电脑监视器是正交性的良好范例,你可以调明暗而不影响饱和度,色彩平衡的控制也彼此独立。如果不是如此,想象一下你会遇到多大麻烦!

软件中的非正交性设计太多了。举个例子,格式转换函数的作者经常会不假思索地要一个源文件的路径名作为参数,可是输入经常来自一个打开的文件句
柄,如果设计成正交的,则该函数不应该有"打开文件"的副作用,则将来这个函数可以处理来自各种源头的数据流。

Doug McIloy的名言"只做好一件事"经常被认为是关于简单性的箴言,而其实这句话里对于正交性的强调至少是同样分量的。
非正交性的主要问题是副作用扰乱了程序员和用户的思维模型,而且经常被遗忘,带来程度不同的灾难。

Unix API整体上是一个很好的正交设计范例,正因为如此,在其他平台上的C库都尽力模仿它。所以就算你不是Unix程序员,也值得研究
它。

SPOT法则
The Pragmatic Programmer 一书中指出了一类特别重要的正交性,"Don't Repeat Yourself"法
则:任何知识点应当是唯一的,无歧义的,在系统中以确定无疑的方式存在的。在本书里,我遵循Brain Kernighan的建议,把这个法则称为
Single Point Of Truth,或简称SPOT法则。

重复导致不一致,对代码构成潜在的危害。因为如果你要改变重复信息中的一个,就必须记得改变它所有的化身。这体现出你根本没有清晰地组织你的代
码。

软件是多层的
宽泛地说,当你在设计函数或者对象层次结构(hierarchy)时,有两个方向可供选择,而你的选择对于代码的分层(layering)将有
重大的影响。

自顶向下,自底向上
一个方向是自下而上,从问题域中一定会用到具体的操作出发向上----从具体到抽象。举个例子,如果你要为磁盘驱动器开发一个固件
(firmware),则在低层可以有一些操作原语如"磁头移至某物理块","读物理快","写物理快","切换LED"等。

另一个方向是自上而下,从抽象到具体,从最顶层的程序或者逻辑整体描述规范出发向下到个别的操作。比如某人设计一个可以控制不同介质的海量存储
器控制器,可以从抽象的操作出发,比如"寻址逻辑块","读逻辑块","写逻辑块","切换指示设备"。这上面所说的硬件层次的操作很不相同,

一个大一些的例子是Web浏览器。自顶向下的设计从一个规范说明出发----能接受哪些URL类型,能显示哪些图像,对Java和
JavaScript支持如何,等等。与这个自顶向下的视图相对应的实现层是应用的主事件循环。
同时Web浏览器必须要调用大量的专用元操作(primitives)。比如建立网络连接,发送数据,接受响应,比如GUI相关的操作,比如HTML解
析操作。

从哪端开始,这事关重大,因为你的起点很可能对你的终点构成了限制。如果你完全的自顶向下,到一定时候你可能会尴尬地发现,逻辑上所需要的元操
作实际上不能完全实现。如果你完全的自低向上,你会发现自己做了大量与程序无关的事情。

从1960年代起,初级程序员们就被教导说,写程序应该"自顶向下,逐渐细分"。自顶向下在下面三个条件成立的时候,是很好的经验:a. 你可
以事先经确定义程序的需求,b. 在实现过程中,该规范不大可能变化,c. 在最底层,你有充分的自由来选择完成工作的方式。

程序层次越高,这些条件越容易被满足。然而,即使在最高层次的应用程序开发中,这些条件仍然经常不成立。

出入自我保护,程序员试图双管齐下。一方面以自顶向下的应用逻辑表达抽象规范,另一方面用函数和库来归纳领域内的元操作,在高层设计发生变化时
可以复用之。

Unix程序员主要做系统程序设计,所以倾向于自底向上的开发方式。

一般来说,自底向上的开发更有吸引力,它使你以一种探索的方式开发,给你相对充裕的时间去细化含糊的规范,也更加符合程序员天生的懒惰----一旦
出错,报废的代码通常要少得多。

不过实际的代码一般是自顶向下和自底向上向结合的。两者经常在一个项目中运用,这直接导致了胶合层(glue layer)的出现。

red...@gmail.com

unread,
Sep 16, 2007, 8:50:48 AM9/16/07
to pon...@googlegroups.com
你的这个问题, 咳咳, 正是软件设计领域中最大的问题啊. 模块之间的关系/或者
是类之间的关系, 怎么组织怎么做, 是影响最大, 成败攸关, 咳咳, 谁能够回答
的, 站到讲台上发言去, 大家洗耳恭听ing.


Huafeng Mo 写道:
> 这是否意味着,应该将业务实体做成尽可能小的对象,然后用形同算法的自由函
> 数,或类函数的东西操作、组合这些对象。因为自由函数之类可以泛化,并且很
> 容易地替换,以此大幅提高灵活性,简化开发。
> 我的理解没错吧。:)
>

Huafeng Mo

unread,
Sep 16, 2007, 8:58:47 AM9/16/07
to pon...@googlegroups.com
这个说法我最初是在Sutter的Exceptional C++ Style里看到的,最后四条。

On 9/16/07, red...@gmail.com < red...@gmail.com> wrote:

pongba

unread,
Sep 16, 2007, 9:11:10 AM9/16/07
to pon...@googlegroups.com
实例代码、实例代码,我要实例代码啊,咳咳..

On 9/16/07, redsea <red...@gmail.com> wrote:

Huafeng Mo

unread,
Sep 16, 2007, 9:16:19 AM9/16/07
to pon...@googlegroups.com
这个么,容我做梦时候想想。:)

red...@gmail.com

unread,
Sep 16, 2007, 9:27:22 AM9/16/07
to pon...@googlegroups.com
pongba 写道:
> 实例代码、实例代码,我要实例代码啊,咳咳..
这么想要, 我给一大堆给你, 包括 ifdef 都不删掉, 誓要看得你发晕.

例子是我以前写的一个 rpc client, server 框架, 不必使用 IDL, 用宏定义好函
数, client 和 server 就可以通信, exception 也可以传递.

定义通信函数的例子如下:

#define bk_server_Functions() \
_DF_BEGIN() \
\
_DF(bksvr, 0, bool, login, _P2(const char * username, const char *
password) ) \
_DF(bksvr, 1, bool, test, _P1(boost::uint16_t v) ) \
_DF(bksvr, 2, void, testv, _P1(boost::uint16_t v) ) \
_DF(bksvr, 3, void, testv1, _P1(boost::uint16_t v) ) \
_DF(bksvr, 4, char *, tests, _P1(const char * name) ) \
\
_DF_END()

#define bk_client_Functions() \
_DF_BEGIN() \
\
_DF(bkclt, 0, char * , get_name, _P0() ) \
_DF(bkclt, 1, void, tests, _P1(const char * name) ) \
\
_DF_END()


客户端代码用法下附, rpc_client 这个模板类就是一个典型的粘合层, 定义等会
再给你一个回帖中给出.

// test.cpp : Defines the entry point for the console application.
//
#pragma warning(disable:4819)

#include "public.h"
#include "npre.h"

#include "rpc_prj_functions.h"

#include <rpc/rpc_client.h>
#include <rpc/rpc_calling_stub.h>
#include <rpc/rpc_dispatcher.h> // client 也提供服务, 需要 dispatcher

#ifndef POSIXTIME_PARSERS_HPP___
#include <boost/date_time/posix_time/time_parsers.hpp>
#endif

#ifndef BOOST_ASIO_BASIC_DEADLINE_TIMER_HPP
#include <boost/asio/deadline_timer.hpp>
#endif

#ifndef BOOST_THREAD_WEK070601_HPP
#include <boost/thread/thread.hpp>
#endif


#define _DEF_STUB_HELPER
#include <rpc/rpc_macro_control.h>

// 定义好服务器的函数, 供 client 调用
bk_server_Functions()


//-----------------------
// client 端的代码
class I_bkclient : public rpc::i_serving_obj
{
public:
//-----------------------------------------
// 这是需要定义的
typedef I_bkclient this_type;

//-----------------------------------------
// 定义 纯虚的 rpc 函数, 以及 lc_xxx 的非虚函数
#define _DEF_INTERFACE_FUNC
#include <rpc/rpc_macro_control.h>
bk_client_Functions()

//-----------------------------------------
// 定义调度函数, 函数定义如下:
// void void call_function(rpc::request * request)

#define _DEF_FUNC_DISPATCH
#include <rpc/rpc_macro_control.h>
bk_client_Functions()
};

class bk_client : public I_bkclient
{
public:
#define _DEF_FUNC_IMPLETEMENT
#include <rpc/rpc_macro_control.h>

_DF(bkclt, 0, char * , get_name, _P0() )
{

}

_DF(bkclt, 1, void, tests, _P1(const char * name) )
{
printf("\ngot server callback");
printf("\nbk_client::tests('%s')", name);
}
};

//------------------------------------
struct io_service_thread
{
io_service_thread(boost::asio::io_service &io_service) :
io_service_(io_service)
{

}

void operator() ()
{
io_service_.run();
LOG_DEBUG((L_INFO, "io_service terminated" ));
}

private:
boost::asio::io_service &io_service_;
};


void timer_handler(const boost::asio::error& error)
{
if (!error)
{
// Timer expired.
}
}

bool init_log()
{
return true;
}


int main(int argc, char *argv[])
{
init_log();

boost::asio::io_service io_service;

// 设置一个 timer, io_service 就不会因为没有活动 connection 就退出了.
boost::asio::deadline_timer timer(io_service);

//boost::asio::deadline_timer timer(io_service,
boost::posix_time::time_from_string("2100-12-07 23:59:59.000"));

// 100 年
timer.expires_from_now(boost::posix_time::hours(24*30*12*100));


// Start an asynchronous wait.
timer.async_wait(timer_handler);


//---------------------------
// 初始化 client
bk_client client_serving_obj;
rpc::simple_service_dispatcher server_dispatcher(client_serving_obj);

rpc::rpc_client<rpc::simple_service_dispatcher> client(io_service,
server_dispatcher);

//---------------------------
// 启动另外一个线程执行 io_serivce
io_service_thread iothread(io_service);
boost::thread thread(iothread);

//---------------------------
{
rpc::connection::scoped_lock lock(client);

for (int i=0; i<3000; i++)
client.connect(lock, "localhost", "8000", 5);

while( ! client.if_connected_dirty_get() )
{
putchar('.');
Sleep(10);
}

LOG_DEBUG((L_INFO, "\nclient connected." ));

rpc::block_call_stub bcs(client, true);


try
{
for (int i=0; i<1000; i++)
bool result = bcs.make_call(lock, rpc::bksvr_login, "abcde", "12345");
//bcs.make_call( rpc::bksvr_testv, boost::uint16_t(1));
printf("\nreply got.");
}
catch (std::exception & expt)
{
LOG_DEBUG((L_INFO, "\nmake_call got exception: %s", expt.what() ));
}


//client.connect(lock, "localhost", "8000", 50);

while( ! client.if_connected_dirty_get() )
{
putchar('.');
Sleep(10);
}


try
{
bool result = bcs.make_call(lock, rpc::bksvr_login, "abcde", "12345");
//bcs.make_call( rpc::bksvr_testv, boost::uint16_t(1));
printf("\nreply got.");
}
catch (std::exception & expt)
{
LOG_DEBUG((L_INFO, "\nmake_call got exception: %s", expt.what() ));
}

try
{
char * res_s = bcs.make_call(lock, rpc::bksvr_tests, "testcall");
printf("\n%s", res_s);
}
catch (std::exception & expt)
{
LOG_DEBUG((L_INFO, "\n make_call got exception: %s", expt.what() ));
}
}

printf("\npress enter to quit.\n");
getchar();

io_service.interrupt();
Sleep(100); // 等待 io_service 清理, 否则这里的变量先析构, 就会有问题.
不是正式做法.

return 1;
}


red...@gmail.com

unread,
Sep 16, 2007, 9:29:18 AM9/16/07
to pon...@googlegroups.com
pongba 写道:
> 实例代码、实例代码,我要实例代码啊,咳咳..
不晕? 接着看, rpc_client.h

rpc_client 本身没有多少功能, 只是将相关的类都组织了起来而已, 这是一个胶
合类或者粘合类, 怎么说都好.


// 主动建立 rpc 连接
// 自身就是一个 rpc connection,
#ifndef __RPC_CLIENT_H
#define __RPC_CLIENT_H


#ifndef BOOST_ASIO_IO_SERVICE_HPP
#include <boost/asio/io_service.hpp>
#endif

#ifndef BOOST_ASIO_ARG_HPP
#include <boost/asio/placeholders.hpp>
#endif


#ifndef BOOST_BIND_HPP_INCLUDED
#include <boost/bind.hpp>
#endif


#ifndef __RPC_CONNECTION_H
#include "rpc/rpc_connection.h"
#endif

namespace rpc
{


// 每一个 rpc_client 只能连接一个目标.
template <class dispatcher_t>
class rpc_client: public i_connections_manager, public rpc::connection
{
typedef rpc_client<dispatcher_t> this_type;

public:
rpc_client(boost::asio::io_service& io_service, dispatcher_t &dispatcher):
rpc::connection(io_service, *this, dispatcher), // 有包收到, 会交给
dispatcher_.
dispatcher_(dispatcher)
{
this->add_reference(); // 给自己增加一个 reference, 从而不会被
remove_reference 析构
}


protected:
// i_connections_manager
virtual void connection_lost_connect(rpc::connection & connection, const
std::exception& e)
{
LOG_DEBUG((L_INFO, "\nrpc_client::connection_lost_connect() got
exception: %s", e.what() ));

// 不必做这个, 底层会做.
//net::connection::lock_visitor lv(*this);
}

// 连接要被删除之前, 通知连接管理器
virtual void last_reference_removed(rpc::connection *connection)
{
// 这里的 connection 不是单独申请的, 不必 delete.
// 反倒是可以尝试重新连接.

// 已经增加了一个 reference, 不应该被调用到
BOOST_ASSERT(false);
}


private:
dispatcher_t & dispatcher_;
};


} // namespace rpc

#endif //__RPC_CLIENT_H


redsea

unread,
Sep 16, 2007, 9:34:04 AM9/16/07
to TopLanguage
CAO (恩, 不是骂人, 是一个高管).

居然空格都没有了, 到 VS 2005 里面格式化一下再看吧.

lijie

unread,
Sep 16, 2007, 10:14:26 AM9/16/07
to pon...@googlegroups.com
OO的接口耦合可以看成是一组函数签名+一个类型的耦合;GP比较宽泛,不太好说,从concept来说应该是基于函数名称和函数签名的耦合。委托、自由函数去掉了名称和类型的耦合,只剩下函数签名,这是个进步。以前用python时,有一段时间热衷于使用类似委托的耦合方式,后来用C++也自己做了委托类,但后来还是回到接口了。为什么选择接口?接口可以看作是几个委托的组合,很多时候我们都需要同时绑定一组委托,所以使用接口也是很自然的事,委托这种C++编译器不提供的东西,自己写着玩玩可以,要让项目团队接受就有难度。

纯GP或纯OO应用我感觉比较少了,只是偏向哪边的问题。我偏向OO是因为它的二进制接口,在项目组里要维护一套基础库,什么时候发现BUG都有可能要升级,如果每次升级都让使用者重新编译部署,这个工作量很大的(虽然目前是以静态库提供,还是免不了要重新编译,但至少留有余地)。OO就比较简单些,如果是提供动态库,只需要分发二进制库就可以了,接口不变就不会影响使用者。GP也有应用,但主要是一些工具类,只在实现代码里使用的这种,通常不会让它出现在函数参数或是其它一些影响二进制复用的地方。

下面有讨论到胶合层,GP和OO都离不了这一层,前面说哪个stack类没有实现IStack的问题,在OO里的解决办法就是外包一个类作接口耦合,GP的办法不好说,我感觉你这里提到的都是委托的功能了,并不是GP提供的吧,或许委托也可以看成一种"泛"型?到底把GP用在哪些能影响架构的地方?我也想看看例子。

在07-9-16, red...@gmail.com <red...@gmail.com> 写道:

redsea

unread,
Sep 16, 2007, 11:04:27 AM9/16/07
to TopLanguage
我也认可你的总结.

GP 我没有试过用作程序级别的 API, 我只是在模块内部使用较多, 所以不怎么影响程序的整体架构.

至于stack 类那种, 功能比较通用的类, 一般我都是使用 GP 来做, 不用 OO.

使用 free function/delegate 这种, 好处是隔离得比较彻底, 坏处是有时候要准备个数较多, 不过一般都尽量控制住数目,
只要可能, 我还是用这种 ---- 增加数目有点麻烦, 似乎可以抑制一些增加 api 数目的冲动 :)


On Sep 16, 10:14 pm, lijie <cpun...@gmail.com> wrote:
> OO的接口耦合可以看成是一组函数签名+一个类型的耦合;GP比较宽泛,不太好说,从concept来说应该是基于函数名称和函数签名的耦合。委托、自由函数去 掉了名称和类型的耦合,只剩下函数签名,这是个进步。以前用python时,有一段时间热衷于使用类似委托的耦合方式,后来用C++也自己做了委托类,但后来还 是回到接口了。为什么选择接口?接口可以看作是几个委托的组合,很多时候我们都需要同时绑定一组委托,所以使用接口也是很自然的事,委托这种C++编译器不提供 的东西,自己写着玩玩可以,要让项目团队接受就有难度。

> 纯GP或纯OO应用我感觉比较少了,只是偏向哪边的问题。我偏向OO是因为它的二进制接口,在项目组里要维护一套基础库,什么时候发现BUG都有可能要升级,如 果每次升级都让使用者重新编译部署,这个工作量很大的(虽然目前是以静态库提供,还是免不了要重新编译,但至少留有余地)。OO就比较简单些,如果是提供动态库 ,只需要分发二进制库就可以了,接口不变就不会影响使用者。GP也有应用,但主要是一些工具类,只在实现代码里使用的这种,通常不会让它出现在函数参数或是其它 一些影响二进制复用的地方。
>
> 下面有讨论到胶合层,GP和OO都离不了这一层,前面说哪个stack类没有实现IStack的问题,在OO里的解决办法就是外包一个类作接口耦合,GP的办法 不好说,我感觉你这里提到的都是委托的功能了,并不是GP提供的吧,或许委托也可以看成一种"泛"型?到底把GP用在哪些能影响架构的地方?我也想看看例子。

pongba

unread,
Sep 16, 2007, 11:12:19 AM9/16/07
to pon...@googlegroups.com
On 9/16/07, lijie <cpu...@gmail.com> wrote:
OO的接口耦合可以看成是一组函数签名+一个类型的耦合;GP比较宽泛,不太好说,从concept来说应该是基于函数名称和函数签名的耦合。委托、自由函数去掉了名称和类型的耦合,只剩下函数签名,这是个进步。以前用python时,有一段时间热衷于使用类似委托的耦合方式,后来用C++也自己做了委托类,但后来还是回到接口了。为什么选择接口?接口可以看作是几个委托的组合,很多时候我们都需要同时绑定一组委托,所以使用接口也是很自然的事,委托这种C++编译器不提供的东西,自己写着玩玩可以,要让项目团队接受就有难度。

这个总结漂亮:-)

pongba

unread,
Sep 16, 2007, 11:13:34 AM9/16/07
to pon...@googlegroups.com
晕了晕了~~啊哈哈

On 9/16/07, red...@gmail.com <red...@gmail.com> wrote:

Atry

unread,
Sep 16, 2007, 12:31:16 PM9/16/07
to pon...@googlegroups.com
其实 Eric 说得对,关键问题是接口粒度问题。一个模块应该有多大?层次过多导致复杂度增加都是因为接口分得太细的缘故。这个接口划分,其实就是一个重复代码和接口复杂度的妥协。粒度越细,接口复杂度就越高,重复代码就越少。反之亦然。重复代码代表实现代码的数量,而接口复杂度则代表接口代码的数量,而我们需要做的就是求二者之和的最小值。

在07-9-16, pongba <pon...@gmail.com> 写道:

Huafeng Mo

unread,
Sep 16, 2007, 10:32:37 PM9/16/07
to pon...@googlegroups.com

我"编"了一个简单的案例,来源于我们的一些项目。故事很简单,一个帐务系统。搞过帐务系统或者学过财务的都应该知道,帐务系统里最核心的是科目。在中国,科目是分级的,(外国人好像没有法定的分级体系),一般有4 5级,多的有78 级,甚至10多级。不管怎么分,科目的结构是树。

在科目上有一个操作称为"汇总",就是把子科目的金额累加起来,作为本科目的金额。这实际上是对指定科目的所有下级科目的遍历汇总。这是一个非常简单,但却非常重要的帐务操作。

我尝试着用两种设计实现这种操作。一种以GP为核心,另一种以OOP 为核心。从中,可以比较出两者的差异。

先看GP

首先我定义了一个类,Account,代表科目:

class Account

{

public:

   typedef vector<Account> child_vect;

pubilc:

   child_vect children();  //子级科目

   int account_type();         //本科目类型

   float ammount();            //本科目的凭证金额累计,非最明细科目返回0

...

}

为了简便,我没有考虑科目的贷方金额、借方金额接待方向等。如需考虑,也很容易扩展出去。然后,我利用一个简单的函数模板执行上述汇总:

template<typename T, typename Pred, typename A>

float collect(T& item, Pred filter, A ammount) {

   if(filter(item)==false)

       return  0;

 

   float result(0);

   T::child_vect& children=item.children();

   if(children.size==0)        //最明细科目,没有子科目

       return  ammount(item.ammount);

 

   T::child_vect::iterator ib(children.begin()), ie(children.end());

   for(; ib!=ie; ++ib)

   {

       result+=collect(*ib, filter, ammount);

   }

}

为了简单起见,我直接做了泛化版的算法,不绕弯子了。参数filter用来过滤某些不用参与运算的科目,参数 ammount是一个"可调用物",用来适配item上的接口,一会儿会看到它的作用。

另外,还需要做一个辅助的functor

template<typename T>

struct all_true

{

   bool operator(T& item) {

       return  true;

   }

};

使用的时候非常简单:

Account& acc=AccountCenter.getAccount("...");

float result=collect(acc, all_true<Account>(),

                       mem_fun_ref(&Account::ammount));

相应的OOP版一般会做成这样(很多实际项目就是这么做的):

class Account

{

public:

   child_vect children();  //子级科目

   int account_type();         //本科目类型

   float ammount() {           //此成员直接执行子级科目的汇总任务

   float result(0);

   if(children.size==0)    //最明细科目,没有子科目

       return  m_ammount;  //或从其他途径获得,如数据库访问

 

   T::child_vect& children=item.children();

   T::child_vect::iterator ib(children.begin()), ie(children.end());

   for(; ib!=ie; ++ib)

   {

       result+=ib->ammount();

   }

   }

}

使用起来更简单:

Account& acc=AccountCenter.getAccount("...");

float result=acc.ammount();

代码逻辑没有什么差别,OOP代码更少,使用上OOP 更简单。到目前为止,的确如此。但接下来的情况就不同了。

假设现实世界的古怪客户,使我们面临一个挑战:他们的业务模型中,有部分科目不参与汇总计算,是一群特殊的科目。(这种科目我还真见过)。

于是,代码需要修改。但GPOOP 的改法完全不同。

OOP必须修改Account类,在 ammount()成员中添加一个判别:

if(g_SpecialAccounts.IsSpecial(account_type()) //假设SpecialAccounts是个

                                           // Singleton,负责管理特殊科目

   return  0;

请注意,从这里开始,AccountSpecialAccounts 已经绑在了一起。

那么GP呢?很简单,不用改任何类代码:

float result= collect(acc,

                       mem_fun_ref(&SpecialAccounts::IsSpecial),

                       mem_fun_ref(&Account::ammount));

主体代码没有任何变化,只是使用时做些变化而已。如果有了lambda,就更简便了。

如果这还算不上很大的差别的话,那么下一个挑战则足以把它们拉开距离:我们如果注意的话,MIS系统中有很多地方同科目有着相同的逻辑结构。比如,销售部门的分销组织机构,一个企业的部门组织机构。在这些结构上,通常也会发生汇总操作,比如某个省的分销商业绩汇总,或者某个部门的电费汇总。

于是,充满优化意识的程序员,会想到复用在帐务系统上已有的成果。假设我们定义了部门类:

class Department

{

public:

   typedef vector<Account> child_vect;

public:

   child_vect Children();

   int dept_type();

   float elec_fee();

   float water_fee();

...

};

由于collect<>是泛化的,独立的,只需在使用时稍加变化即可:

float result=collect(dept1, all_true<Department>(),

                       mem_fun_ref(&Department::elec_fee));

就这么简单。如果有哪一类鸟部门不算电费的,只需把filter的实参改掉即可。如果要统计的不是电费,是水费,只需把最后一个实参换掉即可。其他的业务逻辑,只要是棵树,需要汇总数据的,都可以简单地换掉几个参数解决问题。

实际上,这里把算法也拆开成部件,需要时进行不同的组装。这才真正做到"All for one, one for all"。这些思想来源于Stepanovstl上的杰出贡献。

相反,OOP方案,由于elec_fee() 直接返回汇总的电费,所有的汇总实现代码都在类体中间藏着,两个类要么各自写独立的代码,任劳任怨地Do Repeat Yourself(呵呵,也是DRY:) )。结果就是循环敲到手指头痛。

要么把算法分离出来,做成独立的类,通过统一的接口调用:

class Account

{

public:

   Account(ICollect& collect): m_collect(collect){...}

   float ammount() {

       return m_collect.cacul(*this);

   }

...

private:

   ICollect    m_collect;

};

class Deptmant

{

public:

   Account(ICollect& collect):m_collect(collect){...}

   float elec_fee() {

       return  m_collect.cacul(*this);

   }

private:

   ICollect& m_collect;

};

//计算接口和实现基类

struct ICollect

{

   virtual float cacul(???)=0;

};

class CollectAccount : ICollect

{

public:

   virtual float cacul(???) {...};

};

class CollectDeptmant : ICollect

{

public:

   virtual float cacul(???) {...};

};

破绽,破绽!???该填什么?只能填一个类型。所以必须使Account Deptmant拥有相同的数据交换接口。好吧,再做一个接口:

struct IData

{

   virtual float value()=0;

};

AccountDeptmant都实现这个接口。用 value返回需要的数值。但是如何同时又能计算电费,又能计算水费呢?给这些成员编号,用代号调用:value(1)

最后一招:reflect。没错,reflect 可以解决这个问题。不同的计算枚举出不同的成员函数。不过撇开性能不谈,光折腾那些meta就够让人倒胃口了。

现在,我们便可以真切地看到"耦合是怎样炼成的"。八杆子打不到的两个类Account Department,为了共享一个算法,被绑在了ICollect(以及IData)上。如果还有其他类型的算法呢?继续加接口,还是做一个完全通用的接口(暂且不管他是否做得出来)?或者在 AccountDepartment下面再加一个中间层,处理抽象算法问题?(肥厚的中间层?)

方案一个比一个复杂。很多程序员(也包括我)在这种情况下,宁愿手指头痛,也不愿头痛。这也就是为什么热衷于OO 的程序员经常在Do Repeat Yourself

On 9/16/07, pongba <pon...@gmail.com> wrote:

lijie

unread,
Sep 16, 2007, 11:08:41 PM9/16/07
to pon...@googlegroups.com
看得累,不过总算是看完了。


我怎么感觉是OO和FP的区别而不是GP?难道GP在C++里就是FP?OO也可以和FP一起用嘛,并不是OO就要用得那么难看地。可以测试一下OO和FP一起的效果吗?把collect改成这样:

float collect(ICollect* pcollect, IFilter* pfilter, IGetFloatMemberValue*);

然后Account还是原来的Account,Department还是不变,把算法参数抽出来实现接口就行了。如果语言支持委托,还可以直接使用委托,我感觉最终不是OO和GP的差异。你这个例子使用GP也还是要写胶合代码,用OO也要写,最终的差异在哪?无非是GP更容易内联,效率高,但OO的接口更容易确定,更适合团队开发。

这个例子用FP就很简单,使用委托和lambda也很简单,胶合代码更少,可以试一下D语言版本。。。

在07-9-17,Huafeng Mo <longsh...@gmail.com> 写道:

Huafeng Mo

unread,
Sep 17, 2007, 12:08:16 AM9/17/07
to pon...@googlegroups.com
可以先试着实现一下float collect(ICollect* pcollect, IFilter* pfilter, IGetFloatMemberValue*);
然后便会发现问题。第一个参数应该是IData,或者ITree。于是,Account和Department必须实现这个接口。而这个接口也必须体现两个类的共有特性。这样,两个类就被耦合了。然后,IFilter和IGetFloatMemberValue都必须分别针对不同的类加以实现。由于接口中参数类型必须唯一(相对于不同的实现),而且对于不同的类型可能有完全不同的类型需求,那么为了适应不同的情况,以及未来可能的需求,这些接口的定义将会变得非常困难。
没错,这里看上去似乎是OO和FP的区别。但是,如果没有泛型的能力,无论是否是FP,都无法解决类的耦合的问题。GP实际上不单纯是OOP的延伸。GP更多的是OOP和FP的共同延伸。既可以为OO提供好处,也可以为FP提供优势。

yq chen

unread,
Sep 17, 2007, 12:58:46 AM9/17/07
to pon...@googlegroups.com
不错,GP在类型耦合上天生就是有优势的。
 
在编码的时候,GP实现的组件对其客户类型并无签名方面的要求,只要符合某一些规则(在C++加现在是文档约定俗成的,将来可能有concept来强制)就好,只要客户类型符合该组件中使用的泛型类型就成,而不需要必须是某个类型,不需要你先知先验。
 
而OO则不然,他们天生就是依赖于客户的类型,算法天生就和他使用的类型高耦合,因为OOP首要的目标就是将各种概念和一些含混不清的东西塑造成各种不同的强类型,然后在各种强类型之间发送消息。为了减轻这种由于类型带来的强耦合,基本上所有的OO语言都疯狂的使用interface和抽象类,系统中充满了不是迂回、复杂、杂乱无章的只是为了减少耦合的东西。
 
但不是所有的人都聪明到能从一开始就理清将要实现的系统的所有的类型,能看清楚系统的所有耦合,进而为系统的所有耦合都定义适当职责的接口,能看懂这些的更是少数。所以有了面向对象的设计模式,用来指导人们遇到一个问题,该如何考虑;有了重构来帮助人们挽救由于事先对形势判断失误造成的错误。然而前者太抽象,没有多年的经验是不可能知道系统耦合的泥潭在哪里的,后者只是亡羊补牢而已,造成的时间损失和项目滞后是补不回来的。
 
这个就是我认为的OO薄弱的地方,也是GP相对于OO更优的地方。
个人愚见,OO必须要和GP一起使用才能够到达理想的效果。与其这样使用OO,不如大量使用OB,然后加上GP式的自由函数,少量使用OO,将会更轻量和快速的构造系统。
 
方案一个比一个复杂。很多程序员(也包括我)在这种情况下,宁愿手指头痛,也不愿头痛。这也就是为什么热衷于OO 的程序员经常在 Do Repeat Yourself

Huafeng Mo

unread,
Sep 17, 2007, 1:35:09 AM9/17/07
to pon...@googlegroups.com
嗯,我也这么想。看来我们都是一条战线上的。:)
如果彻底排斥OO,就应该连类也不用。这当然肯定是错误的。类的封装性和RAII等机制很好的简化了系统的结构。比如,Account类本身代表了科目的抽象,其逻辑结构是树。但实际上并没有哪个帐务系统用树形结构存储科目,通常都在数据库表中。于是,Account类肩负了将物理数据结构转换为逻辑数据结构的重任。
其实,真正应该限制的是过度使用OOP。更精确地讲,是动多态。而OO或者OOP的其他方面通常都不会有太多的问题。当然除了那些动辄几百个成员函数的封装。(我的一个同事告诉我,他一个类就有20000多行代码)。
尽管我做出了这个案例,但是还是没有完全理清楚导致OO风格和GP风格代码间的差异的原因。现在我的想法是这样:抛开深奥的OO理论不谈,我把焦点集中在类型上。由于我们使用的通常是静态语言,一个非泛化(非模板)的函数,必须有精确的类型定义----参数、返回值等等。假设这个函数是针对一类问题的一个算法,如树的遍历汇总。而这个函数即将面对的类型符合一定的特征,但并不完全一样。于是,为了使用这个函数,必须将所有的类型归一化,变成一个类型。OOP常用的手段便是动多态,或接口。另一种补充手段是适配器,但也必须实现同样的接口。因此,所有相关的类型通过实现统一的接口,来适应这个函数。由于接口本身也是一种类型,必须有固定的成员和签名,是死的。要么只能定义一个完全开放和自由的接口,如reflect,要么定义出来的接口无法适应变化。
GP的情况恰好相反,不是让类型来适应函数,而是让函数来适应类型:找出所面对的类型的共有特征,比如树形结构,然后面向这种特征编写代码,也就是面向一类问题编写代码。更进一步,为了提高算法的灵活性了,把函数中部分可能发生结构性变化代码(如被调用的成员函数前面不同),抽取出来,以参数的形式组件化。通过这种形式,尽可能多地让代码得到复用。
一旦泛化,算法对于类型不再非常敏感,也就无需通过被操作类型的归一化实现算法的通用了。

lijie

unread,
Sep 17, 2007, 1:49:49 AM9/17/07
to pon...@googlegroups.com
打了一大堆回复,突然发现被你这个例子糊弄了。。。

这个例子显然就是为GP设计的,但从实现上来看,它也并不是很优美,甚至可以和宏相提并论了。它严重依赖于函数名字,可以称为"函数名字接口协议"(这不就是concept嘛),这和接口相比优势在哪?一个算法专为某接口设计和专为一组函数名设计,耦合都是很强的。不同的是接口不吻合时可以简单地写一个包装类来适配接口,名字不匹配就更难处理了。从算法上来说,算法知道类的细节太多了,从这点来说collect函数更像是胶合代码;但是它假定两个不同类型的一组函数名称、功能、函数签名都是一样的(或自动适配的),并把自己当作一个通用算法来对待(从你演示的代码来看你也是这么想的)这真的是一个"万能"算法吗?既然没有接口约束,怎么能强制用户不把ammount函数写成Ammount?用户本来写得好好的,就因为你突然要实现一个collect,就要求他改变这个函数名或者是添加一个函数?还是说你的类本来就已经考虑了collect的需求(这又和接口类似了)。

这个算法里的遍历应该是由对象的ammount函数提供才对,外面的算法切入细节太多了,无论如何算法不应该假定对象是树结构,如果是这样,实现成ITree不是更好?强行扭成这种"病态"形式,我倒觉得是自由的GP帮了大忙。

Huafeng Mo

unread,
Sep 17, 2007, 2:23:59 AM9/17/07
to pon...@googlegroups.com
这个算法里的遍历应该是由对象的ammount函数提供才对
=====================
呵呵,注意看collect的使用:

float result=collect(dept1, all_true<Department>(), mem_fun_ref(&Department::elec_fee));

Department可没有实现过ammount函数,这里使用了elec_fee()成员。同样可用。这个例子并不是为GP设计的,这个例子是为业务设计的,是我针对一个帐务系统开发中发现的问题的改进方案。只是为了做案例,简化了许多。

这和接口的相比的优势,看我上一个回复。

GP的优势在于,无需为了适应一个算法而让数据对象增加ITree之类的接口。不但可以少打很多字,而且可以少很多"香炉和鬼"。:)

算法永远是少数,而数据类型则会不断的变化。OOP要求将数据类型归一化为一种类型,其工作量是O(n)的,而GP则是通过算法的泛化适应不同的类型,其工作量是分摊O(1)的。:)

yq chen

unread,
Sep 17, 2007, 7:02:39 AM9/17/07
to pon...@googlegroups.com
从经典的OO的职责划分原则来看,好像是应该由ammout函数来实现对对象自己信息的统计才是最合理的。但是跳出纯粹的面向对象的局限来看,这个函数由collect实现才是合理的,因为它降低了整个系统的耦合性。依赖于类型的某个方法的签名要比依赖于真个类型更好,原因也很简单,因为函数签名是更小的粒度。一个不确当的比方就好像你的电脑一样,如果键盘坏了,我买一个键盘就可以了,如果键盘坏了,那么要你换掉整个电脑你必然也不愿意。

其实从面向对象的本质来讲,放在collect函数中实现统计功能也是合理的。面向对象的最初目的是通过将对象的行为和对象的数据进行封装,从而分解(通过将整个系统划分成一个个粒度很小的对象)和降低整个系统的设计、理解、实现(重用)和维护的难度。

但是OOP使用20多年来的经验表明,它只是人们过于完美的一个梦想而已,这其中最重要的一点就是由于强类型之间的耦合问题大大降低了它的分解带来的好处。加上类的职责的划分非常微妙,导致了很多智力不堪重负。二十多年来的实践经验表明,自由函数(带GP)往往更能够复用,在很多方面和以依赖于对象闻名的OO有很强的互补性(其实java和C#这样的标榜纯OO的语言中,也还是有utility方法的,现在更是加上了GP)。

在 07-9-17,Huafeng Mo<longsh...@gmail.com> 写道:

Atry

unread,
Sep 17, 2007, 8:28:25 AM9/17/07
to pon...@googlegroups.com
能不能不要用 OO OB GP FP 这样术语。这些术语没有明确的定义,讨论起来毫无意义。
相比之下,模板元编程这样的术语更具有明确的含义。只不过,没有人会用编译期来运算他真正想要算的东西。模板元编程的作用仅限于给编译器补漏,除了 C++ 这样的半搭子语言以外,就没用了。

我不会说 OO 不好,因为,世界上根本没有一个明确定义的东西叫 OO ,但是我可以明确的说,任何拥有层次的继承都是不好的,反倒是在实现代码里面作为某种语法糖使用的继承才有可能是有用的。不过对于 D 语言,因为有了 with 关键字,继承也没有了当成语法糖使用的动机。
忘了谁说的继承层次超过 3 层就不好了,说这句话的人太厚道。继承超过一层就铁定弊大于利。一级的继承只在当成语法糖使用的时候可能利大于弊。
按我的观点,继承应该作为一种语法糖来使用的话,更明确的说,就是匿名成员变量。
因为把继承当成匿名成员变量了,多重继承就不应该被禁止,因为你不能要求一个类只有一个成员变量。

这里所说的继承仅仅指从一个类派生另一个类,以及从一个接口派生另一个接口。一个类实现一个接口不算我这里所说的继承。

在07-9-17,yq chen <mephist...@gmail.com > 写道:

Atry

unread,
Sep 17, 2007, 9:02:34 AM9/17/07
to pon...@googlegroups.com
还有,纯虚类、 Concept 、动态语言的哈希表,用这三种东西的任何一种来实现多态,从设计角度看都是一回事,区别只在于实现上。 Concept 的运行速度最快,编译速度最慢,修改一处可能导致另一处重新编译的几率最大;动态语言运行速度最慢,编译速度最快,修改一处可能导致另一处重新编译的几率最小;纯虚类则介于二者之间。
对于科目统计那个例子,Concept和纯虚类相比,没有任何优势,纯虚类需要提取的接口,Concept一样需要。但是用模板有一个好处就是可以偷懒不提取那个接口,直接依赖于源码来写。直接依赖于源码来写有好处,省事。尤其是本来重复的代码就不多,为了节省 5 行代码要额外花 50 行来提取一个接口,不划算。我觉得模块尺度如果到数百行才应该提取接口,否则用任何丑陋的技巧混过去都是很好的。
还有就是提取接口的时候,要么不提取接口,要么就一定要提取良好定义的接口,必须要符合正交性,必须要简单。没有良好定义的复杂接口根本不可能起到降低耦合的作用。

在07-9-17,Atry < pop....@gmail.com> 写道:

red...@gmail.com

unread,
Sep 17, 2007, 9:07:43 AM9/17/07
to pon...@googlegroups.com
讨论得很精彩, 讨论的含金量好像越来越高了.

从现实中抽出来的有代表性的例子, 科目汇总我也做过.

现在出差到上海, 等忙完后一定要慢慢细读 ;)

Huafeng Mo 写道:

我"编"了一个简单的案例,来源于我们的一些项目。故事很简单,一个帐务系统。搞过帐务系统或者学 过财务的都应该知道,帐务系统里最核心的是科目。在中国,科目是分级的,(外国人好像没有法定的分级体系),一般有4 5级,多的有78 级,甚至10多 级。不管怎么分,科目的结构是树。


red...@gmail.com

unread,
Sep 17, 2007, 9:10:27 AM9/17/07
to pon...@googlegroups.com
不少时候, 接口细, 未必是为了避免重复代码, 可能是为了以后可能的灵活性, 或
者是避免所谓的 bad smell (重构一书中提得最多的).

但是我自己觉得, 为了以后未必用得上的灵活性提早准备, 除非准备工作确实很
少, 也没有多出抽象层次, 还有点意义, 否则多半是得不偿失.

Atry 写道:

yq chen

unread,
Sep 17, 2007, 10:05:12 AM9/17/07
to pon...@googlegroups.com
"为了以后未必用得上的灵活性提早准备, 除非准备工作确实很
少, 也没有多出抽象层次, 还有点意义, 否则多半是得不偿失。"
 
同意。过犹不及,OO方法开发,尤其java社团好像追求这个灵活性到了一定的境界。夸张点说写一个hello,还要定义5、6个类。所以大师提出了"为今天设计,为明天编码"的敏捷概念,其实就是避免为"虚幻"的好处付钱。
 
个人觉得这中间有一个平衡点,矫枉过正也不好。

 
在07-9-17,red...@gmail.com <red...@gmail.com> 写道:

Huafeng Mo

unread,
Sep 17, 2007, 8:12:18 PM9/17/07
to pon...@googlegroups.com
上海这两天可有超强台风啊,小心哦。

Huafeng Mo

unread,
Sep 17, 2007, 10:18:16 PM9/17/07
to pon...@googlegroups.com
On 9/17/07, lijie <cpu...@gmail.com> wrote:看得累,不过总算是看完了。
我怎么感觉是OO和FP的区别而不是GP?...
===================================
啊,是的,是的。lijie说的没错,这个案例的确混杂着OO和SP的比较。我刚想明白,惭愧惭愧。因为我想到了另一种GP的方案:

//Biz_Alg类包含了若干常用的算法。做成 traits形式,便于未来针对特别情况特化。template<typename T>
struct Biz_Alg
{
   template<typename R, typename C, typename M>
   R collect(C& item, M mem) {
       //执行遍历汇总,调用item子节点的mem成员
   }
};

 //Account类和Department

template<typename Alg=Biz_Alg<Account> >

class Account

{

public:

   float ammount() {

       Alg::collect(*this, &Account::ammount);

   }

};

template<typename Alg= Biz_Alg<Department> >

class Department

{

public:

   float elec_fee() {

       return Alg::collect(*this, &Department::elec_fee);

   }

   int employee_num() {

       return Alg::collect(*this, &Department::emoloyee_num);

   }

  

};

对比原先的GP方案,这种方案则是以OOP 为出发点,利用GP技术使其解耦,并组件化。而原先的方案则是以SP为出发点,利用GP 使其泛化和组件化。也就是说,GP被分成了两种风格:OO风格的GPSP风格的GP。从本质上来说,两者是等价的,都是将算法和数据对象分离。只不过前者是以类为主导,而后者则是以自由函数为主导。两者各有优势:前者集成度高,使用起来简便、清晰,更符合业务逻辑;后者更灵活,耦合度更小,扩展性更强。

根本上而言,OO风格的GP 是以业务逻辑为核心,用类封装具体实现,程序员的注意力焦点都在业务逻辑上。比较适合高层的业务级别开发。而SP风格的GP,以数据操作为核心,由自由函数将算法施加在数据实体上,程序员的注意力都集中在算法和数据本身上。这种风格比较适合底层的系统级开发。

从原先纯OO的方案来看,由于受制于强类型系统的约束,无法很优雅地将算法与数据分离。由此造成越来越严重的耦合。而 GP的引入,则利用其泛型特性,消除了强类型的负面作用,实现了解耦。

同样,在纯SP方案中,算法与数据分离,但却没有完全独立于数据。一个自由函数要么只能针对一种类型(除非放弃类型安全的保证,和代码复用),要么促使数据对象的类型归一化。后者将依旧会引发数据对象间的耦合。但 GP引入后,算法不再针对单一的类型,而是可以面向一族类型。在这种情况下,类型安全得到保证,代码复用得到实现,也无需强制地将不相关的类型归一化。

从这两种实现可以看出,无论是OO还是SP ,都可以通过GP来加以扩展,消除各自的缺陷。也就是"用OOSP 设计,用GP来优化(代码)"。

最后,我这里还有一个方案:

template<typename C, typename R, typename PredR (C::*mem)() >

class collect

{

public:

   collect(C const& item): m_Item(item), m_Mem(mem){}

   R operator() {

       //执行遍历汇总

   }

private:

   C&      m_Item;

   Pred    m_Pred;

   R (C::*m_Mem);

}

//用起来像这样

typedef collect<Account, float, AllTrue<Account>,

               &Account::ammount() > AccountCollect;

typedef collect<Account, int, AllTrue<Department>,

               &Department::employee_num > DeptElecFeeCollet;

//使用SP风格的GP方案中的AccountDepartment的对象,即简单类

float res=AccountCollect(acc1)();

float res=AccountCollect(dept1)();

这种方式看上去有些古怪。实际上使用一个函数对象封装了算法,构成了一个"算法适配器"。collect 是个算法框架,通过typedef将相应的判别式、成员函数,同框架一起组装起来,构成一个类型。之后,直接使用这个类型,适配一个对象,以执行相应的计算。这可以看作是介于 OOP SP风格之间的一种GP方案。它比 OOP风格方案稍微灵活一点点,比 SP风格方案使用上稍微简便一点点。但是,在这个方案能为我们带来多少好处,我不敢说。放在这里,只是为了展示GP 的几种可能的设计方案而已。


lijie

unread,
Sep 18, 2007, 12:42:07 AM9/18/07
to pon...@googlegroups.com


在07-9-18,Huafeng Mo <longsh...@gmail.com> 写道:
On 9/17/07, lijie <cpu...@gmail.com > wrote:看得累,不过总算是看完了。
我怎么感觉是OO和FP的区别而不是GP?...
===================================
啊,是的,是的。lijie说的没错,这个案例的确混杂着OO和SP的比较。我刚想明白,惭愧惭愧。因为我想到了另一种GP的方案:

//Biz_Alg类包含了若干常用的算法。做成 traits形式,便于未来针对特别情况特化。template<typename T>
struct Biz_Alg
{
   template<typename R, typename C, typename M>
   R collect(C& item, M mem) {
       //执行遍历汇总,调用item子节点的mem成员
   }
};

 //Account类和Department

template<typename Alg=Biz_Alg<Account> >

class Account

{

public:

   float ammount() {

       Alg::collect(*this, &Account::ammount);

   }

};

template<typename Alg= Biz_Alg<Department> >

class Department

{

public:

   float elec_fee() {

       return Alg::collect(*this, &Department::elec_fee);

   }

   int employee_num() {

       return Alg::collect(*this, &Department::emoloyee_num);

   }

   ...

};

对比原先的GP方案,这种方案则是以OOP 为出发点,利用GP技术使其解耦,并组件化。而原先的方案则是以SP为出发点,利用GP 使其泛化和组件化。也就是说,GP被分成了两种风格:OO风格的GPSP风格的GP。从本质上来说,两者是等价的,都是将算法和数据对象分离。只不过前者是以类为主导,而后者则是以自由函数为主导。两者各有优势:前者集成度高,使用起来简便、清晰,更符合业务逻辑;后者更灵活,耦合度更小,扩展性更强。

根本上而言,OO风格的GP 是以业务逻辑为核心,用类封装具体实现,程序员的注意力焦点都在业务逻辑上。比较适合高层的业务级别开发。而SP风格的GP,以数据操作为核心,由自由函数将算法施加在数据实体上,程序员的注意力都集中在算法和数据本身上。这种风格比较适合底层的系统级开发。

从原先纯OO的方案来看,由于受制于强类型系统的约束,无法很优雅地将算法与数据分离。由此造成越来越严重的耦合。而 GP的引入,则利用其泛型特性,消除了强类型的负面作用,实现了解耦。

同样,在纯SP方案中,算法与数据分离,但却没有完全独立于数据。一个自由函数要么只能针对一种类型(除非放弃类型安全的保证,和代码复用),要么促使数据对象的类型归一化。后者将依旧会引发数据对象间的耦合。但 GP引入后,算法不再针对单一的类型,而是可以面向一族类型。在这种情况下,类型安全得到保证,代码复用得到实现,也无需强制地将不相关的类型归一化。

从这两种实现可以看出,无论是OO还是SP ,都可以通过GP来加以扩展,消除各自的缺陷。也就是"用OOSP 设计,用GP来优化(代码)"。


昨天又把pongba的concept介绍看了下,它有很多类型、名称的约定,实现类有一个确保兼容的功能(#8(火星人…类型系统问题)这部分)。在concept之前,你这里的GP方案假定的东西太多,虽然使用了一些手段把名称提取出来以减少名称上的耦合(上面的return Alg::collect(*this, &Department::elec_fee);),但算法依旧有明显的假设,比如假定实现类是Tree型并且具有某某成员,或许最终为了通用而实现出迭代器,这个又回到接口的老路上了----为了假设的需求写出不一定有用的接口,好的地方就是没有引入新类型。

接口的问题有几个,一是泛型的问题,二是继承问题,三是引入新类型,的确是不少。我想有没有可能使用concept的方式来实现,它和concept的差别就是一个是静态的,另一个是动态的。比较一下看看:

以下纯属虚构。。。

concept Drawable<typename T>
{
  void T::draw() const;
}

class MyClass
{
  void draw() const;
};

concept_map Drawable<MyClass> { } // 特化

interface IDrawable<typename T>
{
  void draw() const;
}

interface_impl IDrawable<MyClass> {} // 实现接口

IDrawable*指针可以直接调用draw,因为draw不依赖模板类型,如果是调用其它一些依赖类型的操作就需要特殊处理了,但至少别因为一个模板参数不同就互不相认。最后把它统一到一种语法上面去,同时有动态和静态支持,作为concept使用它仅是个静态的检查,把它转换成IDrawable接口时自动加一层包装类就成了。想办法消除这种OO的动态和GP的静态之间的鸿沟,就是因为必须要取舍才有了这些争论,实际上哪一种都不完美。



pongba

unread,
Sep 18, 2007, 1:07:30 AM9/18/07
to pon...@googlegroups.com
定义了concept实质上也就是定义了接口。
我认为GP的好处就是可以延迟定义concept。而用interface的话,要复用性就必须一开始就定义出interfaces来,若一开始不定义interface的话,算法又会耦合于特定的类。GP就好多了,只要把参数类型虚化掉就行了。剩下的后面再说..

昨天又把pongba的concept介绍看了下,它有很多类型、名称的约定,实现类有一个确保兼容的功能(#8(火星人...类型系统问题)这部分)。在concept之前,你这里的GP方案假定的东西太多,虽然使用了一些手段把名称提取出来以减少名称上的耦合(上面的return Alg::collect(*this, &Department::elec_fee);),但算法依旧有明显的假设,比如假定实现类是Tree型并且具有某某成员,或许最终为了通用而实现出迭代器,这个又回到接口的老路上了----为了假设的需求写出不一定有用的接口,好的地方就是没有引入新类型。

接口的问题有几个,一是泛型的问题,二是继承问题,三是引入新类型,的确是不少。我想有没有可能使用concept的方式来实现,它和concept的差别就是一个是静态的,另一个是动态的。比较一下看看:

以下纯属虚构。。。

concept Drawable<typename T>
{
  void T::draw() const;
}

class MyClass
{
  void draw() const;
};

concept_map Drawable<MyClass> { } // 特化

interface IDrawable<typename T>
{
  void draw() const;
}

interface_impl IDrawable<MyClass> {} // 实现接口

IDrawable*指针可以直接调用draw,因为draw不依赖模板类型,如果是调用其它一些依赖类型的操作就需要特殊处理了,但至少别因为一个模板参数不同就互不相认。最后把它统一到一种语法上面去,同时有动态和静态支持,作为concept使用它仅是个静态的检查,把它转换成IDrawable接口时自动加一层包装类就成了。想办法消除这种OO的动态和GP的静态之间的鸿沟,就是因为必须要取舍才有了这些争论,实际上哪一种都不完美。

Huafeng Mo

unread,
Sep 18, 2007, 1:37:34 AM9/18/07
to pon...@googlegroups.com
不错,假设是不可避免的。即便是concept也存在假设。在类型的特征检验方面,是否使用concept并没有本质的差别。因为即使不使用concept,编译器也可以在编译整个模板的时候发现不匹配的情况。比如把一个没有children()成员的类型实例作为实参调用collect函数模板,那么编译器便会报错,缺少成员函数children()。而是用了concept只是把报错的时间提前了,在一开始调用的时候,编译器就发现参数不符合concept的要求,而不需要等到编译到函数模板内部。
这里存在的假定,无论是否存在concept,都是要求这个类型在语义上符合树的特征。concept(或者无concept的情况下编译器的检测)只检测类型是否有一个children()成员,并且这个成员返回vector<T>。但对于children()如何完成这种操作,只能假定。假定children()返回的vector的确包含了子对象,而不是父对象或其他七大姑八大姨的东西。
对于interface而言,也存在着这种假设。interface只能保证一个实现它的类型能够有一组要求的接口,但对接口中的成员实际的行为,只能提出要求,而无法强制。
=================================
concept Drawable<typename T>
{
  void T::draw() const;
}

class MyClass
{
  void draw() const;
};

concept_map Drawable<MyClass> { } // 特化
=========================
代码到这里,实际上已经明确MyClass是一个符合Drawable concept的类型。对于泛型算法而言,就已经足够了。
=============================
interface IDrawable<typename T>
{
  void draw() const;
}

interface_impl IDrawable<MyClass> {} // 实现接口
=====================================
而附加了这一部分,则表明,MyClass可能还需要参与到一个非泛型的、纯OOP算法或类型上。因为这些算法无法使用concept。他们只识别固定的类型,而接口IDrawable则是一个单一类型,可以被非泛型算法所使用。
如果所有算法都是非泛型的,那么前面的concept是毫无用处的。如果算法是泛型的,那么后面的IDrawable也是没有意义的。尽管实现了IDrawable的类型也符合concept Drawable的要求,也可以参与到泛型算法中去。

这里有个关键的差别,接口是一种类型,泛型(无论是否带有concept)描述了一组类型。当使用接口时,类型必须实现接口,让自己成为这种类型。这就是一个中间层,以接口作为转换到单一类型的媒介。
而泛型则相反,泛型不是类型,也不需要实现。任何符合泛型要求的类型都可以直接认为是可用的。这样就不需要中间层了。
做一个不太恰当的比方。接口要求一个人长得像卓别林,并且是他的后代,才能演卓别林。而泛型则只要求一个人长得像卓别林,就可以演卓别林。

接口的作用在于动态决断的多态。比如,在绘图系统中,用户绘制一个图形。那么这个图形就必须是运行时生成的。此时,只有OOP的动多态是可以使用的。因为编译已经结束,所有类型已经固定,任何运行时产生的实例,如果需要多态,只能归一化为一种在编译前指定的类型。而只有OOP具备这种能力。

oldrev

unread,
Sep 18, 2007, 2:37:48 AM9/18/07
to pon...@googlegroups.com

一不小心这个讨论已经相当深入了啊,呵呵。

我个人的看法是假如我们的系统分上中下三层的话,GP适用于上层和下层,而 OO 适用于中层。

举个例子,考虑一个用 C++ 实现的 3D 游戏引擎,底层的算法和数据结构用 GP 实现,通用而高效;中层主要使用 OO 封装,容易设计和理解;高层用 GP
实现各种 design patterns,将整个系统组织起来,起到了粘合剂的作用。这样一个系统,底层和中层的代码极具复用性,且也是代码最多的,高层处理最为
困难的设计问题,代码最少,就算是有设计错误,修改甚至重写的代价也不大,类似 C + 脚本的实现方式,可以自底向上的开发。

yq chen

unread,
Sep 18, 2007, 7:19:56 AM9/18/07
to pon...@googlegroups.com
抛开设计阶段需要的大量权衡和脑力劳动不说,在编码阶段,OO有时候未免也让人徒生感叹。一个没有实际价值的例子,但真实存在的例子,往往象下面的代码:
 
struct ILoadable
{
....
};
struct IWalkable
{
virtual walk() = 0;
};
 
void f( IWalkable w )
{
...
}
 
class Something : public IWalkable, ILoadable
{
...
}
 
ILoadable loadObjectFromStream(...)
{
      new Something();
}
 
int main()
{
    ILoadable o = loadObjectFromStream(...);
 
    if( o is IWalkable )
    {
       IWalkable w = o as IWalkable;
       f(w);
    }
 
    return 0;
}
 
上面的代码比较牵强,但是我们经常可以看到一个实现了多个interface的对象在为了降低耦合而采用interface编程的实践面前,不得不一次又一次的向系统询问自己的身份,而GP中我们大多数情况不需要这样。
 
编码实践过程的现状反映了GP约束和OO约束是大为迥异的两个方面。GP的约束不是通过类型,而是通过功能对客户对象进行约束。但是OO则相反,你有这个功能没有用,首先你要能证明你的身份,或许你的身份本身就符合要求,但是先要你父母来确认一下。它们这种约束理念的不同,同样造成了在设计思想和设计行为方面的不同。
 
OO要求整个类型都要达到他的规定,而GP只要求某个行为将达到它的规定。
 
可能考虑到一个接口实现得太大,客户对象要全部符合它的要求难度太大(块头太大了,不利于复用),OO实践建议将接口设计得小小的,但是又不能太小(有难度),最好正交(更加有难度)。虽然小小的,又正交的接口,确实是解决耦合的良药,但是由于OO本身是基于类型系统的依赖,在实践中它往往又阻碍了这种行为,使得各种类型转换成为必须的手段。而类型转换是某个类(因为其方法),往往又和多个接口扯不清关系。
 
GP也鼓励正交功能的分解,但是它不需要依赖于类型的耦合,所以基于正交功能的分解在GP中的实践要比OO中的用户体验要好。
 
在07-9-18,oldrev <old...@gmail.com> 写道:

redsea

unread,
Sep 18, 2007, 8:34:34 AM9/18/07
to TopLanguage
不知道有人做过 delphi 编程没有, delphi 的编程是 PME, property, method, event, 其中的
event 其实就是对象里面有一个 delegate 变量, 让关联对象可以赋值 hook 到上面 (熟悉 QT 的朋友可以相象为退缩的
signal, slot 机制).

delphi 用于界面编程还是很方便的, 这个 PME 的机制带来的好处非常大, 这个比起 Windows message 机制易用, 类型
安全, 没有 interface 那种高耦合和笨重.

event, singal/slot 机制在适用的场合使用起来还是很方便的.

相比之下, OO 的 interface 方法能够适应的范围更广, 但是在其他方法能够适用的场合, 这个方式就显得笨重和高耦合了. 这是
不是和 C++ 语言的处境也很象? 很多地方都能够使用, 但是, 在有合适替代品的时候, 这就很有可能就不是最佳选择.

Atry

unread,
Sep 18, 2007, 9:09:55 AM9/18/07
to pon...@googlegroups.com
这个例子是蠢人才会写出来的,正确的 OO 肯定可以避免这种愚蠢的代码。即使是我这么笨的人都可以避免这么蠢的代码。

在07-9-18,yq chen <mephist...@gmail.com> 写道:

lijie

unread,
Sep 18, 2007, 9:14:50 AM9/18/07
to pon...@googlegroups.com
这个例子实际上是COM里面普遍存在的方式,也没什么不妥。。

这个例子也很好反驳,用GP写出这个功能的也是很困难嘛。。

在07-9-18,Atry <pop....@gmail.com> 写道:

yq chen

unread,
Sep 18, 2007, 9:19:16 AM9/18/07
to pon...@googlegroups.com
不可能全部避免的。拿出100个人的java程序,有90个存在或多或少转型的问题(保守),这其中有80个人可能是设计不好。
 
造成这种现象的根本原因是OO的本质,当然你不细化设计这种转型为少得多,但是会有其它问题。
 
我这个例子是太明显了,只是为了说事。纯OO中,复杂的项目中这种转型难以避免。

 
在07-9-18,Atry <pop....@gmail.com> 写道:

Atry

unread,
Sep 18, 2007, 10:39:11 AM9/18/07
to pon...@googlegroups.com
哈哈,没错,这个例子正是我用 COM 的时候天天骂娘的

在07-9-18,lijie <cpu...@gmail.com> 写道:

yq chen

unread,
Sep 18, 2007, 10:45:35 AM9/18/07
to pon...@googlegroups.com
所以说接口正交的系统中,这样的转型难以避免。

 
在07-9-18,Atry <pop....@gmail.com> 写道:

Atry

unread,
Sep 18, 2007, 10:55:35 AM9/18/07
to pon...@googlegroups.com
蠢人无法避免,但是我就能避免。不过我也不知道我的做法算不算你的 OO 的定义。
我的做法我上面说了,就是不用继承来组织代码,只把继承当成语法糖来使。

yq chen

unread,
Sep 18, 2007, 10:59:46 AM9/18/07
to pon...@googlegroups.com
哈哈,那就不算OO了,我一直认为那是OB,因为OO区别与OB的基本就是多态(继承)。

在07-9-18,Atry <pop....@gmail.com> 写道:

Atry

unread,
Sep 18, 2007, 11:08:36 AM9/18/07
to pon...@googlegroups.com
我也用多态啊,不过只会从一个类实现接口,不用让一个公开的类派生于另一个类。

Atry

unread,
Sep 18, 2007, 11:12:17 AM9/18/07
to pon...@googlegroups.com
要处理事件就用 Handler 或者代理回调函数这样的东西,而不允许从功能类派生出来重写虚函数。那太野蛮。

在07-9-18,Atry <pop....@gmail.com> 写道:

red...@gmail.com

unread,
Sep 18, 2007, 11:18:32 AM9/18/07
to pon...@googlegroups.com
动多态的时候, 虚函数避免不了.

此外的多数情形, 是 GP 或者这种方法(delegate) 好过用虚函数.

Atry 写道:

Atry

unread,
Sep 18, 2007, 11:24:54 AM9/18/07
to pon...@googlegroups.com
我没说不用虚函数,我是说不对公开的类进行实现继承。

在07-9-18,red...@gmail.com <red...@gmail.com> 写道:

oldrev

unread,
Sep 18, 2007, 11:42:45 AM9/18/07
to pon...@googlegroups.com
现在很多开源 C 库都流行像 COM 一样在 struct 里放一堆函数指针,写的人痛苦用的人也痛苦啊,呵呵。


oldrev <oldrev(at)gmail(dot)com>
Regards

>>> 引用的文字:
On Tuesday 18 September 2007 23:18:32 red...@gmail.com wrote:
> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
> <html>
> <head>
> <meta content="text/html;charset=GB2312" http-equiv="Content-Type">
> </head>
> <body bgcolor="#ffffff" text="#000000">
> 动多态的时候, 虚函数避免不了.<br>
> <br>
> 此外的多数情形, 是 GP 或者这种方法(delegate) 好过用虚函数.<br>
> <br>
> Atry 写道:
> <blockquote
> cite="mid:fd8b80680709180812n163...@mail.gmail.com"
> type="cite">要处理事件就用 Handler 或者代理回调函数这样的东西,而不允许从功能类派生出来重写虚函数。那太野蛮。<br>
> <br>
> <div><span class="gmail_quote">在07-9-18,<b
> class="gmail_sendername">Atry</b> &lt;<a moz-do-not-send="true"
> href="mailto:pop....@gmail.com">pop....@gmail.com</a>&gt; 写道:</span>
> <blockquote class="gmail_quote"
> style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt
> 0.8ex; padding-left: 1ex;">我 也用多态啊,不过只会从一个类实现接口,不用让一个公开的类派生于另一个类。
> <div><span class="e" id="q_1151929e1168a0dd_1"><br>
> </span></div>
> </blockquote>
> </div>
> </blockquote>
> <br>
> <br>
> > </html>
> <br>


Atry

unread,
Sep 19, 2007, 1:36:44 AM9/19/07
to pon...@googlegroups.com
对有毅力的蠢人来说,不管是任何一种语言,想要搞得很复杂都是做得到的。

在07-9-18,oldrev <old...@gmail.com> 写道:

Jian Wang

unread,
Sep 19, 2007, 10:35:49 AM9/19/07
to pon...@googlegroups.com
OO系统里真的可以避免向下转型吗?只是蠢人才这样做吗?
JDK里有不少地方是传Object的。类似于 用户类-〉框架-〉用户类
的地方很多都是通过Object来传递用户数据,根本无法避免向下转型。难道设计JDK的人都是蠢人吗?


在 07-9-19,Atry<pop....@gmail.com> 写道:

longshanksmo

unread,
Sep 19, 2007, 7:33:29 PM9/19/07
to TopLanguage
当然,JDK的人不能算蠢人。他们迫不得已。因为Java缺乏GP能力,他们只能通过向下转换到Object来获得泛化的效果,以实现尽可能多的代码重
用。如果Java在GP方面的能力能够接近C++的话,就无需那么多"愚蠢"的向下转型了。

On Sep 19, 10:35 pm, "Jian Wang" <oxygen.jian.w...@gmail.com> wrote:
> OO系统里真的可以避免向下转型吗?只是蠢人才这样做吗?
> JDK里有不少地方是传Object的。类似于 用户类-〉框架-〉用户类
> 的地方很多都是通过Object来传递用户数据,根本无法避免向下转型。难道设计JDK的人都是蠢人吗?
>

> 在 07-9-19,Atry<pop.a...@gmail.com> 写道:


>
> > 对有毅力的蠢人来说,不管是任何一种语言,想要搞得很复杂都是做得到的。
>
> > 在07-9-18,oldrev <old...@gmail.com> 写道:
> > > 现在很多开源 C 库都流行像 COM 一样在 struct 里放一堆函数指针,写的人痛苦用的人也痛苦啊,呵呵。
>
> > > oldrev <oldrev(at)gmail(dot)com>
> > > Regards
>
> > > >>> 引用的文字:
> > > On Tuesday 18 September 2007 23:18:32 red...@gmail.com wrote:
> > > > <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
> > > > <html>
> > > > <head>
> > > > <meta content="text/html;charset=GB2312"
> > http-equiv="Content-Type">
> > > > </head>
> > > > <body bgcolor="#ffffff" text="#000000">
> > > > 动多态的时候, 虚函数避免不了.<br>
> > > > <br>
> > > > 此外的多数情形, 是 GP 或者这种方法(delegate) 好过用虚函数.<br>
> > > > <br>
> > > > Atry 写道:
> > > > <blockquote
>

> > cite="mid:fd8b80680709180812n16310b43u1d7ed241a9055...@mail.gmail.com"


> > > > type="cite">要处理事件就用 Handler
> > 或者代理回调函数这样的东西,而不允许从功能类派生出来重写虚函数。那太野蛮。<br>
> > > > <br>
> > > > <div><span class="gmail_quote">在07-9-18,<b
> > > > class="gmail_sendername">Atry</b> &lt;<a
> > moz-do-not-send="true"

> > > > href="mailto: pop.a...@gmail.com">pop.a...@gmail.com</a>&gt; 写道:</span>

Reply all
Reply to author
Forward
0 new messages