« July 2009 | Main | September 2009 »

August 17, 2009

华丽的桌游:星际争霸

周日开车驮着一帮人去玩桌游。开了预谋以久的 星际争霸

前几次一直想开,但都可耻的失败鸟。原因是,上几次时间都很晚,捧着厚本的说明书,把人都读困了,不了了之。

这次中午一起床就开始了,加上前几次模模糊糊的把规则过了一遍,开展的还比较快,凌晨 3 点的时候,我们已经完成了两局完整的游戏。下一次再开的话,应该可以维持在 3 小时一局的节奏。

别被名字迷惑。虽然游戏名字很恶俗,但是东西还是不错的。缺点是,很难找到合适的桌子来玩。尤其是 6 人游戏的时 候,地图和模型都铺开,实在是,太大太大面积了。难道需要和我小时候一样,一群人跪在地板上玩游戏么?

比较寒的是,我们百般小心,最终还是玩错了一个重要规则:把 Z 通道当成单向的了。也正因为如此,居然玩出了原版规则中没有的乐趣。把地图的拓扑关系弄的更为复杂。

会玩这个游戏的同学可以一试,就是 Z 通道只允许从大头进入,小头出:即是单向的。如果通过 Z 通道出兵,就是有去无回。游戏 setup 阶段,所有人分两个步骤防止 Z 通道大小头(先放大头,放完一圈后在逆向放置小头)。

不过,这不保证地图平衡性。会使得地利在战事里变的更为重要。改变通道的事件卡也会变成神卡。昨天我就因为冒进,掉入对手的埋伏圈,又被人改换了航道,无法撤退,全军覆灭。


这个游戏很适合喜欢冰与火之歌的同学,摆放策略指令时,需要更多考虑。时序变的异常重要。但同时也会拖长游戏时间 :( 。

爱好 AA 的同学可能也会喜欢。场面同样的华丽,甚至需要更大的桌面。figure 做的很不错,如果能自己涂装效果更佳。这样可能会有点玩战锤的感觉 :) 。

August 10, 2009

《程序员修炼之道》书评

一切阅读都是误读

—— 安伯托·艾柯

上次读这本书已经是在五年前。中文版刚出版我就买了一本。那个时候我的工作相对比较清闲,有大量的时间阅读。恰巧我在负责公司的校园招聘以及新员工培训,非常需要一些不错的教材,更早的时候听说过这本书的英文版,但是没能一读,中文版自是不能放过。另外,那年我在写书,记录一些程序员生涯中的心得,对经验的总结都颇有兴趣。

爱不释手,是我第一次读完后的心境。完整的经历了人生中第一个成功的大的软件项目后,我有许多感慨。不少东西知道怎么做对,怎样做不对,但是要一条条写下来,却不知道怎么总结。这本书说出了许多我想说,而不知道该怎么说的道理。

接下来的日子,我在公司做过好几次技术培训,课题都是以这本书中的某个或某几个观点,结合自己的经历展开的。对于信任我的同学,我总是把这本书列在给他们开的书单的第一本。

后来,国内又翻译引进了几本类似的好书。比如《代码大全》,《Unix 编程艺术》。古人云,读书有三上,马上、枕上、厕上。我还真把书买了好几本,床头、办公桌上各置一本,方便睡前、如厕时阅读;手机里放入电子版,上下班路上,偶尔翻阅。这些书的确是值得逐章挑选出来,反复精读的。《程序员修炼之道》却于几年前推荐给新入职的同事,从我的视野里消失了。

这几天,同事把书还了我,加上 周筠老师 发给我电子版,我又重读了一遍。原以为那些嚼烂了的东西,不会再有新味道,但是我错了。

不同的人从不同的角度用不同的方式,阐述相同的道理。其中细微的差异,是需要读者有了许多许多的经历后,才能体会的。比如,在《程序员修炼之道》中花了六页分析 DRY - Don't Repeat Yourself 原则;而在《Unix 编程艺术》中把它称作 SPOT - Single Point of Truth ,大约用了一页半的篇幅。他们真是是想表达完全一致的理念吗?我看未必。所以,作为读者,同样会有许许多多的想法。随着编程经历的越来越多,思考次数的增加,重新和这些前辈的思想相印证,也是一件乐事。

我们以为理解了作者,其实是误解。但我们将再一次理解编程。


这篇书评是应邀而写,但的确是我想说的。也因为完成作业,而重读这本书,还是颇有收获。

August 08, 2009

Ubuntu 升级内核后不能正常引导的问题

这不是个新问题了。前两月开始,自从我的一台 Ubuntu 机器某次更新内核到 2.6.28-13 后,就无法正确引导。而只能引导旧的 2.6.28-11 。

这两天内核又升级到 2.6.28-14 问题依旧。

启动时显示:

Error 13 : Invalid or unsupported executable format

我觉得不能将就了,就花了点时间研究解决了一下。

我的这台出问题的机器上装有 freeBSD Ubuntu 和 Windows 三个系统。最早装的 freeBSD ,然后分出一个区装的 Windows ,再然后从 Windows 分区上又划出了一部分装 Ubuntu 。

出问题前,我尝试过升级文件系统。从 Ext3 到 Ext4 。感觉问题可能来源于这里。

无论如何,跟 grub 有关系。

根据一些搜索来的信息,重新 grub-install 了一下就好了。

sudo grub-install /dev/sda

做这个步骤时,我曾经不小心弄错了。用的 sudo grub-install /dev/sda4 (因为我的 Linux 分区在那里)

导致悲剧了。系统不能启动。只好刻了张 Ubuntu 的 LiveCD 修复 grub 。

sudo grub

root (hd0,4)

setup (hd0)

quit


关于问题的原因,我的揣测是因为 grub 特殊的工作方式引起的。硬件引导的时候,可以加载的代码有限。所以并不能完整处理复杂的文件系统。

这也是为什么,早期的 dos 必须把引导文件放在磁盘的最开头的缘故。

由于我把 ext3 过渡到 ext4 的缘故,导致了 grub 工作失常,不能正确的引导后来复制到文件系统中的内核文件。新的 grub 当然对 ext4 有支持,只是可能是对这种过渡情况支持的不好吧。重新安装 grub 后也就正常了。

ps. 其实当初就应该把 /boot 保留为 ext3 的,害的我原来刻的 Ubuntu 8.10 的盘不能用,又重新下了个 9.04 的 ISO 刻盘。感觉又污染了环境,每次刻盘都有罪恶感。

August 03, 2009

用扑克牌来玩 Condottiere

自从发现大多数人不太有耐心玩桌游以后,我一直致力于寻找更适合向这类人群推荐的游戏,慢慢改造 :D

上周末开了几局 Condottiere (佣兵队长)。发现这个游戏很适合向非桌面游戏玩家群体推广。游戏结束后,已经有人问我游戏名字,在哪买,多少钱的问题了 :D

老实说,游戏也不贵。我买的正版才 140 。taobao 上看到有人 DIY 的 60+ 一套。我个人不赞成盗版游戏拿出来卖,不过自己 DIY 倒是一种乐趣。

仔细研究了一下游戏道具,发现,其实只要买 3 套标准扑克牌,就能开这个游戏了。而且用扑克牌有个好处,就是朋友聚会的时候你不需要把游戏随身带着,可以就地取材。这样就不用一大伙人只有杀人这一个选择了 :(

Condottiere 的卡片共 110 张如下:

  • 白色军队卡 1 ,10 张,可用 10 张 A 代替。
  • 白色 2-6 点,以及 10 点 卡 各 8 张,可用 8 张对应数字牌代替。
  • 暖春 和 寒冬 各 3 张,可用大小王各三张代替。
  • 英雄(红色 10 点)卡 3 张,可用 红色 K 3 张代替。
  • 教皇 卡 6 张,可用黑色 K 6 张代替。
  • 交际花 (红色 1 点)卡 12 张,可用 Q 12 张代替。
  • 战鼓 (军力 x2 )卡 6 张,可用红色 J 6 张代替。
  • 钥匙(投诚)卡 3 张,可用黑色 J 3 张代替。
  • 稻草人(撤军)卡 16 张,可用 7,8 点 16 张代替。

以上,三副扑克牌中还剩下 52 张牌。

由于最多 6 人需要 6 套不同的标记物,我个人推荐把剩下的花色卡 6 张交给一人做标识;9 点可分为红黑两套各 6 张。其它多余点数卡可按花色分成 4 组,每组 6 张。

交战标记和教皇驻地标记,可以用硬币代替,也可以从剩下的牌中挑选。

下面解决最复杂的问题:如何制作一张地图。

原版地图是这样的:

condottiere_map.jpg

有 17 个区域,区域之间有比较复杂的拓扑关系。我决定用 17 张反扣的扑克牌模拟它。占领一个区域后,只需要把表示自己的牌扣在上面即可。经过反复尝试,我排出了比较满意的方案(并把方案图放在手机里:):

condottiere.jpg


佣兵队长的中文规则可以方便的 google 到。不过我发现网上的规则版本不一,还是简述一下我这里的版本:

游戏目的 :赢得更多的战役,比其他玩家更快的占领土地。

当 2-3 人游戏时,胜利条件为:占领 6 块区域,或占领 4 块接壤的区域。 当 4-6 人游戏时,胜利条件为:占领 5 块区域,或占领 3 块接壤的区域。

游戏流程 :游戏为一场场战役构成,每场的战役的胜利者将占领一块区域。区域一旦被占领,则不会被其他玩家夺去。战役有可能以平局结束、这样,就无人占领新的区域。

每场战役开始,由拥有 佣兵队长 标识物的玩家选择 战役发生区域。他把标识物放在地图的某个区域上,战役开始。

一开始,洗乱全部 110 张牌,发给每个玩家 10 张。第一场战役随便选取一个玩家开始即可,以后按规则传递标识物。

发起战役的玩家首先出牌,依次轮下去。每个玩家都可以选择出一张手里的牌,也可以选择 pass 。一旦一个玩家选择 pass ,他在后面的流程里就禁止出牌,只到清算阶段(先 pass 的玩家不见得输掉战役)。

如果手上以无可出牌,则强制 pass 。

当所有玩家 pass ,开始清算结果。以桌面点数最大的玩家为胜利者。如果点数最大的人不只一个,则为和局。弃掉桌面所有牌,进行下一场战役。

在下一场战役开始前,手上没有普通军队牌的玩家可以向大家亮牌,并放弃所有手牌。当场上只有一人拥有手牌,或所有都没有手牌,重新发牌(从未用牌堆里发)。如果牌全部发完,则把弃掉的牌堆洗乱重发。在后来的手牌补充中,版图上每多占一个区域,就可以多得到一张牌。比如,如果你已经占领了两块区域,那么就可以得到 12 张手牌。

大家重新补充手牌前,那个保留有手牌的玩家可以至多保留手上的牌 2 张。补充手牌后,手牌总数也是 10 + 占领区域数量。

如果一场战役结束后,有两人或两人以上有手牌,那么所有人都不可以补牌。有手牌人之间争夺下一场战役的胜利,没有手牌的人只能旁观。

在上一场战役里,如无特殊牌改变规则,战役胜利者得到 佣兵队长 标记,来决定下一场战役在哪里开仗。如果是和局,则把标记传递个下一个玩家。

如果版图全部被玩家占领,但没有人达到胜利条件,占领区域最多的玩家获胜。区域占领区域最多的玩家不只一个。就把全部 110 张牌洗乱,按规则给占领区域最多的玩家补充手牌。他们将进行最后一场战役,决出最后胜利者。


游戏中有普通军队牌 1 点 的 10 张,2 点到 6 点的各 8 张,10 点的 8 张。(注:没有 7,8,9 点的军队牌)

这些普通军队牌一旦打到桌上,实际点数有可能被特殊牌改变。

特殊牌如下:

  • 战鼓(共 6 张):把桌上自己的普通军队牌点数加倍。(无论是桌上已有的,还是以后会放上去的)
  • 寒冬(共 3 张):冬天降临。如果桌上有暖春,就把它们移走。桌面上所有的普通军队牌,全部按一点战斗力计算。(不分敌我)
  • 暖春(共 3 张):春天来临。如果桌上有寒冬,就把它们移走。桌面上最大点数的普通军队牌,每张加 3 点。(不分敌我)
  • 注:以上特殊牌,相同的效果都不重复计算。比如打出两张战鼓,还是乘 2 而不是乘 4 。季节牌和战鼓可以叠加,先计算季节的附加效果,再乘 2 。
  • 教皇(共 6 张):一旦打出教皇,就把桌面上最大的普通军队牌弃掉。不区分是自己的还是敌人的。最大的牌有多张,一起全部去掉。打出教皇的玩家可以在版图上选择一个教皇所在地,这个位置今后不能开战。(除非有玩家再打出教皇,把他从版图上移开)打出教皇的玩家也可以选择把教皇从地图上移走。
  • 稻草人(共 16 张):没有战斗力。打出后,可以选择把桌面上自己的一张普通军队牌收回手中。
  • 英雄 (共 3 张):按 10 点战斗力计算,不受任何特殊牌影响。
  • 交际花(共 12 张):按 1 点战斗力计算,不受任何特殊牌影响。战役结束时,如果有一个玩家桌面的交际花牌比别人的都多,那么就把 佣兵队长 标志交给他,由他决定下一个战场。
  • 钥匙(投降)(共 3 张):一旦有玩家打出这张牌,立刻清算桌面战斗力,结束当场战役。

August 02, 2009

关于 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 的开销。


一家之言,别相信我。请自己思考。实践。

August 01, 2009

捣糨糊

这两天完成一个新需求,需要维护以前一个同事写的代码。

前几天花了一天时间读代码,好不容易都看明白了,感觉真是一团糨糊啊。苦不堪言。

要在以前设计的接口基础上增加新功能,我觉得是件非常违背我的美学的事情。如果就这么干下去,我会做噩梦的。所以一咬牙决定重构。好在是比较底层的代码,只有中间层调用这些接口,和上层无关。

大约删除了 2000 多行代码,重写加上新实现的功能,用了 900 行左右。接口数量,加上新加的接口,总数减少到原来的一半。

另外修改了调用这些接口的大约 30 个源文件。昨天晚上完成的时候已经是一点,似乎都眼冒金星了,才把整个工程编译通过。

今天战战兢兢的测试两天闷头赶出来的战果,上午居然一点问题都没有,让我心虚的很。下午找到一个 bug ,稍微安心了点。晚上解决掉提交了。终于舒心了许多。

精心设计,而不要过度设计,是多么重要的一件事啊。

不要以为你的团队每个人的水平都很高,足以理解你的每个”精妙“的设计。而一旦团队中真的每个人水平都深不可测了,那么,”精妙“的设计反而更容易被唾弃。虽然每个人都有能力看明白它。

所以、如果在十分钟内,不能通过讲解让一个新手明白这些代码该如果使用;或是不借助作者的帮助,一般人两个小时还读不明白;那么,就应该考虑是不是做出了错误的设计。即使你怎么看都没错,总有一天它也会被误用的。

另一方面,也不要轻易放弃设计。如果一个需求很难简单实现,又要考虑这又要考虑那,就不要随手实现了。可悲的是,如果手上的锤子越大,就越容易轻易把钉子敲下去。比如用 C++ ,轻易的加上几个类层次,弄点多态继承出来,最好再玩点模板技巧、来个操作符重载。搬出教科书,复制点设计模式。貌似轻易的把问题摆平了。

问下自己,真的吗?

一定要依赖语言高级特性才能解决的问题,设计方案一定有问题。相比较而言,如果你在用 C ,则不应该抱怨没有类,没有继承,没有虚函数;而应该庆幸你没有这些干扰思维、阻碍你做出真正简洁设计的东西。

少即是多。