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。与ListView 的LayoutTemplate相类似的是,它也能够定义控件展示的整体布局,并且仅仅是迭代输出其中的一小部分。例如说,编写一个有thead的table,并且仅仅是迭代输出thead之后的tr。在这方面,是DataView和ListView完全一致的。唯一不同的是,客户端暂时还没有DataPager这样的控件,所以DataView必须一次性的完整显示整个Array的数据。

如果你把单个Object绑定到DataView上,它就会显示为一个DetailsView。这使得你可以使用两个DetailsView就做出经典的Master-Details展示模式,和在服务器端分别用ListView和DetailsView做出来的一样。当然,你不能指望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 Unleashed和Pro 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的优化。

2009年2月22日星期日

Beijing Open Party

这是我第一次参加Beijing Open Party,主要是为了了解unconference的具体组织形式,其次是想谈谈GMP Party与Beijing Open Party合作的事情。

下午1:00,我们到达了东直门的ThoughtWorks办公室,门口的签到挺方便的——假如曾经在网上报名或者参与过过往活动,输入名字后其他信息就会自动显示出来。然后进入了ThoughtWorks的办公区域——一个无阻隔的大hall,四周除了窗户就是涂鸦墙,水房仅仅用吧台分隔开来,只有娱乐室是用墙壁分隔的。这样的办公区域感觉挺好的,特别是用来做各种创意活动的话,但是如果想要专心coding,不知道会不会很容易受周围讨论的影响。无论如何,这样的地方用来举办Open Party倒是不错的。



1:30开场之后,先有30分钟的『调侃5分钟』活动,任何人都能够上去做个简单的分享或者广告,每个人的时间限制在5分钟之内。有些人上去介绍了一下自己的网站或者开源项目,也有些人上去分享了一些想法和工作成果。这种形式的分享在同一专业人群内总是很有效率的,不需要演讲者有非常优秀的演讲技能把专业内容转化为适合大众口味的内容或调动听众情绪,只要能把专业内容正确表达出来,听众们一定能听懂,并且也知道哪些内容是更能体现演讲者的技术深度。



在『调侃5分钟』结束后,正式开始对话题的投票,虽然不限制每个人的投票次数,不过还是鼓励大家只投真正想听的话题,以便把话题分散开来,人人都能听到自己想听的,而对能拿到奖品的top 3话题评价也更准确一些。投票完毕,根据4 tracks * 3 periods选出了12个话题,然后就正式进入话题演讲阶段了,其中每个period为一个小时。

我一直留在大厅听较为大众化的话题,偶尔去各个会议室看看其他小众话题是如何进行的。由于这次是Mozilla赞助的活动,所以有3个Mozilla相关的话题,其中第一个是在大厅的Seth演讲,讲的是Mozilla Community Localization。

随后还有图灵的书友会以及CC商业模式介绍的话题,讲得都不错。比较可惜的是,到后期饮料都被拿光了,剩下很多曲奇却没有人吃。

总的来说,Beijing Open Party确实挺geeky的,不过放在大厅的话题还算是大众化,而且演讲的质量也挺高的,所以即使没有哪个小众话题吸引你,也可以在大厅听听,或者走到旁边跟其他人聊聊天。

2009年2月14日星期六

Facebook News Feed 上的内容强调与弱化

我之前曾经思考过Facebook News Feed的价值,觉得News Feed中良好的信息筛选与排序算法是我喜欢使用Facebook的原因之一。当时我只想到了News Feed显示的是一种trend,以及这种trend能够带来的经济利益,不过貌似至今Facebook也没有开发这方面潜在赢利点的倾向。

最近我又对比了一下Facebook和众多国内SNS网站的首页输出,发现Facebook的News Feed其实很侧重于用户交互,并且这个侧重点不仅仅与信息输出的条数有关,还与版面设计有关。

如果你把Facebook的News Feed当作报纸来看,那么哪些是能够第一时间成功吸引你注意力的“头条”?我想应该是用户上传或分享的视频、图片,以及有评论的信息。首先,这些信息占的版面大;其次,视频与图片的丰富色彩及评论区域的背景颜色明显地和颜色单调的描述性文字区分开来。我们来看一个具体的例子:

Facebook | Home

我想你肯定能够轻易地看得出中间照片集一段的强调作用——占用空间更大、粗体的标题、字号更大的评论链接。这说明了,Facebook是刻意设计为强调这一条信息的,而非仅仅因为照片占用更多空间而“意外”造成的效果。

那么有评论的信息呢?评论区域淡淡的背景色,也成功地将它从其他同等级别的信息中凸显出来:

Facebook | Home

大量弱化的信息,往往会被淹没在News Feed中,然而只要有评论,你就一定不会错过。评论区域的背景色一定会捕捉到你的注意力,甚至你会因为评论数量之多,而好奇到底是什么事情吸引了那么多人的注意力。

校内在一定程度上抄袭了Facebook的这些设计,所以首页看起来还是挺顺眼的,虽然我实在搞不懂校内是如何选择强调哪些信息的,而且在中文字体上用粗体能得到的强调效果我觉得并不明显,此外弱化信息的弱化做得也不够明显。

海内在这方面显得有点点“自作聪明”了,结果得到的效果更糟糕。例如用户改变状态(或曰“签名”、“迷你博客”……),这类信息数量之大,显然应该弱化之,最多仅仅强调有评论的几条。海内则是反过来做:

首页_海内

这种对好友意义不大的信息(相对发照片或写博客来说),竟然要暂用三行的空间。而且,海内还默认把评论隐藏起来了,在一堆同类信息当中,你实在看不出哪一条是因为能够得到别人注意而拥有评论的,于是扫一眼就全部跳过去了。此外,海内的评论(或曰“回复”)也不突出,即使默认展开了强调效果也不明显。

最后总结一下,SNS的首页信息必须做到两点
  1. 能够有所侧重地选择推送给用户的内容
  2. 在版面设计上把重点强调出来
至于哪些是应该重点推送的信息,我想除了可以通过复杂的算法计算之外,有些简单的规则是普遍适用的。例如说评论,能够引起部分人注意的信息,至少比没人会留意到的信息更值得我关注。又例如说照片,别人原意花时间上传的照片,至少比无意修改的状态更值得我关注。

2008年12月29日星期一

GMP Party EP2

Geeky meets Pretty的第2场大party终于来了(平时的小聚会不计算场次)。自从我在网志年会上见识到了Punch Party后,我就一直想在GMP内搞这样一个活动,在小组内进行过若干讨论后我们最终决定要搞一个2008年年度“大”party,形式当然就是Punch Party了。

这次party,要感谢Chloe和Piggest帮忙组织,是她们帮忙确定了整个活动的rundown和预订到了场地(奇遇花园)。然后要感谢所有的speakers,是他们带来了如此丰富的speech:

Jinghua和Chloe的session做成了series,让大家非常期待下一个episode。其中Jinghua说到清华校园不适合搭讪美女时,方军立即表示反对并解释了清华哪里适合寻找美女。Leo因为是在看过广州Punch Party的视频后做的准备,所以他的session风格也更接近之前广州Punch Party的,据说他之前按照7分钟的时间安排排练了好几次哦。龚纹的星座解释好复杂哦,听着听着就让我感觉在上高中的政治课,可惜现场没有人主动出来让龚纹做一下分析,否则会好玩很多。

作为一个party,少不了的当然是回答问题抢奖品的环节,我的geeky问题加上Chloe的fashion问题还有龚纹的星座问题,确保了大家不仅仅要有实力还要有运气才能拿到奖品。对我来说,最好玩的就是问Office 2007下一版本号的问题了,竟然那么多人不知道Office 13这个版本号被跳过了。

这次的party虽然并没有Carol组织的Punch Party那么紧凑和快节奏,不过我觉得作为第一次已经做得不错了,相对来说不够2.0(主要是单向交流,听众互动少),而且也没有sponsor。方军建议我们可以和Beijing Open Party合作,做成他们的一个track,因为我们在某些方面正好和他们互补哦——他们缺少美女,而我们性别比例极力保持在1:1;他们缺乏非技术话题因而显得范围比较窄,而我们追求多样性有各式各样的话题;我们没有sponsor所以要收场地费用,而他们拥有不少的sponsor;我们还没有固定的流程,而他们已经有相当的活动经验。

anyway,接下来我们GMP也要建立一个对外发布信息及接受投票和留言的blog,然后逐步把GMP Party的筹办过程流程化,让GMP Party成为一个相对稳定的活动。

2008年12月11日星期四

假如你愿意以原版或影印版价格购买翻译书籍的话

这篇文章是接着Tony Qu的《批“觉得有必要记一下的东西——关于翻译”一文》写的,就讨论一个问题——你原意以原版或影印版的价格购买翻译书籍吗?

为什么问这个问题?这源自Tony Qu批判的原文中的一句话:
很多国内程序员看书就只看英文原版,开发就只用原版VS,就是这个原因。


我承认,在英语能力相当的程序员当中,这句话说的是事实。能够看原版书的尽量看原版书,不仅仅因为看起来舒服,更因为容易在P2P网络上找到OCR后完美排版的PDF(甚至是官方PDF)。但这部分人当中,又有多少是真真正正是去买原版书的(原价格按汇率算),或者至少去买影印版的(价格是翻译版的一倍以上,原版的一半以下)?我想就很少了,我仅仅知道Jeffrey Zhao会从国外买二手原版书。很老实说,假如读者原意为翻译版支付引进版的价格,那么翻译版能够做得比现在的要好得多。但有多少人是连翻译版的价格也不愿意支付的,宁愿上网找极度难看的未OCR扫描版?此时,结论已经很明确,翻译版质量差不完全是出版社或者译者的问题,是经济利益驱使,这有电影工业与唱片工业在大陆的做法为证!

虽然现在DVD的分区对购买者来说已经毫无意义,但当初就把大陆分作一个6区是有理由的。话说刚刚有DVD的时候,大陆的DVD市场跟国际市场同一个运作方法,电影上映后至少3个月才发行DVD,以免影响票房。但DVD就是卖不出去,不仅仅是因为太贵,还因为国内的购买者不愿意等那3个月,当然也不愿意买票进电影院看。他们买什么?买盗版DVD。因为DVD必须有正版片源出来后,才可能有正版片源的盗版DVD,所以3个月未到,所有的盗版都只可能是枪版DVD。然而大陆消费者就是不在乎画质和音质,他们就是想以最低的价格抢先看到电影,看完就算。当这种需求被中国电影工业从业人员知道以后,一种新的商业模式发展出来了,以满足这部分消费者的需求——大陆的引进版DVD的发行和电影的上映几乎同步进行,价格仅仅比盗版略高,并且最重要的是,人为地把画质和音质降低到枪版的水平。就这样,电影工业能够在大陆盗版市场上分到一杯羹,我的意思是他们算是卖官方枪版的了,同时也不影响他们的国际市场价格——即使存在购买渠道,港台消费者也不会接受这样廉价的劣质DVD,仍然会等上映3个月后买原版DVD。

唱片工业做的事情是类似的,大陆所谓的引进版虽然价格更容易被消费者接受,但音质是经过人为降低的。这就是价格歧视,因为大陆消费者只愿意支付一个更低的价格,也只需要购买质量更低的商品,所以供应商就可以把这划分为一个独立的价格歧视区域市场。我不知道图书行业是否存在这样人为控制的“阴谋”,但只要大陆消费者的需求还是这样的“另类”,价格歧视原则就会一直适用,这样的事情就会无意识的继续存在,以远低于影印版价格售卖的翻译版质量就肯定还是那么低。对了,顺带说一句,港台翻译版的价格是和大陆影印版差不多的。

总结一下,我也是译者,翻译工作低廉的价格以及编辑工作的效率低下Tony Qu说过我就不再说了。如果大陆翻译版最终按照港台翻译版的价格去销售也能获得同样的销量的话,我相信译者可能愿意为这份价钱做全职的翻译工作,我相信出版社愿意聘请更多专业编辑并且合理安排他们的工作量。

P.S.如果要认真学习和研究中国的传统语言文化,我个人还是建议去台湾。

2008年12月6日星期六

SD2C 2008

这几天参加了SD2C,也就是“软件开发2.0技术大会”。规矩当然是照旧的,social第一,session第二。

Day 1

第一天想着12:00开始签到,于是慢吞吞地准备出发,去到九华山庄已经是11:00。这是我第一次在没有车接送的前提下一个人跑来九华山庄,路上浪费了不少时间。签到后开始乱逛,然后陆续找到了公司不同部门来的同事,发现大家都没吃午餐。虽然我自己从McDonald's带了一个汉堡来,不过还是跟大家一起打车到小汤山街口的镇上吃饭。吃完饭就13:00了,应该开场了,可是打车回九华山庄实在太难,最后无奈打了黑车回去。我个人认为,把开场放在13:00但又不提供午餐是十分不体贴的。在市区的话也就算了,在九华山庄这样一个三流装一流的地方,选择不多又不怎么好吃的自助餐竟然要¥68一位,方便面也要¥20一盒,不提供午餐这样的做法实在是不人道。

下午的议程看起来全是keynote级别,听过后才发现那完全是赞助商专场——逐个赞助商轮流上去宣传自己的技术或者观点,而且还不提供任何课程反馈的机会,它讲得再烂我也不能表达一下我的不满。赞助商专场分为上下两个半场,上半场的人都是来吹水的,就吹嘘自己公司的东西,泛泛而谈,听了也没什么收获;下半场讲的东西稍微实在一些,雷军和戴志康上来讲的话都很真诚,也很有感染力。总之,要是早知道第一天下午是赞助商专场的话,我就去WinHEC玩到15:00再过来好了,然后18:00吃饭,接着参加晚上的活动。这只能怪我参加会议的经验还是太少了,没有把这么明显的安排给看出来。

下午听了那么多个小时,最让我感到震撼观点来自于雷军。他在说他投资的UCWeb时提到了这样一个观点:我们曾经都是互联网用户中的领头羊,有什么新鲜的东西出来我们都先去尝试,然后慢慢普及到大众用户。但在移动互联网就不是这样,移动互联网的主要用户是那些比较少机会通过计算机接入互联网的人,例如学生、民工和军人。也就是说,我们这些每天使用计算机连接着互联网做开发的人,其实是不可能知道移动互联网的真实需求的,我们无法体会使用不到计算机接入互联网会使怎样的。过往我一直认为移动互联网用户就是高端移动用户和高端互联网用户的交集,现在我终于明白到不是这么一回事了。

晚上去各个沙龙看了一下,没有什么印象特别深刻的。只记得在“职业规划与成长论坛”上听到这样一句搞笑的话——“我们的HR自裁了,也就没有HR了”。晚会后有班车把我们送回到地铁站,然后回到家也就是23:00了。

Day 2

第二天早上9:00出发,去到就是11:00了。我觉得把第一个session放在9:00开始是很不合理的,九华山庄这么偏僻的地方外加不包住宿,谁原意那么早起床赶7:00从市区开出的班车啊,结果当然是大家都很晚才到啊。对于speaker来说,如果自己的session被放在早上,可能也会觉得大会不重视他的session,因为早上能赶来参加session的人就是少一些,这个大家都明白的。我去到时已经是11:00,然后去听了一下郭安定的课,中午和郭安定、老孟、莫依等MVP一起吃饭,还是挺好玩的。

从郭安定讲的内容来看,他很注重企业生态圈。也就是说,只要你能够帮助别人解决实际问题,赚钱的机会总是有的,但也没必要自己把什么都做了,也要留机会让别人帮你,赚你的钱。把这个道理传播出去,让更多人赚得到钱,让更多人受惠。这种思想我是十分认同的,如果帮助别人的同时又帮助了你自己,这有什么不好呢?

下午我去听了一下周爱民的JavaScript+Delphi+ErLang,语言层面的东西不用怎么在乎,他也没做很应用性的展示,我觉得最重要的一个思想还是分层——只要实现了分层,层与层之间是针对接口设计的,那么随时换掉一个层都是可以的,只要接口保持不便,什么语言并不重要,不同层的需求不同实现语言自然也可以不同。另外还听了李建忠的.NET设计模式,讲的是一些老生常谈的内容,没什么非常特别的。其实讲什么技术并不重要,最重要是把那种思想说清楚,让听众都能领悟到,那就算是成功了。

晚上是CSDN专家沙龙,本来说是让各位专家说说对CSDN服务的不满与建议的,结果大家的自我介绍都拖得很长,介绍完也就完了,赠送给各位专家一人一本绝影签名的《疯狂的程序员》,然后就赶班车去了。其中最有趣的一幕,就是莫依问老孟说,“是什么让你多年坚持在论坛上帮助别人解答问题”,旁边的郭安定小小声说了两个字,“伟哥”。

Day 3

最后一天,依旧是9:00出发11:00到。原本计划去听听iPhone开发的,发现基础部分都讲完了,开始讲OpenGL的使用,我又完全听不懂,听了三分钟就睡着了。

下午听的是使用YSlow2.0优化网络性能,主要是了解一下YSlow2.0的新特性,也就是如何自己设计符合自身网络特性的网络性能评分规则。接着去听了Amazon运计算服务的架构分析介绍,确实比较介绍性,让你知道就是有那么多的服务在运行着,并且能为企业省钱,就这么简单。最后听的一个session是面向对象的JavaScript,这是作为当今JavaScript开发人员必须知道的基础知识啦,听他说一遍只是为了好玩。

SD2C结束后,马宁准备把我和莫依还是陈敏送到地铁站,最后大发慈悲把我们都送回家了,超级好人,在这里需要感谢一下。建议明年真的不要选择九华山庄这样的地方了,交通实在是不方便。

P.S.发现自己最近参加的会议不少啊,整个blog塞满了关于各个会议的文章,如China MVP Open Day 2008、TechED 2008 China、Chinese Blogger Conference 2008、WinHEC 2008 China。

大陆技术书越来越台湾化了

这次SD2C我总共搬了4本书回家,2本买的2本送的。我发现现在大陆出版的技术书越来越台湾化了,也就是“更多搞笑,更少大道理”,而这正好符合我近期的读书倾向。

在CSDN专家沙龙上,每位专家都拿到了一本绝影的《疯狂的程序员》,听说是绝影自己经历的写照。虽然我还没仔细看,不过相信这本书会是本十分轻松的小说,这正是我喜欢的。之前看《梦断代码》也是足够轻松的,不过轻松背后也发人深思,让你不得不去思考很多软件工程和组织架构的问题。

在SD2C的签售会上,我还买了一本《悟透JavaScript》。我已经不需要看任何对JavaScript泛泛而谈的书,但我仍然买了这本书,就是想看看其娱乐性的一面,如果确实写得好,包括插图画得好,那也是十分值得我学习得。什么时候我也要在我的blog上加上专业插图,否则就太没有竞争力了。

另外我最近还买了一本《大话设计模式》,同样也是因为它搞笑的一面。虽然读起来会觉得某些剧情比较牵强,但至少这是一种写作风格,可能在大陆还不成熟,不过将来会被越来越多的作者采纳,自然会成熟起来。

总的来说,我现在也看纯技术书看得有点累了,况且很多东西看了不用等于没看,过一段时间还真变成没用的了。轻松娱乐的东西,至少保证了阅读的过程是愉快的,即使将来用不着,用得着也就算是赚了。

2008年12月3日星期三

WinHEC 2008 China

今天是WinHEC第一天。WinHEC就是Windows Hardware Engineering Conference,中文名是做Windows硬件工程大会。昨晚紫柔告诉我9:00到场签到就可以了,因为9:30正式开始,但我早上还是忍不住多睡了一会儿,所以10:00才到场。当时还是张亚勤的演讲,我进入主会场后迅速找到剩余的几个空位坐下来了。

张亚勤的演讲当然紧接着若干个demo,其中Surface的demo在MVP Open Day已经看过了,这次还是同一个人来做。还有那个health care的video,也是MVP Open Day上面放过的,看来这两个月没什么变化嘛。整体而言,张亚勤演讲后的中文demo都是做一些我们已经知道的东西,没什么值得看的。

原本张亚勤的演讲只有1个小时,结果延长了很多,然后Dennis Flanagan才上来讲Windows 7。Windows 7的demo就非常有冲击力了,特别是device stage这样的特性。

在Windows 7里面,每个外设(包括网络外设)都有自己的图标,厂商可以设计一个和外设外观一样的图标,使得用户更容易在Windows 7中识别这一设备。例如你有黑白两个USB drive,都插入到电脑后,你会发现不知道哪个盘符是哪个USB drive的,要看看里面的内容才知道。在Windows 7里面,只要厂商制作了对应的图标,用户就能在图形界面上看到有区别的两个图片,也就可以在不了解何谓盘符的前提下正确操作这两个USB drive了。

此外,每一个外设都可以有自己的device stage,例如连接一部Zune后可以显示容量、电量,还可以显示为你推荐的MP3销售信息;又例如连接一部Nokia手机后可以显示有关的内部信息,以及Nokia网站上为这部手机推荐的新游戏。简单来说,硬件设备销售后不再是与硬件厂商脱离关系了,只要它连接到安装了Windows 7的PC,并且这台PC还连接着网络,硬件厂商就能得知这台设备的使用情况,并且为你推介更多的商品和服务,当然也可能包括免费的服务与技术支持。肯定有人会说这会导致隐私问题之类之类的,我相信最终Windows 7和硬件厂商会引入访问控制机制,让用户选择一台设备连接到Windows 7后是否允许相关信息通过网络传输,这个大家不用担心。

两个keynote结束后,就是抽奖环节。早上每一个人进来时都拍了一张照片用于抽奖,然后用这些照片拼成了一个巨大的奥运五环徽标,最终用Silverlight Deep Zoom来随机zoom in到某一个人的照片之上。这样的抽奖形式还是挺有创意的,不过有些照片实在太不清晰了,最后拿到了大奖的人到底是不是照片上的人,我们都不知道。

午餐是跟紫柔以及其它MVP一起吃的,紫柔带来了一个90后的实习生,是一个拿了国家奖学金去新加坡读高中的女生。她应该是紫柔的助手吧,不过我们说的东西她都听不懂,应该会觉得十分无聊。

随后,让我最不满的事情就发生了!今年的WinHEC和TechED一样,通过盖章来增加互动,总共有1200份Windows 7光盘和1000只2G USB drive可以通过盖全10个章后获得,并且日程表写着下午2:00开始换领的。但我们2:00回到会场发现今天的400只2G USB drive已经换领完毕了,虽然Windows 7光盘还是有的,但因为必须同时换领,所以也就相当于不能换领。既然日程表写着下午2:00开始就应该下午2:00开始啊,怎么能够提前开始发呢?!

下午的课程我们都没有去听,一群MVP聚在一起聊天。最有趣的一幕就是,竟然有人来主动搭讪那位90后女生。是不是非80后的人就分不清什么人是90后的呢?又或者是IT人士太闭塞了?反正我觉得90后的化妆和穿衣风格挺容易辨别的。我们分散地坐在媒体展示区的长沙发上,那个人直接无视我们问她还有没有盖章的那张纸。她说没有,那个人就问她是哪里的以及有没有名片。这时候作为大版主的紫柔自然冲出来说——我们都是微软中文技术论坛的!我相信那个人已经很无语了,但又不知道我们是干什么的,还是问我们有没有名片。紫柔就告诉他,你上微软中文论坛技术就知道啦。他估计还是不知道微软中文技术论坛是什么,不过很无奈地走了。真是难得见到有人如此直接搭讪的,而且还是在硬件工程大会上哦!

最后一个session解释时,会根据收回的反馈表抽奖,于是我们就找了一个讲用VHD(虚拟硬盘)引导启动的session去听听。最后提问阶段,有人问了能否引导启动一个装有Linux的VHD,speaker说VHD的技术都是开放的,虽然Microsoft没有实现的意向,但开源社区可以去做这样的事情。其实我有个问题,不过一直懒得问,那就是Microsoft能否和Apple合作,做一个Mac上从VHD引导启动的东东,那么将来我就可以在MacBook装Windows 7并且仅仅装到VHD里。

会议结束后,MVP在讨论是否组织一起吃饭,因为我约了人,要去拿张CF卡,所以就先走了。明天我要去参加SD2C,也就无法参加WinHEC了。

P.S.这篇文章不是因为MVP换取赠票而必须写的,而是因为换领礼品这件事实在搞得太不合理了,当时我就决定要写WinHEC的负面。

2008年11月23日星期日

Chinese Blogger Conference 2008

上个周末请了两天年假,回广州参加网志年会顺便回家。

第一天去到会场,就见到了LEMONed和hidecloud,然后拿到了我的fail whale tee。接下来的session……其实我不知道session讲什么,我想大多数人都跟我一样不关心session讲什么,大家只是找个机会来聚会而已。中午跟Elliot Ng一起吃了午餐,并且拿了一件CNReviews的tee。下午的session也是不知道讲了什么,然后就是自助晚餐了和Punch Party了。

自助晚餐吃的是点心,虾饺和牛肉丸都不错,但是鸡翅就很难吃。接着Punch Party正是开始,第一位上台的是Carol,介绍什么是Punch Party。所谓的Punch Party,就是每位speaker有7分钟的时间,大概就是20个slide每个slide讲20秒的时间再多一点。当晚的Punch Party做得非常perfect,每一个人讲的话题都十分之吸引人,做到了Punch Party所强调的punch效果——让人感觉耳目一新。Carol作为第一个speaker,把全场的气氛都调动起来了。中间有介绍大陆吃喝玩乐之行有多艰险的工头坚,还有介绍胖卡如何把宅男配送到乡下的。最后一位导演先生,表演了7分钟的无声演讲——开头只说了一句,“由于接下来的内容都很晦涩,所以自动进行静音处理”,接着做了7分钟演讲的动作与口型,幻灯片翻了几页,但是没说过一个字。对于所有参加Punch Party的人来说,这样的结尾实在是太impressive了!

Punch Party的好处显而易见,能够在最短的时间内让你了解到一些你可能永远都接触不到的知识,同时给你无限的震撼感。不过我觉得Punch Party对speaker的要求也挺高,否则也不容易做好。而且作为organizer,一场Punch Party必须组合不同类型的speaker。

第二天的session稍微有趣一些,不过依然没怎么听,莫非年会的习俗就是把重点放在晚上?下午年会结束后大家就跟着Issac Mao到“友鸡会”吃了火锅,然后到北风的凸凹酒吧去聊天。我也不知道最后大家是什么时候才回家的,我快到凌晨时已经想睡觉了,于是就回家了。

2008年11月13日星期四

Live Mesh vs MobileMe

最近在用Live Mesh在多台计算机上同步文件,而之前一直用MobileMe同步联系人、日程等信息外加系统备份,真希望把这两个服务的好处叠加起来。

Live Mesh的好处是可以指定任何一个目录进行同步,而非指定一个同步目录然后要同步的东西必须放在里面,这方面就比MobileMe要好。而且Live Mesh的自动同步客户端同时兼容Windows和Mac,而MobileMe仅仅支持Mac同步。Live Mesh唯一不足就是没有版本控制,不过我想要是有版本控制占用的网络空间和流量开销就成倍增加了。

MobileMe给我印象最深刻的是,我重装了我的MacBook后一登录MobileMe帐号就帮我把整个System Preferences给恢复了。当然,联系人和日程信息也都恢复了,这是现在Live Mesh完全没覆盖到的功能。另外对iPhone的push服务也做得很好,更新什么信息都在Mac上进行就可以了,根本不用管它什么时候同步到iPhone上。

我觉得最好就是把这两种服务结合到一起,一个帐号一个服务就把这些都覆盖了,当然我不介意付费。在TechED MVP Dinner上,我和一位做Azure的Microsoft员工聊了一下,说最好Windows上的程序都遵守Vista的规范,把代码和数据分开存放,也就是数据都放到C:\ProgramData下面,这样Windows也就如同Linux一样能够轻松备份纯数据了。接着只要把数据同步到cloud上面,例如说是Azure上层的Live Mesh,重装机器后就能够把数据重新同步回来了,只要再装上应用程序就一切恢复原装了。

2008年11月9日星期日

开始翻译 Adobe AIR in Action

Prototype and Scriptaculous的翻译已经是一年前的事情了,翻译了半年,休息了大半年,现在继续接受翻译的工作。仍然是图灵引进的书,仍然是Manning的In Action系列,这次的是Adobe AIR in Action。

其实AIR出来之前我就想去学习,很多很多技术都如此,不过翻译算是一个很好的学习过程,因为它逼着你认认真真把一本书读懂。这次着手翻译Adobe AIR in Action,我想半年后我也会熟悉AIR相关的技术。虽然我一直以来都对Flash和Flex没什么深入了解,但我希望这些知识反过来也能用到Silverlight开发上,结合两派的模式从而进化出一种更好的实践方法。

另外我还介绍了身边的朋友去翻译Practical Prototype and Script.aculolus。Manning即将发布Silverlight 2 in Action,如果图灵引进这本书的话,我也去看看是否能够参与翻译。

2008年11月4日星期二

以服务器端为中心的 ASP.NET AJAX 模式 (Part 2 - Control)

在上一篇文章当中,也就是《以服务器端为中心的 ASP.NET AJAX 模式 (Part 1 - Behavior)》,我们探讨了较为易用的Behavior模式。之所以说它较为易用,是因为它不涉及和原有Page处理流程的交互,即使访问网络也是访问独立的Web Service(包括Page上的[WebMethod]),因此和Page处理流程的设计绝对是正交的。但有时候我们需要的就是与Page处理流程的交互,这时我们不得不使用与服务器端逻辑紧耦合的Control了,这正是本次文章要讨论的内容。

在基本的 ASP.NET AJAX框架下,我们有三种方法来做基于Control的Ajax操作,它们分别是UpdatePanel、ICallbackEventHandler 和IScriptControl,下面我们就分别看看它们的特点和使用场景。

UpdatePanel

UpdatePanel 是与服务器端逻辑进行交互的多种方案中最易用的一个,甚至就不能称之为交互——你根本就不需要触及任何客户端逻辑。一个服务器端操作,经过 UpdatePanel的“劫持”,变成了一个客户端操作,而这个客户端操作又直接调用对应的服务器端操作,就这么简单。

如果用UpdatePanel来做一个带分支的选择对话框,那应该如何设计?思路可别跑到客户端的confirm方法上去,那可太绕了,或者说太不 ASP.NET AJAX了。用UpdatePanel,就应该坚持它的理念,一切客户端操作都是幻象,所有操作其实都是在服务器端进行的,包括选择对话框。要按 ASP.NET的思路来做,我会做一个选择对话框控件,它的实质可能是一个浮动层模拟的对话框,这属于实现细节,我们不用太关注。重点是,这个选择对话框的分支逻辑是完全在服务器端进行的,Async PostBack之后服务器端根据提交回来的数据决定如何触发事件。这样做整个分支选择的逻辑就是内嵌在Page处理流程当中的,不需要通过 Cookies或者Session来做数据的中转媒介,避免了Page处理流程与更大作用域中的数据的紧耦合。

UpdatePanel 适用于逻辑完全在服务器端的开发,并且我建议使用UpdatePanel时也就把所有逻辑放在服务器端,不要去写一些混合服务器端逻辑与客户端逻辑的代码。有人会说,你看老赵就很喜欢去动那个 Sys.Net.WebRequestExecutor来改变UpdatePanel的行为啊,但其实这属于分层设计思想中的一部分,他去动那个东西改变的也就是一个分层内的逻辑,只要层与层之间的接口不变,具体实现是可以按需设计的。但如果你用了UpdatePanel,同时又用Cookies或者 Session来传值,这就跨越了n个层,增加了不少耦合度。

ICallbackEventHandler

关于ICallbackEventHandler,我已经说过无数次了,重点还是你必须用Page处理流程来思考,只要你理解了Page处理流程,你就明白为什么ICallbackEventHandler在.NET Framework 2.0 Beta2中只有一个方法,而到了RTM要分拆成两个方法。具体可以参考《ASP.NET 2.0 ClientScript Callback》,我就不再重复了。

如果用 ICallbackEventHandler实现一个带分支的选择对话框,又如何做?和使用UpdatePanel的做法类似,我还是会做一个选择对话框控件,并且这个控件继承自ICallbackEventHandler。为这个控件编写JavaScript并实现 ICallbackEventHandler接口时,我会确保JavaScript对Callback给出正确的调用参数,并在接口方法的实现中接收这些参数然后触发正确的事件,就这么简单。和UpdatePanel一样,不要偏离了ICallbackEventHandler的设计思想,它的处理流程必须是合并到Page处理流程中的,你的控件也就必须这样设计。

至于在什么情况下选择 ICallbackEventHandler?如果你有一个轻量级的Ajax操作,但使用UpdatePanel更新整个区域的HTML开销很大的话,那么你可以考虑使用ICallbackEventHandler。当然,前提是你懂得控件开发和JavaScript。

IScriptControl

这是最复杂的解决方案了,你需要实现一个Control的两个副本——一个服务器端的,一个客户端的。有一部分逻辑,是要在客户端和服务器端重复实现两次的,而另外一部分逻辑,只需要在客户端或服务器端之中的一个实现一次。IScriptControl的经典例子,当然是ASP.NET AJAX自带的Timer控件。它的计时器是纯粹的客户端逻辑,然而Tick事件却在服务器端触发,Async PostBack成为了两者之间的桥梁。当然,就Control本身而言,它并不在乎PostBack是不是异步的,Tick事件只因PostBack而触发。

如果用IScriptControl来实现带分支的选择对话框,那将会和 ICallbackEventHandler的版本十分相似,唯一不同的地方就是它在客户端的逻辑会被封装为一个Sys.UI.Control的派生类,而ICallbackEventHandler的客户端逻辑往往是不封装的。这样的好处显而易见,那就是代码更容易维护了,并且客户端的Control可以同样可以加入事件支持,并提供和服务器端一样的代码分支事件。要知道在CTP阶段的Timer控件,其客户端版本Sys.Timer(而非RTM的 Sys.UI._Timer)是拥有tick事件的,和服务器端的Tick事件对应,只不过RTM取消了此项功能,因为ASP.NET AJAX 1.0的侧重点完全就是服务器端功能,客户端功能都被砍掉了。

什么情况下选用IScriptControl?如果你认为你的客户端逻辑应该封装为Sys.UI.Control的派生类,那就选择IScriptControl吧。

小结

我们分别讨论了三种通过Control实现Ajax调用的方案,并且一再强调了设计必须基于Page处理流程,不要在此流程之外增加不必要的复杂度和耦合度。值得一提的是,有很多人质疑为什么要在Web上提供这样一个支持分支的选择对话框功能,我的看法是这样的:既然客户端软件的流程会有此功能,那么 Web应用也有此功能就实在是太正常了,你删除blog post的时候问你一下是否确认删除,难道会有人觉得这个功能是设计错误?可能不同的只是表现形式而已,到底是confirm还是弹出层,甚至是一个专用的过渡页面。然而从用户体验的角度来说,这其实并不是最优的方案,多数时候用户删除就是确认删除,并不需要再问一次是否确认之类的愚蠢问题,但开发人员觉得用户错手删除的后果应当由用户自己承担,所以就做了这样一个对话框来推卸责任。真正好的用户体验是不需要确认的删除,但用户一定能够恢复,最好是按一下 Ctrl+Z就可以了,然而对于开发人员来说还是有很多操作是无法做到可恢复的,这时候除了显示对话框也没有更好的解决方案了。

最后,如果你喜欢我的文章,可以通过订阅feed来及时获得更新:

2008年11月3日星期一

软件安装时到底是否应该让用户选择路径

周末和Google UX Team的Junyu聊天,说到Google现在统一使用Google Installer安装软件了,点一下Web上面的链接就自动开始下载安装并运行,开头我还以为是Microsoft的ClickOnce呢。由于安装过程是全自动的,就如同Microsoft的ClickOnce和Adobe的AIR一样,所以是不允许用户选择安装路径的。

到底不让用户选择安装目标好不好?我们认为用户分为三个层次:
  1. 入门用户 - 购买的是品牌机,可能就一个C:和一个用于一键恢复的D:。这时候有什么好选择的呢?当然是不选择最好了。
  2. 熟练用户 - 经受过文件难以搜索或者忘记及时备份误删文件的种种磨难,学会了分门别类存放文件,自己有计划地把硬盘分成C:, D:, E:, F:四个盘。这时候你不满足他分类存放文件的习惯,他就会觉得很不爽了。
  3. 高级用户 - 经过无数次备份与重装后又在返璞归真,只用一个C:,安装时多数选项都直接next,不选择安装路径。
当然,这个分类主要是针对Windows用户,因为Windows的成长过程就是典型的“问题少年”——不停地向用户抛出各种问题,这个你选什么啊,那个你选什么啊。对于Linux和Mac用户来说,大家早已习惯了安装时一路next到底。Linux命令行安装甚至不用next,只是本地编译的话有时候需要yes一下。

事实上,我认为给用户那么多选择是没必要的,文件的搜索与备份应该通过其它手段来改进,而不是让用户手动归档。要求用户理解文件系统,就如同要求调用者理解API实现方式一样,其实是很不合理的。

对于现在的品牌机而言,只有一个C:,培养Windows用户好像Mac用户那样,什么都装到一个盘里面,这才是正确的发展道路。Mac有Spotlight,Windows Vista现在也有不错的磁盘索引与搜索系统,将来实现了WinFS或许能做得更好。

现在Windows缺的是一套好用的备份与迁移方案,问题来源于Windows软件都没有规范地把可执行代码与数据分离存放,所以没有一种统一的模式来备份软件数据。如果Windows上面的软件能够按照Vista的指引,把数据都存储到C:\ProgramData,那么接下来的事情就容易多了。况且我之前也说过了,这也是UAC的意义所在,有助于提高Windows的安全性。

2008年10月27日星期一

以服务器端为中心的 ASP.NET AJAX 模式 (Part 1 - Behavior)

早在ASP.NET AJAX从CTP转向Beta再转向RTM时,看着客户端的Control被逐步放弃,与此同时ASP.NET AJAX Control Toolkit越来越多地使用Behavior,我就想深入说说ASP.NET AJAX的模式。不过由于我比较懒,所以这个话题只在《理想的 ASP.NET AJAX (Part 2 - Server Centric)》中一笔带过,没有深入讨论。今天看到volnet的《我们究竟是否有在“Asp.net中模仿Winform的MessageBox ”的必要?》,决定写一个文章系列来说说ASP.NET AJAX的模式。

什么是Behavior?

什么是Behavior?Behavior与Control有什么不同?这是首先需要回答的问题。

在Windows开发当中,Behavior的概念是不存在的,有的只是Control。ASP开发连Control都没有的,到了ASP.NET才引入了Control的概念。为什么Ajax开发要引入Behavior这样的概念呢?因为Behavior意味着不需要改变原有的组件逻辑,而改变原有组件的逻辑在客户端往往是不可行的,至少是难以实现的。

举个最简单的例子,一个<input type=”text” />就是一个浏览器内部的对象,你无法扩展这个对象的类型,也无法为它加上新的属性与方法,至少并非所有浏览器都允许你在JavaScript中这样做。然而如果你想要让它加上auto-complete(或曰suggest)的功能,这是可以做到的,并且很多人都做过了,例如ASP.NET AJAX Control Toolkit的AutoComplete,或者是script.aculo.us的autocompleter。这些实现都基于同一种方式,就是尝试基于input已有的接口在它之外添加新功能,而非尝试继承input并在它之内添加功能。

类似的做法在Ajax开发中普遍存在。例如说拖放吧,现在离HTML5拖放的全面普及不知道还有多远,所以大家都只能基于现有的鼠标事件来开发拖放功能。又或者说带有验证功能的输入框,无论是input还是select,无论是客户端验证还是服务器端验证,也都是基于现有HTML元素的事件来完成的。这一切都是Behavior。

什么情况下使用Behavior?

简单归纳,就两个条件:

  1. 需要基于特定的一个组建进行扩展
  2. 组建本身所处的环境缺乏可扩展性

单看第1个条件,我们有丰富的选择。很多人的第一反应就是继承自该组件,把扩展功能做到子类里面。熟悉设计模式的话,可能还会想到decorator pattern。然而在浏览器的环境当中,受到第2个条件的限制,继承或者decorator pattern都是不可行的。这时候,我们就需要使用Behavior了。

在一定程度上,我们可以把Behavior看作一种折衷了的decorator pattern。在decorator pattern中,decorator也继承自组件,因此当一个组件使用decorator pattern后,我们就把decorator放在原组件所处的位置上,而原组件就成了decorator的一个子节点(基于树的角度来看的话)。如果再加一个decorator,原decorator就会如同一个普通组件那样再被封装一次。在多个decorator的情况下,decorator之间是串联的关系。Behavior本身不是一个浏览器内部的组件,它无法继承自input的基类,因此Behavior也不能串联。Behavior本身本也不会取代input在树中的位置,这使得Behavior可以并联起来——一个input可以有多个Behavior,例如一个是自动完成,另外一个是输入验证。

总之,如果你原来的工作就是和UI打交道,并且熟练使用decorator pattern,那么在进行Ajax开发时把decorator pattern换成Behavior就可以了。

小结

回过头来看文章开头所说的MessageBox(或曰confirm)的问题,如果这个功能不需要对服务器端进行反馈,完全可以使用Behavior实现。ASP.NET AJAX Control Toolkit就有ConfirmButton这样一个东西,在客户端叫Behavior,在服务器端叫Extender,其实Extender就是对Behavior在服务器端做一下封装而已。当然,ConfirmButton不能完全实现MessageBox的功能,它只能在用户选择“取消”时取消整个提交操作,但不能够执行另外一个服务器端的代码分支。这算是Behavior的一个限制,就是它只能在客户端添加额外的功能,但是它不能影响到与服务器端的交互。但很多时候,正是这种限制确保了我们开发的组件是与服务器端解耦的。

如果我真的需要一个与服务器端交互的客户端组件,例如支持调用服务器端代码分支的confirm,怎么办?这时候你就真的需要一个与服务器端Control对应的客户端Control了。请关注本系列的下一篇文章,《以服务器端为中心的 ASP.NET AJAX 模式 (Part 2 – Control)》。通过订阅feed,你可以及时获得文章更新:

P.S.既然提到了Behavior和Extender的一一对应关系,就不得不说一下,服务器端的Extender就是真正的decorator pattern。

2008年10月26日星期日

远离国内的 SNS

最近我一直在想是否要把海内的帐号终止掉,原因只有一个——我很久不上海内了。既然我不用它,我就没必要把我的个人信息放在上面,让更多不认识的人有机会获取到这些信息。

如果要详细解释这件事,就要从很久很久以前说起,就是海内刚刚开放注册的时候。在改为邀请注册之前,海内曾经开放注册过几天,让及时获得消息的geek们第一时间获得了帐号,那时候我还没有多少好友在上面,已有的好友没有多少活动,news feed自然不值得一看。

后来,海内上的好友逐渐多起来了,大家也更活跃了,这是我认为海内最好的一段时候了。虽然,这时候的news feed算法也不怎么样,例如不会把同类项合并了,也不会对信息按照重要程度进行过滤。我之前也说过,Facebook的真正价值就在于它的news feed,news feed的算法不行也就以为着海内能为用户带来的价值优先。

随后,海内开始把重心转移到游戏开发上来。买卖好友可以说是为了刺激用户数量的扩张,但越来越多的游戏上线让我觉得整个海内都变得以娱乐白领为首要目标了。我的个人观点是跟Bean和Awflasher差不多,虽然中国人口众多必然导致劳动力过剩,但让每个人都把自己过剩的劳动时间投入到游戏当中并不是好事情。当海内的news feed充斥着各类游戏相关的信息时,我就不再去看海内了。

说完了海内,国内的其他SNS我想也没必要一一列举了。海内曾经是国内最用心用力想称为白领SNS的一个了,国内其他成功SNS的目标从一开始就定为娱乐性SNS,短期内不可能得到比海内更有价值的结果。在这里我所说的价值不是指SNS本身的商业价值,而是指推动国内SNS用户素质提升的价值,是指创造SNS之外的商业价值的可能性。

2008年10月23日星期四

China MVP Open Day 2008

一年又过去了,去年的MVP Open Day在三亚,今年改成了在北京,我甚至连飞机也不用搭了,只需要打的参加(也要¥99)。

我是17号晚上和飞飞一起从中关村打的去汤山的。Bean则是当晚深夜飞过来,很可惜Bean不知道联合航空到北京的飞机是停南苑机场的,结果从南苑机场打的到汤山还费了他不少时间和金钱。Dflying早些时候就飞来北京了,不过不知道去哪里玩了,竟然18号晚上不到酒店让我一人独占房间。18号晚上去到后,人都散了,只是见到了Sisley、Eddie等人在筹备第二天的晚会,当然还有排舞的老葛等人。

由于晚上看Wanted看到凌晨两点,18号一起来就赶去吃早餐,接着就见到了Mango和Bean。由于我第一天晚上没有签到,吃完就马上赶去签到领衣服——没办法,早上的keynote规定大家要穿统一的衣服。keynote上讲了很多东西,还颁了奖——就是MVP每年一块的水晶座,今年是奥运年所以是火炬造型的水晶座——不过整个早上最让大家兴奋的应该就是那台Surface了。据说中国只有一台Surface,放在MSRA,估计就是那一台了,现在拿来向我们展示WPF如何开发Surface应用程序,keynote之间我们就都冲上台玩这台Surface顺便拍照。

中午的午餐组织得有点混乱,实际上整个MVP Open Day都组织得比较混乱,不知道是不是因为会务外包了给CSDN。据说因为会务很赚钱所以CSDN现在热衷于做这个,不过确实不太专业。

接着就是下午的一对breakout session了,不过没有哪个我特别感兴趣的——完全没有ASP.NET或者Silverlight的session嘛……我和Bean在户外找了个地方坐下来上上网聊聊天,后来俞晖也加入进来了,包包跑过来和我们照相,Dflying说公司有事要立即赶回上海(他还真没在酒店住过一晚)……整个下午我就没有专心听过一个session,都是周围跑去见别的MVP。

午餐按照技术特长来分组,晚餐就是按照地区来分组,俞晖和紫柔跑来我们广东桌装广东人,俞晖还是忘记不了说他喜欢吃螃蟹并且对于没有螃蟹感到可惜。我则一再要求紫柔把我从Web版主调回ASP.NET版主,因为当初无拉拉被分到Web版主实在让人郁闷,大部分IIS、IE之类的问题我都无法回答上来,也不确认其他人的回答是否正确。

晚餐之后就是晚会了,一个半小时其实不算长,不过留下更多时间给MVP交流也是对的,随后就是博客园的MVP聚会。在博客园上的大部分MVP都来了,没来的据包包统计大多都是上海的,例如Jeffery Zhao——我很好奇他所谓的减肥成功到底是什么效果,可写他就是不出现。博客园的聚会,就是大家各自自我介绍,然后分组讨论,晚上黎波来了——他虽然没有MVP连任成功,不过刚好也在北京为Microsoft的客户部署项目——我就和他聊了一下Sharepoint。回到房间之后,我叫了Bean过来聊天,不过没聊到什么他就回去睡觉了。

19号早上是拓展训练,就是盲人方阵和毕业墙两个项目,我们组做得还不错。中午吃饭后就是期待依旧的参观鸟巢了——其实我一直不认为鸟巢如何如何特别,不过倒还是个很值得拍照的地方。一群MVP搭大巴来到鸟巢,被告知水立方和玲珑塔都不对外开放了,于是只能进入鸟巢参观。整个广场确实很大,不过设计确实不怎么样,很多地方都很不人性化——鸟巢很多长长的楼梯,出去少数电梯后竟然没有扶梯,这叫来自发达国家习惯走到哪都是扶梯的人怎么适应呢?整个广场那么大,座位放了很多但就是不见洗手间,垃圾分类就不要期望了因为连垃圾桶都是那种很丑陋带轮子的。简单来说,看看鸟巢就知道典型的北京本地价值观了——很追求体积上的庞大,或者说是“排场”,但是不注重细节,也不懂得什么叫人性化。

参观完鸟巢后,拍了几张集体照就解散了,去机场的、去火车站的、直接回家的……基本上都涌进了地铁站,然后各自超自己的方向走。

2008年10月19日星期日

有空要做个 Theme Software Corp 的游戏

当年Theme Hospital的搞怪实在让人怀念。各种奇特而富有创意的病症,与之配合的神奇治疗方法,还有行为略显怪异的医生和护士,组合起来是那么的有趣。现在在IT公司中的生活,感觉也很有趣,虽然有些事情也挺烦人,不过却是不错的谈资。

将来如果有时间做游戏,就做一款Theme Software Corp。市场是有各种奇怪的需求,然后要请PM来做研究并得出产品开发计划,再要请SDE来开发,之后还要请SDET来提高质量。为了保证这群人的满意度,和招聘到更加高级别的人员,必须购买各种设备来满足他们奇怪的生活需求,从咖啡机到按摩椅,还有Wii、XBox360或者PS3,甚至自己建移动大厦加一个游泳池。结果发现这些人每天来到就去拿咖啡或者可乐,然后开始看看feed,最后工作一会儿又闪出吃饭了。你想尽办法利用他们的才能,但是innovation的发挥却是很不稳定的,项目不停delay士气下降,你又要投资点什么来补救。

总之,这个游戏应该继承Theme Hospital的一些关键特性,游戏中的各种事件都是很无厘头的,同时你又要努力balance游戏中不同的指标——正如Dreaming in Code中说到的质量三角,时间、金钱、特性三者不可能都得到,在追求其中一个的时候必然削弱另外的两个。这样在有相当游戏难度的同时,又有娱乐性可言。

2008年10月9日星期四

最糟糕的一次飞行经历

星期天晚上从广州飞北京,登机和下机的安排都极差!

在广州白云机场的B区登机,又是一层登机口,意味着肯定是搭摆渡车的。开头以为好像上次那样,飞机是国际航班转飞国内,所以停在A区空地了,结果还真是这样,而且还不是停在A区空地——是泊在A区登机桥边了!那天广州暴雨,如果飞机停在空地,登机专用车是有遮顶的,这样登机还不至于淋湿。但飞机泊在登机桥边了,就要从地面爬上登机桥(我终于知道登机桥边的楼梯有什么用了),楼梯是没有遮顶的,所有人都是淋雨之后才走进飞机的。

到了北京首都机场后,被告知地面在调整停机位,要重新分配机位后才能下机,然后飞机就停在了机场空地上足足等了30分钟!不知道乘客与空姐吵起来是否有推进作用,最后停机位并没有怎么“分配”法,飞机就在原地把我们放下,然后让摆渡车接我们走。浪费那么多时间才出摆渡车,一早这样做不就好咯,机场的调度真是差。

2008年10月3日星期五

什么时候才能在网上收到广州和香港的节目呢

Sean O'Driscoll的一条tweet,说能够在网上听到Seattle的电台感觉离家里近了,让我再次想起如何能够在北京接受粤语节目的问题。

在北京最郁闷的事情,暂时来说,就是无法接收到任何广州或者是香港的节目,无论是电视还是广播。试用过很多P2P的网络电视平台,暂时来说最满意的就是TVU收看凤凰卫视了,至于TVB——虽然有人转发到网络上,但是peers太少,从来没有成功看到过,就是见到list上面有TVB的名字。

关于接收TVB,Catfish总是建议我接收卫星信号,因为他以前在北大就是负责把信号送上卫星的。但是这个方案对我来说也太不现实了——我连电视机都没有呢!我从来只有电脑,电视对我来说是多余的,所以在北京租的房子自然不会买电视。

除此之外,还有别的办法吗?我不停地问身边的人这个问题,但一直没有找到有效的解决方案。

2008年9月21日星期日

Vista 为什么要引入 UAC

UAC的目标就是:干扰用户!这其实不是我独创的,而是从TWiT上听到的一种见解。

UNIX的命令行有sudo,Mac在GUI上也能够在需要sudo调用时弹出对话框要求输入密码,然而这两者的出现频率比Vista的UAC要低得多。为什么UAC的出现频率那么高呢,难道是Vista的用户体验设计得有问题?显然不是这样,Vista的UAC就是设计来不停地干扰用户的。

干扰用户随之而来的是什么?就是用户觉得这个软件很烦,然后逐步放弃使用这个软件,转而使用同类软件中不那么烦人的。这个过程会逐步把经常进行UAC调用的软件从是场中淘汰掉,如果你的软件不希望被市场淘汰,你就必须尽量减少UAC调用,也就是减少系统调用。

最终,市面上大量滥用系统调用的软件会逐步消失,要么自身改进,要么自然淘汰,从而提高了Windows的安全性。等一下,这跟Windows的安全性有什么关系?假若你的代码要执行系统调用,同时你的代码是有漏洞的,就可能由于你的程序漏洞而导致系统受到攻击。但如果你的软件本来就不需要做任何系统调用,就算有漏洞也不会连累Windows,Windows也就显得安全多了。

P.S.当然,还有一些非系统调用也需要UAC,例如对C:\Program Files的写入操作。这是因为Vista认为这也是个危险操作,而程序应该将数据存放到C:\ProgramData来实现可执行代码与数据的分离。

2008年9月18日星期四

强烈推荐 Spore 这个游戏

由Spore发布到现在,一直在玩这个游戏,觉得挺有趣的。虽然有人说创意没利用好,操作具有重复性,不过这个游戏享受的正式创造的乐趣,而非好像RPG或者RTS那样,要通过竞技赢取什么。如果你以竞技的心态去玩,那么这个游戏就很无聊了,因为你赢了一次之后,就发现再玩也是走类似的路线,不值得重玩。但是如果你享受创造不同的生物,利用他们不同的特性来进行游戏,那就很好玩。

Spore有趣的地方在于,你在里面总能够碰到各种各样奇怪的事情。在cell stage,你会发现合理组合装备的重要性,刚刚开始有针刺的时候就可以把针刺装在前侧,然后刺体积比你大多了的生物。如果你发现不够灵敏,总是没把对方刺死就被吃掉了,那么你可以加高性能的运动器官,但前提是你又要先杀死一只有该器官的生物来获取它。到了creature stage,你可以看到举行的epic creature,这其实是其它space stage文明通过supersize工具制造的。在tribal stage,你可能会看到UFO──也就是其它文明在space stage的飞船,他们可能把地上的生物吸走。总之,在整个游戏过程中,你都会感觉到银河系中充满了各式各样的文明,在你还没觉醒的时候他们就来造访你所在的行星了,而且还会制造各种奇怪的事件,例如麦田怪圈。

暂时Spore只有英文版和繁体中文版,不知道什么时候才在内地上市,或许我会去买一套简体中文版,把它的cd-key用到英文版上,然后连上EA的服务器玩。Spore是我认为挺值得收藏的一个游戏。

2008年9月17日星期三

第一次开 OT 开到 1:30AM

终于体会到软件工程嘅焦油坑,唔单止需求唔明确成日变,而且相关人员太多──我啲嘅项目相当于通用型项目,同各个已有嘅产品线都有合作,项目嘅任何改动都要通知各个有关产品线嘅PM,等佢哋嚟确认新嘅行为係合理嘅,同时又要各个产品线个QA嚟重新测试,确保哩啲改动唔会影响到原有嘅协同功能。

2008年9月15日星期一

如果浏览器 geek 到能够随意换 kernel

Google Chrome是开源的,所以只要你有兴趣你都可以去Google Chrome的SVN把源代码check out下来看看,然后你会发现它用到了大量第三方的开源组件。

在过去,浏览器总让人觉得是一个monolith般的东西,一个大整体,内部耦合度高,很难在一个浏览器中复用另外一个浏览器的组件。准确来说,浏览器按照内核分成了几族,族内的组件倒是通用的,但族外的就不通用了。但是在看到Google Chrome如此组装开源组件后,才发现其实浏览器也有可能发展到能够跨族相互融合的程度。例如Google Chrome是基于Webkit的,然而JavaScript引擎换了V8,所以执行效率比Safari高多了,而渲染引擎换了Skia,因此暂时看起来没Safari那么精致,支持的CSS3也没有Safari那么全面。

将来的浏览器,或许会像Linux一样按照distro(distribution)来划分。安装插件已经不能满足geek的需求,所以选用什么组件变得可以自定义。浏览器与浏览器之间的组件复用壁垒不会好像现在那么高,programmer已经可以轻易地从一个浏览器借用代码复用到另一个浏览器上,然后制作自己的distro。

2008年9月2日星期二

Firefox Ubiquity

对于喜欢命令行(command-line)或者喜欢Mac上的QuickSilver的人来说,Ubiquity实在是太好用了。如果你的Firefox还没装上Ubiquity,那就快掉去装一个吧,虽然现在的只是0.1.1 alpha。

装完之后,可以体验一下Ubiquity内置的command,地图搜索或者翻译之类的其实还是挺好用的,但我觉得重点还是在于可以自己写新的command。官方的Author Tutorial简单介绍了一些开发command的基础知识,如果要深入学习的话就要看看别人的command是怎么写的,以及看看Ubiquity的源代码。

由于我觉得Ubiquity实在太符合QuickSilver的用户习惯了,所以我以后做Web应用程序都会考虑加上Ubiquity的支持。

2008年8月30日星期六

如何动态加载 JavaScript 与 CSS

Omar AL Zabir这位MVP总是喜欢搞些稀奇古怪同时又很实用的小东西,并且还十分值得参考。最近他就做了一个叫做ensure的小工具用于动态加载JavaScript、CSS与HTML,而且IE、Firefox、Opera、Safari都支持了,那么我们就来看看ensure是如何做到动态加载JavaScript与CSS的。

在介绍ensure内部的实现之前,让我们先来看看其功能:

ensure({
    html: "popup.html",
    javascript: "popup.js",
    css: "popup.css"
  }, function() {
    Popup.show("hello world");
  }
);

在这段代码中,ensure首先会确保popup.html、popup.js、popup.css这3个文件的加载,如果都没加载过ensure就会动态加载它们;如果已经加载过了,ensure不会再次加载。在确保这3个文件都加载后,ensure会调用后面的匿名函数,也就是执行Popup.show("hello world");。

接下来,就让我们看看ensure是如何动态加载JavaScript与CSS的。

加载JavaScript

在ensure当中,加载JavaScript分两种情况来执行,也就是Safari与非Safari这两种情况。

在IE、Firefox、Opera中加载JavaScript

在这三款浏览器中加载JavaScript,其实只需要创建一个script元素,把src指向要加载的URL,最后把script元素追加到head元素上,那就搞掂了。此项工作是在HttpLibrary.createScriptTag()中完成的。不过我们不仅仅要加载JavaScript,同时还需要知道它什么时候完成加载,这可以通过script元素的onload事件或onreadystatechange事件来实现。

在Safari中加载JavaScript

因为Safari 2不支持onload或者onreadystatechange,所以只能手动通过XHR把URL读去过来,然后再手动eval这段代码,这就带来了一个限制──只能加载本域的JavaScript文件。在ensure当中,eval的工作是通过HttpLibrary.globalEval()来完成的。为了让JavaScript代码在全局(global)上下文中eval,ensure还是使用了创建script元素的方法,并将要eval的JavaScript置于其内,最后把script元素追加到head元素内。

细心的人肯定要问,为什么HttpLibrary.globalEval()要如此设计,而非直接window.eval或者eval.call。这是因为,window.eval和eval.call都无法在IE6中实现和script标签加载JavaScript代码一模一样的效果,这两种做法的eval在IE6下仍然不是在全局上下文中执行的。搜索一下你就会发现一些相关的讨论,例如jQuery就曾经使用window.execScript()来完成此项任务。不过最终大家都发现添加script元素才是最好的跨浏览器解决方案,所以现在的jQuery和ensure都是如此实现的了。

加载CSS

相对于加载JavaScript而言,加载CSS就简单多了,而且方法也是类似的:在head元素内直接加入link元素就可以了。这也正是loadCSS()所完成的工作。

实际上,ensure没有确保CSS完成加载后再执行下去。这估计是因为浏览器都能够在CSS加载完成后自动应用到页面上,因此Omar AL Zabir就认为CSS的加载顺序是无关紧要的,不过假如CSS加载速度实在太慢,其实还是会影响显示效果的。

在IE6中加载CSS

这次需要特别照顾的是IE6,而非Safari。IE6在往head元素添加link元素时,必须在window的上下文中完成,因此添加link的函数通过call调用切换了上下文。

总结

实际上动态加载JavaScript与CSS都并不难,在大多数情况下只需要向head元素追加对应的子元素就可以了,只有Safari2和IE6这两款古老的浏览器是需要特殊照顾的。

2008年8月29日星期五

开始使用 MarsEdit 写 blog

缺少一个客户端,我就总是懒得打开Blogger来写blog。当然,Blogger还算好的,我可以写着写着就按一下save按钮,然后继续写。博客园还不能就地保存,保存一下必须离开编辑页,然后还要重新打开编辑页。我实在无法忍受从PC切换到Mac之后没有客户端的情况……其实是有过一段时间用客户端的,那就是MarsEdit。

在MarsEdit的试用期过后,我又懒得写blog了。在忍受不写blog与购买MarsEdit之间,最终我选择了购买MarsEdit,这个价值$29.95还周围都找不到coupon的东西。作为一个shareware,它真的做得不错,但$29.95又实在太贵了,Speed Download那么强大的下载软件也就$25嘛。

在买MarsEdit时,发现可以用PayPal,想起之前注册PayPal时验证信用卡后有$1.95存在PayPal上面了,决定就用PayPal支付,把那$1.95余额花掉。接着在PayPal的页面看到,原来通过PayPal来购买外汇超级是超级贵的。根据今天Yahoo Finance公布的汇率,1CNY能够买入0.1459USD,但是PayPal说1CNY只能买入0.141154USD,实在相差太远了。幸好,PayPal允许你选择不用它来做购汇,用回VISA的购汇功能,只不过支付时无法确定最终购汇汇率。如果VISA的购汇汇率不至于那么离谱,还是VISA比较省钱。不过到底VISA购汇价格如何,我还真的没有留意过,以后要仔细研究一下帐单。

2008年8月20日星期三

MobileMe 真应该免费 beta 一年

服务上线两个星期,就因为服务质量问题表示道歉,每人送30天试用期。然后因为港币用户的验证费用扣错了(应该扣1USD,实际扣了100HK$),又再送30天试用期。现在又因为近期服务不稳定,再给大家60天试用期。加上注册时的60天试用期,这已经是180天了。

对比起这样不断道歉和延长试用期的做法,Apple还不如学习一下Google,让服务免费beta,让更多用户来发现bug和汇报bug,等服务成熟了再开始收费。当然,这样做的坏处是Apple必须保留免费用户,否则全面转为收费的话,很多beta阶段的免费用户都会流失掉。

2008年7月29日星期二

北京地铁比广州地铁落后多少年?

星期六跑去三里屯看了一下“中国第一家Apple Store(非Apple Authorized Reseller)”,是搭最近开通的10号线过去的,感受了一下10号线的整体装修,觉得能够比得上广州3号线、4号线的水平,对北京地铁略微有改观。非常可惜的是,10号线全线没有手机信号覆盖,我觉得这实在是太落后了。

据说以前北京1号线和2号线也是没有信号覆盖的,但是后来改进了。10号线作为全新开通的线路,应该在设计阶段就考虑到这个问题,然而现实却是正好相反。想想10年前广州1号线开通,就已经解决了这个问题,而北京现在开一条10号线,还要是迎奥运了,都解决不了这样一个问题,只能说北京地铁比广州地铁要落后10年。

北京地铁还有一个搞笑的地方,就是所谓的“自觉安检”,安检不是放在必经通道上,必经通道旁边,人少的时候安检人员还能认出谁的包包经过机检了,人多的时候根本分不清,只听到安检人员说“请自觉打开包包接受检查”,很多人根本就不理,直接走过去了。还有一个搞笑的地方是,Singzy说她早上赶去机场时,本来想坐机场线,结果地铁安检说他们的行李体积过大,不能带进地铁──竟然航空公司能够接受的行李体积,地铁安检不能接受,那么北京赶在奥运之前开通机场线又是为什么呢?貌似机场线还有一列迎宾专列,估计就是专门用来迎宾的吧。

2008年7月5日星期六

去给韶关移动做讲座

Bean之前一直说去给移动做讲座的事情,说要讲讲互联网的新玩法新思维,最终在这个星期五去给韶关移动做了第一讲。

玩转互联网信息沙龙

这次我们讲的是购物和SNS这两块,Bean负责讲购物,而我负责讲SNS。我看了一眼听众名单,发现来的都是韶关移动的老总,至少都是部门经理级别的,第一次做这样界别的讲座,我当然紧张。Bean说,要讲得简单一点,避免涉及技术,让他们明白怎样好玩就行了。所以我准备的内容,更多是介绍性的,而不涉及更深层次的问题。最终讲完,得到的反馈还算不错,不过我想他们还是希望多听听关于商业模式的东西。移动很有野心,想要做自己的互联网平台,从而避免自己最重成为纯粹的互联网访问介质,因此移动的老总们都很关心现在的互联网思维模式。

另外,这是我第二次尝试用Apple的Keynote做演示,当然想要做得比第一次好,让画面和动画效果更加深入人心。感觉无论是PowerPoint还是Keynote,其实质都是差不多的,最重要还是表现手法,软件只是工具而已。

2008年6月29日星期日

Non-Catonese Speaker 取个 Cantonese 的名字干什么呢

时不时就能在网上认识到一些non-Cantonese speaker,却发现他们用的英文id取个港式音标,例如姓杨的取Young,姓蔡的取Choi,实在搞笑。

事实上,港式音标是威妥瑪拼音,并且是按照粤语发音来注的威妥瑪拼音。如果是non-Cantonese speaker,那么你可以用国语或者任何你所说的汉语口语方言来注威妥瑪拼音,但没必要用基于粤语的威妥瑪拼音。举个实际的例子,蔡依林的英文名是Jolin Tsai,就是按照国语发音来取姓氏的威妥瑪拼音,如果她按照粤语发音来取Jolin Choi,那就显得很不伦不类了。

因此,如果你不想用汉语拼音来给自己的中文名字取个拉丁形态,那么你可以选择威妥瑪拼音,但不要盲目选用基于粤语的威妥瑪拼音,假如你是non-Cantonese speaker的话。

2008年6月26日星期四

Purpose and Reality of Test

测试的目的是为了挑选,例如挑选高质量的人,然而这可能会成为一种反向激励──应试。这会导致候选人偏离测试所期望达到的质量,甚至出现劣币驱逐良币的景象。

还记得和状元同学开过的一个玩笑。由于某些琐碎事情,我讽刺他说,“做人怎么能如此不求甚解呢?”他说,“有什么不可以的,我就跟着老师说是正确的去做,我也不知道为什么这样做是对的,不求甚解当然是可以的。”当时我就把这当作一个玩笑,现在我明白这是十分正确的一件事,而且这就是反向激励的实现形式──为了应试,你必须牺牲测试所期望达到的质量,以换取应试能力。

因此,现实中的测试不一定真正能够挑选到高质量的人,而且还把人赶往了一个相反的方向。

2008年5月31日星期六

Social Software Configuration

social software满街都是,连一个最普通的论坛也算一个social software,但是social software最难的部分在于social而不在于software。

一个software可以有完备的需求分析,在很清楚我们要解决怎样的问题以及如何解决之后,再去实现这个software。然而social则是动态的,当一个social software投入运行后,social的部分才开始“实现”。举个例子来说,你想从零开始做一个博客园,software方面你可以从任何一个blog platform开始修改,甚至是从零开始编写,但是social方面呢?你明白你要实现的效果是一个有人气的技术博客社区,但随后没有OOAD这样的理论模型让你把这变成一组描述social模型的规则。虽然你知道要去吸引潜在用户、激励用户多写好的文章……但是这一切都仅仅是经验列成的规则。

随后,软件平台做好了,意味着software的编码与测试阶段都完成了,但是对social部分的实现才刚刚开始──用户逐步进驻,他们之间开始交互,而你则通过设置各种各样的规则来刺激整个social氛围朝你期望的方向发展。这个过程不像是开发,反而更像是测试于调试──首先,你测试现在的social氛围是否符合你的要求了,例如用户数量够不够,他们发文质量高不高;然后你找到表面上不满意的地方,对底层的规则进行修改,这些规则有可能是显而易见的,也可能是需要深度挖掘才能弄清楚的;如果要深度挖掘,那就成了调试了,在密密麻麻的规则当中尝试修改一下,然后看看效果,不行再改回来然后再做下一个尝试。

总之,social software拥有比一般software长得多的研发周期,假如把social部分也算上的话。并且最重要的是,每一次social开发的迭代都没有严格的方法论可言,大部分人都是通过经验来进行控制的。那么到底现在有哪些经验可以参考,这个以后再说。

2008年5月24日星期六

原来并非人人能习惯中英混杂

在Microsoft 2008 Launch广州站的会议结束后,我和Bean、黎波、张亮去了中酒旁边的McDonald's聊天。我说到“这是100%确定的”,读出来是“这是一百percent确定的”,然后黎波马上就问“你说一百什么”,Bean解释道“一百percent啊,就是百分之一白”。随后Bean又补了一句,“和Cat聊天聊多了你就能习惯这种中英混杂的说法了”。这时候我才突然发现,原来不是所有人都天生能适应中英混杂的说法的,即使他中文和英文水平都没问题。

随后,我又在豆瓣的搭讪小组碰到了一些更加negative的反馈,我才开始仔细分析这个问题。这应该属于文化背景的差异,港台文化都是习惯中英混杂的,geek自然也常常用英文,因为我属于上述两个圈子,自然我很习惯这样说话。但是对另外一些人来说,他们从小长大的文化背景中不包含英语这一元素,甚至在英语学习中遇到了种种障碍导致厌恶情绪的产生,这时候他们不仅仅不适应中英混杂的内容,甚至就认定你使用英文就是在炫耀你的英文能力。

廣東話俗語——雞髀打人牙骱軟

本文由Piggest授权转发

“雞髀”——鸡腿;“牙骱”——牙关(jaw, jawbone)。


整句合起来的“繁→简”机翻是:鸡腿打人牙关软。

在文字MUD年代,每每看见“鸡腿”这件武器/补给品,可以吃又可以打人,不由得想起“雞髀打人牙骱軟”这句俗语。用鸡腿打人到底可以达到什么效果呢?

“雞髀打人牙骱軟”的意思是,予之以小利,使对方对自己透露消息。有时亦指,予之以小利,使对方为自己美言几句或者在言语上偏袒自己。

例句:咁着数,唔通想“雞髀打人牙骱軟”?

“雞髀打人”实在太生动,所以这句话又广泛应用于以下情境:

为了获取利益或求人帮忙而请人吃饭;政府派利市拢人心;甚至在办公室派零食讨好同事亦算有“雞髀打人”的嫌疑。

可见得“雞髀打人”是中国人常用的社交手段,不含褒贬,是大家都能会心一笑了然于心的潜规则。

引申:“髀”即小腿,“大髀”即大腿。“打牙骱”——chip chat。

以上粤语发音,请见“粤语会馆”:http://www.yueyu.net/。

2008年4月16日星期三

飞到美国了

离境的时候,觉得北京首都机场的国际安检很弱智。首先,在明明没有人排队的情况下,就把长蛇阵拉起来了,这证明排队从来没被很好地管理过。国外的长蛇阵,甚至是广州的各大银行里的,都是先放好若干横排,但是留两边的直行通道,随着排队人数增加,再把通道逐步封闭,这样排队就转到横排里了。在北京机场,根本没有直行通道这回事,再少人也要绕啊绕啊绕,太过弱智了。

过安检门的时候,首先要看登机牌,我们是自助打印登机牌的,一开始安检人员又看不懂这是否是合法通行证,又要确认一下。因为我们是去美国的,特意要求我们要脱鞋,也就是去其它国家可能不用脱鞋检查啦?那就是要根据机票来决定安检严谨程度啦?这样效率不低才怪呢。现在在登机之前,还有一个液体物品的检查,也就相当于两次安检了。我们到得很早,见到第二次检查的人由队长带着来打登机口前,然后一轮训话,说今天开始严查,所以大家不能随便开包看看问问就行,一定要认真查。结果呢?开包之后,还是很随便看看就行了。

在美国入境时,又是长蛇阵的排队,接着是入境面谈,问了一下来干什么的和要停留多久。开头我在海关申报单选了非business,结果入境官员说,别人付钱让你来的就是business,我说那就business吧。我可从来没想过,作为一个学生,有人出钱让我来开会,这也叫做business。在领到托运行李后,又被人“随机”扔去了过X光检查,真是不幸,不过X光检查的黑人倒好人,看到我是到Microsoft的,就很欢迎的样子,可能最近几天到Microsoft的人太多了,而Microsoft又是Seattle的支柱吧。我的箱子有北京托运来的标记,他就问我北京是不是在紧张准备奥运。

之后就出去了,就这么简单。当然,也有朋友被拦下来的,估计因为他在日本买了个新相机,然后未出关就在那里狂拍照,哈哈。

2008年4月12日星期六

黄金周

记得某几个学期,课非常少,一个星期就上两三天课,于是我们说“周周都是黄金周”。其实怎样才算是黄金周呢?我觉得你要是数着日子过,每一天都是回忆的,那才是黄金周。在大学里天天睡觉,又不记得梦见什么,上网太多导致时空感错落,这显然不是黄金周。

明天就飞西雅图了,非常向往,但也非常留恋在北京的不到5天。为什么呢?因为这5天很早就计划好了,从第1天开始你就是倒数着过,每天很清楚记得自己干过什么,因此印象特别深刻。举个例子,同样是在北京,我在百度实习3个月,印象就要浅一些,大概只记得第几周做过什么。这不是因为上班的问题,而是因为我站在第1天向前看就是12周,按周来倒数。如果我在百度的实习是5天,那么我也会清晰记住每天干了什么。

因此,黄金周在于日子倒数着过,而最好的方式就是去旅行。在北京这5天没有怎么出去玩,明天飞到西雅图一定要抓住唯一大家都有空外出的一天,去爽一下!另外期待暑假的德国之旅,如果能去成的话。

2008年4月11日星期五

广州地铁三号线的算术陷阱

三号线作了一个奇怪的设计,就是一开始的列车只有三节,然后说将来要扩容了再加车厢。加车厢的问题在于,一旦要加一节车厢,现有的列车都必须同时加一节,这样才能确保开闭的屏蔽门是统一的。否则一列短车一列长车的话,乘客在长车车厢位置候车,发现来的是短车,他正对着的那个屏蔽门不开,那就很郁闷了。

这种设计,鼓励的是加独立的列车,而不是加车厢。然而,列车加得越多,加一节车厢的成本也就越高,因为同时要加一节车厢的列车数多了。这就变成了,列车越多,越不鼓励加车厢,越倾向于再加独立的列车。最终,整条线路已经无法再加新的列车时,列出数肯定已经相当多了,这时候加一节车厢的成本就会很大,因此加车厢的可能性反而很低。

我觉得唯一的可能就是,在列车很多的时候选择把短列车重新分组为长列车,多余的机车没所谓,反正机车肯定有冗余的,这样子才能避开这个算术陷阱。

2008年4月2日星期三

救救 Web Developers ,拒绝 IE6 !

这是最近的一场于web developers相关的campaign,官方站点是SaveTheDevelopes.org。在上面你可以下载一段脚本和对应的图片,放到你自己的网站上,然后只要有用户使用IE6访问你的网站,他就会看到一个小小的提示框,建议他升级到IE7或选用其它非IE浏览器。当然,你也可以直接引用SaveTheDevelopers.org上面的脚本文件,但大家都知道潜在的风险,因此最好不要这样做,如果你有自己的空间的话最好还是下载下来放到自己的空间上。

为了支持这个campaign,我特意为他们做了一个中文翻译。我本来是想和aw一起做的,不过他们把内容发给我的那个晚上找不到aw,于是有翻译问题就直接找Junyu问了,最后在Junyu的帮忙下把翻译搞定了。我在想是不是应该多做一个粤语翻译,这样会比较好玩,反正也就一个晚上的事情而已。

2008年3月24日星期一

深入理解 ASP.NET 动态控件 (Part 6 - 模板控件)

在之前的文章中,我极力推荐大家使用Repeater和MultiView这类TemplateControl,为什么呢?因为只有这样做,才算是符合MVP或MVC模式。(到底是MVP还是MVC,这视乎你选用什么呈现引擎了。)

虽然我们要动态创建控件,但实际上这部分控件仍然属于View的部分,我们应该尽量采用ASPX的声明性名义来描述这些控件,避免用C#代码来创建控件、设置属性并添加为子控件。就拿最简单的例子来说,创建一个LinkButton,通常我们都需要设置它的ID、Text、OnClick属性/事件,甚至还要设置OnCommand、CommandName、CommandArgument等属性/事件,那就是大概3到5个属性了,用ASPX来声明只需要1~2行代码,而用C#代码则需要写至少5行(把new和Add()也算上的话),由此可见在定义控件这类声明上ASPX比C#代码的可读性要高。

接下来,我们来研究一下TemplateControl是如何工作的,这自然要从如何编写一个TemplateControl讲起。

编写模板控件

在这里,我们假设要编写一个SimpleRepeater控件,自身不支持数据绑定,只有唯一一个名为ItemTemplate的模板,并且就按照Count属性指定的次数重复出现该模板。首先,让我们来定义这两个属性:
public ITemplate ItemTemplate { get; set; }
public int Count { get; set; }

因为ItemTemplate属性不是以键值对的形式在SimpleRepeater的声明中给出的,而是以内嵌一对标签的方式定义的,因此我们需要让解释器去把<ItemTemplate>...</ItemTemplate>中间的内容读取出来,并把解释结果作为ItemTemplate属性的值处理。这时候,我们就需要为SimpleRepeater类加上ParseChildrenAttribute,也就是这样子:
[ParseChildren(true)] public class SimpleRepeater {...}

最后,我们需要重载一下CreateChildControl()方法,把ItemTemplate的内容作为子控件添加到SimpleRepeater之内:
protected override void CreateChildControls()
{
if (ItemTemplate != null)
{
Controls.Clear();
for (int i = 0; i < Count; i++)
{
Control control = new Control();
ItemTemplate.InstantiateIn(control);
Controls.Add(control);
}
}
}

这段代码的用意应该是相当清晰的了,就是循环Count指定那么多次,每次循环创建一个空白的Control,用ItemTemplate.InstantiateIn()方法填充它,最后把它添加到SimpleRepeater.Controls里面,就那么简单。那么这个神秘的InstantiateIn()方法到底是干什么的呢?后面来解释。

编译模板控件

在之前的《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》里面,我详细地解释了ASP.NET 2.0的编译模型。在《深入理解 ASP.NET 动态控件 (Part 5 - 编译实验)》中,我们又做了一个动手实验,亲眼看到了ASPX和C#代码是如何编译到一起的。现在让我们来看看当碰上模板控件时,代码会被如何编译吧。我们把上面编写的SimpleRepeater注册到页面上,前缀为ctrl,并且编写如下一段代码:
<ctrl:SimpleRepeater ID="SimpleRepeater1" Count="10" runat="server">
<ItemTemplate>
<asp:Button ID="Button1" Text="Button" runat="server" />
</ItemTemplate>
</ctrl:SimpleRepeater>

然后还是用aspnet_compiler编译一下,并且用Reflector打开编译出来的dll看看。我们可以看到构造SimpleRepeater实例是通过这样一个语句完成的:
return new SimpleRepeater { ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control1)), Count = 10 };

这个语句其实就是一个普通的new语句,并且给ItemTemplate和Count两个属性赋值了,唯一值得关注的就是ItemTemplate的值。ItemTemplate的类型是ITemplate,因此任何实现了ITemplate接口的类都可以复制给它,然而我们却从来没指定过到底要赋值什么类型的实例给它,因为这已经由ASP.NET帮我们想好了,假若我们不指定的话,那就是CompiledTemplateBuilder类型。还是熟悉的Builder模式,它只需要已委托的形式接受一个实例化ITemplate的函数,然后就能返回实例化好的ITemplate控件子树。可能你会问,既然我已经有实例化ITemplate的函数,干什么要先传给你CompiledTemplateBuilder,让你来调用一下,再把实例化好的给我?我自己实例化不好吗?在此,ASP.NET引擎的做法只是为了保持Builder模式的一致性,处处用Builder模式来分离逻辑而已。

那么这个用于实例化ITemplate的函数从哪来呢?在解释器进入到<ItemTemplate>...</ItemTemplate>内部时,它会继续层层构建Builder模式,就如同在整个页面内执行的一样。因此,整个<ItemTemplate>...</ItemTemplate>会被解释器转化为一个函数,它也是通过层层调用内部函数完成自身的控件子树的构建,传递给BuilderTemplateBuilder构造函数的委托正是指向此函数。

因此,模板控件里面的内容将如同模板外的内容一样,被无缝地解释和构建到一起来。

INamingContainer

如果你查看SimpleRepeater输出的HTML代码,你会发现里面有10个<input id="Button1" name="Button1" type="button" value="Button" />。我们都知道,重复的id是不符合标准的,因此我们需要通过INamingContainer把这个问题解决掉。因为重复的控件是ITemplate,所以应该对它加上INamingContainer,然而它的实例编译时自动使用了CompiledTemplateBuilder,我们如何把INamingContainer加上去呢?我们就只能把INamingContainer加到它的父控件上面去。此时,我们需要一个实现了INamingContainer的简单控件:
public class SimpleRepeaterItem : System.Web.UI.Control, System.Web.UI.INamingContainer {}

然后我们把SimpleRepeater.CreateChildControls()方法的这个语句:Control control = new Control(),替换为:SimpleRepeaterItem control = new SimpleRepeaterItem()。这样,ITemplate的容器就变成了一个具有INamingContainer接口的控件,这时候各个Button的客户端id就会自动加上其容器的id作为前缀,因为容器的服务器端ID是自动变好的,所以必然是各不相同的,这样就解决了Button客户端id相同的问题。

通过这个例子,我们了解到了编写模板控件时必须为模板的容器加上INamingContainer,因为模板内的控件ID命名是可能重复的,加上INamingContainer就可以避免它们的客户端id重复。

小结

这次的文章解释了为什么我们应该尽量使用模板控件来实现动态控件,并且也说明了如何编写自己的模板控件,以及模板控件最终是被如何编译为Builder模式的代码的。

2008年3月21日星期五

每个 blog 都会有一堆 draft posts

lulu说她有个blog post选题的文件夹,我也有一堆blog post都draft存放于各处,可能是邮箱中,可能是Blogger的草稿中,也可能是Windows Live Writer等软件的草稿中。通常,一篇写得好的长文章都不是即兴而作的,而是用draft慢慢写出来的。有点点feel的时候,要逼着自己写成一篇完整的文章很难,反而当有几个draft都是同一个相近的题目时,可以合并在一起写成一篇好文章。

这估计是因为,如果只是有一点灵感,这是不全面的,很难自圆其说。当几个灵感合并在一起,就能看到概貌了,前因后果清晰了,正方反方都出场了,自然写出来的文章就会显得内容丰富一些。

深入理解 ASP.NET 动态控件 (Part 5 - 编译实验)

这次的文章是一个小小的动手实验,你需要准备好Visual Studio 2005或者Visual Studio 2008,以及最新版本的Reflector。通过这次的实验,你将对ASPX与C#代码如何合并编译为一个dll代码有所理解。

在实验开始之前,首先来一个小问题:如果不允许你使用ASPX,要你完全使用C#代码写一个具备复杂控件树的页面你会怎么写?把声明控件的代码都放在Page_Load里面吗?或者有更好的代码编写方法?先想想这个问题,然后继续往下看。

实验的第一步,也就是在Visual Studio里面创建一个ASP.NET项目,并编写一个简单的ASPX页面。例如下面这个例子:(以下代码仅包括HtmlForm内的主体部分)
<asp:MultiView ID="MultiView1" runat="server">
  <asp:View ID="View1" runat="server">
    <div>Please choose either of the followings:</div>
    <asp:RadioButton ID="RadioButton1" runat="server" />
    <asp:RadioButton ID="RadioButton2" runat="server" />
  </asp:View>
  <asp:View ID="View2" runat="server">
    <div>Please choose any of the following:</div>
    <asp:CheckBox ID="CheckBox1" runat="server" />
    <asp:CheckBox ID="CheckBox2" runat="server" />
  </asp:View>
</asp:MultiView>

在这个例子中,我们构建了一个简单的控件树,同时又不至于过于复杂,确保了编译出来的代码相对简单一些。接下来我们就需要将它编译了,最简单的手动编译方法就是用ASP.NET 2.0自带的aspnet_compiler.exe,这个文件默认会在这个目录中:C:\Windows\Microsoft.NET\Framework\v2.0.50727。你可以使用aspnet_compiler -h来查看完整的帮助,例如编译一个IIS默认站点中的ASP.NET子站点可以使用这样的代码:
aspnet_compiler  -v / -p C:\inetpub\wwwroot\site C:\output\site

接下来,我们到输出目录的bin子目录里把dll抓到Reflector里面看看吧。你会看到这个dll里面有三个namespace,分别是-(在Reflector中代表没有namespace)、__ASP、ASP。假设你编译的站点有一个Default.aspx,那么在无namespace的类当中就会有一个_Default的类,对应的就是Default.aspx.cs编译出来的类。大家应该还记得《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》里面提到的,直接继承自Page的类是用后台代码编译出来的,_Default类就是这样一个具体例子了。我们打开_Default类来看看,就会发现MultiView1已经是其成员了,为什么呢?MultiView1仅仅在ASPX中声明,没有在C#中声明啊。回头看看《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》就能解释了这种现象——Default.aspx.cs是标记为partial的,而在你手动编辑的文件中,这是唯一一个partial,另外一个partial由编译器根据Default.aspx自动生成,编译器解释完Default.aspx后在自动生成的partial中定义了MultiView1,因此两个partial合并编译后,_Default类自然就有了MultiView1这个成员了。

接下来,我们再看看ASP这个namespace下的default_aspx类,这个是ASPX文件继承自上述_Default类后编译的结果,它完整表述了ASPX文件中整个控件树的逻辑,而不仅仅是一个包含一堆成员控件定义的Page派生类。这个类的执行入口是FrameworkInitialize()方法,它通过调用__BuildControlTree()方法来构建控件树。在这个方法里面,你可以看到<!DOCTYPE ...>这样的字符串是被解释为LiteralControl的,LiteralControl在Render()时就会把这段文本原样输出。同时你还可以看到,它调用了另外两个方法,分别用来构建HtmlHead和HtmlForm,这两个方法通过类似的形式继续调用其他方法来构建更深层次的控件。

通过阅读default_aspx类的代码,你已经能够理解ASPX的控件树是如何转化为C#代码的了——采用的正是Builder模式。了解到这一点,这次动手试验的目的也就达到了。如果你看到文章开头的那个问题时,你已经想到了使用Builder模式,那么此时也就验证了你的想法是完全正确的。

下一次的文章将是与TemplateControl相关的,我们将继续动手做一些小实验,敬请期待。欢迎订阅我的blog:

2008年3月14日星期五

在校学生找实习、找工作、了解企业情况等等等等

因为时不时就有低年级的同学跑来问我这类问题,所以我干脆写篇文章好了。

信息获取

最先想到的,也是最重要的,是你想干什么,而不是你父母想你去干什么,或者哪个赚钱之类的。在计算机系里面,你总能碰到一些对这个行业没什么感觉的人,他们会说当年填报志愿的时候根本没什么喜欢不喜欢可言,于是在父母驱使下或者金钱诱惑下就报了计算机系。显然你不想犯这类错误,因此第一步是弄清楚你想要什么,或者说,有什么是你可以选择不要的。世界上没有十全十美的事情,所以一切都是权衡取舍(trade off),这个思想贯穿着计算机体系设计的方方面面,缺乏这种思想肯定会导致你在这个行业里发展受到限制,所以首先你要想好的就是你将来的职业发展取什么而舍什么。

在你先清晰了解自己之后,才有如何了解企业实际情况的问题。如果说第一个问题严重依赖于你个人悟性的话,那么第二个问题就依赖于你的人脉了,至少是你的外向程度和活跃程度。

一个企业,其自身对外的公关肯定只会说好话,就算没有任何夸张成分,也不会让你看到这个企业内部任何的不足。因此,如果你需要知道一些细节,看看这企业是否如你想象中那么好,你就必须认识在这家企业里面的员工,通过他们好好了解一下。如果你是一个在学校里已经很活跃的学生,整天在BBS上板聊,或者参加各种学生社团的活动,那么你肯定有机会认识到不少比你高一两个年级的同学,看看他们里面有没有一些正好在你想去的企业工作的,联系他们了解一下企业的情况,例如工资和各种福利怎么算啊,工作与学习的氛围如何啊,以及他们是否也认为你适合这个企业。

如果有机会多问几个人的话,最好都问问,并且以人生经历和价值取向与你相近的人的说法为主要参考。这是因为,在同一家企业里面,不同的员工看到它不同的方面,并且都带有自己的主观想法,因此得出的评价可能截然不同。这就好像假如你问我北京是否好住,我肯定说不好住,因为我看重的是商业环境,我觉得北京的服务业经常让我失望,因此就这样说。但肯定也有人很喜欢住北京的,他看重其他一些因素,并且觉得我看重的那些因素可以忽略不计。如果你确实很想去一家企业,就应该多找几个员工了解情况,特别是头脑清晰能看清楚自己所在企业优劣势的人。如果你碰到一个已经被洗脑的员工,那么问什么也没用,反正他就只能帮你洗脑……

联系途径

根据往年的经验,现在正是2009年毕业的学生找实习的时候,甚至有2010年毕业的学生也提前开始找实习了。积极的人肯定早已开始四处打听消息,包括各大公司招聘什么实习职位,往年难度如何,转正比例多少,等等。在这里,我就要“曝光”我身边的一些朋友了,大家不要怪我“出卖”朋友了哦,因为谁都知道成功把有才华的人推荐给自己公司的意义所在。

在这里,我主要说的是BGM(Baidu、Google、Microsoft的意思,不是background music),如果你对其他IT企业有兴趣的话,也可以找我帮忙,只要我认识该企业的人,我会充当一下“路由”的角色为你“尽力服务”的。

Baidu

如果分别把BGM比作人的话,Baidu是一个典型的中国年轻人,一点都不张扬。就如同在中国随手抓一个ASP.NET MVP问他,“你是不是很熟悉页面生命周期”,他可能回答道,“懂一点吧,有什么问题你先说说看。”虽然从资源(resources)的级别来说,Baidu是很难和Google、Microsoft去比的,但是开放程度(openness)还是是相当高的,你可以做自己喜欢做的项目,可以获取到项目所需的资源。

想去Baidu的可以找我,或者布丁,先了解一下。当然,如果你来找我的话,最好你也能说服我为什么值得推荐你。我主要做Web前端开发的,而布丁则是做后端开发的,所以如果你想申请这两个方面的职位,可以直接找我们了解Baidu现在所采用的技术或者流程。如果是其他职位,也可以帮你联系其他同事问问。完整的实习职位列表,请看这里。

Google

Google是一个典型的美国年轻人,你不问他也会很主动地把他的优势展示给你看。在我看来,Google最大的诱惑不在于公司内的“饮食娱乐”项目(虽然这些也很吸引),而是作为一般的中国员工也可以随便跑到美国总部去,顺道参加美国举行的一些会议或者讲座。看看Junyu同学就跑到Austin去开SXSW了。

如果想去Google的话,去找Junyu问问吧。别告诉我找不到他的联系方式,我通常只链接别人的blog,因为该URL就是个人的标识(即使抛开OpenID不谈),拿着这个URL你手上就已经掌握了搜索他联系方式的一切资料。Junyu喜欢把人拐卖进Google里,想了解招聘职位什么的,找他就好了。

Microsoft

Microsoft是个稳重的美国中年男人,Google所晒的东西,Microsoft会暗自想着“我当年又不是没晒过,现在我已经不屑于晒了”。很成熟的流程,健全的管理体制,都很好地说明了这一点。

貌似我没有很熟的朋友在Microsoft,不算太熟那些又不敢晒出来,所以想找人推荐的话还是联系我,然后我再帮你联系吧。想了解的话,可以找Jeffrey或者Dflying这两位老员工加现任MVP,他们现在不再是员工了,可能观点会更加客观一些。

其他事情

据我所知,很多人也是准备找实习了,才想到要写简历的。这是一项需要创意的工作,在此我提供我的简历和Junyu的简历给大家作为参考。虽然我们的网站下方都写着Creative Commons License,不过不建议你直接使用别人设计好的模板。特别是,假如你想申请Web Developer/Designer类别的职位的话,你可不能够错过这个机会好好展示你的设计风格和编码艺术。

最后,祝大家在08年里都能找到自己喜欢的工作学习环境,好好享受生活。

2008年3月8日星期六

使用 .NET 实现 Ajax 长连接 (Part 2 - Mutex Wait & Signal)

在上一次的文章中,我们说到了如何设计一个ASP.NET Web Service来处理长连接请求。很多人对此就提出了问题,如何hold住请求让它30秒不断开了?这其实很简单,只需要Sleep()一下就可以了:

Thread.Sleep(30 * 1000);

然而问题是,我们不是要等30秒然后看看是否有事件需要返回,而是在这30秒内随时有事件随时返回。因此,我们需要一套机制来在等待的过程中检查是否有事件发生了。

Monitor模型

在.NET里面,大家最熟悉的线程同步模型应该就是Monitor模型了。没听说过?就是C#的那个lock关键字,实际上它编译出来就是一对Monitor.Enter()和Monitor.Exit()。

通过lock命令,我们可以针对一个对象创建一个临界区,代码执行到临界区入口时必须获取到该对象的锁才能执行下去,并且在临界区的出口释放该锁。然而这种模型不太适用于解决我们的问题,因为我们需要等待一个事件,如果使用lock来等待的话,那就是说要先在Web Service外部把对象锁上,然后等事件触发了就解锁,这时候Web Service才顺利进入临界区域。

事实上,要进行这类型的阻塞,还有一个更好的选择,那就是Mutex。

Mutex模型

Mutex,也就是mutual exclusive的缩写,“互斥”的意思。Mutex是如何运作的?这有点像是银行的排队叫号系统,所有等待服务的人都坐在大厅里等候(wait)被叫,当一个服务窗口空闲时它就会发出一个信号(signal)来通知下一位等候服务的人。总之,所有执行wait指令的线程都在等候,而每一个signal能够让一个线程结束等候继续执行。

在.NET里面,wait和signal这两个操作分别对应Mutex.WaitOne()和Mutex.ReleaseMutex()这两个方法。我们可以让Web Service的线程使用Mutex.WaitOne()进入等候状态,而在事件发生时使用Mutex.ReleaseMutex()来通知Web Service线程。因为必须在Mutex.ReleaseMutex()发生后Mutex.WaitOne()才可能继续执行下去,因此能够执行下去就证明必然有事件发生了并且调用了Mutex.ReleaseMutext(),这时候就可以放心地去读取事件消息了。

简单示例

在选定使用Mutex模型后,我们来编写一个简单的示例。首先,我们要在WebService派生类内定义一个Mutex,还有一个代表消息的字符串。

Mutex mutex = new Mutex();
string message;

然后,我们定义两个WebMethod。为了把问题简单化,我们选用上一篇文章中开头所说的两个函数签名,也就说只能在一个Web Service内自己发自己收,没有发送目标的概念,也没有超时的概念,还没有可靠性设计。同时,我们将Message类型替换为普通字符串,以便于我们测试。

我们先编写发送消息的函数:

public void Send(string message) {
  this.message = message;
  this.mutex.ReleaseMutex();
}

在这个发送函数里,首先我们把消息放进了类内全局的变量中,然后让全局的Mutex类释放一个signal。这时候,如果有线程在等待,它可以马上执行下去。如果此时没有线程在等待,那么下一个wait的线程执行到该阻塞的地方就能够不受阻塞继续执行下去。

现在我们来编写接收消息的函数:

public string Wait() {
  this.mutex.WaitOne();
  return this.message;
}

接收函数一开始就进入wait状态。在得到signal后,需要做的事情就是把全局的消息返回给客户端。

亲身体验

最后,我们可以通过ASP.NET Web Service本身支持的Web测试界面来测试一下我们的代码。我们开两个浏览器窗口,一个进入Send()调用,一个进入Wait()调用。然后我们按照如下方法来测试:

  1. 首先执行Send("Hello"),然后执行Wait()。这时候你可以马上看到"Hello"。
  2. 首先执行Wait(),让它等待返回,这时候执行Send("Hello")。随后你可以看到Wait()那段返回"Hello"了。
  3. 按如下顺序执行:Send("Hello");Wait();Send("World");Wait();
  4. 按如下顺序执行:Send("Hello");Send("World");Wait();Wait();
  5. 按如下顺序执行:Wait();Wait();Send("Hello");Send("World");
  6. 按如下顺序执行:Wait();Send("Hello");Wait();Send("World");

你会发现这样一些奇怪的结果:第3个测试返回的是"World"和"World"。第5个测试先返回"Hello"的并不一定是先执行的那个Wait()线程。后者在某些情况下不是什么问题,特别是长连接中一般之后一个Wait()线程在等待中,所以我们可以不管。而前者,则是因为没有消息队列所造成的,我们只有长度为1的消息窗口,所以只能缓存最后一个消息。这个问题我们将在下一篇文章中解决。

小结

在本文中,我们看到了不同的线程同步模型的差异。Monitor模型的lock本质上是一个Semaphore,也就是一个不能连续signal的Mutex,一个signal发出去后必须被一个wait接收了才能进行下一次的signal。同时,Semaphore也限制了signal和wait必须在同一个线程内成对执行,而Mutex则没有此限制。虽然.NET是针对Monitor模型优化的,但在我们的需求当中,只能通过Mutex模型来解决。

接着,我们便写了一个小小的消协发送与接收函数,实现了我们想要的阻塞式Web Service。同时我们也看到了没有消息队列造成的问题,因此确定接下来我们要做一个消息队列。如果你想知道消息队列如何编写,欢迎订阅我的blog:

2008年3月4日星期二

拿到了美国签证

准备了一大堆的材料,包括在校证明和百度的工资证明,结果签证官只看了我的邀请函,问了几个问题就搞定了,真爽快。他问的问题包括,我现在在什么学校(企业),去美国干什么,MVP是什么,大概就这样了,超级简单,还弄得我之前紧张了那么久。

最有趣的地方是,原来驻广州的是美国总领馆,而驻北京北京的只是大使馆,移民业务都在广州处理。整个广州大使馆都是说广东话的,除非看到你的签证签发地是非广东话地区,那就会说普通话或者英语。在移民大厅内,新移民在广东话的带读下进行宣誓,非常好玩。