2010年3月16日星期二

MVP Summit 2010 Trip (CA)

跟张诚、马志文飞抵 San Francisco 以后,我父亲的朋友来机场接我们到酒店。我们住在 Hilton ,当初 bid 这个酒店是想看看五星级酒店如何,确实是 bid 到了很好的价格,但是网络、早餐都不免费。相比之下,之后住的那些三星四星酒店都有免费的网络和早餐。当晚安顿好之后,父亲的朋友开车带我们到 Bay Bridge 对面的 Oakland 吃晚餐──到美国之后的第一顿中餐,而且还有海鲜。之后回到 San Francisco ,游览了一下 San Francisco 的夜景。

DSC02810

第二天才是真正的 San Francisco 游览。我们去看了 China Town 那个「天下为公」的牌坊,这里所有的商店都有中文招牌,只要你懂粤语就能生存。我们去逛了著名的 City Lights Bookstore ,尽管里面的文学作品我们都看不懂,所以我只好买了一本 Lonely Planet Los Angeles Encounter Guide 。然后还去了 Lombard Street (九曲花街),可惜季节不对,看不到花。还好的是,尽管当天没有晒太阳的机会, Pier 39 的海豹们还是爬到木板上来了,让我们拍摄。

DSC02881DSC02960DSC02987

最后一个景点,当然也是最重要的,自然就是 Golden Gate Bridge 了。其实这不是一个景点,我们去了很多不同的观景地点,从不同角度拍摄了 Golden Gate Bridge 。至于哪个观景地点才是最好的,这个争论从来没有停止过。

DSC03018DSC03019DSC03049DSC03074DSC03079

第三天的行程是开车去 Monterey Bay 看 17 Mile Drive 。这是加州西海岸很漂亮的一段路,可惜去的路上一直阴天,到了之后稍微出来了一会儿阳光,接着又变成暴风雨了。这是我们第一天在美国租车,张诚作为司机就紧张得要死了,特别是迎着暴风雨开的那一段。

DSC03182DSC03192DSC03212DSC03215DSC03232DSC03280DSC03283

美国的 Freeway 都是限速 65mph (105kph) 的,天气晴朗的时候所有人都超速到 80mph (129kph) ,就算是暴风雨也能开到 60mph (97kph) 。相比起国内的高速,这样的速度是挺快的,不习惯美国开车方式的话,也是挺危险的。在国内,你胆小的话可以开慢一点,别人爱超你就让他超;在美国,汽车就如同轨道交通工具一样,所有车都走在自己的车道上,都按照一个速度来开,整条 Freeway 非常有秩序地流动,变道比较少发生。在国内,你要变道就先减速,因为大家觉得这样做比较安全;在美国,变道必须先打灯,确认有位置给你变道了,再加速然后变道。

为什么需要加速然后变道?设想一下,你要变道进入一条有轨电车车道,前后两辆有轨电车都开着 80mph ,你要如何才能插进去?为了保证后面那辆车不会撞上你,你的速度必须不低于 80mph 。如果慢了,你也不知道慢了多少,更不知道还差多少就会被后面车撞上;但如果快了,你是知道还差多少会撞上前面那辆车的,这个你可以控制,变道之后再把速度降下来。走在后面的车不是应该让前面的车吗?这是中国的法律。在美国,人家保持车道和保持速度就是没错的,你要变道,你就要小心。所以说,美国人守规矩,马路上没有轨道,但人们按照有轨道的方式来开车,轨道在人们心中。

晚上在 San Jose 一家叫 Pearl River Restaurant 的粤菜餐馆吃饭,味道还不错。随后在跟 Elliott Ng 吃早餐时,他说到一个观点:国内有很多人并不原意相信,美国的粤菜做得比北京要好,但这是事实,不相信的人只是没能在美国找到一家正中的粤菜餐厅而已。我非常认同这个观点,因为每一次我从北京跨越太平洋飞到美国,就饮食方面而言,都让我感觉到离家更近了。

DSC03303

为什么一个离家两万公里的地方比一个离家两千公里的地方更有家的感觉?或许这是文化上的差异吧。说不定,广东人当中愿意飘洋过海到美国的,比愿意到北京的还要多,而且这部分人比到北京的那部分人更有影响力。

第四天早上从 San Jose 开车出来,去跟 Elliott 吃早餐。这是我们第一次在没人带的情况下自己开车,导航全靠 Nuo Yan 借给我们的 GPS 。为了让张诚专心开车,所以就不用他和 GPS 直接交互了,我来负责操作 GPS 和将 GPS 的语音提示用中文转述一遍。即使是这样,我们还是绕了很多弯路,因为我们还不习惯 GPS 的导航方式。当 GPS 提示转弯的时候,我们往往会提前转了,或者来不及转了,于是又要绕回来。

DSC03309

吃完一顿正中的美式早餐后, Elliott 带我们去一家投资公司参观了一下他们的收藏品。我问 Elliott ,加州每天天气都那么好吗? Elliott 说,是的,基本上不用看天气预报,加州每天都会经历四季,但每天的天气都差不多。然后我又问,那为什么我们之前在华盛顿州就阳光灿烂,反而来到加州就遇上暴风雨了呢,这对这两个周来说都很反常啊。 Elliott 说,一定是我们把华盛顿州的坏天气带到加州来了。

随后 Elliott 因为有会议,而让我们自己去参观 Stanford ,于是我们就开车来到了 Stanford 。 我们在 Stanford 里面转了一圈,也不知道看什么,于是就开到 Stanford Shopping Center 停下来,找到有无线信号的地方,上网搜索 Stanford Visitor Center 的位置。接着开去 Stanford Visitor Center ,在 Visitor Center 前面的停车场绕了 20 分钟才搞明白什么车位是给访客的以及哪里有空余访客车位。在 Visitor Center 我们被告知, Golf Cart Tour 仅在提前预约的情况下才能参加,所以我只好就打电话找我在 Stanford 的同学,问他 Stanford 有什么好看的。他建议我们去看 Hoover Tower 和 Memorial Church ,顺便到 Alumni Cafe 吃饭。

DSC03347DSC03348DSC03351DSC03352DSC03355DSC03367DSC03368DSC03371DSC03373DSC03376DSC03381DSC03393DSC03396DSC03399DSC03411

晚上跟 Elliott 去参加 GSR Ventures 的一个新年聚会,由一群华裔投资者和创业者分享回中国投资创业的经验。说实在的,我过去对 entreprenuership 一直没什么兴趣,参加完这次聚会以后至少有那么一点点兴趣想要去了解了。在这里,我必须要感谢 Elliott 为我们提供这样一次机会,尽管后来 Elliott 在 Stanford 的课程我们没能参与旁听。

张诚在开了两天车后已经疲劳得要死了,而且我的 Stanford 同学向我介绍了加州有名的 In-N-Out Burger ,于是第五天起来后我们就来到一家 In-N-Out Burger 补充能量。据说 In-N-Out Burger 有 secret menu ,我们就问服务员 secret menu 是什么,然后被告知其实没有什么 secret menu ,只是有一些特殊的汉堡制作方法而已,例如说放 4 块牛肉加 4片芝士的 4 * 4 。我们三个人各自要了一份 4 * 4 的套餐,我很享受地吃完了,然后张诚和马志文吃了一半就饱得要吐了。

DSC03422

沿着 US-101 开车到 Santa Maria ,尽管无聊但很舒服。习惯了美国的 Freeway 之后,跑起来真的比中国的告诉公路要舒服──你固定车道固定速度巡航就是了,不会有人乱变道,你只要盯着自己车道前方的道路以及头顶的路牌就可以了。下午到达 Santa Maria 后,发现我们住的 motel 竟然还有游泳池,可惜这个季节不开放。晚上到 Ichiban Japanese Restaurant 吃寿司,发现店主一家原来是台湾人,见到有中国游客就特别热情。估计因为 Santa Maria 是小地方,很少有机会见到中国游客。

DSC03445

在 motel 睡了一觉以后,继续沿着 US-101 赶往 Los Angeles 。正如别人跟我们说的一样, Los Angeles 的司机素质比较低, Freeway 的状况跟国内高速类似,不是你守规矩就行了,你还要提防那些不守规矩的人。到达 Los Angeles 后,我们在把 Hollywood Boulevard 上的几个经典都看了一眼,也就回酒店去了。晚上张诚和马志文去看 NBA ,我自己宅在酒店上网,结果他们也只看了一般就因为无聊而回来了。

DSC03480DSC03485DSC03488DSC03492DSC03511

在美国最后完整的一天,我们安排给了 Universal Studio ,想着要花一天的时间来参观这个地方,结果大半天就搞掂了。 Universal Studio 里面的每一项游乐项目都很赞,让你觉得完全值回票价。在 Studio Tour 里面,你能看到很多著名电影当中的场景,而且电瓶车会停在这些场景旁边等候着特技的出现──在山谷里体验洪水从山上而来;在地铁隧道里体验大地震,迎面开来的地铁出轨,路面上的油罐车塌方掉到隧道里;在湖边体验大白鲨出没,即使点燃了码头上的油桶,大白鲨还是敢跃到水面上来。

DSC03560DSC03565DSC03576DSC03581DSC03589DSC03590DSC03593DSC03595DSC03608DSC03616DSC03619DSC03634DSC03636DSC03643DSC03669DSC03675DSC03676DSC03677

离开 Universal Studio 后,我们发现还有时间,于是想赶到 Santa Monica 看日落。可惜我们没有选择让 GPS 给出路线方案,只是朝着大致方向开,结果在绕来绕去的 Sunset Boulevard 上浪费了不少时间,最后赶到 Santa Monica 的时候太阳已经贴着地平线了。

DSC03693

最后一天,我们一早开车到机场,我搭上了回国的飞机,而张诚和马志文则等晚上的红眼航班飞往 New York City 。

2010年3月14日星期日

MVP Summit 2010 Trip (WA)

简单记录一下今年的 MVP Summit 之旅。上半部分是在华盛顿州的一个星期,下半部分是在加州的一个星期。

MVP Summit 期间,我住在 Bellevue 的 Westin 。第一天和第四天的会议在 Hyatt 举行,而第二天和第三天则在 Microsoft Campus 。经历了前年的「 too much salmon 」和去年的「 no salmon 」之后,今年三文鱼又被请回来了,而且是在第一天的 keynote 上── Toby 请来了 Pike Place Market 那一家著名的抛鱼档,让他们现场表演抛三文鱼。

DSC02732

接下来的欢迎晚宴上,有三文鱼扒,也有三文鱼寿司,这通常是整个 MVP Summit 期间吃得最好的一餐,因为大家都不赶时间,可以尽情交流。之后会议期间的餐饮尽管也不错,但更多地只是为了吃饭问题而已。最赞的是 Microsoft Campus 里面的冷柜,各种饮料都有,包括无糖的或者是无咖啡因的。

DSC02751

会议中间我抽空在 Microsoft Campus 里面走走,碰巧这两天还算有点阳光,感觉非常舒服──环境很好,人都很 nice 。当然, Microsoft Campus 如此之大,没有 shuttle 是不行的,所以如果要去一个稍微远一点的地方就需要找 shuttle 。在此也感谢马骐和 Darren ,开车带着我周围去。

DSC02789

说到周围去玩,就必须提及第一天去 Seattle Downtown 的行程。从 Bellevue 搭上大巴一小时候,就来到了熟悉的 Seattle Downtown ──因为之前两年的 MVP Summit 都住在这里,所以相对 Bellevue 来说显得更熟悉一些。在这里有 Pike Place Market ,而我每年来这里的主要原因就是到第一家 Starbucks 买一点纪念品。

DSC02685

Seattle 让我感到最不可思议的是,它在城市中心地下有一条快速公交隧道,而且是巴士与轻轨共用的。国内很多大城市都有快速公交系统,其实也就是一对公交专用车道,但我从来不觉得究竟有多快速。 Seattle 的快速公交隧道如同地铁一样,藏在城市中心区域的地底下,没有需要等候的十字路口,也不会堵车。

DSC02716

在华盛顿州前后总共停留了5天,有一些想去的地方没去成,例如说 Future of Flight ,在这里可以参观到 Boeing 的工厂以及飞机是如何组装出来的。明年以机会再来的话,一定要找机会去看看。 MVP Summit 结束之后,我和张诚、马志文飞往了阳光灿烂的加州,事实上等待我们的却是狂风暴雨。

2010年3月7日星期日

技术大会的时间安排

技术大会的时间安排其实是一场博弈。如果你问程序员,希望技术大会选择在星期几进行,他们一定会说是星期二、星期三、星期四。原因很简单,星期一往往是做本周计划用的,星期五往往是做本周总结用的,实际工作主要集中在中间三天。当然,经理绝对不会喜欢这样的时间安排,因为员工宝贵的工作时间都用于参会了。因此大会组织方一般会选择折衷方案,把会议放在星期四、星期五、星期六。

这又带来了一个新问题,星期六原本是休息日,程序员自然更倾向于休息而不是参会。如果星期四、星期五算是因公参会,星期六就算是因私参会了,除非公司原意把你星期六参会的时间算作加班。为此,大会组织方要给你一个充分的理由,让你因私也来参会。举例来说, Microsoft 的会议往往会把最重要的礼品留到最后一天下午发放,确保了程序员因私也乐意来参会。至于 SD2C 的做法就显得不够成熟了,第一天签到就把礼品都发出去了,星期六来的人就会少一些。

活动的组织是一门学问,不同的 stakeholder 看重不同的价值,如果不小心把哪个 stakeholder 的价值给忽略了,活动的效果就一定不好。

2010年2月6日星期六

Apple 谈论产品 vs Microsoft 谈论技术

看了一下 Jeff 的《李笑来激起千层浪,赵姐夫力拒众强敌》,回顾了一下之前 Twitter 上的讨论。我个人觉得, Apple 观点和 Microsoft 观点是不同的,所以才造成了如此之多的争论。很多人在使用 Microsoft 技术的同时,由于 Microsoft 铺天盖地的宣传信息,自然而然地也接纳了 Microsoft 观点。使用这种观点去跟持有 Apple 观点的人争论是没有意义的,因为大家根本就是站在不同的角度来看到同一个事物。

看看 Jeff 整理的对话记录,里面有多少 Microsoft 技术名词? .NET 、 COM 、 DDE 、 OLE 、 CLR 、MFC ……这就是 Microsoft 观点──我们每天都有众多的技术革新,通过这些技术革新让技术人员感受到震撼( Microsoft 在推 .NET 初期确实很喜欢用 ROCK 这个词)。当然, @virushuo@tinyfool 被震撼多了,就说自己被强奸了。就算是在博客园里,也每天有人抱怨 Microsoft 技术更新太快,没办法跟上技术革新的速度。这就是 Microsoft 观点,满眼都是技术名词,名词刷新速度越快,越 Microsoft 。

再看看对话记录里面有多少 Apple 的技术名词。能看到的也就是 Carbon 和 Cocoa 两个,连 Cocoa Touch 也不提。是因为大家觉得 Cocoa Touch 和 Cocoa 就是足够相似,所以在讨论 iPhone 时没必要提及 Cocoa Touch 吗?显然不是。你看 Microsoft ,除了炒作 .NET ,还炒作 .NET CF 、 .NET MF 。就凭这一点,你就能察觉到 Apple 观点是和 Microsoft 观点不同的。 Apple 观点并不看重技术名词。

那么 Apple 观点看重的是什么?我们可以来对比一下 WWDC 2009 KeynotePDC 2009 Keynote 。在看完 PDC Keynote 之后,我来问你一个问题:这段视频中的哪些信息给你留下了印象?可能你会说,有 Windows Azure ,有 Silverlight 4,等等一堆技术名词。那么我再问你: Silverlight 4 是什么啊?我想你会用更多的名词来解释它比 Silverlight 3 多了什么新增功能。回头来看看 WWDC Keynote ,给你留下印象的是什么。那可能是「 Safari 4 比 IE 8 快7.8倍」,以及 Grand Central Dispatch 。那么 Grand Central Dispatch 又是什么?它可以让应用程序更有效地利用线程,从而提高响应速度,不需要提及更多的技术细节就能把重点说清楚。

Apple 看重的是产品,特别是用户使用产品时的体验。幕后的技术?就跟爱一样,是做出来的,而不是说出来的。最近我读了一篇文章,叫做《 Revolution vs Evolution 》。文章里面说到,用户接触到的人机交互界面应该是 evolution 的,用户原有的使用习惯得到了保持,同时很多细微的地方又有所改进,而这些改进所依赖的正是背后技术的 revolution 。留意 WWDC 中关于 Snow Leopard 内置应用改进的部分,第一个提到的就是 Finder ,其中第一句话就是「我们都爱 Leopard 中的 Finder ,因此我们没有改变 Snow Leopard 中的 Finder ,至少用户界面没有发生变化」。然后才说到, Snow Leopard 中的 Finder 其实是基于 Cocoa 完全重写的,带来的好处包括「更快地绘制图标」、「更快地清空 Trash 」等等。 Finder 背后的技术由 Carbon 转变为 Cocoa ,这是 revolution ,而 Finder 用起来的感觉则只是 evolution 。

既然说到产品,我们再来对比一下 iPad 发布会Windows 7 发布会。在 iPad 发布会上, Steve Jobs 把 iPad 拿出来,然后坐在沙发上摆弄了12分钟。这12分钟里面,他没有介绍任何的新特性,只是把内置的应用逐一打开,使用一下,然后关掉。为什么他不需要介绍新特性?因为这里面根本就没有新特性,任何一个熟悉 Mac 或者 iPhone 的人都能轻易理解 iPad 的用户界面,这正好符合上面所说的「用户界面只能做 evolution 」。当然,这12分钟也不是白白浪费的, Steve Jobs 就是要让所有人看到,作为一个普通用户,使用 iPad 是多么流畅的一件事,常用的功能触手可及,无需进行任何学习。至于 Windows 7 的发布会?请问这里有多少人能够说出 Homegroup 跟一般 Windows 共享有什么区别的?我想没多少人能够说清楚,但 Microsoft 偏要为此创造出一个新名词,以及一种需要重新学习的交互方式。

Microsoft 不仅仅不断地为技术创造新名词,还不断地为产品创造新名词。有很多程序员都抱怨, .NET 3.5 带来一堆诸如 WPF 、 WCF 、 WF 的东西,根本没时间去学。我觉得这没什么好抱怨的,受害的又不仅仅是程序员,最终用户一样面对一堆搞不明白的新概念,例如我就不知道 Homegroup 有什么特别的。最终用户是上帝,犯错的当然不可能是最终用户。类推可得,程序员也没有犯错。在这个问题上,犯错的只可能是 Microsoft 了。

正是这两种观点之间的差异,使得持有 Microsoft 观点的程序员每天都在忙于追逐那些英文里叫做「 buzzword 」的东西,觉得弄懂了某个 buzzword 就是某种进步,而持有 Apple 观点的程序员则在围绕着产品谈论「 how does it look and feel 」。这让我想起了一幅漫画,说的是如何区分科学粉丝与科学家。同样是问「你记得住圆周率的多少位」,科学粉丝会说出一个很大的数字来,而科学家则会说他只记得住1位。或许我们应该思考一下,宣称自己懂得多少种技术,这到底有没有意义。

正所谓「每一个优雅的接口,背后都有一个龌龊的实现」。这个接口( Interface )可能是用户界面( UI )也可能是编程接口( API ),这不重要,重要的是它是否优雅,也就是 @xiaolai 所说的「表面功夫一定要做足」。至于背后的实现是否龌龊,这也不重要,重要的是它必须站在幕后。就算它比接口更优雅,它也不能跑到台前来喧宾夺主。该做的,老老实实地做了,没必要拿出来说;该说的,说到每一个人都心动了,但不一定去做。

2010年1月30日星期六

程序员的品味

上个月跟刘江以及一些《程序员》的热心作者读者吃了一顿饭,刘江让大家谈谈《程序员》的内容以及未来的方向。在激烈的讨论之后,我觉得我应该把自己的想法写下来,但那篇文章在我的草稿箱里躺了一个月了,就是无法结尾。

那篇文章想要表达的意思很简单,我认为《程序员》应该提供一流的阅读体验。至于作者和编辑手上的内容,就如同程序员手上的代码一样,都只是为了实现特定体验的手段而已。就拿《Avatar》来做例子吧,它的内容值多少钱?先别说它内容做得怎么样,就算做得再好,在中国消费者眼里也就值一张盗版光盘的价格。但是大家都原意花钱去电影院看《Avatar》,而且非 IMAX 不看,这说明大家花钱买的是体验,而非内容。

文章写到最后,我认为《程序员》的阅读体验相对于一般 IT 杂志而言,就应该如同 IMAX 跟家里电视机的区别一样大。价格不是问题,大家天天都在家里看电视也不是问题,只要体验是值回票价的,大家就原意排队买票看 IMAX 。然而为什么《程序员》应该去扮演 IT 杂志中 IMAX 的角色,为什么《程序员》就不能老老实实地做电视机,这个问题我无法解答,因此那篇文章也无法发布。

最近我重看了一次 Paul Graham 那篇《创造者的品味》,并且花了些时间去看原版。然后周末在 Open Party 上有和 Tin 聊了一下如何培养程序员的品味,我想我已经知道如何回答上面那个问题了。

什么构成品质

对于博客园这样的草根媒体来说,有高品质的博客,也有低品质的博客,这是可以容忍的。高品质的博客,说的不仅仅是文章内容质量好。就拿 Jeff 的博客来说,模板源自一个漂亮的 WordPress 模板,然后 Jeff 根据他的需求对细节作出了调整。 Jeff 还选择了一个视觉效果比较贴近 Visual Studio 的代码着色器,这使得代码块的整体视觉效果不会太糟糕。而且 Jeff 自己写的代码通常都很简介很清晰,这保证了他在文章中嵌入一些代码块后,可读性还是十分高的。此外,清晰的逻辑结构和小节的划分,使得读者能够快速跳扫描一遍文章,然后认真阅读自己想要深入理解的段落。

至于《程序员》,你随便找一本翻开,和屏幕上 Jeff 的博客对比一下,你可能会觉的 Jeff 的博客才是专业媒体。就拿版式来说,《程序员》和同等价位的时尚、美食、旅游杂志实在没得比。文章中内嵌大量的代码块,而且为了不破坏分栏结构,大多数原本一行的代码必须分作两行写,第二行还不能有缩进,因为缩进了可能就要分作三行写。至于插入 UML ,往往就意味着破坏分栏结构了,最终导致文字环绕的出现。我想任何一个看过《写给大家看的设计书》的人,都能指出这些版式设计的错误。

我知道作为一本程序员的杂志,插入代码和图表是必须的,就如同时尚杂志必须出现服饰、美食杂志必须出现食物、旅游杂志必须出现景点一样。区别在于,凡是出现在杂志上的服饰、食物、景点,展示的都是事物最美好最让人向往的一面,然而《程序员》杂志上的代码和图表并非如此,你看到这些代码后只会感到厌恶,并且心里想着,「我用 TextMate 随便打开一份我写下的代码都比你这要好看」。

说到品质,有一个对比效果非常强烈的例子,那就是2009年10月《时尚先生》上的 IT 企业家李开复,和2009年12月《程序员》上的 IT 民工李开复。我真的很想知道,如果把这两张照片剪下来,然后拿去小学做访谈,问小学生想要长大后成为这两个人当中的哪一个,最终的统计结果会是怎样的。这个统计的结果,或许可以通过类比猜出来。在《程序员》中找到《架构师接龙》这篇文章,把你大脑里对架构师的认识先抹掉,看着文章开头两位架构师的照片问自己一个问题,「我想要成为架构师吗?我想要成为这两个人当中的任何一个吗?」

选择符合自己审美观的事物,是人的天性。这种选择,同时也就是对品质的选择。这种审美观,源自长期的积累,就如同熟练的程序员能够无意识地一下子嗅出代码中坏味道,而无需刻意去思考「这段代码违反 DRY 原则了吗?」

品质影响品味

有人说,「北京有西直门换乘,就不可能有一流的设计师」。因为如果一位设计师每天都要经过西直门换乘,他就必须学会容忍别人的恶心设计,久而久之,他也就学会了忍自己的恶心设计。同样地,如果你每天看在线版的 MSDN ,你就会习惯了里面一些反模式的示例代码,习惯了这种满眼内容但就是找不到你所需信息的文档,习惯了 MSDN 网站打开的龟速。接下来,你就会觉得自己的代码偶尔反模式不是什么问题,自己的文档没人能读懂也不是什么问题,自己的 ASP.NET 网站比 MSDN 网站还慢更不是什么问题。

因此,品味是可以被引导的。你每天使用 Visual Studio 的体验(打开速度、键入代码便捷程度),查阅 MSDN 的体验(搜索多少次才能找到你要的信息),浏览技术博客的体验(文字是否容易理解、代码排版与着色是否易读)……这一切都会影响你的品味。在你写程序写文章时,你的大脑里会呈现出「一个优美的程序应该是这样子的」,或者是「一篇优美的文章应该是这样子的」。这个具体的想象,源自于你平时阅读到的程序和文章。正所谓「 Garbage in. Garbage out. 」,输入的品质决定了输出的品质。

程序员每天都要接触大量的信息源。聪明的程序员会选择那些高品质的信息源,同时避免在低品质的信息源上浪费时间。如果我从博客园首页打开一篇文章,然后看到作者在模板上乱改,例如插入拍得不怎么样的个人照片、消耗我 CPU 的 Flash 时钟等等影响视觉效果的东西,又或者是把 CSS 改得完全没有美感连裸奔都不如,我都不会再去看文章的具体内容,直接把窗口关掉。至于 CSDN 的博客,看到链接我就绕开了,因为我知道那个链接背后的页面有多恶心,右下角还一定会弹一个小窗口出来。这样做的原因很简单,如果你不在乎博客给人的第一印象如何,我不相信你对文章质量能有多在乎。

有品味才有品质

为什么有些杂志能够有如此高的品质,来迎合读者对品味的追求?我觉得这是因为那些杂志的背后有一群有品味的作者、摄影师、编辑……最最重要的是,有一位有品味的 CEO 。尽管我对杂志行业了解不多,但这个道理在软硬件行业是普遍存在的。

Jonathan Ive , Apple 负责工业设计的高级副总裁,在 Steve Jobs 回归 Apple 之后带领着设计团队创造了一系列突破性的产品,包括拯救了 Apple 的 iMac 、美国人手一部的 iPod 以及最近发布的 iPad 。那么在他加入 Apple 之后到 Steve Jobs 回归之前的这段时间里,他又做了什么呢?在这段时间里, Apple 一直在下滑, CEO 换了两任, Jonathan Ive 没能造出任何能拯救 Apple 的产品来。 Steve Jobs 和 Jonathan Ive ,谁的品味更重要一些?

有品质自然会有面包

我一直不明白,为什么博客园要把资源浪费在小组、问答、招聘这类项目上。在我看来,博客园有比这重要得多的问题有待解决。举个例子,文章编辑器和评论编辑器。尽管有不少人用 Windows Live Writer 之类的客户端,但编辑器依然显著地影响着作者和评论者的写作体验,而这个体验决定了最终的文章质量、评论质量。

为什么 Mac 软件的界面设计往往比 Windows 的要好?仅仅是因为 Apple 有 HIG (人机交互指引)吗?这是因为 Interface Builder 会引导你把控件放到符合 HIG 的位置上去。如果你把一个 Label 和一个 TextFiled 放到一行, Interface Builder 会提示你按照文本的基线对齐,而非按照控件边框对齐。回头看看 Visual Studio ,我相信不少人遇到过 LabelTextBox 难以对齐的问题,调来调去都不好看,最后索性就不调了。

写文章的人其实也会遇到同样的问题。谁不希望自己随手写出来的文章就很美观呢?但能够花时间去折腾模板和对每一篇文章进行细节调整的毕竟是少数人。这时候,编辑器能够达到什么样的品质,往往也就决定了文章和评论能够达到什么样的品质。

或许有人会说,问答很重要,因为工作上的问题解决不了我就会被炒掉。类似地,应该也有人会说,招聘很重要,如果连工作都找不到我哪里有闲情来写博客。这样的想法太过短视了。如果你有一个高品质的博客,你不可能没有好工作。不相信的话,你可以看看那些业界大牛的网站,有哪一个品质不高的?对自己的代码有什么样的品质要求,对自己的网站也就有什么样的品质要求。反过来,企业通过看网站的品质,也就能看出你的品质。

我知道,并不是每一个程序员都懂前端开发,也不是每一个程序员都懂视觉设计,但你必须意识到一点──那些大牛也不是天生下来就懂这些的,他们也必须经历学习的过程,只是他们为了把自己的网站做到符合自己的品味,不得不学习更多的知识而已。 Jeff 在做模板的时候,也问过我一些前端开发的问题,因此我觉得关键不在于你是否懂前端开发,而在于你是否真的想要把事情做好。(当然,你心目中「好」的定义由你的品味决定。)

对于网站和杂志来说,同样的道理是成立的。有些时候,不是覆盖越广的人群和满足越多的需求,就是越好的商业策划。如果能力有限,就用心做好一件事情。做到品质完全超越所有同行,钱自然会找上门来。

我最近读了一篇对 P1.cn 创始人王宇的采访。 P1.cn 是一个面向高消费人群的 SNS 。王宇说, P1.cn 用户的家庭月均收入为 ¥8000 ,开心的这数字是 ¥3000 ,人人的则是 ¥2000 , QQ 就更低了。尽管这是未经第三方证实的数据,但至少 P1.cn 上的广告能够说明问题──我想你不可能在 P1.cn 以外的 SNS 见到这些广告:轩尼诗的酒、佳能的数码单反、宾利的汽车……道理很简单,你去开心投放这类广告,回报率不可能高。

最后,用 Steve Jobs 的一句名言来结束这篇文章。在《 Macworld 》的 Mac 20周年采访中,记者问到了关于 Mac 市场份额的问题, Steve Jobs 的回答是:
Apple 的市场份额比宝马、奔驰或者保时捷在汽车市场中所占的份额都要大。成为数字领域的宝马或奔驰有什么不好的吗?

2010年1月29日星期五

能承载移动 Web 应用的唯一浏览器: Mobile Safari

最近拿 iPhone 、 Android 、 Windows Mobile 这三个平台上的内置浏览器来做了一番对比,结果是只有 iPhone 的 Mobile Safari 能够承载现代化的移动 Web 应用,其他移动浏览器的设计思路还停留在上个世纪──能看网页就行,不存在移动应用一说。

我用来做对比的平台是 iPhone 2.0 、 Android 2.0 、 Windows Mobile 6.5 。 iPhone 之所以选择 2.0 ,是因为 3.0 的浏览器跟 2.0 是一样的,尽管我的测试是在 3.0 上做的,但对 2.0 来说也是适用的。 Windows Mobile 6.5 则是在 Windows Mobile 系列中的唯一选择,因为只有它的浏览器是 IE6 内核的,再往前的 Windows Mobile 6.1 都是 IE4 内核的。

拖放

iPhone 对拖放的支持是完美的,使用 touch 系列事件监控触击及拖动,然后使用 preventDefault 禁用浏览器的默认行为(也就是拖动页面显示区域),这就搞掂了。

Android 也支持拖放,具体方案我没有研究过,估计是改用 mouse 系列事件监控触击及拖动,然后使用 preventDefault 禁用浏览器的默认行为。

Windows Mobile 让人很绝望,连拖放也不支持。估计因为这是一个对 IE6 的封装,封装的时候就把浏览器默认行为写死了,所以你不能够通过 mouse 事件监控到任何东西,也无法禁用浏览器默认行为。

手势

最简单的手势,就是通过两个手指靠进和远离来实现放大和缩小,如果这个手势都实现不了,别的手势也就不用测试了。

iPhone 对手势的支持也是完美的,你可以直接使用 gesture 系列事件监控手势操作,也可以用 touch 系列事件监控手势中每一个触点的活动。 preventDefault 同样可以禁用浏览器默认行为(也就是缩放页面显示区域)。

Android 的浏览器不支持多点触击,尽管 Android 的另一些内置应用是支持的,这估计是因为 Google 和 Apple 纠缠不清的多点触击专利官司。因此,如果你要在 Web 应用上支持缩放,就只能在界面上放两个按钮来实现了。

Window Mobile 就不用说了,你不能期望一个缩小版的 IE6 能做什么针对移动 Web 应用的优化。

定位

iPhone 可以使用 W3C 的 Geolocation API 进行定位,而且可以跟踪定位(也就是定位变化时通过事件调用你的函数)。

Android 内置的地图可以定位,但是浏览器就是不提供这个接口,这只能说 Android 就设计为不考虑移动 Web 应用。

Windows Mobile 在安装 Google Gears 后,理论上可以支持定位。这个 ActiveX 嘛,能让你扩展浏览器,实现任何其他浏览器上难以实现的东西。唯一的问题是,这会让人产生疑问──我到底是在 Web 应用开发还是在做客户端应用开发? Microsoft 背负了太多的历史包袱,使得开发者要从客户端应用开发转向 Web 应用开发也不容易。

绘图

iPhone 同时支持 SVG 和 Canvas 。如果你需要面向对象的图形组合方式,你可以选择 SVG 。如果你需要命令式(类似 GDI )的绘图接口,你可以选择 Canvas 。

Android 只支持 Canvas ,这能够满足大多数绘图需求,唯一的问题是你不能把已经绘制的形状如同对象般删除,需要的话只能全部重绘,这就是 Canvas 跟 SVG 在使用上的主要区别。

至于 Windows Mobile ,不仅仅 SVG 和 Canvas 这样的公开标准不支持,连 VML 这样的私有标准也不支持,基本上也就无法绘图了。

样式

iPhone 支持大部分的 CSS3 特性。这是因为 iPhone 1.0 并不支持客户端应用, Apple 为了让开发者能在 Web 上实现跟客户端应用一样的视觉效果, Mobile Safari 必须做到跟 Safari 一样,支持大多数的 CSS3 特性。

Android 支持的 CSS3 特性要少一些。尽管它的浏览器用的也是 Webkit 内核,但 Webkit 管的是布局而不是渲染,在样式渲染方面 Google 的实现总是比 Apple 要慢一步。

Windows Mobile 的话……跟一个 IE6 核心的浏览器讨论 CSS3 ,这不是纯粹浪费口水吗?

体验

上面列举了一大堆的功能,但这一切只是为了实现特定的体验,所以我们真正应该关注的是一款移动浏览器设计为满足怎样的体验目标。

iPhone 上的 Mobile Safari 拥有最高的体验目标设定。在 iPhone 1.0 的时候, Apple 禁止开发人员开发本地应用,只允许开发 Web 应用,因此 Apple 必须为开发人员提供最好的 Web 应用开发平台。

此外, Apple 拥有业界最好的 HIG (人机交互指引),对比之下 Microsoft 的 UX Guide 连 Windows Mobile 都覆盖不到,而 Android 的 UI Guildlines 则显得十分山寨。由于 Apple 鼓励开发者按照 HIG 进行 iPhone 应用程序的交互设计,所以 Mobile Safari 当初就设计为帮助开发者实践 HIG 。至于其他移动浏览器,它们的设计目标则简单得多──只要用户能够在上面流畅使用面向桌面设计的网站就可以了。

正是由于设计目标的差异,使得最终产出的产品存在着巨大的差异。只有 Mobile Safari 能够承载移动 Web 应用,其他移动浏览器只是桌面浏览器在移动设备上的延伸。

2009年12月20日星期日

如何购买 Amazon Kindle 书籍 (Updated)

之前写过一篇《如何购买 Amazon Kindle 书籍》,后来那篇文章成为了除首页外访问量最高的文章,这让我觉得非常奇怪——首页PV占全部PV的50%,而那一篇文章的PV比例竟然超过5%,同时其它文章的PV比例都低于2%——难道真的有那么多人搜索购买Kindle书的方法找到我的博客来?直到昨天,有人打电话来询问我购买Kindle书的方法(因为她看到我的文章然后找到我的电话),我才确信原来真的有那么多人需要这方面的信息。

其实现在在国内购买Kindle书已经很方便的,使用国内银行的信用卡就可以买,而且不需要经过Gift Card中转。如果你使用一个老的Amazon账号,里面关联过你的各种信用卡和地址信息,这有可能导致Amazon判断你不是指定区域的用户而不卖书给你。这时候你有两个选择:如果你愿意放弃你这个账号所有消费记录的话,你可以重新注册一个Amazon账号,然后就绑定一张国内银行卡,然后就可以正常通过1-click买书了。如果你不愿意放弃已有的账号,那么你可以尝试把上面关联过的各种信用卡和地址信息都删除,这也能让你通过Amazon的判断。

最后,我今年圣诞的最大消费竟然是购买各种打折电子书(不仅仅是Kindle的),看来这真的是一个很有潜力的事情哦!

看对的书 (Part 0 - 何谓对错)

在《老赵书托》里面,Jeff把人脑比喻为「寄存器」,而我则更倾向于把人脑比喻为「神经网络」。但是「神经网络」的定义本身就源自人脑啊,这不是循环引用了吗?其实我的意思是,我们应该参考训练神经网络的方式来优化人脑的思维方式。

我们都知道,训练神经网络应该用对的数据,这样才能让神经网络逼近于我们期望的行为模式。如果使用错的数据进行训练,结果将是不可预知的,而且往往意味着偏离我们期望的行为模式。基于同样的道理,书必须选择对的,因为读错书不仅仅浪费时间,还有副作用,比不读书还糟糕。基于这一点,我推荐的书都是观点导向型的,而非知识导向型的。尽管其中一部分可能也包含大量关于「怎么做」的知识,但是我看重的是「站在什么立场思考」的观点,因此后者才会是我推荐一本书的原因。

如果你关心的是「get the right thing」,你可以来看看我的推荐;如果你关心的是「get the thing right」,你可以去看看Jeff的推荐。当然,绝大多数人都会同时追求这两者,所以我并不反对把我们的推荐书籍混合起来看。但是我必须提醒你注意一件事情,不要尝试在一本观点导向型的书里面刻意寻找知识,这只会让你感到迷茫和挫败。很多观点导向型的书并不会告诉你具体「怎么做」,或者作者介绍的「怎么做」并不适合你的情况,因为书的重点在于「怎么思考」,你必须自行摸索适合你的「怎么做」。

最后,跟Jeff一样,我无法保证推荐的周期,也无法保证书的来源。我提供的只是观点,做法请自行摸索,或参考他人(如《老赵书托》中的最后一句注释)。

2009年12月15日星期二

China MVP Open Day 2009

又是一年一度的MVP Open Day,前年在三亚去年在北京,今年还在北京,明年能不能换个地方啊?我觉得两岸三地是个很好的主意,让我能认识到平时接触不到的港台MVP。不过,也请容我私下阴谋论一下,是不是好像TechEd裁减预算三场合办为一场那样,MVP Open Day也用两岸三地的预算合办了啊?

去MVP Open Day的路上,第一感觉是会务安排的失职(CSDN,又在说你呢!)——活动地点写的是「北京东方嘉宾国际酒店」,建议行车路线写的则是如何达到「东方太阳城」。说实在的,参会者谁知道「北京东方嘉宾国际酒店」就在「东方太阳城」里面呢,整份文档又没有提及过这个事情,行车路线描述得又非常不清晰。最搞笑的是,4条路线里面有3条都是到潮白河大桥后往南走的,这条路线非常饶,告诉人家走机场高速不就好了吗?之后从酒店往返微软利星行的大楼都是走机场高速嘛。

我到酒店check-in后,发现同房的衣明志已经到了,他刚刚去找吃的回来。接着我们就好好欣赏了一下酒店房间里的「开放式浴室」——把浴室和房间之间的窗户打开,浴缸基本上就贴着床了,非常有趣的设计,可惜当时忘记拍照留念了。之后就是聊天时间喇,尽管我想上网看看,但是发现酒店的网络用不了(第二天才明白到,酒店的网络必须狂刷DHCP,然后才可能分到一个IP)。

Day 1的开场keynote,感觉就是讲给新来的MVP听的,因为这部分keynote几乎每年一样(除非有人想要站出来提及一下那个敏感的$150问题)。下午我先回酒店睡了个觉,然后再跑出来听breakout session,尽管没有哪个题目是特别吸引我的,不过跟MVP参与session是和参加TechEd session不同的,因为大家提出的问题要更深入一下,而且speaker解答不了的问题或许别的MVP能解答。

跟speaker聊得比较好的一个session是Windows Home Server的那个。尽管session上演示的是Windows Home Server第一代产品,并且论存储跟Drobo没得比,论备份也跟Time Capsule没得比,不过如果价格能够降下来,功能更加模块化,我觉得还是有市场的。我相信现在有很多人买家用电脑已经不是为了买一部样样都不精的机器,他们要的是一个大容量存储器,或者是一个高清电影播放器,又或者是家庭网络服务器之类的。

相比之下,Day 2下午的MSPY见面会就惨烈得多咯——上次见面会提出的众多high priority需求都未能在Beta 2中实现,也不可能在MSPY 2010中加入,只可能等到下一个版本才能看到。举例来说,衣明志最看重的就是数据同步功能,不仅仅词库要能同步,调整后的词频也要同步。这个功能其它输入法都做了几年了,MSPY却说要下一个版本才能支持,这使得MSPY足足落后别人两个大版本啊。

From Day 2 Party of #mvpopenday

Day 1晚上有个名义上的party,不过party这个词本土化之后就成了「文艺晚会」,各位「被报名」的MVP上台唱歌,最终获得前三的三位MVP分别来自台湾、内地、香港。上台的港台MVP算是100%获奖的(因为港台分别只有一位MVP上台),这不是说评比顾及平均性什么的(因为我们的是外籍评为团),而是说港台MVP确实更懂得娱乐,看来内地忙于工作的MVP应该多多学习哦。

Day 2参观微软办公室,对于我们这些长期在北京的MVP来说就比较无聊喇,因为我们经常去,也没什么好参观的。比较有价值的是,在往返的大巴上跟香港的MVP聊聊天,顺便了解一下大家去Global Summit的意向。晚上的session我选择了王洪超讲的「面向技术人员的演讲技巧」,详情可以看我的另一篇文章

Day 3是最无聊的一天了,来参加颁奖的人本来就少,大家都自行去市区活动了,或者提前离开了。keynote讲的是Office的内容,对Developer MVP来说十分无趣。总体来说,今年的MVP Open Day是比去年要好的,包括在会务安排上(除了没提供详细路线这点外),还有两岸三地联合举办的做法。吃饭是的餐桌主要还是按专长和按地区来划分,希望以后能够再鼓励一下跨界的MVP交互。

最后,今年很高兴认识了WillMartin等港台MVP,跟张欣等MVP玩杀人也很开心(原来那么多MVP不懂玩杀人啊,搞得我很有优势呢),跟衣明志聊天则注定让我缺乏睡眠 @.@

2009年12月14日星期一

程序之外的事情 (Part 1 - Speech)

相信大家最近都看到了有一篇题为《程序员需要培养企业家式的能力》的热文吧。在我首次读到这篇文章时,我并不太同意文章开头的几段话——“程序员在公众面前讲话会脸红,不能很好地表现自己”,你开玩笑吧?在我熟悉的程序员当中,没几个是这样子的,我很喜欢跟他们聊各种各样的事情,从而获得更多有趣的观点,也激发自己的创意。后来想想,或许仅仅是我所在的圈子如此吧,我熟悉的都是那些拥有良好沟通能力的程序员。

还记得2007年第一次参加MVP Open Day的时候,我所认识的MVP不超过5个,看别人都是连任好几届互相认识好几年的老朋友了,在酒店大堂里一起聊天,我实在不知道怎么融入进去。随后在游泳池边的自助晚宴上,Bean对我说了这样一番话:
MVP主要包括三种人:搞线上活动的,例如写博客、回答问题;搞线下活动的,例如各地.NET俱乐部主席;还有就是企业中的职业讲师。其中第一种人很容易辨认出来,那些拿着一杯饮料一个人站在那儿的就是。


没错,我就是第一种人,生活在与世隔绝的校园里,天天上网写文章回答问题。站在游泳池的边上,看着别人都三五成群地在聊天,我知道如果没有Bean介绍我认识其它MVP的话,我也会拿着一杯饮料孤零零地一个人站在那里。我当时唯一的想法就是,我不要永远这样子下去,我需要作出改变。

后来,我成为了微软广州.NET俱乐部的讲师,到北京后又活跃于各种Ajax交流活动,再到今年成为TechEd的讲师,在这个过程当中有沟通技能的提升,但更多的是心态上的改变。我觉得,要跟陌生人说话并不是难事,我知道什么就说什么。如果是我说错了,我很欢迎别人指出来。如果双方立场不一样,而不存在绝对的对与错,那就互相包容好了。

在今年的MVP Open Day上,我参加了王洪超的一个session,题目是「面向技术人员的演讲技巧」。这个session提到了一些演讲方面经验与技巧,当然也鼓励现场的MVP们多进行演讲,并从中锻炼自己的自信心与沟通能力。到最后,现场的一位MVP提出了这样一个问题,他说他可以面对上千人做演讲,但在面对官员做演讲时仍然会感到紧张,并且觉得这是无法改变的。我跟他说,「你可以拥有一万个理由来解释为什么这对你来说是无法改变的,但关键问题其实在于你是否真的想要改变」。

是的,很多程序员总能列出一堆的理由来,说明为什么自己不适合学习或者不需要掌握某项与程序无关的技能,例如说演讲、英语、设计等等。但其实问题并没有那么复杂,你需要考虑的只是多学一项技能是否对你的职业发展更有利,只要你愿意,没什么是不能改变的。

「关键问题就在于你是否真的想要改变」——这是我必须放在这个文章系列首篇末尾的一句话。这是一个关于程序以外各项技能的文章系列,我们在这里不再讨论某个技术点应该如何设计与实现,我们所做的也就是分享一下学习某项技能过程当中的经历而已。如果你对这方面的话题感兴趣,可以留言分享你的经历,也欢迎订阅我的博客:

2009年11月24日星期二

编写 iPhone Friendly 的 Web 应用程序 (Part 7 - 多点触击)

这个系列的上一篇文章差不多是两年之前的事情了,在这两年里Mobile Safari并非停滞不前,从iPhone 2.0开始Mobile Safari就加入了对多点触击的支持,现在我们就来看一下我们可以利用它来干什么。

相信很多人都看过WPF为Surface设备做的一个简单demo,也就是在桌面上显示若干张照片,你可以通过单点触击拖放,也可以通过多点触击缩放和旋转。这在iPhone上能够做到,甚至在Mobile Safari里面也能做到,因为Mobile Safari提供了一套专门用于多点触击的JavaScript接口。现在我们就来看看如何利用这套接口吧。

我们都知道,Mobile Safari自身会处理多点触击,默认行为包括滚动和缩放。我们可以接管相应的事件,同时使用e.preventDefault()禁用浏览器默认行为,使得我们的Web应用程序能够如同WPF桌面应用一样处理多点触击。下面我们来深入看看Mobile Safari提供的多点触击事件。

单点触击

首先我们要处理的是单点触击事件,禁用浏览器的滚动行为,同时为我们的照片(一个img元素)增加拖动行为。在这里,我们需要用到touchstarttouchmovetouchend事件。在这三个事件里,我们可以通过e.targetTouches获取到用户点击的坐标,从而计算相对的位置变化。

首先,我们要在touchstart事件里面记录下初始坐标:

var transform = {
  x: 0,
  y: 0,
  rotation: 0,
  scale: 1
};

var startX;
var startY;
var touching = false;

element.addEventListener("touchstart", function(e){
  e.preventDefault();
  startX = e.targetTouches[0].clientX;
  startY = e.targetTouches[0].clientY;
  touching = true;
});


接着,我们要在touchmove事件里面计算相对位置变化,并且更新element坐标:

element.addEventListener("touchmove", function(e){
  e.preventDefault();
  if (!touching) return;
  transform.x += e.targetTouches[0].clientX - startX;
  transform.y += e.targetTouches[0].clientY - startY;
  updateTransform();
  startX = e.targetTouches[0].clientX;
  startY = e.targetTouches[0].clientY;
});
updateTransform做了什么?现在先不讨论,我们只要把事件相关数据正确地更新到transform的四个属性即可,如何把这些属性反映到界面上稍后再说。

最后,我们还要在touchend事件里面处理一下标志位:

element.addEventListener("touchend", function(e){
  e.preventDefault();
  touching = false;
)};


就这么简单?是的。关键点也就在于touchmove时跟踪e.targetTouches的变化,并更新transform里面的信息。

CSS3变换

接下来我们看看如何将transform里面的信息作用到界面上。在没有CSS3的时代,这是极之痛苦的事情,我们需要修改元素的多个样式属性才能实现这部分的功能,并且还没办法实现旋转。现在有了CSS3,只需要修改一下transform属性就可以了:

var updateTransform = function(){
  element.style.webkitTransform
    = "translate(" + transform.x + "px, "
    + transform.y + "px) "
    + "rotate(" + transform.rotation + "deg) "
    + "scale(" + transform.scale + ")";
}


一句代码就把位置、旋转、缩放都设置好了!尽管我们现在还没用到旋转和缩放属性,那就让它们保持默认值吧,我们在多点触击的事件里面会设置它们的。

多点触击

多点触击涉及到三个事件:gesturestartgesturechangegestureend。这三个事件跟单点触击的三个事件非常类似,使用起来甚至可以说是更简单一些:

var startRotation;
var startScale
var gesturing = false;

element.addEventListener("gesturestart", function(e){
  e.preventDefault();
  startRotation = transform.rotation;
  startScale = transform.scale;
  gesturing = true;
});

element.addEventListener("gesturechange", function(e){
  e.preventDefault();
  if (!gesturing) return;
  transform.rotation = startRotation + e.rotation;
  transform.scale = startScale * e.scale;
  updateTransform();
});

element.addEventListener("gestureend", function(e){
  e.preventDefault();
  gesturing = false;
});


代码确实比之前的还要少一些,重点就是正确设置transform的两个属性,随后调用一下updateTransform就能把最近的状态更新的界面上。

小结

在这篇文章里,我们了解到了Mobile Safari的6个特有事件,以及如何利用这6个特有事件处理多点触击。

如果你直接使用我的代码去实现开头所说的照片拖放应用,你会发现一个小问题——在进行多点触击操作时,旋转与缩放都是很自然的,就是拖动不自然,好像拖动只跟随第一个触点似的。原因很简单,在多点触击时,管理触点移动的还是touchmove事件,但上述代码只处理e.targetTouches[0],所以拖动只跟随第一个触点。

如果需要同时跟随两个触点,你需要对代码稍作改动,使得移动距离为e.targetTouches[0]e.targetTouches[1]的平均值。为什么呢?如果一个触点往上移动30px,另一个触点往下移动10px,除去旋转与缩放效果外,照片的中点应该是往上移动10px的,也就是两个移动的平均值。那么我如何知道当前有多少个触点呢?看看e.targetTouches.length就知道了。

最后,如果你关注移动设备上的Web开发,欢迎订阅我的博客:

2009年11月23日星期一

不看《时尚先生》的你竟然看《程序员》?!

我的博客订户有多少人看《时尚先生》的,举手来看看?博客园里又有多少人是看《时尚先生》的,喊出来听听?我相信绝大多数程序员都是不看《时尚先生》的,尽管它定位为男性杂志,但不看的人不会因此而觉得自己不够男人,也不会去指责一本男性杂志的编辑为何不考虑男性程序员。(为了表示我没有性别歧视的倾向,请女程序员自行将本文的《时尚先生》替换为《时尚》,将Esquire替换成Cosmopolitan。)我们都明白一个道理,一本男性杂志的目标受众往往只是一小部分的男性而已,自己不属于这个群体并不是什么大不了的事情。我相信包包也懂这个道理,只是他没有尝试用同样的思维方式去理解《程序员》而已。

Looking into commons between these two magazines.
2009年11月的《程序员》(感谢CSDN赠送)与《时尚先生》

在我眼里,《程序员》和《时尚先生》是目标受众不同但定位相似的两本杂志。《时尚先生》中国内地版遇到的问题(相对美国版的Esquire而言),《程序员》或多或少地也会遇到。

谁为杂志买单?

你上街买份¥1的报纸,搞不好还送你一瓶水,你觉得这份报纸的制作成本是谁买单的?难道是你买单的?除非你想着的是,报纸是国有的,国家是人民的,你也有一份。准确来说,你在报摊看到的各种报刊杂志都是由广告主买单的,你支付的价格只是个象征性收费,这些钱根本不足以回本,没有广告主也就没有这些报刊杂志。因此,一本杂志要活下来,首先要有广告主为它的生存买单。

广告主为何要为《程序员》的生存买单?肯定是因为广告主能够有所收益嘛,例如说使得读者购买广告主的商品,或者是使得读者试用广告主的服务,也可能是为广告主建立特定的品牌形象。无论如何,《程序员》总得为广告主带来收益,否则广告主没理由要养着它。

现在设想一下,假如你是广告主,你选择投放广告到Google还是Baidu?当然是哪个效果好投放到哪个啊!Anders Liu提到《程序员》曾经还有两个兄弟叫做《CSDN开发高手》和《MSDN开发精选》。假如你是广告主,你要在这三本杂志中选一本投放广告,你会如何作出选择?我想答案还是一样的,哪本的投放效果好就投哪本。

《程序员》上面曾经有过Visual Studio、DB2的广告,也曾经有过不少控件组件的广告。然而,作为程序员的你是否真的有购买过Visual Studio、DB2以及第三方控件组件,又或者建议公司购买?我想绝大多数程序员都不会选择个人购买,建议公司购买的也甚少。因此,无论《程序员》能够覆盖到多少真正的程序员,都不能为广告主带来真正的价值。与其将发行量浪费在购买力低下的程序员身上,不如直接瞄准有权左右公司购买行为的高管。

回到刚才的问题,如果摆在面前的是面向高管的《程序员》和面向程序员的《CSDN开发高手》和《MSDN开发精选》,作为广告主的你会怎样选择?答案应该简单得不能再简单了吧。

杂志做给谁看?

《时尚先生》是做给哪些男性看的?当然是做给有购买力的男性看的,否则为何能有那么多的广告主愿意向它投放汽车服饰广告,没有这些广告又如何支撑起一本杂志的制作成本。《程序员》是做个哪些程序员看的?当然是做给有购买力的程序员看的,道理是类似的,我就不再复述了。

那么到底哪些程序员有购买力?有购买力的前提是有可供支配的资金,这既可能是公有的,也可能是私有的。例如说,假设你能够影响公司的采购策略,服务器选用HP还是Dell,个人桌面选用Windows、Linux还是Mac,开发平台选择.NET还是Java,那么你就是有购买力的,《程序员》也就是做给你看的。这也正是《程序员》现在瞄准的市场。

相对来说,私有财产的购买力是无法跟公有财产比的。你不可能指望程序员个人去购买Visual Studio,你甚至不能指望程序员个人去购买控件,程序员在职业发展方面的消费通常也就覆盖到图书市场,偶尔有人好像Jeff这样的会买Kindle和原版书,也有少数人会自费参加一些行业性大会。

在这里面有一个有趣的现象,程序员收入越高越不需要把自己的收入投入到职业发展里面去,因为高收入的程序员都在大公司的重要岗位上,公司都愿意为他们的职业发展做投资。因此,说到底,与程序员职业相关的各项支出大多由公司支付,Windows服务器是公司买的,Visual Studio也是公司买的,去参加TechEd的门票是公司买的,书架上的那本《代码大全》也是公司买的。

综上所述,《程序员》就是做给那些决定怎么把公司的钱花掉的人看的。如果你不属于这部分人,你是否阅读《程序员》,阅读之后有什么想法,都不会对杂志本身产生什么实质性影响。

杂志为谁而做?

Andres Liu说《程序员》借人物提高知名度,同时也帮助人物提高知名度,这个模式就跟时尚杂志一样嘛。李开复登上《时尚先生》封面,对谁有利?当然是对双方都有利啊——李开复在这个时候需要出镜来促进自己的新事业,《时尚先生》也需要这个话题来满足读者的需求,甚至吸引潜在读者中的李开复粉丝。

举个更遥远的例子,Playboy也是采用同样的模式,所以才有那么多名人愿意接受采访,所以才有那么多美女愿意接受拍摄,自然Playboy本身也很出名。你看到Playboy上的美女,会觉得她们是在沽名钓誉吗?你根本就不在乎这是不是沽名钓誉,你在乎的只是美女本身。因此,如果包包觉得《程序员》上的人物都是为了沽名钓誉,这只能怪他自己没站对角度来看问题。站在看美女的角度来看这些人物,他们是否沽名钓誉与我有何关系呢?

我相信,美国的电视电影导演不会在乎美女上Playboy是否为了沽名钓誉,能通过试镜的都是合适的演员,Playboy只是提供了一种第三方认可外加选拔来源而已。我也相信,中国的公司不会在乎程序员上《程序员》是否为了沽名钓誉,能通过面试的都是合格的员工,《程序员》的作用自然是和Playboy一样。

小结

为什么我选择在包包跟Andres Liu的争论过后那么久还来讨论这件事?我只是希望大家明白,看待问题的角度是多种多样的。程序员往往比较理想化,因为代码总能按照理论的方式运作,但把这种理想化套在《程序员》的真实运作上就不可行了,或者套在一些更复杂的商业场景上就很容易出错。

举个例子,你觉得CCTV为什么喜欢曝光Baidu和Google的搜索结果问题呢?有很多人很单纯地认为,在竞争对手之间,一方受损肯定使得另外一方获利,因为这是个零和竞争。但在这不是一个封闭的零和竞争,把视觉锁定在这样一个竞争之内得出的结论不一定是对的。不信?我可以提供一些信息来拓宽你的思路。国内的传统媒体都喜欢负面新闻,当别人陷入公关危机的时候,就打个电话给对方的公关部门,“我们已经写好了整版你们的负面新闻准备付印,你们有计划增加明年的广告预算吗?”这使得一些公司投放广告只是为了跟媒体建立共同利益关系,限制媒体无法作出伤害自己的行为,否则媒体自己也会受到伤害(我营收减少股价下跌还哪有那么多广告预算给你)。

尝试换个角度看问题,你自然能够得出不同的结论。在你想要对一个你觉得很简单的问题下定论的时候,为什么不尝试换个角度再看看呢?

2009年11月17日星期二

我在 TechEd 2009 演讲的资源 (Silverlight & Ajax)

这是TechEd第二天下午的Silverlight课程资源。关于百度Hi的Silverlight实现方面的任何问题,都欢迎与我讨论。



这是TechEd第三天下午ASP.NET 4课程资源。与ASP.NET AJAX 4.0相关的问题可以在此讨论。

User Friendly 2009

这个周末有幸去上海参加了UPA China组织的用户体验行业大会User Friendly 2009。参会人员大多数都是设计师,会议内容也是他们熟悉的内容,但对于像我这样少数的参会工程师来说,这样的会议确实让人大开眼界。

在整场会议当中,我觉得让我印象最深刻的就是Jared M. Spool的Keynote了,我觉得其中一些观点是非常有价值的。至于其它的Workshop,也都能学到一些实实在在的东西,不过就没有Keynote那么震撼了。下面就分享一些我印象最深刻的内容:

从ROI到ROX

Jared M. Spool的Keynote标题为The Dawning of the Age of Experience(体验时代的来临)。这跟我们传统所说的产品设计有什么不同?那就是我们需要把视觉从产品放大到体验上来。在这里,我想借用MIX 09上Bill Buxton的Keynote中的几页幻灯片来解释一下:

Day One Keynote hosted by Ember

Bill Buxton有一辆这个型号的自行车,而且这也正是Roland Green获得公路赛冠军所用的自行车型号。画面上给你传达的是这辆自行车相关的信息,但又如何?这世界上还有千万款不同的自行车,为什么要在乎这特定的一辆?这不是人们想要购买的东西。

Day One Keynote hosted by Ember

这张幻灯片是同一辆自行车的不同摆放方式,就像是Pixar动画短片里面的那个独轮车一样。这赋予了它一定的个性,这个思路是对的,但这仍然不是你想要购买的东西。

Day One Keynote hosted by Ember

这才是体验设计师应该关注的事情——你骑着车从山上沿着小径冲下来,一直冲到溪谷里,伴随着你的尖叫水花四溅。这是一辆什么型号的自行车?从画面上看不出来。这是一辆什么牌子的自行车?也说不清楚。这从根本上就和自行没有关系,真正有关系的是你尖叫着冲进小溪里的那种刺激的体验。

过往我们谈论的是投资回报(ROI),现在我们谈论的是体验回报(ROX)。我们想要向用户推销我们的产品,但实际上他们想要从我们这里购买体验,因此我们必须改变思维方式,考虑向用户提供什么样的体验。

举个例子,用户在你的网站上购买机票,他想要买的其实不是机票这样一个商品,而是一趟舒适的商务旅行。因此,过往只卖机票的网站开始加上酒店服务。在你输入往返日期后,不仅仅搜索往返机票,还搜索目的地的酒店信息。只有我们向用户提供符合他们预期的体验时,我们才能够获取到高回报。

Chicken Sexer的故事

什么是Chicken Sexer?就是一些判断小鸡性别的专业人员,他们盯着眼前一群两星期大的小鸡,就能指出每一只是公的还是母的,精确率最高可达97%!你让他们说说经验,他们也可以归纳出一些来,例如根据毛色、屁股来判断小鸡性别,但这些规则对于外行人来说一点意义都没有,你实在无法看着一只小鸡然后利用这些规则判断出它的性别。

那么Chicken Sexer是如何练成的呢?新人必须跟着一位大师一起工作,新人负责指出小鸡性别,说错了大师就狠狠地打一下新人的手臂。在接受训练之前,任何正常人都有 50%的正确率;在开始接受训练之后,新人的正确率会先下跌到40%——因为他想得太多了。三个月后,正确率会慢慢恢复到50%,一年之后,你的正确率能够提高到60%;再过两三年,你的正确率能够提高到80%。如果你真的把这当作你毕生的事业来做,正确率可以提升到95%。

Jared M. Spool对这个故事的总结是,有些事情是可以学习而无法自省(introspection)的。在这里,“自省”是指对自身心智或情感变化的观察和检验。设计工作也属于可以学习无法自省——你可以通过学习让自己的设计能力获得提升,但你无法归纳出你在学习过程中发生了哪些变化,以便让其他人直接通过同样的变化以获得同样的结果。

我对这类技能的学习方法看法是,你必须在大师级人物的指导下学习,你才能成为大师。这不是任何人能够通过理论知识的学习或者基础技能的训练就能习得的,这些只能让你胜任需要这些技能的工作,但不能让你成为大师。在你成为大师后,你也无法将你的技能归纳为一组理论,你只能通过亲自训练让更多人习得跟你一样的技能。

企业内推广的两个关键

在Tencent CDC的统一体验的设计Workshop上,我了解到了在企业内推广统一体验的两个关键:老板和项目。

首先,需要老板重视这件事情。例如在Tencent内部,马化腾相当于“首席体验官”的角色,他重视这个事情,对于做得不好的他会站出来指责,别人自然愿意接受你要推广的,因为没有谁想被老板指责。其次,需要通过有价值的项目来证实你推广的东西。如果你的东西你应用于QQ,对其它Tencent产品来说自然就有说服力了。

上述就是User Friendly 2009对我最有启发的几点。如果你对此类会议的信息有兴趣,欢迎订阅我的博客:

2009年11月16日星期一

Tech·Ed 2009

今年是我第一次以讲师身份参加TechEd,碰巧TechEd所在的周末我们要搬家到百度大厦,所以我星期四早上搞掂打包工作后就赶到了会场。凭着《讲师指南》进入会场后,我直奔讲师休息室领取证件和衣服,然后再跑到MVP站台领取印章,接着等待第一天下午的Keynote开始。

尽管我没有参加Windows 7 Launch而跑去参加SD2C了,但我仍然觉得第一天Keynote的一半是Windows 7 Launch的重播。至于Keynote的另外一半,则是与Windows 7并列于New Efficiency系列的Windows Server 2008 R2以及Exchange Server 2010。我认为比较有趣的是Exchange Server 2010的演示,这个演示通过丢失笔记本电脑的场景来说明虚拟桌面的强大——换一台电脑,同一个桌面;这个演示还通过丢失手机的场景来说明远程擦除手机数据的特性。没错,这些功能都很好,但我个人的观点是——如果你经常丢失手机和电脑,Exchange Server 2010的价值才会被凸显出来。最后,我觉得Exchange Server 2010最有价值的功能就是内置的VoIP了,这相当于内置了一个Google Voice,搭配Outlook 2010的邮件视图,你就可以在自己的企业内享受全套的Gmail + Google Voice体验了。

Keynote之后的是面向Microsoft员工和讲师的Welcome Dinner,也是难得的交流时间——白天大家都忙于会务、讲课、停课,晚上才有时间停下来聊聊技术。当看到有人说自己讲了十年TechEd之后,就如同看到Rick Strahl博客上的的“MVP since 1997”一样震撼——原来有人一直在做这样的事情,并且坚持了10年之久。有兴趣有动力去做好一件事情不难,难就难在坚持做10年。

第二天上午我抽空去郭晓颖的Session上听了一下她是怎么讲Silverlight的,顺便在她的Q&A环节为自己的Session卖了一下广告。其余时间,我都在忙于调整我自己的pptx,直到下午我的Session开始为止。在Session结束后,我又花了一些时间解答大家的问题,以及帮大家盖MVP的章,然后才赶往Jeff的Session听他讲MVC。

第二天晚上是MVP Dinner,很高兴又能跟一群MVP朋友聚会了。为什么MVP Dinner有价值?这并不在于MVP为Microsoft创造了多少价值,而在于MVP擅长通过沟通来创造价值,因此能够跟一群MVP沟通一定能为你带来价值。因此,MVP Global Summit、MVP Open Day、MVP Dinner等等每年固定的聚会我都尽量不缺席。

最后一天,除了在王洪超的Session上讲讲我熟悉的ASP.NET AJAX 4.0,我就没什么别的工作了,因此我有时间就在会场周围走走,看看有谁要盖章的。有一次我走在三层展区,看到一位美女迎面走来,她看着我想要说什么但又没说,我就猜她想要找我盖章,但又不确定我是不是宣传单张上的那个人。于是我直接走过去问她,“你是要找我盖章吗?”她说是,然后还呼唤她的同伴过来盖章。我不知道如果我不主动问她的话事情会怎么样,但机会有时候就是这个样子,如果你看到了又不主动问,你就有可能错过,而问一下又不会有什么不好的。对我来说,不为她盖章并没什么损失;对她来说,或者就要多花一些时间去找另外一位MVP了。

2009年10月25日星期日

SD2C 2009 (Part 2 - Session & Forum)

SD2C的第一天晚上,我在「开放平台」和「PPT制作秘诀」两者之间犹豫,最后选择了去听蔡学镛的「尼古丁+咖啡因...不瞌睡的PPT制作秘诀」,原因是我觉得如果蔡学镛能教别人写不瞌睡PPT,那么他自己的PPT至少也应该是不瞌睡的。事后证明我的选择是没错的——别人告诉我「开放平台」论坛成了平台和开发人员互相责骂对方的战场。

蔡学镛把制作PPT比喻为制作生鱼片,要经过选材、处理、装饰这三步。在选材方面,一个PPT讲的内容最好是相当于一章书的内容,时间不能超过90分钟,而讲述的事情不能超过7件。接下来的处理,要注意做好破题,引起受众的兴趣,并且每一小节的结束时都要有总结。一页PPT所表达的意思,有可能相当于一个词、一句话、一段话,我们应该多用表达一句话的PPT页面。最后的装饰会需要用到不少图片,通过使用正确的关键字联想策略,往往都能搜索到你想要的图片。

SD2C的第二天,我原计划要听听周爱民的「实践者思想」和蔡学镛的「DSL设计与实践」的,结果上午听完Gary Bennett的「iPhone SDK简介」之后就被吸引住了,下午的时间都放在iPhone相关课程上了。这说明了,课程的首要条件还是要能把人吸引住,不会让人想要睡觉,也不会让人想要换个课室看看。在满足这个前提条件后,才有资格来讨论内容的知识性如何。我在听了几节iPhone开发的课程后,又开始对iPhone开发充满兴趣了。

晚上的「程序员软技能」论坛是我最期待的,因为这个话题可以讲得非常有震撼性。讲得好的话,这可以让很多程序员发现自身的unknown unknown(不知道自己不知道的事情)来,把这变为known unknown(知道自己不知道的事情)之后,只要努力了就能获得改善。结果主持人刘江自己戴上了嘉宾的帽子就开始不停地说,搞到台下有人举手说「在刚才的30分钟里面您已经讲了20分钟喇」。同时,估计因为刘江是做图书的,所以花了不少时间在读书的话题上,此外再提到了一些企业文化和沟通技能之类的东西,顺便还鄙视了一轮内地的高等教育连幼儿教育都不如(来自台湾清华的蔡学镛很无辜地无法参与到这个话题中来)。可能是我原本对这个话题的期望太高,所以我对这个论坛挺失望的。将来有空我会专门写文章讨论一下程序员常见的unknown unknown。

最后一天的SD2C我没有参加,因为已经没什么让我想要听的了,所以我选择了去参加Windows 7 Community Launch。有微软社区的人说SD2C是「傻蛋2.0大会」,选择了Visual Studio 2010 Beta 2 Launch、Windows 7 Launch、Windows 7 Community Launch这三天来举行,分明就是跟微软过不去。说实在的,我也觉得今年的SD2C日期选择失策了,明知道Windows 7对Microsoft有多么重要,也明知道发布日期是哪一天,还偏要选择一个冲突的日期。希望明年的SD2C能够选择一个更好的时间吧,相信会务今年已经通过举手投票搞清楚大家的喜好了——最好是在星期二三四开会,因为星期一和星期五工作压力小,同时星期六日是用来休息的。

2009年10月22日星期四

SD2C 2009 (Part 1 - Keynote & Meal)

一年一度的SD2C又来了,今年的时间由去年的两天半扩充到三天,第一天只设主会场,全部都是keynote。

跟去年的情况类似的是,keynote环节基本上就是赞助商专场,每个赞助商都来从技术的角度说一下自己当前最重视的市场。例如说Microsoft开始重视Team Foundation Server的市场占有率了,于是就在Visual Studio 2010的keynote上介绍TFS的特性,所占比例比VS2010自身还要多。当然,keynote里面也有一些是有启发性的,例如IBM讲的大型企业中的敏捷实践,这就属于我认为比较好的keynote。

由于今年的话题集中在云和移动上面,所以keynote大多与这两个领域相关。能够上去讲keynote的人,当然是要有一定水平的,不能讲一些很肤浅的内容,但如果因此就把keynote弄得很高深或者很抽象又弄巧成拙了。高深的内容固然能够表现出演讲者很有水平,问题是这样一个面向上千受众的活动,有多少人能够对如此多个keynote的话题都有深入了解呢?就算你对其中一个话题很有了解,觉得讲得如此有高度很对你胃口,但你也总有n个其它不了解的话题吧,那些话题你听起来也会觉得是云里雾里的。

在我看来,优秀的keynote应该是这样子的——它能够阐述一个很新颖的观点,或者提供一种全新的视觉,使得无论你原本对这个话题了解多少,你都会被震撼到——这个事情我有所了解,但为什么我之前就没有尝试过从这个角度来思考呢?这其实也就是Punch Party的优势,无论受众具备怎么样的背景,都能感受到全新观点带来的冲击力。如果能够打到这样的效果,我并不在意一个keynote是否顺便在推销某个企业或者产品。

接下来要说说吃饭的问题,但在此之前让我先说说我对MVP Program的一些观点,这纯粹是个人观点,也不一定符合事实。我觉得,就拿MVP Summit这样一个全球峰会来说,每年要接待来自全球各地的几千人,为他们提供四天的食宿,平均分摊到每个与会者身上的费用肯定是不少的。如果MVP Summit的作用仅仅在于让大家聊聊技术,我想Microsoft作为一家商业公司是不会愿意花这笔钱的,就算Microsoft想要这样做,股东们也不会同意。因此,MVP Program必然是能够产生价值的,并且分摊下来每位MVP为Microsoft创造的价值应该大于Microsoft为他所作的花费。这些价值可能包括,MVP为用户解答问题,为客户带来技术培训,也就相当于节省了Microsoft的服务成本和市场费用。同时,MVP也会积极反馈关于产品的各种问题,这也属于创造价值的部分。

回到SD2C的事情上来,CSDN其实对热心贡献的用户非常好,例如说提供SD2C赠票。但是CSDN从来都对此事保持非常低调的姿态,生怕激怒那些付钱买票的人,同时也怕更多人去找他们要赠票。我的疑问是,为什么这些对热心用户的正面激励不能好像MVP Program那样光明正大的做呢?如果一位热心用户为CSDN创造的价值能够大于SD2C门票的成本价,那送他一张赠票肯定还是赚了的。

为什么我要说这件事?因为今年的赠票不包括背包和晚餐。不包括背包我觉得可以理解,金融危机嘛,能够让我免费来参加SD2C我已经非常满足了,没有背包也没所谓。但是不包括晚餐就非常不人性化了——这不是逼拿赠票的人不参加晚上的活动提前离场吗?况且,班车还要等晚上活动结束后才有,傍晚离开还是挺不方便的。于是我就在会场的做啥大屏幕发起讨论,问问没晚餐票的人准备如何吃晚餐——我的本意只是想组织落单的赠票使用者一起吃饭,反正拿赠票的大多是CSDN活跃用户,大家也都是认识的。同时我考虑到CSDN不希望公开有赠票这件事,我一开始也刻意回避了这个话题,后来有人公开讨论了我也就跟着讨论了。

结果是什么呢?CSDN会务组单独给我一张晚餐票,并告诉我原因是蒋涛看到做啥大屏幕的内容了。这就让我感觉到很奇怪了,两天的晚餐票值多少钱呢?值得把事情搞到这样子吗?CSDN自己是做媒体的,应该十分清楚媒体的影响力。赠票邀请来的人,都是技术社区上最活跃的人,他们的博客都是具有相当影响力的媒体,给这些人留下这样一个负面印象,CSDN损失的是多少价值呢?这是否已经超过了两张晚餐票的价钱呢?我相信以蒋涛在CSDN这么多年来做媒体的经验,应该在这件事被搬上大屏幕讨论之前就把帐算清楚了吧。

最后一点,也就是这样做是否厚道。如果CSDN以媒体身份受邀参加这样一场大会,本来是希望借此机会向自己的读者传递更多更好的技术资讯的,然而却受到大会主办方不公平的对待,搞到连饭都没得吃,你们会喜欢被这样对待吗?如果你们自己也不喜欢被这样对待,那就在如此待人之前先考虑清楚。

2009年10月16日星期五

国际长途比国内市话便宜

众所周知,在国外买国际长途电话卡打到中国,比国内市话还要便宜。这说明了国内话费之高完全是垄断造成的,其实根本就不值这样的价格,因此我们能够打国际长途就打国际长途吧,尽量别再为国内垄断行业支付额外的话费了。

在国内要通过国际长途拨打国内电话,通过VoIP就行了,Skype和Google Voice都可以,它们的价格都很便宜。就拿移动话费来说,非漫游非长途要¥0.29,漫游或/且长途要¥0.39,但是Skype只要$0.021(每次接通需要$0.039连接费),而Google Voice只要$0.02(无需连接费)。

如何使用Skype拨打国内电话呢?你可以用信用卡通过PayPal购买Skype credit,然后就可以用任何Skype客户端拨打国内号码了。对我来说最方便的是,iPhone上面有Skype客户端,只要在有Wi-Fi的地方我都可以用它来打电话。当然,Skype通话质量会受带宽影响,如果你所在的区域信号不好或者带宽有限,通话质量就会下降。

在没有Wi-Fi的时候怎么办呢?通过Google Voice的回拨服务可以解决问题,当然前提是要通过网络(GPRS/EDGE或3G)访问Google Voice。Google Voice拨号可以拨通对方的国内电话,但回拨只能拨美国电话,怎么办呢?我们可以通过购买Skype Online Number解决这个问题。这时候Google Voice和Skype会同时拨通双方电话,然后建立双方通话,话费也就是$0.041而已,还是比移动话费稍微便宜一点。

可能很多人会怀疑,花那么多美元来拨打国际长途,真的比国内话费要便宜吗?这就要看你的具体使用方式和套餐组合方式喇。我曾经因为每天漫游加长途贡献了不少人民币给中国移动,换成Skype后至少省了一半的钱,所以从此我就决定改为贡献美元给Skype好了。如果你并不介意花$10购买Skype credit试试,可以实际使用一段时间后对比一下烧钱速度后再决定。

2009年9月17日星期四

你的网站「被兼容」了吗?

一般情况下,我们只会讨论我们的网站如何主动兼容某某浏览器,被动地等待浏览器来兼容我们的网站是不切实际的幻想——哪个浏览器会那么伟大,原意主动为一个不兼容的网站而作出改变呢?IE8就是这样一个伟大的浏览器,Microsoft就是一家这样伟大的企业。

故事是这样的,我们有一小段JavaScript依赖于userAgent属性,同样是用IE8进行浏览,在测试环境上userAgent显示为MSIE 7.0,而在生产环境上userAgent显示为MSIE 8.0。为什么会这样呢?打开Developer Toolbar后,发现原来是Browser Mode这个开关在搞鬼——当Browser Mode是Internet Explorer 8的时候,userAgent就是MSIE 8.0;当Browser Mode是Internet Explorer 8 Compatibility View(兼容性视图)或Internet Explorer 7的时候,userAgent就是MSIE 7.0了。

接下来的问题是,我们并没有刻意去拨动这个开关啊,两个相同的页面怎么在不同的环境中默认显示为不同的Browser Mode呢?我的猜想是,这是由于域名不同而引起的——Microsoft自己维护着一个Compatibility View List,当访问该List中的站点时,IE8会自动启用Compatibility View,也就是将Browser Mode切换到Internet Explorer 8 Compatibility View。接着我在地址栏输入以下地址,检查了一下我本地最近更新的List:

res://iecompat.dll/iecompatdata.xml

事实表明,我们测试用的baidu.com域名确实在上述List中,但部署到baidu.jp后也就脱离了该List。这就很好地解释了我们遇到问题,同时也提醒我们域名已经成为了IE8测试中不可避免的一个紧耦合因素。在过去,我们可以简单地认为,部署在不同URL的相同页面在同一款浏览器中显示出来总是一样的。但现在我们必须修正这句话了,仅当不同URL都基于同一个域名时上述命题仍然成立。

通过这个案例,希望能让大家了解到在开发与测试过程中保持域名一致的重要性。如果你开发的页面要部署到example.com,你最好在develop.example.com上开发,在test.example.com上测试,然后再部署。如果你需要在本机进行开发测试,也要通过改hosts模拟一个localhost.example.com来进行测试与调试,以确保代码在最终部署后能执行在相同的环境下。

2009年8月23日星期日

Twitter Suspension

我的Twitter帐号,也就是@CatChen,经历了一次长达半个月的suspension。

Twitter Suspension

8月5日早上醒来,发现我的Tweetie不能再更新,打开Web看看,发现如下提示:
This account is currently suspended and is being investigated due to strange activity. If we have suspended your account mistakenly, please let us know. See Suspended Accounts for more information.


这时候我觉得很无辜,竟然被误判为发垃圾信息的用户了。当然,这可能和最近Twitter系统不稳定有关,因为有些人的following被清空了,自然会导致另外一些人的followers数量暴跌,由此触发垃圾用户判别标准也是有可能的。无论如何,我只能按照它给出的Suspended Account帮助链接寻求帮助了。

打开帮助页后,发现了一条令人极度无奈的信息——“帐号有可能要被封禁至少30天以备研究之用”。你Twitter要研究和改进反垃圾信息策略可以啊,但你不应该为了自己的研究就封禁我的帐号30天吧!不过我还是先提交请求好了,反正现在也没什么更好的解决方案了。

第一次在Web上提交请求,系统发了一封自动回复给我,我大概看了一下,就存档了。结果请求第二天就关闭了,问题没有得到解决。第二次提交请求,质询为什么请求关闭了但封禁没结束,系统再发了一封同样的自动回复给我,我仔细阅读后才发现,信的末端说明要回复此信解释为什么我觉得这个封禁是错误的。回信后,系统指派了人负责跟踪这个问题,但一直没有任何进展。

后来我在Twitter上看到有人说,要联系suspended@twitter.com,我就再发信去质询进度,这相当于创建了第三个请求。系统继续发回自动回复,我又继续回信解释,接着问题第二天就解决了。虽然我并不能确定,是不是联系suspended@twitter.com确实帮助我快速解决了这个问题,但我建议不幸被封禁的人都应该试一试这个方法,然后告诉我这是不是一个有效的方法。

最后,这次事件引起了我的一个想法——Twitter如此错误封禁我的帐号半个月,和GFW随意封禁一些无关紧要的网站随后又解封,有什么本质上的区别吗?对我造成的影响其实是没有区别的,这两者都只会提高我日常生活的成本。

一直以来,我们依赖于网络从不同的网站获取信息,而GFW有能力随时终止网络服务的能力,所以GFW让人觉得讨厌。最近,Twitter也成为了我们获取信息必不可缺的一个途径,它自身同样具备随时终止服务的能力,因此Twitter并不可爱。与Twitter类似的其它核心服务也如此,如果你的Gmail帐号有一天被封禁了,你使用的所有Google服务随之而终止,Google无论如何都坚持它封禁你的帐号是没有违反服务条款的,这时候Google本质上和GFW还有任何区别吗?

2009年8月16日星期日

jQuery is DSL (Part 2 - jQuery)

jQuery的Internal DSL形式

在上一篇文章里面,我们了解到了Internal DSL的具体形式,形如:

/* Method Chaining */
computer()
  .processor()
    .cores(2)
    .i386()
  .disk()
    .size(150)
  .disk()
    .size(75)
    .speed(7200)
    .sata()
  .end();


然后我们在看看一段典型的jQuery代码:

$("ul#contacts li.item")
  .find("span.name")
    .click(function(e) { $(e.target).siblings(".more").toggle(); })
    .end()
  .find("input.delete")
    .click(function(e) { $(e.target).parents(".item").remove(); })
    .end()
  .find("div.more")
    .hide()
    .end();


从结构上来说,是不是跟上面那一段Internal DSL的例子很相似?就算我们不看对应的HTML,我们也能猜到这段jQuery代码的含义:
  • 遍历<ul id="contacts">中的每一个<li class="item">
    (这看起来是个联系人列表)
    • 对于里面的<span class="name">
      • 绑定click事件,操作是显示/隐藏class="more"兄弟节点
        (这是估计联系人姓名,点击后切换详细信息的显示/隐藏)
    • 对于里面的<input class="delete">
      • 绑定click事件,操作是把class="item"父节点删除
        (这应该是用来删除联系人的)
    • 对于里面的<div class="more">
      • 隐藏这个div
        (默认隐藏详细信息?)
从这里我们已经能够看出jQuery的Internal DSL形式带来的好处——编写代码时,让代码更贴近作者的思维模式;阅读代码时,让读者更容易理解代码的含义。不信?我们看看与jQuery拥有相似功能的Prototype是如何实现上述逻辑:

$$("ul#contacts li.item span.name")
  .invoke("observe", "click",
    function(e) { $(e.target).next(".more").toggle(); });
$$("ul#contacts li.item input.delete")
  .invoke("observe", "click",
    function(e) { $(e.target).up(".item").remove(); });
$$("ul#contacts li.item div.more")
  .invoke("hide");


这是我用Prototype所能写出的最贴近Internal DSL的形式了。(如果你能够写出一个更自然的版本,欢迎分享。)在Prototype里面,能够返回一组元素的操作就只有$$(),并且它只能作用于全局,缺乏jQuery中find()或者filter()的功能,所以这一组描述联系人列表行为的语句无法组合在一起,必须逐一定义每类元素的行为。此外,此例子中每类元素都仅仅指定了一个行为,因此Prototype的invoke()写法看起来还是和jQuery的click()写法很相近的。但如果一类元素拥有多个行为,Prototype的invoke()就不能好像jQuery那样链式调用下去了,必须每一个行为重头写一个$$(),或者把invoke()改成each()加匿名函数。无论是那种做法,都只会降低代码的可读性。

jQuery的语法分析器

我们都知道,Internal DSL的实现依赖于对语法分析器的封装,对Internal DSL的调用其实都是对语法分析器的调用,经过语法分析后再构造出对底层API的调用。例如jQuery当中的click(),它依赖于当前的状态,也就是前面$()筛选出来的节点集合,把click()解释为要为这一组节点绑定DOM的click事件,最后再调用DOM API完成任务。在这个例子当中,DOM API相对jQuery API而言就是底层API了。

jQuery可以说是挑了一个最容易实现的语法模型来做,永远只有一种token,因此永远也只有一种状态,这种状态当然也是永远有效的,你根本不可能给jQuery输入一个当前状态无效的token。jQuery的唯一状态就是一个jQuery对象实例,其本质就是一个元素集合。读入的token可能是各种针对这个元素集合的操作,但它的返回一定还是一个元素集合。这使得jQuery的语法分析器不会进入无效状态,也就无需判断无效状态,因此大大简化了Internal DSL实现中常见的一个难题。

小结

通过拿jQuery和Prototype做对比,我们可以发现jQuery用非常低的成本实现了Internal DSL,同时带来了Prototype所没有的明显好处。这可以看作是一个很好的范例——如果你需要描述的业务逻辑能够归纳为简单的语言模式,为此实现一门Internal DSL的性价比将会是很高的。你需要做的仅仅是为这个简单的语言模型实现一个简单的解释器,接着你就可以享受贴近人类思维模式的接口了。

最后,如果你喜欢我的文章,可以考虑订阅我的博客:

2009年8月10日星期一

jQuery is DSL (Part 1 - DSL)

jQuery刚刚出来的时候,我没有太多关注它,觉得这不过是Yet Another JavaScript Library。早期的jQuery专注于DOM节点的筛选与操作,不提供众多的基础类扩展,更不提供UI组件,因此体积能够做到很小。然而,我实在看不出它和我熟悉的Prototype比有什么明显的优势——jQuery能做的各项独立的操作,Prototype都能做。

后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP Omar AL Zabir就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考AsyncOverload)。最后,我突然明白到,这其实就是一种Internal DSL嘛!

在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——

什么是Internal DSL?

DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。例如说,我们有专门描述字符串特征的正则表达式,有专门描述数据库查询的SQL,有专门描述XML结构的DTD和XSD,甚至有专门描述XML变换的XSLT,这些都是DSL。

当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
  1. 使用通用语言描述该领域的问题(non-DSL)
  2. 发明一门全新的语言描述该领域的问题(External DSL
  3. 在一门现成语言内实现针对领域问题的描述(Internal DSL
例如说,我们现在要描述一个很简单的金融领域问题,“我在花旗银行存款$200”这样一句话对应的三种法写法可能是:(假设已经存在I和CitiBank两个实体实例)
  1. I.DepositTo(new USD(200), CitiBank); /* C# */
  2. I deposit 200USD to CitiBank /* E-DSL */
  3. I.deposit(200.USD()).to(CitiBank); /* I-DSL */
第1种做法的成本最低,你只需要有OO的思想就可以了,你总能把实体类设计出来,但可能和人类描述此领域问题的思维方式有一定偏差(为什么USD可以new?为什么不是deposit [something] to [somewhere]?)。

第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。

第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。

如何构造Internal DSL?

常见的两种Internal DSL实现方法是Method ChainingFunction Sequence。如果我们需要描述一台机器的硬件组成,两种实现方式的代码分别如下:

/* Method Chaining */
computer()
  .processor()
    .cores(2)
    .i386()
  .disk()
    .size(150)
  .disk()
    .size(75)
    .speed(7200)
    .sata()
  .end();


/* Function Sequence */
computer();
  processor();
    cores(2);
    processorType(i386);
  disk();
    diskSize(150);
  disk();
    diskSize(75);
    diskSpeed(7200);
    diskInterface(SATA);


无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。

由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。

小结

在这篇文章里,我们了解了Internal DSL与External DSL之间的区别,同时还了解到实现Internal DSL的具体方式,这为我们接下来讨论jQuery的Internal DSL式接口做好了铺垫。在下一篇文章里,我们将深入地来看看为什么jQuery的接口要如此设计,它能为用户带来了怎样的便利,同时它自身的实现上又有什么优势。

如果你不希望错过下一篇文章,你可以考虑订阅我的博客:

2009年7月2日星期四

让 JavaScript 轻松支持函数重载 (Part 2 - 实现)

上一篇文章里,我们设计了一套能在JavaScript中描述函数重载的方法,这套方法依赖于一个叫做Overload的静态类,现在我们就来看看如何实现这个静态类。

识别文本签名

我们先来回顾一下上一篇文章中提到的Overload用例:

var extend = Overload
  .add("*, ...",
    function(target) { })
  .add("Boolean, *, ...",
    function(deep, target) { });


我们允许用户输入一个字符串,表示某一个重载的签名。在用户调用函数时,我们需要拿着用户输入的参数实例去跟签名上的每一个参数类型作比较,因此我们需要先把这个字符串转换为类型数组。也就是说,字符串"Boolean, Number, Array"应该转换为数组[Boolean, Number, Array]。

在进行转换之前,我们先要考虑处理两个特殊类型,就是代表任意类型的"*",和代表任意数量的"..."。我们可以为它们定义两个专有的类型,以便在Overload内对它们做出特殊的兼容性处理:

Overload.Any = function() {};
Overload.More = function() {};


在有了这两个类型之后,字符串"Boolean, *, ..."就会被正确转换为数组[Boolean, Overload.Any, Overload.More]。由于Overload.Any和Overload.More都是函数,自然也都可以看做类型。

在这两个类型得到正确处理后,我们就可以开始编写识别文本签名的转换函数了:

if (signature.replace(/(^\s+|\s+$)/ig, "") == "") {
  signature = [];
} else {
  signature = signature.split(",");
  for (var i = 0; i < signature.length; i++) {
    var typeExpression =
      signature[i].replace(/(^\s+|\s+$)/ig, "");
    var type = null;
    if (typeExpression == "*") {
      type = Overload.Any;
    } else if (typeExpression == "...") {
      type = Overload.More;
    } else {
      type = eval("(" + typeExpression + ")");
    }
    signature[i] = type;
  }
}


我想这段代码相当容易理解,因此就不再解释了。我第一次写这段代码时忘记写上面的第一个if了,导致空白签名字符串""无法被正确识别为空白签名数组[],幸好我的unit test代码第一时间发现了这个缺陷。看来编写unit test代码还是十分重要的。

匹配函数签名

在我们得到函数签名的类型数组后,我们就可以用它和输入参数的实例数组做匹配了,以此找出正确的重载。在讨论具体如何匹配函数签名以前,我们先来看看C#VB.NET这样的语言是如何处理函数重载匹配的。一般语言进行函数重载匹配的流程都是这样子的:
  1. 参数个数 - 参数个数不对的重载会被排除掉
  2. 参数类型 - 参数类型无法隐式转换为签名类型的会被排除掉
  3. 匹配个数 - 排除完毕后,剩下匹配的签名个数不同处理方法也不同
    • 0个匹配 - 没有命中的匹配
    • 1个匹配 - 这个就是命中的匹配
    • 2个或以上的匹配 - 如果能在这些匹配中找出一个最佳匹配,那就命中最佳匹配;否则不命中任何匹配
在这一节里面,我们先处理流程中的前两个步骤,把参数个数或参数类型不一致的签名去掉:

var matchSignature = function(argumentsArray, signature) {
  if (argumentsArray.length < signature.length) {
    return false;
  } else if (argumentsArray.length > signature.length
      && !signature.more) {
        return false;
  }
  for (var i = 0; i < signature.length; i++) {
    if (!(signature[i] == Overload.Any
      || argumentsArray[i] instanceof signature[i]
      || argumentsArray[i].constructor
        == signature[i])) {
          return false;
    }
  }
  return true;
};


为了作长度对比,我们需要在这个函数外对表示任何参数个数的"..."作一下特殊处理:

if (signature[signature.length - 1] == Overload.More) {
  signature.length = signature.length - 1;
  signature.more = true;
}


这一段代码将会整合到第一节的转换函数末端,以便matchSignature函数能够轻易判断出参数与签名是否匹配。在最理想的情况下,我们对输入参数类型匹配到0个或1个重载,这样我们很容易就判断出命中哪个重载了。但如果有2个或以上的重载匹配,那么我们就要从中挑选一个最优的了,这正是下一节要讨论的内容。

处理多重匹配

关于C#是如何从多重匹配中选出较为匹配的重载,可以看C# Language Specification中的有关章节。我觉得通过三个简单的例子就能说明问题:

long Sum(int x, int y) { return x + y; }
long Sum(long x, long y) { return x + y; }
Sum(0, 1);


由于0和1这两个参数会被编译器理解为int类型,对于第1个重载它们都不用进行类型转换,都与第2个重载它们都要进行类型转换,因此第1个重载较优。

long Sum(int x, long y) { return x + y; }
long Sum(long x, int y) { return x + y; }
Sum(0, 1);


在第1个参数上,第1个重载较优;在第2个参数上,第2个重载较优。在这种情况下,任何一个重载都不优于另一个,找不到较优重载编译器就报错。

long Sum(int x, int y) { return x + y; }
long Sum(int x, long y) { return x + y; }
long Sum(long x, int y) { return x + y; }
Sum(0, 1);


在第1个参数上,第1个重载优于第3个重载,于第2个重载无异;在第2个参数上,第1个重载优于第2个重载,于第3个重载无异。尽管第2个重载于第3个重载分不出个优劣来,但我们可以确定第1个重载比它们都要好,因此编译器选择了第1个重载。

假设我们有一个overloadComparator的比较函数,可以比较任意两个签名之间的优劣,我们需要对签名仅仅两两比较,以找出最优重载吗?事实上是不需要的,我们可以利用Array的sort方法,让它调用overloadComparator进行排序,排序后再验证前两名的关系就可以了——如果并列,则不命中任何一个;如果有先后之分,则命中第一个。

具体的overloadComparator代码就不在这里给出了,它依赖于另一个名为inheritanceComparator的比较函数来对比两个签名的参数类型哪一个更贴实际传入的参数类型,里面用到了一种比较巧妙的方法来判断两个类型是否为继承关系,以及是谁继承自谁。

小结

现在我们有了一个JavaScript的函数重载库,完整代码请看这里:函数重载库Overload。希望这个库能有效帮助大家提升JavaScript代码的可读性,降低大型Ajax项目的维护成本。如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:

2009年7月1日星期三

让 JavaScript 轻松支持函数重载 (Part 1 - 设计)

JavaScript支持重载吗?

JavaScript支持函数重载吗?可以说不支持,也可以说支持。说不支持,是因为JavaScript不能好像其它原生支持函数重载的语言一样,直接写多个同名函数,让编译器来判断某个调用对应的是哪一个重载。说支持,是因为JavaScript函数对参数列表不作任何限制,可以在函数内部模拟对函数重载的支持。

实际上,在很多著名的开源库当中,我们都可以看到函数内部模拟重载支持的设计。例如说jQuery的jQuery.extend方法,就是通过参数类型判断出可选参数是否存在,如果不存在的话就对参数进行移位以确保后面的逻辑正确运行。我相信很多人在写JavaScript时也写过类似的代码,以求为功能丰富的函数提供一个(或多个)简单的调用入口。

不过做种做法一个根本的问题,那就是违反了DRY原则。每个支持重载的函数内部都多出来一段代码,用于根据参数个数和参数类型处理重载,这些代码暗含着重复的逻辑,写出来却又每一段都不一样。此外,这些代码要维护起来也不容易,因为阅读代码时你并不能一眼看出函数支持的几种重载方式是什么,要对重载做出维护自然也困难。

描述重载入口的DSL

我希望能够在JavaScript中以一种简单的方式来描述重载入口。最好就如同在其它语言中一样,使用函数签名来区分重载入口,因为我认为函数签名就是这方面最好的DSL。我假想中最符合JavaScript语法的重载入口描述DSL应该是这样子的:

var sum = new Overload();
sum.add("Number, Number",
  function(x, y) { return x + y; });
sum.add("Number, Number, Number",
  function(x, y, z) { return x + y + z; });


在描述好重载入口与对应函数体后,对sum函数的调用应该是这样子的:

sum(1, 2);
sum(1, 2, 3);


上述代码在我看来非常清晰,也非常容易维护——你可以一眼看得出重载入口的签名,并且要修改或者增加重载入口都是很容易的事情。但是我们遇到了一个问题,那就是JavaScript里面的函数是不能new出来的,通过new Overload()获得的对象一定不能被调用,为此我们只能把Overload做成一个静态类,静态方法返回的是Function实例:

var sum = Overload
  .add("Number, Number",
    function(x, y) { return x + y; })
  .add("Number, Number, Number",
    function(x, y, z) { return x + y + z; });

必要的重载入口支持

想象一下,有哪些常见的JavaScript函数入口是用上述DSL无法描述的?我所知道的有两种:
任意类型参数
假想我们要写一个each函数,对于Array就迭代它的下标,对于其它类型就迭代它的所有成员,这两个函数入口的参数列表如何声明?如果用C#,我们会如此描述两个函数入口:

void Each(IEnumerable iterator) { }
void Each(object iterator) { }


然而在JavaScript当中,Object不是一切类型的基类,(100) instanceof Object的结果为false,所以我们不能用Object来指代任意类型,必须引入一个新的符号来指代任意类型。考虑到这个符号不应该与任何可能存在的类名冲突,所以我选择了用"*"来表示任意类型。上述C#代码对应的JavaScript应该是这样子的:

var each = Overload
  .add("Array",
    function(array) { })
  .add("*",
    function(object) { });
任意数量参数
在JavaScript的函数里面,要求支持任意数量参数是很常见的需求,相信使用率比C#里面的params关键字要多得多。在我们之前制定的规则当中,这也无法描述的,因此我们要引入一个不和类名冲突的符号来表示C#中的params。我选择了用"..."表示params,意思是这里出现任意多个参数都是可以接受的。让我们看看jQuery.extend的重载应该如何描述:

var extend = Overload
  .add("*, ...",
    function(target) { })
  .add("Boolean, *, ...",
    function(deep, target) { });

小结

在这篇文章当中,我们尝试设计出一种适用于JavaScript且易读易维护的函数重载写法。在下一篇文章当中,我们将会尝试编写Overload类,以实现这一设计。如果你不希望错过的话,欢迎订阅:

写个 JavaScript 异步调用框架 (Part 6 - 实例 & 模式)

我们用了5篇文章来讨论如何编写一个JavaScript异步调用框架(问题 & 场景用例设计代码实现链式调用链式实现),现在是时候让我们看一下在各种常见开发情景中如何使用它了。

封装Ajax

设计Async.Operation的最初目的就是解决Ajax调用需要传递callback参数的问题,为此我们先把Ajax请求封装为Async.Operation。我在这里使用的是jQuery,当然无论你用什么基础库,在使用Async.Operation时都可以做这种简单的封装。

var Ajax = {};

Ajax.get = function(url, data) {
  var operation = new Async.Operation();
  $.get(url, data, function(result) {
    operation.yield(result);
  }, "json");
  return operation;
};

Ajax.post = function(url, data) {
  var operation = new Async.Operation();
  $.post(url, data, function(result) {
    operation.yield(result);
  }, "json");
  return operation;
};


在我所调用的服务器端API中,只需要GET和POST,且数据都为JSON,所以我就直接把jQuery提供的其它Ajax选项屏蔽掉了,并设置数据类型为JSON。在你的项目当中,也可以用类似的方式将Ajax封装为若干仅仅返回Async.Operation的方法,将jQuery提供的选项都封装在Ajax这一层内,不再向上层暴露这些选项。

调用Ajax

把Ajax封装好后,我们就可以开始专心写业务逻辑了。

假设我们有一个Friend对象,它的get方法用于返回单个好友对象,而getAll方法用于返回所有好友对象。于此对应的是两个服务器端API,friend接口会返回单个好友JSON,而friendlist接口会返回所有好友名称组成的JSON。

首先我们看看较为基础的get方法怎么写:

function get(name) {
  return Ajax.get("/friend",
    "name=" + encodeURIComponent(name));
}


就这么简单?对的,假如服务器端API返回的JSON结构正好就是你要的好友对象结构的话。如果JSON结构和好友对象结构是异构的,或许你还要加点代码来把JSON映射为对象:

function get(name) {
  var operation = new Async.Operation()
  Ajax.get("/friend", "name=" + encodeURIComponent(name))
    .addCallback(function(json) {
      operation.yield(createFriendFromJson(json));
    });
  return operation;
}

Ajax队列

接下来我们要编写的是getAll方法。因为friendlist接口只返回好友名称列表,因此在取得这份列表后我们还要逐一调用get方法获取具体的好友对象。考虑到在同时进行多个friend接口调用可能触发服务器的防攻击策略,导致被关小黑屋一段时间,所以对friend接口的调用必须排队。

function getAll(){
  var operation = new Async.Operation();
  var friends = [];
  var chain = Async.chain();
  Ajax.get("/friendlist", "")
    .addCallback(function(json) {
      for (var i = 0; i < json.length; i++) {
        chain.next(function() {
          return get(json.shift())
            .addCallback(function(friend) {
              friends.push(friend);
            });
        });
      }
      chain
        .next(function() { operation.yield(friends); })
        .go();
    })
  return operation;
}


在这里,我们假设friendlist接口返回的JSON就是一个Array,在获取到这个Array后构造一个等长的异步调用队列,其中每一个调用的逻辑都是一样的——取出Array中首个好友的名称,用get方法获取对应的好友对象,再将好友对象放入另一个Array中。在调用队列的末端,我们再追加了一个调用,用于返回保存好友对象的Array。

在这个例子当中,我们没有利用调用队列会把上一个函数的结果传递给下一个函数的特性,不过也足够展示调用队列的用途了——让多个底层为Ajax请求的异步操作按照固定的顺序阻塞式执行。

由于底层异步函数返回的就是Async.Operation,你可以直接把它传递给next方法,也可以用匿名函数包装后传递给next方法,而匿名函数内部只需要一个return。

延时函数

在上面的例子中,使用队列是为了避免触发服务器的防攻击策略,但有时候这还是不够的。例如说,服务器要求两个请求之间至少间隔500毫秒,否则就认为是攻击,那么我们就要在队列里面插入这个间隔了。

在原本next方法调用的匿名函数中手动加入setTimeout是一个办法,但为什么我们不写一个辅助函数来解决这类问题呢?让我们来写一个辅助方法并让它和Async.Operation无缝结合起来。

Async.wait = function(delay, context) {
  var operation = new Async.Operation();
  setTimeout(function() {
    operation.yield(context);
  }, delay);
  return operation;
};

Async.Operation.prototype.wait = function(delay, context) {
  return this.next(function(context) {
    return Async.wait(delay, context);
  });
}


在有了这个辅助方法后,我们就可以在上述getAll方法中轻松实现在每个Ajax请求之间间隔500毫秒。在for循环内的加上对wait的调用就可以了。

for (var i = 0; i < json.length; i++) {
  chain
    .wait(500)
    .next(function() {
      return get(json.shift())
        .addCallback(function(friend) {
          friends.push(friend);
        });
  });
}

小结

通过一些简单的例子,我们了解到了Async.Operation常见的使用方式,以及在有需要的时候如何扩展它的功能。希望Async.Operation能够有效帮助大家提高Ajax应用的代码可读性。

最后,如果大家希望将来继续读到类似的JavaScript开发模式相关的文章,不妨考虑订阅我的博客:

2009年6月30日星期二

写个 JavaScript 异步调用框架 (Part 5 - 链式实现)

在上一篇文章里面,我们为异步调用框架设计了一种链式调用方式,来增强异步调用队列的代码可读性,现在我们就来编写实现这部分功能的代码。

调用入口

链式调用存在Async.go方法和Async.chain方法两个入口,这两个入口本质上是一致的,只是Async.chain方法在调用时先不提供初始参数,而Async.go方法在调用时提供了初始参数并启动异步调用链。

Async.chain = function() {
  var chain = new Async.Operation({ chain: true });
  return chain;
};

Async.go = function(initialArgument) {
  return Async.chain().go(initialArgument);
}


在这里我们可以看到,链式调用本身也是一个Async.Operation,链式调用所需的go方法和next方法都是在Async.Operation上面做的扩展,并且这个扩展不会很难,这将在下一小节说明。

扩展方法

我们都知道,通过addCallback方法添加的回调函数是会被逐一执行的,至少同步函数如此,因此我们可以用Async.Operation的这一特性来维护异步调用队列,前提是我们为它加上对异步调用进行队列的支持。

对于异步调用进行队列的支持,我们稍后再来处理,首先我们利用现成的addCallback方法和yield方法扩展出go方法和next方法。

this.go = function(initialArgument) {
  return this.yield(initialArgument);
}

this.next = function(nextFunction) {
  return this.addCallback(nextFunction);
};


实际上,go方法和next方法直接调用的正是yield方法和addCallback方法。go方法的语义与yield方法一样,传递一个参数给Async.Operation实例,并且启动调用队列。同时,next方法的语义和addCallback方法,添加一个调用到队列的末端。

异步队列

如何才能让原本仅支持同步的队列变得也支持异步?这需要检测队列中的每一个调用的返回,如果返回类型为Async.Operation,我们知道是异步调用,从而使用特殊的方法等它执行完后再执行下去。

callbackResult = callback(self.result);
self.result = callbackResult;
if (callbackResult && callbackResult instanceof Async.Operation) {
  innerChain = Async.chain();
  while (callbackQueue.length > 0) {
    innerChain.next(callbackQueue.shift());
  }
  innerChain.next(function(result) {
    self.result = result;
    self.state = "completed";
    self.completed = true;
    return result;
  });
  callbackResult.addCallback(function(result) {
    self.result = result;
    innerChain.go(result);
  });
}


如果调用返回了一个Async.Operation实例,我们就利用它自身的addCallback方法帮我们执行队列中余下的调用。准确来说,是我们构造了一个新的调用链,把队列余下的调用都转移到新的调用链上,然后让当前异步调用在回调中启动这个新的调用链。

此外还有一些地方我们需要略作修改,以兼容新的异步调用队列的。例如result、state、completed的状态变更,在链式调用中是有所不同的。

小结

我们在原有的Async.Operation上略作修改,使得它支持异步调用队列,完整的代码看这里:支持链式调用的异步调用框架Async.Operation

现在我们已经拥有了一个功能强大的Async.Operation,接下来我们就要看看如何将它投入到更多常见的使用模式中去,如果你不希望错过相关讨论的话,欢迎订阅我的博客:

2009年6月14日星期日

中国程序员有美国梦吗?

Jeff最近转载了一篇名为《贺计算机成“就业最困难专业”》的文章,然后抛出了一个问题来,问大家对此看法如何,接着自然又引起了新一轮博客园首页发文热潮。对此,我站在我的角度说说我的看法。

大浪淘沙,金子难寻

1848年,美国爆发了加州淘金热潮,大量人口涌到加州进行淘金,其直接后果就是让一个叫旧金山小村庄的转眼间变成了一座大城市。在淘金热潮之初,你拿个筛子在河床里筛泥沙也能找到金子,这是何其容易的事情。但我们假想一下,如果当时一个淘金者拿着他的筛子跑到中国来,他能够淘到金子吗?我觉得是不可以的。这不是说中国的黄金矿藏量就比不上美国的一个州,而是中国没有如此高浓度而且可以进行露天开采的金矿。

1971年,旧金山湾区南端由于“淘沙子”出了名,因而被大家叫做硅谷。1998年,Google在硅谷诞生了,然后开始疯狂地吸收硅谷乃至全球的技术人才。2006至2007年,Google在中国进行大规模招聘,这时候他们遇到了跟淘金者中国之行一样的难题……

中国是第一个成功迫使Google进行笔试的国家。这句话说出来可能会让大家觉得十分搞笑,国内哪家公司招聘一线工程师不是先笔试再面试的。在人力资源供求相当的国家里,确实不需要笔试这轮筛选。有资质并且自信有资格来应聘的人,一般都能够得到充分的沟通,然后再确定这份工作是否适合。但这在人力资源明显供过于求的中国来说,就一点也不现实。

Google中国也有高层承认,以ACM/ICPC竞赛成绩来挑人确实有所缺陷,很多适合这份工作的人会被直接淘汰掉,连面对面或电话沟通的机会也没有。但假如你去看一下Google中国的校园招聘场面,看一下在各省重点大学里连开多个大教室进行笔试的情景,你不得不承认,要跟所有这些学生进行沟通的成本实在是连Google中国也支付不起。

总结一下,中国不是没有人才,也不是培养人才的方法有严重缺陷,至少这些都不是根本问题。根本问题是,在如此庞大的人口基数下,再好的企业都只能选用一套折中的人才选拔方案。你不能怪企业,说假如能生在美国你就能被Google美国所挖掘,而现在则被Google中国拒之门外。

人人都会发光的时候你该怎么办?

有时候用“是金子早晚会发光”的说法来自我安慰一下,也不是什么坏事。但当你发现人人都会发光的时候,你就知道事情有多不好了。

我们来做个数学建模,假设美国计算机人才中的top 100, top 1,000, top 10,000分别叫做A类、B类、C类。那么美国的人力资源供需关系大概是这样的:
  • 需求:100人;供应:100人。
  • 需求:1,000人;供应:1,000人。
  • 需求:10,000人;供应:10,000人。


在保持A类、B类、C类界线不变的情况下,放到中国来很可能就是这样子的:
  • 需求:500人;供应:1,000人。
  • 需求:5,000人;供应:100,000人。
  • 需求:50,000人;供应:10,000,000人。


就算中国的人才需求是美国的5倍,但依然供过于求。就算顶级人才的供求矛盾不明显,人才结构上的不合理也会给底层造成巨大的压力。

在A类里面,只有500人做不了自己应做的工作,被迫降级去做B类的工作。对于人口如此众多的一个国家来说,让500人屈就一下不是什么大问题。在B类里面,会有95,000人做不了自己应做的工作。而到了C类里面,会有9,950,000人做不了自己应做的工作,这时候问题就很严重了。

如果你搞不懂上面那堆数字说的是什么,那么我尝试换用浅白一些的语言来再说一次。如果你在美国,并且你达到了Microsoft美国招聘的要求,那么你成功加入Microsoft的概率就相当高了,因为你和Microsoft相互需要对方。

但如果你在中国,并且你达到了Microsoft中国招聘的要求,那么你成功加入Microsoft的概率并不怎么高,因为跟你一样符合这一条件的人数要远多于Microsoft的需求。如果你想要确保一个较高的应聘成功率,那么你必须远高于Microsoft的招聘要求。

没有压力的都跳槽了

假如我们把胜任某个职位的人简单分为两类:刚刚好胜任的,以及过度胜任的。那么,我们可以得到一个有趣的推论。

刚刚好胜任的人,是一定很有压力的。如果上面的分析没错的话,你之所以能够获得这个职位,一定程度上依赖于运气。好几个拥有同等实力的人跟你抢这个职位,尽管他们没能抢到,但依旧对此虎视眈眈,时刻准备着抢你饭碗。企业没理由对此坐视不理,一定会让你一直处于失业的边缘,以此作为筹码逼你好好干活,这时候你没压力才怪。

过度胜任的人,忠诚度一定会打折扣。总有更好的职位在对你招手,尽管你知道那些职位你只能凭运气获得,尽管你知道得到了你也站到了失业的边缘上,但你就不心动吗?肯定还是会心动的。

知足常乐者?估计在顶端会多一些。我和Jeff这样的,属于在国内少数算是享有American Dream的人,也就是能够通过自己的奋斗来获得应有的财富与社会地位。Jeff倾向于认为这对广大程序员普遍适用,并且鼓励大家都去追求自己的梦想。然而我得到的结论却并非如此。对于这事情,你怎么看?

2009年5月8日星期五

写个 JavaScript 异步调用框架 (Part 4 - 链式调用)

我们已经实现了一个简单的异步调用框架,然而还有一些美中不足,那就是顺序执行的异步函数需要用嵌套的方式来声明。

现实开发中,要按顺序执行一系列的同步异步操作又是很常见的。还是用百度Hi网页版中的例子,我们先要异步获取联系人列表,然后再异步获取每一个联系人的具体信息,而且后者是分页获取的,每次请求发送10个联系人的名称然后取回对应的具体信息。这就是多个需要顺序执行的异步请求。

为此,我们需要设计一种新的操作方式来优化代码可读性,让顺序异步操作代码看起来和传统的顺序同步操作代码一样优雅。

传统做法

大多数程序员都能够很好的理解顺序执行的代码,例如这样子的:

var firstResult = firstOperation(initialArgument);
var secondResult = secondOperation(firstResult);
var finalResult = thirdOperation(secondResult);
alert(finalResult);


其中先执行的函数为后执行的函数提供所需的数据。然而使用我们的异步调用框架后,同样的逻辑必须变成这样子:

firstAsyncOperation(initialArgument)
  .addCallback(function(firstResult) {
    secondAsyncOperation(firstResult)
      .addCallback(function(secondResult) {
        thirdAsyncOperation(secondResult)
          .addCallback(function(finalResult) {
            alert(finalResult);
        });
    });
});

链式写法

我认为上面的代码实在是太不美观了,并且希望能够改造为jQuery风格的链式写法。为此,我们先构造一个用例:

Async.go(initialArgument)
  .next(firstAsyncOperation)
  .next(secondAsyncOperation)
  .next(thirdAsyncOperation)
  .next(function(finalResult) { alert(finalResult); })


在这个用例当中,我们在go传入初始化数据,然后每一个next后面传入一个数据处理函数,这些处理函数按顺序对数据进行处理。

同步并存

上面的用例调用到的全部都是异步函数,不过我们最好能够兼容同步函数,让使用者无需关心函数的具体实现,也能使用这项功能。为此我们再写一个这样的用例:

Async.go(0)
  .next(function(i) { alert(i); return i + 1; })
  .next(function(i) {
    alert(i);
    var operation = new Async.Operation();
    setTimeout(function() { operation.yield(i + 1); }, 1000);
    return operation;
  })
  .next(function(i) { alert(i); return i + 1; })
  .next(function(i) { alert(i); return i; });


在上述用例中,我们期待能够看到0, 1, 2, 3的提示信息序列,并且1和2之间间隔为1000毫秒。

异步本质

一个链式调用,本质上也是一个异步调用,所以它返回的也是一个Operation实例。这个实例自然也有result、state和completed这几个字段,并且当整个链式调用完成时,result等于最后一个调用返回的结果,而completed自然是等于true。

我们可以扩展一下上一个用例,得到如下用例代码:

var chainOperation = Async.go(0)
  .next(function(i) { alert(i); return i + 1; })
  .next(function(i) {
    alert(i);
    var operation = new Async.Operation();
    setTimeout(function() { operation.yield(i + 1); }, 1000);
    return operation;
  })
  .next(function(i) { alert(i); return i + 1; })
  .next(function(i) { alert(i); return i; });

setTiemout(function() { alert(chainOperation.result; }, 2000);


把链式调用的返回保存下来,在链式调用完成时,它的result应该与最后一个操作的返回一致。在上述用例中,也就是3。

调用时机

尽管我们提供了一种链式调用方式,但是用户不一定会按照这种固定的方式来调用,所以我们仍然要考虑兼容用户的各种可能用法,例如说异步地用next往调用链添加操作:

var chainOperation = Async.go(0);
chainOperation.next(function(i) { alert(i); return i + 1; });
setTimeout(function() {
  chainOperation.next(function(i) {
    alert(i);
    var operation = new Async.Operation();
    setTimeout(function() { operation.yield(i + 1); }, 2000);
    return operation;
  })
}, 1000);
setTimeout(function() {
  chainOperation.next(function(i) { alert(i); return i + 1; });
}, 2000);


在这个用例当中,用户每隔1000毫秒添加一个操作,而其中第二个操作耗时2000毫秒。也就是说,添加第三个操作时第二个操作还没返回。作为一个健壮的框架,必须要能兼容这样的使用方式。

此外我们还要考虑,用户可能想要先构造调用链,然后再执行调用链。这时候用户就会先使用next方法添加操作,再使用go方法执行。

var chainOperation = Async
  .chain(function(i) { alert(i); return i + 1; })
  .next(function(i) {
    alert(i);
    var operation = new Async.Operation();
    setTimeout(function() { operation.yield(i + 1); }, 2000);
    return operation;
  })
  .go(0)
setTimeout(function() {
  chainOperation.next(function(i) { alert(i); return i + 1; })
}, 1000);


在上述用例中,用户通过chain和next添加了头同步操作和异步操作各一个,然后用go执行调用链,在调用链执行完毕之前又用next异步追加了一个操作。一个健壮的框架,在这样的用例当中应该能够如同用户所期望的那样提示0, 1, 2。

小结

针对链式调用的需求,我们设计了如此多的用例,包括各种奇怪的异步调用方式。最终如何实现这样的功能呢?如果你想知道的话,欢迎订阅我的博客:

2009年5月6日星期三

写个 JavaScript 异步调用框架 (Part 3 - 代码实现)

在上一篇文章里,我们说到了要实现一个Async.Operation类,通过addCallback方法传递回调函数,并且通过yield方法返回回调结果。现在我们就来实现这个类吧。

类结构

首先我们来搭一个架子,把需要用到的似有变量都列出来。我们需要一个数组,来保存回调函数列表;需要一个标志位,来表示异步操作是否已完成;还可以学IAsyncResult,加一个state,允许异步操作的实现者对外暴露自定义的执行状态;最后加一个变量保存异步操作结果。

var Cat = {};

Async = {
  Operation: {
    var callbackQueue = [];
    this.result = undefined;
    this.state = "waiting";
    this.completed = false;
  }
}

addCallback方法

接下来,我们要实现addCallback方法,它的工作职责很简单,就是把回调函数放到callbackQueue中。此外,如果此时completed为true,说明异步操作已经yield过了,则立即调用此回调。

this.yield = function(callback) {
  callbackQueue.push(callback);
  if (this.completed) {
    this.yield(this.result);
  }
  return this;
}


我们假设yield方法会把callbackQueue中的回调函数逐个取出来然后调用,因此如果compeleted为true,则使用已有的result再调用一次yield就可以了,这样yield自然会调用这次添加到callbackQueue的回调函数。

至于最后的return this;,只是为了方便jQuery风格的链式写法,可以通过点号分隔连续添加多个回调函数:

asyncOperation(argument)
  .addCallback(firstCallback)
  .addCallback(secondCallback);

yield方法

最后,我们要实现yield方法。它需要将callbackQueue中的回调函数逐个取出来,然后都调用一遍,并且保证这个操作是异步吧。

this.yield = function(result) {
  var self = this;
  setTimeout(function() {
    self.result = result;
    self.state = "completed";
    self.completed = true;
    while (callbackQueue.length > 0) {
      var callback = callbackQueue.shift();
      callback(self.result);
    }
  }, 1);
  return this;
}


通过使用setTimeout,我们确保了yield的实际操作是异步进行的。然后我们把用户传入yield的结果及相关状态更新到对象属性之上,最后遍历callbackQueue调用所有的回调函数。

小结

这样我们就做好了一个简单的JavaScript异步调用框架,完整的代码可以看这里:异步调用框架Async.Operation

这个框架能够很好的解决调用栈中出现同步异步操作并存的情况,假设所有函数都返回Async.Operation,框架的使用者可以使用一种统一的模式来编写代码,处理函数返回,而无需关心这个函数实际上是同步返回了还是异步返回了。

对于串行调用多个异步函数的情况,我们现在可以用嵌套addCallback的方式来书写,但随着嵌套层数的增多,代码会变得越来越不美观:

firstAsyncOperation().addCallback(function() {
  secondAsyncOperation().addCallback(function() {
    thirdAsyncOperation().addCallback(function() {
      finalSyncOperation();
    });
  });
});


我们能否把嵌套形式改为jQuery风格的链式写法呢?这是我们接下来要思考的问题,如果你不希望错过相关讨论的话,欢迎订阅我的博客:

写个 JavaScript 异步调用框架 (Part 2 - 用例设计)

在上一篇文章里说到,我们要设计一个异步调用框架,最好能够统一同步异步调用的接口,同时具体调用顺序与实现方式无关。那么我们现在就来设计这样一个框架的用例。

传递回调

我们首先要考虑的一个问题是,如何传递回调入口。在最传统的XHR调用当中,回调函数会被作为最后一个参数传递给异步函数:

function asyncOperation(argument, callback)

在参数相当多的时候,我们可以把参数放到一个JSON里面,这样参数就如同具名参数一样,可以通过参数名选择性的传递参数,不传递的参数相当于使用默认值。这是从Prototype开始就流行起来的做法:

function asyncOperation(argument, options)

然而这两种做法都有一个坏处,就是把同步函数改为异步函数(或同步异步混合函数)时,必须显式地修改函数签名,在最后增加一个(或多个)参数。

由于在调用栈的底层引入异步函数对我们来说太常见了,为此可能要更改一大堆上层调用函数签名的成本实在是太高了,所以我们还是想一个不用修改函数签名的做法吧。

在这里我参考了.NET Framework的IAsyncResult设计,把异步操作有关的一切信息集中到一个对象上来,从而避免了对函数签名的修改。在此,我们假设一个异步函数的调用原型是这样子的:

function asyncOperation(argument) {
  operation = new Async.Operation();
  setTimeout(function() { operation.yield("hello world"); }, 1000);
  return operation;
}


在这段代码里,我们返回了一个Operation对象,用于将来传递回调函数。同时,我们通过setTimeout模拟了异步返回结果,而具体的返回方式就是yield方法。

接着,我们还要设计传递回调函数的方法。由于我们不能好像C#那样重载+=运算符,所以只能用函数传递回调函数:

var operation = asyncOperation(argument);
operation.addCallback(function(result) { alert(result); });


在C#里面做这样的设计是不安全的,因为在异步操作可能在添加回调之前就完成了。但在JavaScript里面这样写是安全的,因为JavaScript是单线程的,紧接着asyncOperation的同步addCallback必然先执行,asyncOperation中的异步yield必然后执行。

调用顺序

可能有人要问,如果用户使用同步的方式来调用yield,这时候执行顺序不一样依赖于yield的实现吗?没错,不过yeild是在框架中一次性实现的,我们只要把它做成异步的就可以了,这样即使对它进行同步调用,也不影响执行顺序:

function psudoAsyncOperation(argument) {
  operation = new Async.Operation();
  operation.yield("hello world");
  return operation;
}
var operation = asyncOperation(argument);
operation.addCallback(function(result) { alert(result); });


就算把代码写成这个样子,我们也能确保addCallback先于yield的实际逻辑执行。

事后回调

有时候,框架的使用者可能真的写出了先yield后addCallback的代码。这时候,我认为必须保证addCallback中添加的回调函数会被立即触发。因为用户添加这个回调函数,意味着他期望当异步操作有结果时通知这个回调函数,而这与添加回调函数时异步操作是否完成无关。为此,我们再添加一个用例:

function psudoAsyncOperation(argument) {
  operation = new Async.Operation();
  operation.yield("hello world");
  return operation;
}
var operation = asyncOperation(argument);
setTimeout(function() {
  operation.addCallback(function(result) { alert(result); });
}, 1000);

小结

到这里,我们就设计好了一个名为Async.Operation的异步操作对象,具体如何实现关键的yield方法和addCallback方法将在下一篇文章讲述如果。你不希望错过的话,欢迎订阅我的博客:

2009年5月5日星期二

写个 JavaScript 异步调用框架 (Part 1 - 问题 & 场景)

问题

在Ajax应用中,调用XMLHttpRequest是很常见的情况。特别是以客户端为中心的Ajax应用,各种需要从服务器端获取数据的操作都通过XHR异步调用完成。然而在单线程的JavaScript编程中,XHR异步调用的代码风格实在是与一般的JavaScript代码格格不入。
额外参数
考虑一个除法函数,如果它是纯客户端的同步函数,那么签名会是这样的:

function divide(operand1, operand2)

然而假设我们对客户端除法的精度不满意,于是把除法转移到服务器端来执行,那么它是个需要调用XHR的异步函数,签名也就可能会是以下几种之一:

function divide(operand1, operand2, callback)
function divide(operand1, operand2, successCallback, failureCallback)
function divide(operand1, operand2, options)


我们必须在签名中引入新的参数来传递回调函数,不能选择让函数变成阻塞式的同步调用。
可传递性
不仅仅直接操作XHR的函数需要引入新的参数,这种复杂性还会顺着调用栈向外传递。例如说,我们对加减乘除四则运算作了封装,只向外暴露一个运算接口:

function calculate(operand1, operand2, operator)

这个calculate函数根据operator参数来调用内部的plus, subtract, multiply, divide函数。然而,因为divide函数变成了异步函数,所以整个calculate函数不得不也转变为异步函数:

function calculate(operand1, operand2, operator, callback)

同时,在调用栈之上凡是需要调用到calculate的函数,都必须变成异步的,除非它并不需要向上一级调用函数返回结果。
同步并存
尽管calculate函数变成了一个异步函数,它所调用的plus, subtract, multiply函数还是同步函数,只有调用divide时是异步的,这时候calculate就是一个异步同步并存函数。

这会带来什么问题?calculate的调用者看到函数签名自然会认为calculate是个异步函数,因为它需要传递回调函数,然而calculate的执行方式却是不确定的。考虑如下调用:

calculate(operand1, operand2, operator, callback);
next();


这里涉及到callback和next两个函数,它们哪个先执行哪个后执行是不确定的,或者说是依赖于calculate具体实现的。

如果calculate的实现是,当不需要进行异步操作时,直接调用callback。那么,在执行加减乘法时callback会在next之前被调用;在执行除法时next会在callback之前调用。

如果我们不喜欢这种不确定性,我们可以改变一下calculate的实现,把同步调用也都改为setTimeout形式的,这样在任何情况下next都一定会在callback之前被调用。

然而后面一种做法依赖于成本较高的实现方式,开发者一个不小心(或者摆明偷懒)就会漏掉setTimeout,导致函数调用顺序变得不确定,所以我们会希望这是框架帮助实现的功能,在使用框架时无法把这绕过。

场景

在这里,我举一个关于上述问题的具体应用场景。(为简化问题,描述已略作修改,与实际应用并不一致。)

百度Hi网页版里面,我们会在客户端保存一个用户对象列表,在打开和这个用户的聊天窗口时,我们需要从中读取这个用户的信息。这个操作就涉及很多可能同步又可能异步的分支:
  • 用户对象未缓存
    • 异步读取用户信息
  • 用户对象已缓存
    • 用户是好友(信息更新会由服务器端推送)
      • 同步读取用户信息
    • 用户不是好友(信息更新需要由客户端拉取)
      • 可以接受缓存信息
        • 同步读取用户信息
      • 必须获取最新信息
        • 异步读取用户信息
可以看到,分支的结果最终既有同步的,也有异步的。并且这些分支还不是在一个函数里完成,而是在几个函数里实现。也就是说,按照传统的模式,这些函数一部分是同步的,一部分是异步的,由于异步的传递性,最终调用栈顶层的函数都是异步的。

为了解决这个问题,我们需要写一个异步调用框架,用一种统一的方式来进行调用,把同步和异步调用都合并为一种返回方式。

具体的解决方案会在下一篇文章中给出,如果你不希望错过的话,欢迎订阅我的博客:

2009年4月11日星期六

豆瓣的『请勿联系我们』页面

如果说Don't Make Me Think的基本思想是让用户凭直觉都能找到他们需要的东西,那么豆瓣的联系我们页面就做了一个绝佳的例子,或者说是绝佳的反例,这视乎你怎么看这个问题。

联系豆瓣 - Mozilla Firefox (Build 2009032608)

联系我们

还是用Don't Make Me Think里面的比喻,在网站上寻找内容就如同在商店里寻找商品,最理想的情况自然是一抬头就看到导向牌说你想要的商品在哪一行的哪一个货架上。在这方面,豆瓣的联系我们页面是做得非常好的,通过清晰的分类说明了不同情况应该联系不同的邮件地址。就如同清晰的导向牌一样,你能够径直走到摆满你想要的商品的货架前。

接着搞笑的事情发生了,你发现这个货架其实是个锁上了的玻璃橱窗,上面有一个牌子写着『如需取商品,请找售货员』。这时候你会想,这不是一家便利店吗?还是有什么我理解错了?这就是豆瓣的联系我们页面给我的感觉,因为那些邮件地址不仅仅不可以点击,还刻意设计为图片使得不能够复制。

如果你跟我一样,习惯有什么想法直接点击网站的联系我们链接,你应该明白常见的联系我们页面是怎样的。网站往往为了保护邮件地址免受垃圾邮件骚扰而不会直接公开邮件地址,为此他们会设计一个专门的表单让你提交反馈信息并留下你的联系方式。随后如果你提交的信息确实有价值,网站会主动联系你,之后的通信就都可以在邮件里进行了。

这是用户对联系我们页面的印象,就如同对便利店的印象一样——自由地在货架前选取商品然后再去埋单。然而豆瓣选择了在便利店里放置锁上了的玻璃橱窗,这就会导致用户迷失方向,他们会想『这是一家便利店吗?还是说我走进了一家「珠宝便利店」?那算了,我就不买了,或者到附近的便利店买吧。』

帮助中心

这个联系我们页面上面,只有一个链接可点击(除去全局链接外),那就是帮助中心链接了。用户找不到熟悉的反馈表单,自然会凭直觉沿着链接点击下去,而这个链接是唯一的选择。进去之后仍然是一个清晰分类的页面,不过就是没有用户需要的反馈表单。或许用户会浏览一下分类列表,然后发现他要反馈的问题不能算是严格属于某个特定分类。这时候就如同顾客根据导向牌走进了唯一一行他认为正确的货架,但是货架上的分类标识他想要的东西不属于这里的任何一个货架。

这又是一个让用户感受挫折的地方,然而这还不是最后一个。如果用户坚信反馈表单存在,就如同顾客坚信这是每一家便利店都卖的东西一样,那么他们自然会继续找下去。在点击进入任何一个分类链接后,用户会看到又一个清晰的Q&A列表,然后他必须再一次经历同样的挫折,最后才在一个不显眼的地方找到他想要的东西,一个写着『没搞定?给管理员捎个话』的链接。

为什么说这个链接不显眼?因为特殊链接往往会放在页头、页脚或侧栏这类视觉上与内容分离开来的区域,而这个链接放在问题列表和解答列表之间的狭缝。这处的留白本应只用于表示两个列表互相对立的关系,在两个块区元素之间插入一个行内元素只会让人以为那也是分隔符的一部分而已。

小结

这就是为什么我说豆瓣的『联系我们』页面应该叫做『请勿联系我们』页面。它在向用户传达一种意思,那就是『我们豆瓣是刻意增加你联系我们的成本的,因为我们就是不欢迎你联系我们。』这就好比说,我们都知道每一家7-11都会在cashier前有一个放满了condom的架子,而豆瓣是一家和特别的7-11,它在那里放了一个牌子然后写上『我们就是不卖condom,你就不要找了,也不要问我们的售货员』。

2009年3月29日星期日

Microsoft MVP Global Summit 2009 (Part 2 - Sessions)

这次去参加MVP Summit,我计划主要是听ASP.NET 4.0及Silverlight 3有关的session,结果在Microsoft Campus的两天也就泡在MSCC,也就是往返于Hood和Rainier这两个room。这些内容之前一直都不能说,现在MIX09开完之后,大部分的内容我们都可以自由讨论了。

ASP.NET 4.0

ASP.NET 4.0方面,我最关注的是ASP.NET AJAX 4.0,不过Preview 4和Preview 3比起来没多少新增功能,我只能希望在Preview 3中存在的bug在Preview 4都修复了。在demo方面,DataView控件加上Live Binding确实非常cool,可以一个DataView做master另一个做details,在其中任何一处作出的修改立即同步到所有地方,包括对ADO.NET Data Service作出更新请求。不过demo始终就只是demo,现实中的场景总有一些更复杂更需要创意的地方。

此外,ASP.NET 4.0还包括ASP.NET MVC和Dynamic Data,前者终于发布了1.0,后者比最初我看到的第一个Preview已经要强大很多了,当然也换上了Entity Model这样的数据访问层组件。

Silverlight 3

我对Silverlight 3的关注原本只是很表面的,但却发现Silverlight 3的新增功能比ASP.NET 4.0要多得多,这意味着Microsoft对Silverlight的投入巨大吧,因此我也觉得有必要去好好研究Silverlight 3了。

Silverlight 3的突破是巨大的,假如我们都承认原来的Silverlight 2其实很弱的话。在工具方面,现在终于可以在Visual Studio 2010里面拖放编辑XAML了,而Expression Blend也支持C#编辑了,不再像过去的版本那样,Visual Studio和Blend各自只能完成一半的工作,必须在两个工具之间切换来切换去。

视频播放一直都是Silverlight非常重视的功能之一,Silverlight 3将会支持H.264,并把H.264/AAC/MP4作为标准的视频封装格式。如今有越来越多的设备支持H.264的硬件解码,而且Adobe与Apple的软件也广泛采用H.264作为高清视频的编码格式,所以将来我们无需再为编码格式的区别而烦恼了。

另外,Silverlight 3还引入了GPU硬件加速的支持,用于UIElement的合成与拉伸,同时支持浏览器内嵌模式与全屏模式。不过,如果你要让你的Silverlight 3应用支持GPU加速,还必须在浏览器插件及UIElement两个层面声明启用GPU加速,以保证其他内容不受此新功能引入的影响。

GPU硬件加速带来的是什么?当然是视觉上的新功能啦。过去Silverlight 2无法做梯形变换,所以也就做不了真正的Coverflow。现在Silverlight 3可以做Perspective 3D了,Coverflow也就能实现了。另外WPF拥有的Effects在Silverlight 3中也能用了,虽然Silverlight 3现在的Preview仅仅自带了两个Effects,但WPF自带的Effects甚至是第三方的Effects都可以导入到Silverlight 3中使用,因为它们都是用HLSL写的Pixel Shaders。

如果你对这些新功能感兴趣,我建议你去看看MIX 09的视频,里面涵盖了我所说到的所有内容。我准备接下来用Silverlight 3做一个Coverflow,如果你有兴趣了解这是怎么做的,欢迎订阅Cat in Chinese

2009年3月22日星期日

Microsoft MVP Global Summit 2009 (Part 1 - Trip & Food)

虽然MVP Summit发生在3月初,但由于我一直都很懒,所以现在才来写写文章。而且,很多MVP Summit上讨论的受NDA保护的内容,过了MIX09也就成为公开内容了,我也就可以在这里说说了。

今年买票的时候,竟然找不到第二个人跟我同行。上海的一群MVP,都为了省钱买了海航的机票,从上海飞到北京再直飞西雅图。我还是更愿意飞NWA,因为里程可以积累到我的南航帐号上,所以虽然票价贵一些,我还是选择了一个人飞NWA。直到起飞前两天,才确定上海的包包跑来北京了,因为面签时被check了拖到最后一刻才买票,结果就和我同机了。

飞到东京之后,有趣的事情发生了,我们见到了一群穿着校服的日本女生走进来候机室,登机之后其中一位就坐在我旁边。我在吃完机上第一餐之后,无所事事就开始找她聊天,问问她有没有用过Baidu.jp,然后说说去美国做什么,她说她其实是去Vancouver参加交流活动。因为飞机引擎噪声很大,我们又都不太懂对方的口音,所以我拿了一个小本子出来把英语写下来让她看。在她有不懂表达的意思时,她拿了一个电子词典出来用日文查对应的英文,不过有时候我看看其中的日文解释,上面的汉字也足够我理解了。这样的沟通挺有趣的。

到了Seattle之后,打车去Sheraton Seattle Hotel,然后还是和去年一样带着今年第一次来的MVP去海边走走,去Crab Pot吃蟹。我们要了便宜的snow crab和dungeon crab,没有选最贵的king crab,吃起来感觉还不错,不过snow crab实在是太小了。最后一天晚上也是在Crab Pot吃,就是吃king crab,但这其实不是我们吃过最好的king crab。

说到这里,就不得不重点推荐一下我们在Oceanaire Seafood Room吃的那一餐Alaska red king crab了。虽然这里的king crab要$45/pound,而Crab Pot的只要$22/pound,但是这里的king crab确实比较大,而且非常甜!如果你想要尝一尝king crab是有多么的好吃,那就一定要去Oceanaire Seafood Room点Alaska red king crab。一般来说,一个人吃1~2 pounds是没问题的,如果你不再点其他东西的话。面包是送的,但既然是专门来吃king crab的,当然就不再点其他东西了,甚至连面包都不吃。

Alaska Red king crab

美国人吃法比较奇怪的地方在于,他们用石头那么硬的面包来搭配甜美的king crab。不仅仅这样,他们还会上热好了的黄油,让你点king crab来吃。我试了一下,发现这一点都不好吃,原本爽口的蟹肉变得油油的,感觉很奇怪。

今年MVP Summit的开场中说到去年MVP Summit抱怨最厉害的事情,那就是salmon吃得太多了,每餐都有salmon,所以今年的口号是No Salmon。我自然很失望啦,Seattle作为一个salmon产量如此多的地方,我过来自然要吃salmon啊,怎么可以没salmon呢?结果后来几天真的很多人在Twitter上抱怨No Salmon,于是乎今年最大的抱怨也就正好与去年相反了。看来做如此大的一场活动,关于吃什么的问题真是众口难调哦。

有关MVP Summit旅行与饮食的话题就说到这里吧,如果你有兴趣了解我接下来要说的技术内容,欢迎订阅Cat in Chinese

2009年3月13日星期五

ASP.NET AJAX 4.0 Preview 3 (Part 2 - ASP.NET AJAX Template)

在上一篇文章里,我们说到了如何使用ADO.NET Data Service Client Library能够轻松访问到存在服务器端的数据,然而将数据展现出来仍需要人手拼接HTML这点就实在是让人难以接受,所以我们现在就来看看如何利用 ASP.NET AJAX Template解决这个问题。文章中所用到的示例代码,可以在这里下载:ASP.NET AJAX 4.0 Preview 3 Demo,然后参考里面的AspNetAjaxTemplateDemo.aspx。

Sys.UI.DataView

为了解决展示数据的问题,我们需要用到一个全新的客户端控件,那就是Sys.UI.DataView 了,简称DataView。我们会用DataView替换掉上一篇文章中所说到的人手拼接HTML的部分,用于迭代生成一个ul中的li元素,因此看起来是把DataView当作Repeater来用。实际上,DataView的功能类似于ListView加上DetailsView

如果你把一个Array绑定到DataView上,它会显示为一个ListView。与ListViewLayoutTemplate相类似的是,它也能够定义控件展示的整体布局,并且仅仅是迭代输出其中的一小部分。例如说,编写一个有thead的table,并且仅仅是迭代输出thead之后的tr。在这方面,是DataViewListView完全一致的。唯一不同的是,客户端暂时还没有DataPager这样的控件,所以DataView必须一次性的完整显示整个Array的数据。

如果你把单个Object绑定到DataView上,它就会显示为一个DetailsView。这使得你可以使用两个DetailsView就做出经典的Master-Details展示模式,和在服务器端分别用ListViewDetailsView做出来的一样。当然,你不能指望DataView能够好像DetailsView那样,自动帮你分析每一个数据项并映射出对应的HTML模板,因此HTML 模板还是要你自己手写的,但至少那是在HTML中编写模板,编写时能够享受IDE带来的各种好处,维护时也更容易看懂自己(或别人)原来写下的 HTML。

JavaScript语法

接下来,我们就要把DataView投入到实际应用中去了。首先,我们说一下如何用JavaScript来实例化一个DataView。有编写ASP.NET AJAX客户端代码经验的人对$create应该不会觉得陌生,因为DataView继承自Sys.UI.Control,我们仍然可以用$create来实例化它。不过,在此之前,我们先要把对应的 HTML编写好:

<ul id="itemTemplate" class="sysTemplate">
  <li>
    <span class="award">{{ Award }}</span>
    <span class="winner">{{ Winner }}</span>
    <span class="film">{{ Film }}</span>
  </li>
</ul>


然后我们就可以基于itemTemplate这个HTML元素创建控件了:

$create(Sys.UI.DataView, {
    dataSource: new Sys.Data.AdoDataSource(),
    serviceUri: "WebDataService.svc",
    query: "OscarWinners"
  }, {}, {}, $get("itemTemplate"));


现在,页面显示出来的结果和之前我们人手拼接HTML的版本完全一致,不过我们已经不在需要维护嵌入在JavaScript中的HTML代码了。

声明性语法

如果你觉得上面的做法还不够好,要在pageLoad()里面写一个$create,那么声明性语法可能正是你需要的。大家应该记得很久很久之前,在ASP.NET AJAX还叫做Atlas的时候,就已经有过声明性语法的设计,那就是xml-script。不知为何,后来Microsoft放弃了这一设计,现在声明性语法又回来了,而且设计得比以前的xml-script还要更好。假如不用$create的话,通过声明性语法实例化一个DataView仅需要这样做:

<body
  xmlns:sys="javascript:Sys"
  xmlns:dataView="javascript:Sys.UI.DataView"
  sys:activate="*">
  <ul id="itemTemplate" class="sysTemplate"
    sys:attach="dataView"
    dataView:datasource="{{ new Sys.Data.AdoNetDataSource() }}"
    dataView:serviceuri="WebDataService.svc"
    dataView:query="OscarWinners">
    <li>
      <span class="award">{{ Award }}</span>
      <span class="winner">{{ Winner }}</span>
      <span class="film">{{ Film }}</span>
    </li>
  </ul>
</body>


我们所需要更改的代码包括:
  1. 在body元素上声明有关的xmlns,将JavaScript中的名字空间映射到HTML上,或者你可以理解为映射到XML/XHTML上。
  2. 通过sys:activate="*"这个声明,让ASP.NET AJAX知道它需要去解释页面上所有的声明性语法,并激活对应的组件。
  3. 将原本使用$create初始化时传递给实例的属性、事件、引用改为用声明性语法,直接写在HTML元素的定义上。


经过这三步,我们就可以将原来使用$create创建的组件改为使用声明性语法创建了。

小结

DataView 使得我们能够使用HTML模板,来避免手工拼接HTML带来的种种问题,同时声明性语法让我们能够如同声明服务器端控件一样声明客户端组件。虽然在 ASP.NET AJAX 4.0 Preview 3中这些功能仍有小bug,例如我想用声明性语法创建我自己编写的InPlaceEditoBehavior,这在初始化阶段毫无问题,但却会在页面卸载销毁对象之时抛出脚本错误。

由于我觉得ASP.NET AJAX 4.0 Preview 4很快就要出来了,所以我也就不准备去深入研究Preview 3了,等Preview 4出来了再去好好看看源代码。如果你有兴趣关注这方面的技术文章,欢迎订阅我的博客,点击侧栏上的订阅链接就可以了。

ASP.NET AJAX 4.0 Preview 3 (Part 1 - ADO.NET Data Service Client Library)

自从Microsoft与jQuery合作以来,ASP.NET AJAX与jQuery就被定位为两个互补的AJAX库。既然jQuery已经实现了如此多轻量级的AJAX特性,自然ASP.NET AJAX会继续专注于富客户端所需的一些重量级特性。

在ASP.NET AJAX 4.0 Preview 3里面,开发人员能够接触到的两个重要的新特性就是ADO.NET Data Service Client Library以及ASP.NET AJAX Template。对于熟悉ASP.NET服务器端开发但不熟悉客户端开发的人来说,你可以简单地把这两个特性理解为存在于客户端的DataSource 以及ListView,只要把数据通过ADO.NET Data Service输出到前端,你就可以如同使用DataSource和ListView的组合一样在客户端开发数据驱动的应用程序了。

在这篇文章里,我们将来看看如何使用ADO.NET Data Service Client Library,将ADO.NET Data Service暴露的REST数据接口直接拿到客户端JavaScript代码中去调用。文章中所用到的示例代码,可以在这里下载:ASP.NET AJAX 4.0 Preview 3 Demo,然后参考里面的AdoNetDataServiceDemo.aspx。

服务器端准备工作

在我们接下来要讲到的示例当中,我们会用到一个SQL Server 2005 Express Edition的数据库,里面有一张名为OscarWinners的表,记录的是本年度奥斯卡获奖名单,字段包括AwardID、Award、 Winner、Film。然后我们为这张表创建ADO.NET Entity Model,接着再为它生成的实体类创建ADO.NET Data Service。这些都是在Visual Studio 2008中点几下鼠标就能完成的操作,就不再详细解释了。在ADO.NET Data Service的InitializeService()方法内,我们仅仅给它提供一个最宽松的规则:

config.SetEntitySetAccessRule("*", EntitySetRights.All);

到这里,我们就把服务器端的要做的工作都准备好了。打开你创建的ADO.NET Data Service地址,看看是否输出了正确的Atom格式数据。如果没有,请检查一下你机器上的WCF是否已经正确安装和配置好了。确保服务器端的准备工作都做好了,然后再进入客户端的开发工作。

连接Data Service

在客户端使用ADO.NET Data Service,我们需要接触到的类只有一个,那就是Sys.Data.AdoNetServiceProxy。首先,我们要连接到ADO.NET Data Service,也就是使用ADO.NET Data Service的URL来实例化此类:

var dataService = new Sys.Data.AdoNetServiceProxy("WebDataService.svc");

然后,我们就可以利用dataService来调用ADO.NET Data Service进行CRUD操作了。

CRUD操作

所有的CRUD操作都在Sys.Data.AdoNetServiceProxy对象上执行,方法分别名为query()insert()update()remove()。在我们的示例当中,会用到query()update()方法,另外两个方法是用起来和update()很类似,就不再详细说明了。
查询操作
dataService.query("OscarWinners", function(result, context, operation) {
  /* display result */
}, errorHandler);


使用上述语句,我们查询出了OscarWinners表中的所有数据。随后的第一个回调函数会在查询成功时被调用,因此我们可以在其中编写拼接HTML以显示结果的逻辑,具体的代码请参考下载中的AdoNetDataServiceDemo.aspx。第二个回调函数会在查询失败时被调用,我们可以编写一个统一的错误处理函数,名为errorHandler,然后将它传递给此参数。

如果需要传递复杂的查询参数,使用ADO.NET Data Service的格式就可以了,这可以在MSDN上查到。例如说查询Slumdog Millionaire这部电影夺取了多少个奥斯卡奖项,然后把奖项按照名称排序输出,可以这样子写:

dataService.query("OscarWinners?$filter=Film eq 'Slumdog Millionaire'&$orderby=Award", function(result, context, operation) {
  /* display result */
}, errorHandler);
更新操作
dataService.update(item, function(result, context, operation) { }, errorHandler);

尽管将查询结果保存下来成为items集合,并且根据用户在界面上执行的操作修改item上的属性,这些逻辑都需要我们手动维护,然而最后将item更新到服务器上则只需要如此简单的一句调用。

在我给出的示例代码中,我自己写了一个InPlaceEditBehavior,也就是所谓的“就地编辑器”,能够让用户点击显示文本后把显示文本变成输入框。然后我把这个InPlaceEditBehavior绑定到每一条记录显示的Winner字段和Film字段的span上,使得这些span都能接收用户输入。最后,我为InPlaceEditBehavior添加了一个onchanged事件,并在该事件的处理函数中完成更新item以及调用update()的操作。

小结

在这篇文章里,我简单地介绍了ADO.NET Data Service Client Library的易用性,并且通过一个具体的示例说明了如何用它来节省大量的数据交互代码。

如果你曾经写过AJAX-Enabled WCF Service,你应该知道把实体类暴露为WCF Service接口是多么麻烦的事情,就算每个实体类就简单地支持CRUD方法,你也必须手动编写这4个方法。ADO.NET Data Service相当于帮你把这一切都做好了,只要给它实体类和规则,它就帮你生成一个Data Service。另外,通过AJAX-Enabled WCF Service所包括的数据接口,会自动生成一大堆客户端代理类,而ADO.NET Data Service Client Library则只有一个固定的代理类,客户端代码体积不会随着接口复杂度的增加而增加。

说了 ADO.NET Data Service Client Library的那么多好处,那么这个示例中又有什么做得不够好的地方呢?我觉得最难维护的地方就是获取到数据后拼接HTML的代码了,人手写的HTML 拼接代码难免容易出错,而且日后更新起来也很麻烦,出错了调试时也不容易定位问题。ASP.NET AJAX Template能够帮助我们解决这个问题,这就是下一篇文章中将会讲到的内容。

如果你不想错过接下来的文章,欢迎订阅我的博客,点击侧栏上的订阅链接就可以了。

如何购买 Amazon Kindle 书籍

在美国旅行时,无聊地在iPhone上装了Kindle for iPhone,然后挑了几本技术书的sample来看看,发现在iPhone上这样看电子书还是挺方便的,就是有些代码块不能自动放大到正常显示字体看起来有点辛苦。我觉得Amazon的sample还是做得挺大方的,每本书都开放了整个chapter 1,技术书的话基本上看chapter 1就能看出作者讨论问题的深度。

我利用旅行的时间,看完了LINQ UnleashedPro LINQ的sample,然后决定买一本来学习一下LINQ。我个人更倾向于买Pro LINQ,因为它在chapter 1就提到了deferred query这样的概念,应该是一个本深入讨论LINQ内部技术的书籍。不过我还是看了一下Amazon上的评分,发现两本都是5星的,但Pro LINQ有32个review,而Linq Unleashed只有4个,于是决定就买Pro LINQ。

开头以为买本Kindle书就是直接刷卡那么简单的事情,结果发现Kindle书是有区域限制的,只能发送到美国。但是我上次买LEGO Mindstorm NXT也是这样买啊,中国的信用卡加美国的送货地址就可以了,不允许送到美国之外的东西就让别人在美国待我签收。神奇的是,Kindle书是只能1-Click购买的,不能在购买时填写送货地址,那Amazon凭什么说我就是在美国之外呢?虽然我禁用了1-Click购买,但保留在1-Click购买的默认地址是美国地址,真是想不明白。

在网上看了很多资料,别人都是可以在美国境外购买Kindle书的,我想来想去也不明白Amazon凭什么判定不能把Kindle书卖给我。本来我不想把帐号上记录了的信用卡信息和地址信息删除的,这样将来要用时又要重新输入,最后实在想不到办法了,决定把这些信息都删掉!接着点1-Click购买,Amazon重新问我要了一个地址,我填了一个美国地址,就购买成功了。

另外,大家都说只能用中国的信用卡购买Amazon Gift Card,再用Gift Card买Kindle书,我也不知道是不是这样,反正我是这样买的。一开始Amazon提示区域限制时,我就怀疑是信用卡的区域问题,所以就去买了Gift Card。能否不用Gift Card直接购买我还不知道,下次可以这样试试。

最后,Amazon是有iPhone版页面的,但它默认打开的购买Kindle书页面缺不是iPhone版的。如果在iPhone版页面上搜索Kindle书,结果能显示出来,然而旁边会写着『This item is currently unavailable』,并且点击进去就会说找不到。只能说,Kindle for iPhone确实还没完全准备好,配套的购买功能都还做好针对iPhone的优化。