2010年7月15日 星期四

用 JavaScript 对 JSON 进行模式匹配 (Part 2 - 实现)

上一篇文章里,我们完成了 Dispatcher 类的接口设计,现在我们就来考虑一下如何实现这个类。

Notify & Capture



要实现 notifycapture 就太容易了,我们只需要把 capture 传入的 handler 都保存下来,然后在 notify 里面找到匹配的 handler 就可以了。

var filterHandlerBundles = [];

Dispatch.capture = function(pattern, handler) {
  var filter = createFilter(pattern);
  filterHandlerBundles.push({
    "filter": filter,
    "handler": handler
  });
};

Dispatcher.notify = function(json) {
  for (var i = 0; i < filterHandlerBundles.length; i++) {
    if (filterHandlerBundles[i].filter.apply(this, arguments)) {
      filterHandlerBundles[i].handler(json);
    }
  }
};


这段代码的逻辑很清晰,关键就在于 createFilter 的部分。这个函数负责把一个描述模式的 JSON 转换为一个判断 JSON 是否匹配的函数。

Operators



我们设计了不少的运算法,如何实现他们呢?记住,我们不要 switch case 。因此,我们使用一个关联数组来保存运算符与实现之间的映射关系好了。

var operators = {};

operators["lt"] = function(testValue, value) {
  return arguments.length == 2 && value < testValue;
};
operators["lte"] = function(testValue, value) {
  return arguments.length == 2 && value <= testValue;
};
operators["gt"] = function(testValue, value) {
  return arguments.length == 2 && value > testValue;
};
operators["gte"] = function(testValue, value) {
  return arguments.length == 2 && value >= testValue;
};


这样我们只要把 "$" 后面的运算符抽取出来,就可以立即找到对应的判断函数了。上面4个是比较运算符,由于实现比较容易,所以放在这里做例子。

一个比较难的函数是 eq ,因为它需要根据数据类型来选择具体的判断方式。对于 String 、 Number 、 Boolean , eq 的含义就是 == ;对于 Array , eq 的含义就是里面的每一个元素都 eq ,而且顺序一致;对于 Object , eq 的含义是每一个子条件都符合,因此我们需要将每一个子条件的运算符字符串提取出来,然后调用对应的运算符。具体可以参考完整代码

其他运算符会简单一些,在此我仅仅给出提示,大家可以根据自己的实际需求这些运算符的子集或超集:

  • in - 遍历数组,看能否找到至少一个 eq 的。
  • all - 遍历数组,看是否每一个都存在 eq 的。
  • ex - 如果有传入值,则子元素存在。
  • re - 用正则表达式判断字符串是否匹配。
  • ld - 直接调用函数进行判断。


写好了吗?不太确信自己写得是否正确?这是我们下一篇文章要讨论的内容,让我们先加上一个默认运算符。

operators[""] = function(testValue, value) {
  if (testValue instanceof Array) {
    return operators["in"].apply(this, arguments);
  } else if (testValue instanceof RegExp) {
    return operators["re"].apply(this, arguments);
  } else if (testValue instanceof Function) {
    return operators["ld"].apply(this, arguments);
  } else {
    return operators["eq"].apply(this, arguments);
  }
};


为什么需要一个默认运算符?这其实只是一个快捷方式。在大多数时候,我们需要的都是 eq 运算,如果每一处都要把运算符写上,代码将变得很复杂,也不美观。对比一下两个 JSON ,你觉得哪个更自然?

Dispatcher.capture({
  "status": 200,
  "command": "message"
}, function(json) { /* display message */ });

Dispatcher.capture({
  "status$eq": 200,
  "command$eq": "message"
}, function(json) { /* display message */ });


显然,第一个更直观一些。因此,我们需要一个默认运算符,当运算符字符串就是 "" 时,就通过默认运算符选择一个运算符。

Pattern to Filter



最后,我们需要把 operatorscreateFilter 接上。这部分工作其实也不难,只要调用默认运算符就可以了。

var createFilter = function(condition) {
  return function(json) {
    if (arguments.length > 0) {
      return operators[""](condition, json);
    } else {
      return operators[""](condition);
    }
  };
};


为什么需要考虑 json 参数没有传入的情况?下次文章再告诉你。不这样做也可以,只是有些很细小的问题而已。

写运算符,最需要的是严谨性。因为 Dispatcher 是一个封装好的组件,运算符一点点的不严谨,都会把缺陷埋藏得很深,很难找出来。因此,下一篇文章我们要讨论的是单元测试,通过单元测试我们可以大大提高 Dispatcher 的健壮性。如果你对此感兴趣,记得到 Twitter 上来关注我: @CatChen

2010年7月13日 星期二

用 JavaScript 对 JSON 进行模式匹配 (Part 1 - 设计)

在《从 if else 到 switch case 再到抽象》这篇文章里面说到,解决 if else 和 switch case 分支过多的一个方法,就是做一个专用的 dispatcher ,让它来负责进行筛选与转发。至于筛选条件的描述,模式匹配是一种很常见也很好用的方式。在 JavaScript 里面,用 JSON 来描述模式又是相当方便的事情,所以我们来做一个 JSON 模式匹配工具吧。

用例设计



作为一个 dispatcher ,我们只需要两个方法: notifycapture 。一个最简单的用例是这样的:

Dispatcher.capture({
  "status": 200,
  "command": "message"
}, function(json) { /* display message */ });

Dispatcher.notify({
  “status": 200,
  "command": "message",
  "content": {
    "from": "user1",
    "to": "user2",
    "text": "hello"
  }
});


当然,只有局部的全等匹配是不够的,我们还需要一些其他运算符。

Dispatcher.capture({
  "value1$eq": "hello", /* equal */
  "value2$ne": true, /* not equal */
  "value3$lt": 0, /* less than */
  "value4$lte: 1, /* less than or equal */
  "value5$gt": 2, /* greater than */
  "value6$gte": 3, /* greater than or equal */
  "value7$in": [1, 3, 5, 7, 9], /* in */
  "value8$nin": [2, 4, 6, 8, 10], /* not in */
  "value9$all": [1, 2, 3, 4, 5], /* all */
  "value10$ex": true, /* exists */
  "value11$re": /^A.*/, /* regular expression */
  "value12$ld": function(json) { return true; } /* lambda */
}, function(json) { /* handler */ });

Dispatcher.notify({
  "value1": "hello",
  "value2": false,
  "value3": -1,
  "value4": 1,
  "value5": 3,
  "value6": 3,
  "value7": 5,
  "value8": 5,
  "value9": [1, 3, 5, 2, 4],
  "value10": "hello",
  "value11": "A13579",
  "value12": "anything"
})


随手写下来一堆运算符,看起来实现会很复杂?其实不会有多复杂。在下一篇文章里面,我们会讨论如何设计一个运算符接口,然后逐一实现这些运算符。如果你对此有兴趣,欢迎在 Twitter 上关注我: @CatChen

2010年7月5日 星期一

从 if else 到 switch case 再到抽象

大家觉得在接手遗留代码时,见到什么东东是最让人感到不耐烦的?复杂无比的 UML ?我觉得不是。我的答案是,超过两个 else 的 if ,或者是超过两个 case 的 switch 。可是在代码中大量使用 if else 和 switch case 是很正常的事情吧?错!绝大多数分支超过两个的 if else 和 switch case 都不应该以硬编码( hard-coded )的形式出现。

复杂分支从何而来



首先我们要讨论的第一个问题是,为什么遗留代码里面往往有那么多复杂分支。这些复杂分支在代码的首个版本中往往是不存在的,假设做设计的人还是有点经验的话,他应该预见将来可能需要进行扩展的地方,并且预留抽象接口。

但是代码经过若干个版本的迭代以后,尤其是经过若干次需求细节的调整以后,复杂分支就会出现了。需求的细节调整,往往不会反映到 UML 上,而会直接反映到代码上。例如说,原本消息分为聊天消息和系统消息两类,设计的时候自然会把这设计为消息类的两个子类。但接着有一天需求发生细节调整了,系统消息里面有一部分是重要的,它们的标题要显示为红色,这时候程序员往往会做如下修改:
  1. 在系统消息类上面加一个 important 属性
  2. 在相应的 render 方法里面加入一个关于 important 属性的分支,用于控制标题颜色
程序员为什么会作出这样的修改?有可能因为他没意识到应该抽象。因为需求说的是「系统消息里面有一部分是重要的」,对于接受命令式编程语言训练比较多的程序员来说,他或许首先想到的是标志位──一个标志位就可以区分重要跟不重要。他没想到这个需求可以用另一种方式来解读,「系统消息分为重要和不重要两种类别」。这样子解读,他就知道应该对系统消息进行抽象了。

当然也有可能,程序员知道可以抽象,但基于某些原因,他选择了不这样做。很常见的一种情况就是有人逼着程序员,以牺牲代码质量来换取项目进展速度──加入一个属性和一个分支,远比抽象重构要简单得多,如果要做10个这种形式的修改,是做10个分支快还是做10个抽象快?区别显而易见。

当然, if else 多了,就有聪明人站出来说「不如我们改成 switch case 」吧。在某些情况下,这确实能够提升代码可读性,假设每一个分支都是互斥的话。但是当 switch case 的数量也多起来以后,代码一样会变得不可读。

复杂分支有何坏处



复杂分支有什么坏处?让我从百度 Hi 网页版的老代码里面截取一段出来做个例子。

switch (json.result) {
  case "ok":
    switch (json.command) {
      case "message":
      case "systemmessage":
        if (json.content.from == ""
          && json.content.content == "kicked") {
          /* disconnect */
        } else if (json.command == "systemmessage"
          || json.content.type == "sysmsg") {
          /* render system message */
        } else {
          /* render chat message */
        }
        break;
    }
    break;


这段代码要看懂不难,因此我提一个简单问题,以下这个 JSON 命中哪个分支:

{
  "result": "ok",
  "command": "message",
  "content": {
    "from": "CatChen",
    "content": "Hello!"
  }
}


你很容易就能得到正确答案:这个 JSON 命中 /* render chat message */ (显示聊天消息)这个分支。那么我想了解一下,你是如何作出这个判断的?首先,你要看它是否命中 case "ok": 分支,结果是命中了;然后,你要看它是否命中 case "message": 分支,结果也是命中了,所以 case "systemmessage": 就不用看了;接下来,它不命中 if 里面的条件;并且,它也不命中 else if 里面的条件,所以它命中了 else 这个分支。

看出问题来了吗?为什么你不能看着这个 else 就说出这个 JSON 命中这个分支?因为 else 本身不包含任何条件,它只隐含条件!每一个 else 的条件,都是对它之前的每一个 ifelse if 进行先非后与运算的结果。也就是说,判断命中这个 else ,相当于判断命中这样一组复杂的条件:

!(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

再套上外层的两个 switch case ,这个分支的条件就是这样子的:

json.result == "ok" && (json.command == "message" || json.command == "systemmessage") && !(json.content.from == "" && json.content.content == "kicked") && !(json.command == "systemmessage" || json.content.type == "sysmsg")

这里面有重复逻辑,省略后是这样子的:

json.result == "ok" && json.command == "message" && !(json.content.from == "" && json.content.content == "kicked") && !(json.content.type == "sysmsg")

我们花了多大力气才从简简单单的 else 这四个字母中推导出这样一长串逻辑运算表达式来?况且,不仔细看还真的看不懂这个表达式说的是什么。

这就是复杂分支难以阅读和管理的地方。想象你面对一个 switch case 套一个 if else ,总共有3个 case ,每个 case 里面有3个 else ,这就够你研究的了──每一个分支,条件中都隐含着它所有前置分支以及所有祖先分支的前置分支先非后与的结果。

如何避免复杂分支



首先,复杂逻辑运算是不能避免的。重构得到的结果应该是等价的逻辑,我们能做的只是让代码变得更加容易阅读和管理。因此,我们的重点应该在于如何使得复杂逻辑运算变得易于阅读和管理。

抽象为类或者工厂


对于习惯于做面向对象设计的人来说,可能这意味着将复杂逻辑运算打散并分布到不同的类里面:

switch (json.result) {
  case "ok":
    var factory = commandFactories.getFactory(json.command);
    var command = factory.buildCommand(json);
    command.execute();
    break;
}


这看起来不错,至少分支变短了,代码变得容易阅读了。这个 switch case 只管状态码分支,对于 "ok" 这个状态码具体怎么处理,那是其他类管的事情。 getFactory 里面可能有一组分支,专注于创建这条指令应该选择哪一个工厂的选择。同时 buildCommand 可能又有另外一些琐碎的分支,决定如何构建这条指令。

这样做的好处是,分支之间的嵌套关系解除了,每一个分支只要在自己的上下文中保持正确就可以了。举个例子来说, getFactory 现在是一个具名函数,因此这个函数内的分支只要实现 getFactory 这个名字暗示的契约就可以了,无需关注实际调用 getFactory 的上下文。

抽象为模式匹配


另外一种做法,就是把这种复杂逻辑运算转述为模式匹配:

Network.listen({
  "result": "ok",
  "command": "message",
  "content": { "from": "", "content": "kicked" }
}, function(json) { /* disconnect */ });

Network.listen([{
  "result": "ok",
  "command": "message",
  "content": { "type": "sysmsg" }
}, {
  "result": "ok",
  "command": "systemmessage"
}], function(json) { /* render system message */ });

Network.listen({
  "result": "ok",
  "command": "message",
  "content": { "from$ne": "", "type$ne": "sysmsg" }
}, function(json) { /* render chat message */ });


现在这样子是不是清晰多了?第一种情况,是被踢下线,必须匹配指定的 fromcontent 值。第二种情况,是显示系统消息,由于系统消息在两个版本的协议中略有不同,所以我们要捕捉两种不同的 JSON ,匹配任意一个都算是命中。第三种情况,是显示聊天消息,由于在老版本协议中系统消息和踢下线指令都属于特殊的聊天消息,为了兼容老版本协议,这两种情况要从显示聊天消息中排除出去,所以就使用了 "$ne" (表示 not equal )这样的后缀进行匹配。

由于 listen 方法是上下文无关的,每一个 listen 都独立声明自己匹配什么样的 JSON ,因此不存在任何隐含逻辑。例如说,要捕捉聊天消息,就必须显式声明排除 from == "" 以及 type == "sysmsg" 这两种情况,这不需要由上下文的 if else 推断得出。

使用模式匹配,可以大大提高代码的可读性和可维护性。由于我们要捕捉的是 JSON ,所以我们就使用 JSON 来描述每一个分支要捕捉什么,这比一个长长的逻辑运算表达式要清晰多了。同时在这个 JSON 上的每一处修改都是独立的,修改一个条件并不影响其他条件。

最后,如何编写一个这样的模式匹配模块,这已经超出了本文的范围。如果你感兴趣,你可以订阅我的博客,我会在将来写文章描述这个模块:

2010年5月27日 星期四

iPad 到手

首先不得不说的是,买 iPad 的过程有多么地折腾,所以上个周末才拿到 iPad 。

一开始预定 iPad 的时候,我的态度是观望的,直到机器出来了,我也亲自玩过了,才决定要买。那是某个周末,我们在西单吃饭, @Paveo@heqian@petefang 都把自己的 iPad 带来了,然后我每一个都拿过来玩了一下,发现最好玩的就是 @Paveo 那个了,因为游戏比较多。当晚 @Paveo 在 Twitter 上说可以帮朋友买 iPad ,我就立即向 iPad 下单了。

@Paveo 买 iPad 的方法比较省钱,也比较折腾──到 Apple 那里下单,东西送到免税州朋友的手上,从而把税省掉;然后再发到加州某位专业代购的手上,他帮忙发回国内,并且承诺不发生关税(发生关税他承担)。这样算来,价格就是 iPad 加美国国内快递费用再加代购费用,国际快递费用和中国国内快递费用都包含在代购费用里面了,因此总额不到 ¥4000 。

比较神奇的是, @Paveo 的 iPad 有两个错误发往广州了,最后到了 @LEMONed 手上,而且比北京的早到了,于是我就请当时在广州出差的 @eustacia 帮我把其中一个领回来了。整个购买周期加起来一个月,其实挺长的,不过也没办法──为了省钱。去中关村买的话,比这个折腾价要贵 ¥500 左右。

Got my iPad from @eustacia. It's sealed by @LEMONed.

在 iPad 拿到手之前,我就下载了一个 iPhone SDK 3.2 来看看 iPad 模拟器是什么样子的,结果发现模拟器里面的 iPad 挺 beta 的── iPhone 模拟器是内置照片和联系人数据的,这让测试 App 变得容易──如果你的 App 需要调用内置的照片和联系人,打开就有样板数据,你不需要操心任何事情。 iPad 是没有样板数据的,测试调用联系人还勉强可以,最多就自己手动创建一组样板数据,但是照片就无法创建了,除非你再写一个 App 往模拟器里灌样板照片。

拿到 iPad 后,发现这个产品确实有点 beta ,不像 iPhone 那样做到尽善尽美之后才放出来。当然, iPhone 所谓的尽善尽美也只是发布时的观点,发布后发现只有 Web 不行,又增加 App 了。 iPad 拿到手后,你会发现内置应用跟 iPod Touch 差不多,甚至就不如 iPod Touch ── Stocks 、 Weather 、 Voice Memos 、 Clock 、 Voice Memos 都没有了。此外, iPad 硬件上是有数字罗盘的,但就是不配 Compass 这个应用,内置的 Maps 也不提供此项功能。不知道这些功能会否在未来的 iPhone OS 4.0 中补上,补上的话不知道 iPad 未来的 OS 升级会不会如 iPod Touch 一样收费。

硬件方面,感觉没什么大问题。谁第一次拿起来都会觉得它有点重,并且怀疑长期使用是否能锻炼上肢肌肉,结果是对上肢肌肉完全没有锻炼效果的,拿着拿着就习惯这个重量了。屏幕是 iPad 最大的亮点,看起来非常鲜艳,看图片和视频的效果都非常好。屏幕背后是两块大电池,这是 iPad 比较重的原因,因此才有所谓的10小时续航能力。需要对 iPad 充电,必须用 iPad 附带的 10W 充电器,一般 USB 充电器(包括 iPhone 自带的)都是 5W 的,没办法向 iPad 充电。我的 MacBook 也能对 iPad 充电,这依赖的是 MacBook 对 USB 良好的供电能力,其他电脑的 USB 就不一定能对 iPad 充电了。

软件使用方面,我主要用它来做阅读器,偶尔也玩玩游戏。我买过一些 Kindle 书,也买过一些 PDF ,在 iPhone 上阅读的体验并不算好,现在有了 iPad 终于可以很爽地看书了,而且可以睡觉前抱 iPad 上床看书。阅读器 App 方面,我个人推荐 iBunkoHD ,它的翻页效果很好,同时也支持无缝的双页并排,这对看杂志来说非常重要,因为杂志往往会做一些跨越两页的整版大图。

http://farm5.static.flickr.com/4020/4643874451_202c24765c.jpg

Kindle 书看起来也很舒服,相对 iPhone 而言,一个屏幕内能够容纳下更多的内容,自然也就不用经常翻页了。图片也能看清楚,不需要放大后才能看到细节。当然,有了全屏幕的 Mobile Safari ,也更诱惑人去买书。

Kindle

游戏方面,推荐 iPhone 上非常耐玩的 Flight Control 对应的 iPad 版,也就是 Flight Control HD 。无聊时打发时间不错,随时可以开始玩,随时可以结束,而且还不需要网络。

http://farm4.static.flickr.com/3389/4643874609_47274a77df.jpg

我的 iPad 暂时没装太多的东东,除了阅读和游戏什么都不干,而且主要也就是阅读。用 iPad 阅读确实需要更好的定力,因为你随时可以退出阅读器然后切换到游戏,这是 Kindle 做不到的。

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 的海豹们还是爬到木板上来了,让我们拍摄。

DSC02881

DSC02960

DSC02987

最后一个景点,当然也是最重要的,自然就是 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 。