之前写过一篇《如何购买 Amazon Kindle 书籍》,后来那篇文章成为了除首页外访问量最高的文章,这让我觉得非常奇怪——首页PV占全部PV的50%,而那一篇文章的PV比例竟然超过5%,同时其它文章的PV比例都低于2%——难道真的有那么多人搜索购买Kindle书的方法找到我的博客来?直到昨天,有人打电话来询问我购买Kindle书的方法(因为她看到我的文章然后找到我的电话),我才确信原来真的有那么多人需要这方面的信息。
其实现在在国内购买Kindle书已经很方便的,使用国内银行的信用卡就可以买,而且不需要经过Gift Card中转。如果你使用一个老的Amazon账号,里面关联过你的各种信用卡和地址信息,这有可能导致Amazon判断你不是指定区域的用户而不卖书给你。这时候你有两个选择:如果你愿意放弃你这个账号所有消费记录的话,你可以重新注册一个Amazon账号,然后就绑定一张国内银行卡,然后就可以正常通过1-click买书了。如果你不愿意放弃已有的账号,那么你可以尝试把上面关联过的各种信用卡和地址信息都删除,这也能让你通过Amazon的判断。
最后,我今年圣诞的最大消费竟然是购买各种打折电子书(不仅仅是Kindle的),看来这真的是一个很有潜力的事情哦!
2009年12月20日星期日
看对的书 (Part 0 - 何谓对错)
在《老赵书托》里面,Jeff把人脑比喻为「寄存器」,而我则更倾向于把人脑比喻为「神经网络」。但是「神经网络」的定义本身就源自人脑啊,这不是循环引用了吗?其实我的意思是,我们应该参考训练神经网络的方式来优化人脑的思维方式。
我们都知道,训练神经网络应该用对的数据,这样才能让神经网络逼近于我们期望的行为模式。如果使用错的数据进行训练,结果将是不可预知的,而且往往意味着偏离我们期望的行为模式。基于同样的道理,书必须选择对的,因为读错书不仅仅浪费时间,还有副作用,比不读书还糟糕。基于这一点,我推荐的书都是观点导向型的,而非知识导向型的。尽管其中一部分可能也包含大量关于「怎么做」的知识,但是我看重的是「站在什么立场思考」的观点,因此后者才会是我推荐一本书的原因。
如果你关心的是「get the right thing」,你可以来看看我的推荐;如果你关心的是「get the thing right」,你可以去看看Jeff的推荐。当然,绝大多数人都会同时追求这两者,所以我并不反对把我们的推荐书籍混合起来看。但是我必须提醒你注意一件事情,不要尝试在一本观点导向型的书里面刻意寻找知识,这只会让你感到迷茫和挫败。很多观点导向型的书并不会告诉你具体「怎么做」,或者作者介绍的「怎么做」并不适合你的情况,因为书的重点在于「怎么思考」,你必须自行摸索适合你的「怎么做」。
最后,跟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足足落后别人两个大版本啊。

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交互。
最后,今年很高兴认识了Will、Martin等港台MVP,跟张欣等MVP玩杀人也很开心(原来那么多MVP不懂玩杀人啊,搞得我很有优势呢),跟衣明志聊天则注定让我缺乏睡眠 @.@
去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足足落后别人两个大版本啊。
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交互。
最后,今年很高兴认识了Will、Martin等港台MVP,跟张欣等MVP玩杀人也很开心(原来那么多MVP不懂玩杀人啊,搞得我很有优势呢),跟衣明志聊天则注定让我缺乏睡眠 @.@
2009年12月14日星期一
程序之外的事情 (Part 1 - Speech)
相信大家最近都看到了有一篇题为《程序员需要培养企业家式的能力》的热文吧。在我首次读到这篇文章时,我并不太同意文章开头的几段话——“程序员在公众面前讲话会脸红,不能很好地表现自己”,你开玩笑吧?在我熟悉的程序员当中,没几个是这样子的,我很喜欢跟他们聊各种各样的事情,从而获得更多有趣的观点,也激发自己的创意。后来想想,或许仅仅是我所在的圈子如此吧,我熟悉的都是那些拥有良好沟通能力的程序员。
还记得2007年第一次参加MVP Open Day的时候,我所认识的MVP不超过5个,看别人都是连任好几届互相认识好几年的老朋友了,在酒店大堂里一起聊天,我实在不知道怎么融入进去。随后在游泳池边的自助晚宴上,Bean对我说了这样一番话:
没错,我就是第一种人,生活在与世隔绝的校园里,天天上网写文章回答问题。站在游泳池的边上,看着别人都三五成群地在聊天,我知道如果没有Bean介绍我认识其它MVP的话,我也会拿着一杯饮料孤零零地一个人站在那里。我当时唯一的想法就是,我不要永远这样子下去,我需要作出改变。
后来,我成为了微软广州.NET俱乐部的讲师,到北京后又活跃于各种Ajax交流活动,再到今年成为TechEd的讲师,在这个过程当中有沟通技能的提升,但更多的是心态上的改变。我觉得,要跟陌生人说话并不是难事,我知道什么就说什么。如果是我说错了,我很欢迎别人指出来。如果双方立场不一样,而不存在绝对的对与错,那就互相包容好了。
在今年的MVP Open Day上,我参加了王洪超的一个session,题目是「面向技术人员的演讲技巧」。这个session提到了一些演讲方面经验与技巧,当然也鼓励现场的MVP们多进行演讲,并从中锻炼自己的自信心与沟通能力。到最后,现场的一位MVP提出了这样一个问题,他说他可以面对上千人做演讲,但在面对官员做演讲时仍然会感到紧张,并且觉得这是无法改变的。我跟他说,「你可以拥有一万个理由来解释为什么这对你来说是无法改变的,但关键问题其实在于你是否真的想要改变」。
是的,很多程序员总能列出一堆的理由来,说明为什么自己不适合学习或者不需要掌握某项与程序无关的技能,例如说演讲、英语、设计等等。但其实问题并没有那么复杂,你需要考虑的只是多学一项技能是否对你的职业发展更有利,只要你愿意,没什么是不能改变的。
「关键问题就在于你是否真的想要改变」——这是我必须放在这个文章系列首篇末尾的一句话。这是一个关于程序以外各项技能的文章系列,我们在这里不再讨论某个技术点应该如何设计与实现,我们所做的也就是分享一下学习某项技能过程当中的经历而已。如果你对这方面的话题感兴趣,可以留言分享你的经历,也欢迎订阅我的博客:
还记得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自身会处理多点触击,默认行为包括滚动和缩放。我们可以接管相应的事件,同时使用
首先,我们要在
接着,我们要在
最后,我们还要在
就这么简单?是的。关键点也就在于
一句代码就把位置、旋转、缩放都设置好了!尽管我们现在还没用到旋转和缩放属性,那就让它们保持默认值吧,我们在多点触击的事件里面会设置它们的。
代码确实比之前的还要少一些,重点就是正确设置
如果你直接使用我的代码去实现开头所说的照片拖放应用,你会发现一个小问题——在进行多点触击操作时,旋转与缩放都是很自然的,就是拖动不自然,好像拖动只跟随第一个触点似的。原因很简单,在多点触击时,管理触点移动的还是
如果需要同时跟随两个触点,你需要对代码稍作改动,使得移动距离为
最后,如果你关注移动设备上的Web开发,欢迎订阅我的博客:
相信很多人都看过WPF为Surface设备做的一个简单demo,也就是在桌面上显示若干张照片,你可以通过单点触击拖放,也可以通过多点触击缩放和旋转。这在iPhone上能够做到,甚至在Mobile Safari里面也能做到,因为Mobile Safari提供了一套专门用于多点触击的JavaScript接口。现在我们就来看看如何利用这套接口吧。
我们都知道,Mobile Safari自身会处理多点触击,默认行为包括滚动和缩放。我们可以接管相应的事件,同时使用
e.preventDefault()
禁用浏览器默认行为,使得我们的Web应用程序能够如同WPF桌面应用一样处理多点触击。下面我们来深入看看Mobile Safari提供的多点触击事件。单点触击
首先我们要处理的是单点触击事件,禁用浏览器的滚动行为,同时为我们的照片(一个img
元素)增加拖动行为。在这里,我们需要用到touchstart
、touchmove
、touchend
事件。在这三个事件里,我们可以通过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 + ")";
}
一句代码就把位置、旋转、缩放都设置好了!尽管我们现在还没用到旋转和缩放属性,那就让它们保持默认值吧,我们在多点触击的事件里面会设置它们的。
多点触击
多点触击涉及到三个事件:gesturestart
、gesturechange
、gestureend
。这三个事件跟单点触击的三个事件非常类似,使用起来甚至可以说是更简单一些: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。)我们都明白一个道理,一本男性杂志的目标受众往往只是一小部分的男性而已,自己不属于这个群体并不是什么大不了的事情。我相信包包也懂这个道理,只是他没有尝试用同样的思维方式去理解《程序员》而已。

2009年11月的《程序员》(感谢CSDN赠送)与《时尚先生》
在我眼里,《程序员》和《时尚先生》是目标受众不同但定位相似的两本杂志。《时尚先生》中国内地版遇到的问题(相对美国版的Esquire而言),《程序员》或多或少地也会遇到。
广告主为何要为《程序员》的生存买单?肯定是因为广告主能够有所收益嘛,例如说使得读者购买广告主的商品,或者是使得读者试用广告主的服务,也可能是为广告主建立特定的品牌形象。无论如何,《程序员》总得为广告主带来收益,否则广告主没理由要养着它。
现在设想一下,假如你是广告主,你选择投放广告到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的门票是公司买的,书架上的那本《代码大全》也是公司买的。
综上所述,《程序员》就是做给那些决定怎么把公司的钱花掉的人看的。如果你不属于这部分人,你是否阅读《程序员》,阅读之后有什么想法,都不会对杂志本身产生什么实质性影响。
举个更遥远的例子,Playboy也是采用同样的模式,所以才有那么多名人愿意接受采访,所以才有那么多美女愿意接受拍摄,自然Playboy本身也很出名。你看到Playboy上的美女,会觉得她们是在沽名钓誉吗?你根本就不在乎这是不是沽名钓誉,你在乎的只是美女本身。因此,如果包包觉得《程序员》上的人物都是为了沽名钓誉,这只能怪他自己没站对角度来看问题。站在看美女的角度来看这些人物,他们是否沽名钓誉与我有何关系呢?
我相信,美国的电视电影导演不会在乎美女上Playboy是否为了沽名钓誉,能通过试镜的都是合适的演员,Playboy只是提供了一种第三方认可外加选拔来源而已。我也相信,中国的公司不会在乎程序员上《程序员》是否为了沽名钓誉,能通过面试的都是合格的员工,《程序员》的作用自然是和Playboy一样。
举个例子,你觉得CCTV为什么喜欢曝光Baidu和Google的搜索结果问题呢?有很多人很单纯地认为,在竞争对手之间,一方受损肯定使得另外一方获利,因为这是个零和竞争。但在这不是一个封闭的零和竞争,把视觉锁定在这样一个竞争之内得出的结论不一定是对的。不信?我可以提供一些信息来拓宽你的思路。国内的传统媒体都喜欢负面新闻,当别人陷入公关危机的时候,就打个电话给对方的公关部门,“我们已经写好了整版你们的负面新闻准备付印,你们有计划增加明年的广告预算吗?”这使得一些公司投放广告只是为了跟媒体建立共同利益关系,限制媒体无法作出伤害自己的行为,否则媒体自己也会受到伤害(我营收减少股价下跌还哪有那么多广告预算给你)。
尝试换个角度看问题,你自然能够得出不同的结论。在你想要对一个你觉得很简单的问题下定论的时候,为什么不尝试换个角度再看看呢?
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相关的问题可以在此讨论。
基于Silverlight的RIA架构及百度应用
View more presentations from Cat Chen.
这是TechEd第三天下午ASP.NET 4课程资源。与ASP.NET AJAX 4.0相关的问题可以在此讨论。
建站大业,实战ASP.NET 4
View more presentations from Cat Chen.
User Friendly 2009
这个周末有幸去上海参加了UPA China组织的用户体验行业大会User Friendly 2009。参会人员大多数都是设计师,会议内容也是他们熟悉的内容,但对于像我这样少数的参会工程师来说,这样的会议确实让人大开眼界。
在整场会议当中,我觉得让我印象最深刻的就是Jared M. Spool的Keynote了,我觉得其中一些观点是非常有价值的。至于其它的Workshop,也都能学到一些实实在在的东西,不过就没有Keynote那么震撼了。下面就分享一些我印象最深刻的内容:

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

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

这才是体验设计师应该关注的事情——你骑着车从山上沿着小径冲下来,一直冲到溪谷里,伴随着你的尖叫水花四溅。这是一辆什么型号的自行车?从画面上看不出来。这是一辆什么牌子的自行车?也说不清楚。这从根本上就和自行没有关系,真正有关系的是你尖叫着冲进小溪里的那种刺激的体验。
过往我们谈论的是投资回报(ROI),现在我们谈论的是体验回报(ROX)。我们想要向用户推销我们的产品,但实际上他们想要从我们这里购买体验,因此我们必须改变思维方式,考虑向用户提供什么样的体验。
举个例子,用户在你的网站上购买机票,他想要买的其实不是机票这样一个商品,而是一趟舒适的商务旅行。因此,过往只卖机票的网站开始加上酒店服务。在你输入往返日期后,不仅仅搜索往返机票,还搜索目的地的酒店信息。只有我们向用户提供符合他们预期的体验时,我们才能够获取到高回报。
那么Chicken Sexer是如何练成的呢?新人必须跟着一位大师一起工作,新人负责指出小鸡性别,说错了大师就狠狠地打一下新人的手臂。在接受训练之前,任何正常人都有 50%的正确率;在开始接受训练之后,新人的正确率会先下跌到40%——因为他想得太多了。三个月后,正确率会慢慢恢复到50%,一年之后,你的正确率能够提高到60%;再过两三年,你的正确率能够提高到80%。如果你真的把这当作你毕生的事业来做,正确率可以提升到95%。
Jared M. Spool对这个故事的总结是,有些事情是可以学习而无法自省(introspection)的。在这里,“自省”是指对自身心智或情感变化的观察和检验。设计工作也属于可以学习无法自省——你可以通过学习让自己的设计能力获得提升,但你无法归纳出你在学习过程中发生了哪些变化,以便让其他人直接通过同样的变化以获得同样的结果。
我对这类技能的学习方法看法是,你必须在大师级人物的指导下学习,你才能成为大师。这不是任何人能够通过理论知识的学习或者基础技能的训练就能习得的,这些只能让你胜任需要这些技能的工作,但不能让你成为大师。在你成为大师后,你也无法将你的技能归纳为一组理论,你只能通过亲自训练让更多人习得跟你一样的技能。
首先,需要老板重视这件事情。例如在Tencent内部,马化腾相当于“首席体验官”的角色,他重视这个事情,对于做得不好的他会站出来指责,别人自然愿意接受你要推广的,因为没有谁想被老板指责。其次,需要通过有价值的项目来证实你推广的东西。如果你的东西你应用于QQ,对其它Tencent产品来说自然就有说服力了。
上述就是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中的几页幻灯片来解释一下:Bill Buxton有一辆这个型号的自行车,而且这也正是Roland Green获得公路赛冠军所用的自行车型号。画面上给你传达的是这辆自行车相关的信息,但又如何?这世界上还有千万款不同的自行车,为什么要在乎这特定的一辆?这不是人们想要购买的东西。
这张幻灯片是同一辆自行车的不同摆放方式,就像是Pixar动画短片里面的那个独轮车一样。这赋予了它一定的个性,这个思路是对的,但这仍然不是你想要购买的东西。
这才是体验设计师应该关注的事情——你骑着车从山上沿着小径冲下来,一直冲到溪谷里,伴随着你的尖叫水花四溅。这是一辆什么型号的自行车?从画面上看不出来。这是一辆什么牌子的自行车?也说不清楚。这从根本上就和自行没有关系,真正有关系的是你尖叫着冲进小溪里的那种刺激的体验。
过往我们谈论的是投资回报(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了。
尽管我没有参加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能够选择一个更好的时间吧,相信会务今年已经通过举手投票搞清楚大家的喜好了——最好是在星期二三四开会,因为星期一和星期五工作压力小,同时星期六日是用来休息的。
蔡学镛把制作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以媒体身份受邀参加这样一场大会,本来是希望借此机会向自己的读者传递更多更好的技术资讯的,然而却受到大会主办方不公平的对待,搞到连饭都没得吃,你们会喜欢被这样对待吗?如果你们自己也不喜欢被这样对待,那就在如此待人之前先考虑清楚。
跟去年的情况类似的是,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试试,可以实际使用一段时间后对比一下烧钱速度后再决定。
在国内要通过国际长途拨打国内电话,通过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:
事实表明,我们测试用的baidu.com域名确实在上述List中,但部署到baidu.jp后也就脱离了该List。这就很好地解释了我们遇到问题,同时也提醒我们域名已经成为了IE8测试中不可避免的一个紧耦合因素。在过去,我们可以简单地认为,部署在不同URL的相同页面在同一款浏览器中显示出来总是一样的。但现在我们必须修正这句话了,仅当不同URL都基于同一个域名时上述命题仍然成立。
通过这个案例,希望能让大家了解到在开发与测试过程中保持域名一致的重要性。如果你开发的页面要部署到example.com,你最好在develop.example.com上开发,在test.example.com上测试,然后再部署。如果你需要在本机进行开发测试,也要通过改hosts模拟一个localhost.example.com来进行测试与调试,以确保代码在最终部署后能执行在相同的环境下。
故事是这样的,我们有一小段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。

8月5日早上醒来,发现我的Tweetie不能再更新,打开Web看看,发现如下提示:
这时候我觉得很无辜,竟然被误判为发垃圾信息的用户了。当然,这可能和最近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还有任何区别吗?
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
(默认隐藏详细信息?)
- 隐藏这个
- 对于里面的
$$("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(参考Async和Overload)。最后,我突然明白到,这其实就是一种Internal DSL嘛!
在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——
当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。
第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。
无论是哪一种写法,中间都必须写一个分析器层。就如同语法分析器需要使用状态机一样,Internal DSL的实现也必须内置一个状态机,以记录当前执行到什么状态了,并且接下来可以转移到哪些有效状态。
由于这不是一篇专门讲语法分析器和状态机实现的文章,所以我们把关注点保持在API层面就可以了,不深入讨论其实现细节和成本。我们知道链式方法调用能够实现Internal DSL就够了,至于jQuery是如何利用好这一点的,我们在下一篇文章里再作讨论。
如果你不希望错过下一篇文章,你可以考虑订阅我的博客:
后来用jQuery的人越来越多,并且大家都爱用它的链式方法调用,甚至还把这种写法推广到其它语言中去。例如ASP.NET MVP Omar AL Zabir就把他的服务器端C#组件设计为支持链式方法调用的。这时候我才开始关注jQuery,并且逐渐喜欢上了链式方法调用的写法,也在我自己的JavaScript组件中实现类似的API(参考Async和Overload)。最后,我突然明白到,这其实就是一种Internal DSL嘛!
在这篇文章里,我准备先讨论Internal DSL,在下一篇文章里面再解释为什么jQuery是Internal DSL。现在我们就从最根本的问题开始吧——
什么是Internal DSL?
DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。例如说,我们有专门描述字符串特征的正则表达式,有专门描述数据库查询的SQL,有专门描述XML结构的DTD和XSD,甚至有专门描述XML变换的XSLT,这些都是DSL。当然,并非我们关注的领域都有现成的DSL,这时候我们有三个选择:
- 使用通用语言描述该领域的问题(non-DSL)
- 发明一门全新的语言描述该领域的问题(External DSL)
- 在一门现成语言内实现针对领域问题的描述(Internal DSL)
I.DepositTo(new USD(200), CitiBank); /* C# */
I deposit 200USD to CitiBank /* E-DSL */
I.deposit(200.USD()).to(CitiBank); /* I-DSL */
deposit [something] to [somewhere]
?)。第2种做法的成本最高,你需要写一个全新的解释器,至少是写一组全新的规则,然后让YACC这类工具帮你生成一个解释器,但这样出来的语法最贴近人类思维方式,甚至就如同自然语言一样流畅。
第3种做法术语上述两者的折中方案,如果语法不太复杂可以使用Builder模式实现语法分析,写出来的语法相当贴近自然语言,但还是有学习门槛。由于脚本语言有相当的灵活性,所以现在很多人倾向于选择在脚本语言内实现Internal DSL。
如何构造Internal DSL?
常见的两种Internal DSL实现方法是Method Chaining和Function 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的静态类,现在我们就来看看如何实现这个静态类。
我们允许用户输入一个字符串,表示某一个重载的签名。在用户调用函数时,我们需要拿着用户输入的参数实例去跟签名上的每一个参数类型作比较,因此我们需要先把这个字符串转换为类型数组。也就是说,字符串"Boolean, Number, Array"应该转换为数组[Boolean, Number, Array]。
在进行转换之前,我们先要考虑处理两个特殊类型,就是代表任意类型的"*",和代表任意数量的"..."。我们可以为它们定义两个专有的类型,以便在Overload内对它们做出特殊的兼容性处理:
在有了这两个类型之后,字符串"Boolean, *, ..."就会被正确转换为数组[Boolean, Overload.Any, Overload.More]。由于Overload.Any和Overload.More都是函数,自然也都可以看做类型。
在这两个类型得到正确处理后,我们就可以开始编写识别文本签名的转换函数了:
我想这段代码相当容易理解,因此就不再解释了。我第一次写这段代码时忘记写上面的第一个if了,导致空白签名字符串""无法被正确识别为空白签名数组[],幸好我的unit test代码第一时间发现了这个缺陷。看来编写unit test代码还是十分重要的。
为了作长度对比,我们需要在这个函数外对表示任何参数个数的"..."作一下特殊处理:
这一段代码将会整合到第一节的转换函数末端,以便matchSignature函数能够轻易判断出参数与签名是否匹配。在最理想的情况下,我们对输入参数类型匹配到0个或1个重载,这样我们很容易就判断出命中哪个重载了。但如果有2个或以上的重载匹配,那么我们就要从中挑选一个最优的了,这正是下一节要讨论的内容。
由于0和1这两个参数会被编译器理解为int类型,对于第1个重载它们都不用进行类型转换,都与第2个重载它们都要进行类型转换,因此第1个重载较优。
在第1个参数上,第1个重载较优;在第2个参数上,第2个重载较优。在这种情况下,任何一个重载都不优于另一个,找不到较优重载编译器就报错。
在第1个参数上,第1个重载优于第3个重载,于第2个重载无异;在第2个参数上,第1个重载优于第2个重载,于第3个重载无异。尽管第2个重载于第3个重载分不出个优劣来,但我们可以确定第1个重载比它们都要好,因此编译器选择了第1个重载。
假设我们有一个overloadComparator的比较函数,可以比较任意两个签名之间的优劣,我们需要对签名仅仅两两比较,以找出最优重载吗?事实上是不需要的,我们可以利用Array的sort方法,让它调用overloadComparator进行排序,排序后再验证前两名的关系就可以了——如果并列,则不命中任何一个;如果有先后之分,则命中第一个。
具体的overloadComparator代码就不在这里给出了,它依赖于另一个名为inheritanceComparator的比较函数来对比两个签名的参数类型哪一个更贴实际传入的参数类型,里面用到了一种比较巧妙的方法来判断两个类型是否为继承关系,以及是谁继承自谁。
识别文本签名
我们先来回顾一下上一篇文章中提到的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这样的语言是如何处理函数重载匹配的。一般语言进行函数重载匹配的流程都是这样子的:- 参数个数 - 参数个数不对的重载会被排除掉
- 参数类型 - 参数类型无法隐式转换为签名类型的会被排除掉
- 匹配个数 - 排除完毕后,剩下匹配的签名个数不同处理方法也不同
- 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异步调用框架(问题 & 场景、用例设计、代码实现、链式调用、链式实现),现在是时候让我们看一下在各种常见开发情景中如何使用它了。
在我所调用的服务器端API中,只需要GET和POST,且数据都为JSON,所以我就直接把jQuery提供的其它Ajax选项屏蔽掉了,并设置数据类型为JSON。在你的项目当中,也可以用类似的方式将Ajax封装为若干仅仅返回Async.Operation的方法,将jQuery提供的选项都封装在Ajax这一层内,不再向上层暴露这些选项。
假设我们有一个Friend对象,它的get方法用于返回单个好友对象,而getAll方法用于返回所有好友对象。于此对应的是两个服务器端API,friend接口会返回单个好友JSON,而friendlist接口会返回所有好友名称组成的JSON。
首先我们看看较为基础的get方法怎么写:
就这么简单?对的,假如服务器端API返回的JSON结构正好就是你要的好友对象结构的话。如果JSON结构和好友对象结构是异构的,或许你还要加点代码来把JSON映射为对象:
在这里,我们假设friendlist接口返回的JSON就是一个Array,在获取到这个Array后构造一个等长的异步调用队列,其中每一个调用的逻辑都是一样的——取出Array中首个好友的名称,用get方法获取对应的好友对象,再将好友对象放入另一个Array中。在调用队列的末端,我们再追加了一个调用,用于返回保存好友对象的Array。
在这个例子当中,我们没有利用调用队列会把上一个函数的结果传递给下一个函数的特性,不过也足够展示调用队列的用途了——让多个底层为Ajax请求的异步操作按照固定的顺序阻塞式执行。
由于底层异步函数返回的就是Async.Operation,你可以直接把它传递给next方法,也可以用匿名函数包装后传递给next方法,而匿名函数内部只需要一个return。
在原本next方法调用的匿名函数中手动加入setTimeout是一个办法,但为什么我们不写一个辅助函数来解决这类问题呢?让我们来写一个辅助方法并让它和Async.Operation无缝结合起来。
在有了这个辅助方法后,我们就可以在上述getAll方法中轻松实现在每个Ajax请求之间间隔500毫秒。在for循环内的加上对wait的调用就可以了。
最后,如果大家希望将来继续读到类似的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.Operation,链式调用所需的go方法和next方法都是在Async.Operation上面做的扩展,并且这个扩展不会很难,这将在下一小节说明。
对于异步调用进行队列的支持,我们稍后再来处理,首先我们利用现成的addCallback方法和yield方法扩展出go方法和next方法。
实际上,go方法和next方法直接调用的正是yield方法和addCallback方法。go方法的语义与yield方法一样,传递一个参数给Async.Operation实例,并且启动调用队列。同时,next方法的语义和addCallback方法,添加一个调用到队列的末端。
如果调用返回了一个Async.Operation实例,我们就利用它自身的addCallback方法帮我们执行队列中余下的调用。准确来说,是我们构造了一个新的调用链,把队列余下的调用都转移到新的调用链上,然后让当前异步调用在回调中启动这个新的调用链。
此外还有一些地方我们需要略作修改,以兼容新的异步调用队列的。例如result、state、completed的状态变更,在链式调用中是有所不同的。
现在我们已经拥有了一个功能强大的Async.Operation,接下来我们就要看看如何将它投入到更多常见的使用模式中去,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
调用入口
链式调用存在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,接下来我们就要看看如何将它投入到更多常见的使用模式中去,如果你不希望错过相关讨论的话,欢迎订阅我的博客:
订阅:
博文 (Atom)