关于 getter 和 setter
网友 "sjinny" 在上篇评论里写:
云风对那种所有成员数据都写setter/getter的做法有什么看法吗……这两天试图精简三个太庞大的类,但是单单setter/getter就让接口数目变得非常多了……
我谈谈我的看法吧。
首先,几乎任何设计问题都没有标准答案。如果有,就不需要人来做这件事了。无论多复杂的事情,只要你能定义出精确的解决方案,总可以用机器帮你实现。
下面谈谈我的大体设计原则。记住、一切皆有例外,但这里少谈例外。因为这涉及更复杂的衡量标准。
KISS 当然是首要原则。但有许多诠释角度,每个设计师眼中都有自己的 KISS 原则。
今天的我认为,我们应该尽量少提供新的概念。所以,如果你用 C 就尽量不要用函数指针数组去模拟虚表;如果你用 C++ 就别想着用模板之类的东西弄出个“属性”的概念出来…… 这些语言原本不提供的东西,对于用户(可能是你的队友、可能是今后的你自己,可能是你未来的继任者)就是新的东西。
大部分情况下,设计一个所谓框架,也是新东西。限制用户以一定的规范来编写程序,最合适的是在语言级、而且是大家都熟知的并成熟的语言特性。
我们应该坚信:简洁优良的设计一定是和语言工具无关的。优雅的接口设计,总可以以简单的方式表达出来。
第一件事情,就是寻找你选择的开发语言的惯例。因为,如果一个语言足够成熟,抽象化的需求一定有无数人遇到过,好的方案会经过时间的洗练留下来;不用我们重新发明。setter/getter 这种需求莫如是。
最近几年,我用的比较多的是 C 语言。C 语言的惯例是什么?C 语言因为 Unix 而生,并是 Unix 的原生开发语言。我们从 Unix 的接口中寻找答案。
举个大家都熟悉的例子:getsockopt / setsockopt 。几乎是一样的需求:向一个对象读取或设置某一属性值。
传统上,C 语言构建造的系统中较少为每个属性值分别留下两个接口(读/写)。对一个对象的内部状态的修改,一般会用统一的一对 API 去操控。
少即是多。
第二要点是效率。
不考虑效率的程序员不是好程序员。这是我的个人观点。可能有些老程序员不会同意,他们会苦口婆心的教导新人:性能并不总是那么重要,为了性能,你会失去很多东西,当你剩下了“性能”后,最后,还是会失去它。
在我学会编程的头十年里,我疯狂的追求速度。读了大量的书、写了大量的代码。小心翼翼的优化每处我觉得值得优化的部分,重写再重写。
慢慢的,我学会接受一些东西:
比如相信编译器。
比如别耍小聪明。
比如不要牺牲代码清晰性。
比如防御式编程。
比如先把代码做的可靠。
比如采用时间复杂度更高,但简洁的算法。
……
对于一个性能偏执狂来说,这些浅显的道理接受起来是多么的不容易。
我这里要写的,并不是重复证明这些道理多么的有价值;而是想反过来说,每次采用和性能相违背的方案时,我的内心都会抗拒和怀疑。我依旧认为应该考虑例外情况,从而破坏这些规则。如何判定什么时候该遵循、什么时候该违背。以我目前的水平,无法精确总结。只能靠大量的实践磨练出得感觉了。
同上,如果有精确的准则,我们应该让机器去选择,而不是人。
我敢肯定,无条件相信教条的程序员,不会成长。
相信 C++ 可以取得比 C 更高性能的程序员认为:C++ 语言设施会带来更高的效率。他们最喜欢举的例子是 algorithm::sort() 和 qsort() 的比较。
模板会内嵌比较函数,去掉函数调用之消耗。从而在性能测试中完全击败 qsort() (后者需要为每次比较做一次 C 函数调用)
前两周在有道难题 的决赛颁奖仪式后,我和参赛同学的交流中,我谈到了这个问题。当时,我先讲了另一段:如果你的程序要处理一组数据,是从前往后处理性能高、还是从后往前、还是间隔着处理…… 这其实取决于很多和你的算法关系不大的东西:比如内存控制器的工作方式、CPU Cache 的管理、OS 的虚拟内存调度,等等。
有时候,我们需要关心这些、有时候我们不关心这些。如果想把整个系统做的高效,何时关系,何时不关心,这个决策比如何优化更难,更要功力。对于性能偏执狂来说,影响他决策的才不是哪些重要,哪些不重要;不是把有限的精力投入到关键点的优化中;因为对于他来说,反正重要不重要的都会去优化的,他会无视旁人的嘲笑,做他觉得有兴趣的事情。做的久了,不存在事后优化,因为对他来说,第一次编写时就考虑了种种,而且随着经验的增加,代码可以在保证高效的同时清晰可靠。
但是,我们有时就不去关心那些底层的性能差异。这同样出于性能考虑。因为追究到细节,全体和局部很可能得到相反的结论。因为代码本身也是系统的一部分。就连代码的规模也会影响到机器的运作效率。
当你可以从更高层次来看问题,你就会对性能有更多的理解。我们编写和设计软件,最大的敌人是复杂度。性能的敌人同样也是它。控制软件的每个层次上处理对象的粒度就是减小复杂度的武器。
回到 sort 的问题。函数调用真的是不可饶恕的开销吗?如果你对一个整数数组排序,那么性能考虑就很巨大。对于整数比较操作而言,函数调用,寄存器压栈出栈这些,会有成倍的开销。
但是,除了教科书和考试题中,我们有多少机会对一个整数数组排序?
排序是为了重新组织一组对象(往往这还不是最终目的。比如说,排序只是为了更有效的检索,有效检索才是目的),在一个特定层次上,对象的数量不宜很多,对象的粒度应该保持相当的规模。其结果就是,对对象的比较的开销会大于函数调用。因为涉及对象细节的操作是跨层次的。正如大多数情况下,你不会考虑访问一个内存字节的操作,对于机器意味着什么,OS 怎样调用虚拟内存、CPU 怎样管理 Cache 、内存管理器怎么收发控制信号。跨越层次的数据访问,函数调用是应该被忽略的。
C++ 的 sort 想获得更高的性能,代价是破坏了层次间的封闭性,往往是得不偿失的。
关于函数调用的开销这个问题,和不把性能做为第一位的程序员讲是很容易的;但对于看中性能的程序员来说,其实是很纠结的一件事。
《Unix 编程艺术》在 Part 2 开篇(模块性:保持清晰,保持简洁)就提到了这点。中文版 P84 引用了一段话:
Dennis Ritchie 告诉所有人 C 中的函数调用开销真的很小很小,极力倡导模块化。于是人人都开始编写小函数,搞模块化。然而几年后,我们发现在 PDP-11 中函数调用开销仍然昂贵,而 VAX 代码往往在 “CLASS" 指令上花费掉 50% 的运行时间。 Dennis 对我们撒了谎!但为时已晚,我们已经欲罢不能……
我想说的是,我们得承认一些损失。承认它们最终是为了更好的性能。而不是在同一层次上用语言技巧去抹平它。过度依赖 template inline 这些,并不仅仅是浪费你的时间去等待编译。
舍就是得。
上面写了这么多,只想引出下面这个话题:
有时候,对于宏定义式的属性管理方式,并不满足我们的需求。
C 语言里其实还有另一种惯例:我们可以考察 FILE 的接口。fopen 在打开文件的时候,可以传入多个选项。它是用字符串传入的。每个字符表示了一个属性。这使得使用它们更加友好,并极具灵活性。
还可以看 XLib 的接口设计。虽然不算太好,以至于后来又有人制作 XCB 。但我个人觉得,已经比 Windows 的对应部分设计的好太多了。
Xlib 里,使用联合 + 位域的方式管理对象的内部结构。也是相当不错的。
谈及 GUI ,我推荐大家读读 IUP 的代码,或许你会喜欢上它。在折腾 GUI 的东东时,在 QT/GTK/WsWidgets 等等之外,又可以多一个选择。它的接口设计采用了一些原则,使得足够简洁。而我在经历了太多次的重构 GUI 模块后,才领悟了点点东西。之后,发现了 IUP ,看到了许多我最终认同的东西,感慨颇多。
btw, 真的,boost::python 或是 LuaBind 这样的,利用一大套代码,让机器转换繁杂的接口,从一个语言的接口转换另一个语言的方式,最终都是权益之计。把接口设计简洁方是正道。
这里引出另一种需求,我们需要保留属性的名字信息。这样在分割明确的模块之间使用更加人性。尤其是在需要跨语言使用的时候。这种情况下,我会选择使用 string 做 key 而不是宏定义出来的整数。
这就牵扯到实现的效率问题了。好吧,我们又绕回了性能这个话题。
为此,我 谨慎 的给我们的系统添加了一个新概念:const_string
这个类型(注:在《C语言接口与实现》中,这个东西叫 atom ,这是个贴切的名字)。在我的项目中,反对随意的使用 typedef ,因为那意味着不断的新概念的加入,为此,付出更大的体力代价也是值得的。也就是说,宁可在每个结构和联合前显式的敲上 struct 和 union 。
这个类型其实是个特殊的指针,指向一个不变的字符串。如果需要调试输出,可以直接用 (const char *) 强制转换。但是,一个字符串必须通过 api ,build 出这个类型来。
其实现就是建立一个全局的字符串池,用 hash 表索引。里面存放不重复的字符串。我们只在其间存放那些系统中的标识符(用来索引资源用的字符串)。我们在进程生命期间,不再释放任何标识符。因为它们是限的,所以我们不用担心它们会吞噬我们的内存。也不需要用复杂的引用计数或是 gc 来管理它们。
这些特殊 string 可以用简单的指针去使用,不用再顾及生命期。可以直接用高效的变量比较、可以方便的在模块间传递、可以参与排序、可以用于 hash 映射的 key …… 总之,当成基本类型用就好了。一定程度上,可以弥补 C 语言没有原生字符串类型的不足。
我用它们去索引对象的属性名字。这样可以兼顾性能。
具体的实现是:我在每个 C 模块(利用宏)初始化一些字符串常量。当然这本是链接器应该干的事情。可 C 语言的模型并不原生支持这个,依赖 C 语言模型的编译器就不会给你代劳。忍住 C++ 的诱惑,我用丑陋的宏辅助我实现了一点东西。给语言增加并不存在的概念(新的类型)、使用宏、这都让我非常有罪恶感。带着这种感觉做设计,不至于犯太多错误,不至于破坏 KISS 。
然后在系统运行起来后,这个模块中的函数,可以通过简单的 if else if 来筛选不同的属性访问请求。如果对性能要求再苛刻一点的,还可以做一个简单的映射,最终转换为 switch case 。不要问我为什么不使用函数指针数组,在前面已经解释过了。如果真的需要,也值得考虑。
注:虽然 const_string
其实就是一个 const char * ,但也不要直接 typedef const char * const_string;
。这样编译器不会帮你找出错误的类型匹配。正确的方式是定义成 typedef struct literal * const_string;
,我们不需要让使用 const_string
的模块了解 struct literal 是什么,实际上它什么都不是。想当成 const char * 的时候,依旧可以强制转换。但直接赋值是编译通不过的。
最终,一个对象的 getter 和 setter 可能被统一成两个 api :
int object_get(struct object *, const_string property, ...); int object_set(struct object *, const_string property, ...);
有点 printf 风格?这就是 C 语言。
还想问这样会不会导致性能问题?如果在系统里,模块之间存在大量的交互,会高频率的访问对象的属性。这样不关是函数调用的开销了。可能还涉及 hash 表,涉及大量的 if else 比较……
那、就是你的设计问题了。你怎么可以允许这样的存在?
不同的对象活在不同的层次,它们最好是自生子灭。尽量少干涉它们。只在极少情况下改变它们的状态。大部分时间让它们自己运转去。至于模块内部,毋用我说,你不会使用外部接口去操控自己内部的数据吧?
对于 C++ ,不要幻想 inline 总能帮你解决问题;对于 C ,inline 并非传统。
承认 getter 和 setter 的开销。
一家之言,别相信我。请自己思考。实践。
Comments
Posted by: 幻の上帝 | (39) June 7, 2013 10:18 PM
Posted by: Nrrgk | (38) June 16, 2012 09:29 AM
Posted by: 七心葵 | (37) May 4, 2011 10:59 AM
Posted by: xxp | (36) February 12, 2011 05:25 PM
Posted by: kmplayer | (35) December 10, 2010 10:20 AM
Posted by: cat | (34) December 22, 2009 09:27 PM
Posted by: houmingyuan | (33) November 15, 2009 06:08 PM
Posted by: jimsmorong | (32) October 31, 2009 01:33 PM
Posted by: jimsmorong | (31) October 31, 2009 01:33 PM
Posted by: chen3feng | (30) October 30, 2009 04:59 PM
Posted by: ablmf | (29) September 9, 2009 09:59 PM
Posted by: Noting | (28) August 16, 2009 03:51 AM
Posted by: ideawu | (27) August 10, 2009 01:39 PM
Posted by: gerald | (26) August 9, 2009 01:11 AM
Posted by: Clara. | (25) August 7, 2009 09:18 PM
Posted by: fnsoxt | (24) August 5, 2009 06:25 PM
Posted by: Nil | (23) August 5, 2009 05:14 PM
Posted by: sjinny | (22) August 4, 2009 09:22 PM
Posted by: Anonymous | (21) August 4, 2009 02:13 PM
Posted by: Anonymous | (20) August 4, 2009 02:12 PM
Posted by: asking | (19) August 4, 2009 09:53 AM
Posted by: chentan | (18) August 4, 2009 08:40 AM
Posted by: Cloud | (17) August 4, 2009 12:20 AM
Posted by: sjinny | (16) August 4, 2009 12:12 AM
Posted by: ideawu | (15) August 4, 2009 12:05 AM
Posted by: analyst | (14) August 3, 2009 05:04 PM
Posted by: leafyoung | (13) August 3, 2009 02:48 PM
Posted by: egamesir | (12) August 3, 2009 02:24 PM
Posted by: 李马 | (11) August 3, 2009 02:23 PM
Posted by: cloud | (10) August 3, 2009 02:13 PM
Posted by: analyst | (9) August 3, 2009 12:23 PM
Posted by: chenglong | (8) August 3, 2009 11:39 AM
Posted by: cloud | (7) August 3, 2009 11:34 AM
Posted by: analyst | (6) August 3, 2009 11:06 AM
Posted by: hacker47 | (5) August 3, 2009 09:16 AM
Posted by: GlacJAY | (4) August 3, 2009 09:05 AM
Posted by: kevinxue | (3) August 3, 2009 06:29 AM
Posted by: MM | (2) August 3, 2009 03:31 AM
Posted by: kbtrace | (1) August 3, 2009 01:50 AM