2012年1月18日 星期三

如何设计大规模 JavaScript 应用 (Part 1 - 概览)

背景

我一直很关注如何设计大规模的 JavaScript 应用,因为我一直在做的都是大规模的 JavaScript 应用。从百度 Hi 网页版到百度地图,从 Yahoo Search Direct 到豌豆荚客户端。好吧……Yahoo Search Direct 本身的规模不大,但作为一个网页插件它要能跑在任何宿主页 面上,其复杂度也不低。在大规模 JavaScript 应用开发和维护的过程中,有两个问题尤其值得关注:设计和性能。前者是必须在开发阶段之前做好的,开发开始后就来不及改了,只能事后重构;后者则更多发生在开发的中后期,等 profiling 结果出来了,再针对瓶颈做优化。在这个文章系列中,我们的关注点是设计。

尽管我一直在写跟设计相关的文章,不过现在看来我过去写下的文章都有点山寨了,也不够 update。在我看到 Large-scale JavaScript Application Architecture 这个幻灯片后,我决定重新写一个系列来说说我在大规模 JavaScript 应用方面的经验,也包括一些设想。

如果说大规模 JavaScript 应用设计方面有什么核心原则的话,我觉得核心原则就是这一条:永远不要尝试构建大规模应用。构建小应用,保证它们的可测性,然后把它们组装成大应用。因此,我觉得在讨论任何设计模式之前,我们先要讨论一下如何把大应用拆分成小应用。

“The secret to building large apps is never build large apps. Break your applications into small pieces. Then, assemble those testable, bite-sized pieces into your big application”

- Justin Meyer

模块化

CommonJS Modules

先说最理想的模块化方式,那就是 CommonJS Modules。一个模块系统至少要能解决两个问题:依赖项的加载、私有作用域和公有导出成员的区分。CommonJS Modules 通过 requireexports 很好地实现了上述两个功能。下面是一个 CommonJS Modules 模块定义和使用的例子:

util.js

var util = {
    extend: function(target, source) {
        /* implementation */
    }
};
util.extend(exports, util);

feature.js

var util = require('util');
var features = {
    core: {
        start: function() {
            /* implementation */
        }
    }
};
util.extend(exports,features);

app.js

var features = require('features');
features.core.start();
AMD

考虑到浏览器里面没有 CommonJS 的 Modules 接口,也不可能完整实现这样的 Modules 接口,所以就有了 AMD (Asynchronous Module Definition) 这样的解决方案。AMD 使用 define 函数定义模块,要求模块提前声明依赖项,然后通过回调加载模块,解决了浏览器无法同步加载模块的问题。下面是一个 AMD 模块定义和使用的例子:

util.js

define('util', [], function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
};

feature.js

define('features', ['util'], function(util) {
    return {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
});

app.js

define('app', ['feature'], function(feature) {
    features.core.start();
});
CommonJS Modules/Wrappings

考虑到 AMD 的写法跟 CommonJS Modules 的写法区别十分之大,要把已有的 CommonJS Module 写法模块改为兼容 AMD 不容易,所以又有人设计一些改动不那么大的写法,如 AMD 工厂函数的 function(require, exports, ...) {...} 签名,或 CommonJS Modules/Wrappings (意思是 Modules 的 Wrappings)。由于浏览器必须异步加载依赖项,所以这些写法只能通过对工厂函数源代码做静态分析提前找出依赖项,在异步加载好之后再执行工厂函数。这样做的坏处是工厂函数内部对 require 的调用缺乏灵活性。下面是一个 CommonJS Modules/Wrappings 模块定义和使用的例子:

util.js

define(function(require, exports, module) {
    var util = {
        extend: function(target, source) {
            /* implementation */
        }
    };
    util.extend(exports, util);
});

feature.js

define(function(require, exports, module) {
    var util = require('util');
    var features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

define(function(require, exports, module) {
    var features = require('features');
    features.core.start();
});
UMD

尽管 Node.js 写好的 CommonJS Modules 模块可以通过 CommonJS Modules/Wrapping 包装一下使得它能用在浏览器内,尽管包装过的模块通过 r.js 也能用于 Node.js 环境下,不过这不是完美的解决方案。因此,又有人提出 UMD (Universal Module Definition),希望提供跨平台的模块定义方案。

UMD 现在还没有定稿,不同的人提出了不同的解决方案。最全面的方案同时支持 AMD 和 Node.js,顺便还把传统的浏览器脚本顺序加载模式也兼容了。具体的做法就是判断环境中是否存在 AMD 所依赖的 define,如果存在的话就使用 AMD 加载,不存在的话就使用别的方式模拟 AMD 加载。

util.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define('util', [], factory);
    } else {
        root.util = factory();
    }
})(this, function() {
    return {
        extend: function(target, source) {
            /* implementation */
        }
    };
});

feature.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('util'));
    } else if (typeof define === 'function' && define.amd) {
        define('feature', ['util'], factory);
    } else {
        root.feature = factory(root.util);
    }
})(this, function(util) {
    return features = {
        core: {
            start: function() {
                /* implementation */
            }
        }
    };
    util.extend(exports,features);
});

app.js

(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory(require('feature'));
    } else if (typeof define === 'function' && define.amd) {
        define('app', ['feature'], factory);
    } else {
        root.app = factory(root.feature);
    }
})(this, function(feature) {
    var features = require('features');
    features.core.start();
});

小结

在这篇文章里面,我们了解了大规模 JavaScript 应用设计的核心原则:使用模块化的方式把大应用分解为小应用来编写和维护。同时我们也看了不同的模块定义方式,我们可以针对平台来选择一种合适的方式,也可以选择通用的方式但需要维护更多的代码。

在有了模块的概念之后,我们就可以讨论具体的设计模式了。这个系列接下来将会深入讨论每一个对大规模 JavaScript 应用设计有用的设计模式。如果你对这个系列的文章感兴趣,可以选择订阅本博客。

2012年1月12日 星期四

Covariant(协变)与 Contravariant(逆变)

今天为了解释某个问题而提到协变和逆变,发现每次解释这两个概念都会忘掉它们的本质,然后要重新看看定义,重新消化一下才能说明白。所以我决定把自己对协变和逆变的理解写下来,以免将来再次忘掉。

我知道 .NET 的用户喜欢用 delegate TResult Func<in T, out TResult>(T arg); 来解释协变逆变,我则喜欢把 Func 的签名简写为 Haskell 签名形式。也就是说,把 Func<T, TResult> 写成 f :: a -> b 的形式;把 Func<T1, T2, Result> 写成 f :: a -> b -> c 的形式。

其实无论是协变还是逆变,本质都是一样的:对于签名为 f :: A -> B 的函数,实际可接受的参数范围为 ASub,实际可返回的参数范围为 BSub。这个很容易理解吧?任何时候子类的实例都可以当做超类实例来使用,无论是接受还是返回。

协变和逆变用于描述高阶函数签名,如 f :: (X -> Y) -> Z。那上面的 f :: A -> B 做模版,我们可以把 (X -> Y) 看做 A,把 Z 看做 B。应用同样的逻辑,函数实际可接受的参数范围是 (X -> Y) 的子类,实际可返回的参数范围是 Z 的子类。对于后者我们没什么疑问,但 (X -> Y) 的子类到底是什么呢?它的所谓「子类」应该是 (XSuper -> YSub)

为什么说 (X -> Y) 的「子类」应该是 (XSuper -> YSub) 呢?因为子类在能力上应该完整覆盖超类的能力,因此如果对方要求你提供一个函数,这个函数接受 X 类型返回 Y 类型,你提供的函数至少要能接受 X 的超类而返回必须是 Y 的子类。这时候 X 是逆变参数(类型可以更宽松),而 Y 是协变参数(类型可以更严格)。

一般来说,如果把「类型可以更严格」看做协变的话,函数的返回类型一定可以协变,非高阶函数的参数也可以协变,高阶函数的非函数参数同样可以协变。把「类型可以更宽松」看做逆变的话,只有高阶函数中的函数参数中会出现逆变,也就是作为参数的参数出现。那么参数的参数的参数呢?也就是说高阶函数的参数仍然是高阶函数,那会怎么样呢?这个大家可以尝试自行分析,盯住 f :: ((X -> Y) -> Z) -> W 看一会儿,再不停类比上文的 f :: A -> B,或许你就明白了。

2012年1月7日 星期六

世界顶级黑客自传:Ghost in the Wires

上个星期终于把 Ghost in the Wires 看完了,现在就抽时间来写写书评吧。尽管我一向懒得写书评,不过这本书真的是超级推荐!

最初我是在 Audible 上面买了这本书的有声读物,然后发现朗读得超级好,尤其是 Kevin 知道危险逼近时心中的独白——「Fuuuuuuuck!」朗读的语气掌握得非常好。后来我决定买 Kindle 版来仔细读一下,因为有声读物不会专心听,只能把我到大意。随后我边听边读把整本书看完了。

这本书整体上分为 4 大部分。第 1 部分讲述 Kevin Mitnick 是如何入行的——他父亲和叔叔都是商人,这使得他很小的时候就意识到通过言语可以驱使他人做一些对自己有利的事情。同时他又是个超级 geek,从小喜欢在书店里面看一些稀奇古怪的书,例如说如何伪造身份让自己凭空消失,或者是如何获取他人的驾驶记录、资产记录、信用报告、银行信息、非公开电话号码。

由于这些爱好的驱使,他接触到了电话玩家(phone phreak),也就是一群喜欢研究电话系统和拿电话系统做实验的人。然后他开始学习如何打免费电话(Steve Jobs 和 Steve Wozniak 也做个类似的事情),并且如同研究黑箱电路一样通过不停地打电话探索电话公司内部的组织形式。

后来由于同伴的出卖(这又是因为 Kevin 爱在同伴面前耍小聪明),Kevin 被捕并被起诉。在这个过程中,公诉人和媒体想尽办法抹黑 Kevin,给他安上各种奇怪的罪名,例如黑进 NSA、切断假释官电话、篡改法官信用报表等等,其中最莫名其妙的是说他由于喜欢明星 Kristy McNichol 而切断她的电话服务。(Kevin 独白:有见过因为喜欢一个人而切断她电话的吗?)正是由于这段经历,导致 Kevin 后来被 FBI 追捕时选择了逃亡,因为他认定政府是邪恶的,只会想往他头上加更多的罪名。

第 2 部分讲 Kevin 假释后的生活。尽管他的假释条款规定了他不能再进行违背道德的黑客行为,也不能再跟别的黑客有所接触,但为了调查弟弟死亡的真相,他还是忍不住黑进电话公司并且监听被怀疑对象的电话。随后他又回到了黑客道路上来。这时候他碰到了一位神秘黑客 Eric,看样子跟娱乐圈混得很近,不用工作也能过得很滋润,而且跟 Kevin 一样对电话系统有着深入的了解。最最重要的是,Eric 知道 Kevin 不知道的一种设备,叫做 SAS,可以用来全自动地监听任意线路。

Kevin 一方面对 Eric 有所怀疑,觉得他可能是卧底或者是线人;另一方面又受不住 SAS 的诱惑,希望搞清楚这套系统是怎么用的。最终他假装电话公司员工,向 SAS 制造商要到了完整的 SAS 设计手册,里面的资料比电话公司拥有的 SAS 用户手册还要丰富,包括用户用不到的调试指令。随后 Kevin 要想办法绕过 SAS 的安全策略来监听别人。

SAS 如此强大的设备自然有相对严谨的安全策略——你可以拨号给 SAS 让它监听一条线路,SAS 也能帮你监听这条线路,但监听的结果不会输出到你拨入 SAS 的线路,只会输出到你指定的回拨线路,问题就在于每一台 SAS 都限制了回拨线路,你不能让它回拨到任意一个号码上。Kevin 想了一个很聪明的方法来绕过这个限制:他先用线路 A 拨入到 SAS 的回拨线路上,然后用线路 B 向 SAS 发送指令。SAS 要回拨时它自然会接上回拨线路,实际上就接到了线路 A 上,这时候 Kevin 就对着话筒哼一个线路就绪音(就是我们平时拿起听筒听到的声音),SAS 以为线路就绪了就开始拨号,实际上线路早就被接通了。

电话公司发现有人入侵了,安全小组就联通 FBI 一起侦察,而 Kevin 发现自己被监听以后,就想办法监听电话公司安全小组以及 FBI 的通话,以便搞清楚到底对方都收集到多少证据了,自己是否还有机会脱身。在如此几个回合后,Kevin 决定是时候人间蒸发了,于是就开始了他的逃亡之旅。

第 3 部分讲 Kevin 逃亡的过程。他先换了一个临时假身份来到了丹佛,然后再换一个长期假身份。每一次他都先要把出生证明和驾驶证骗到手,然后再去办社会保障号码卡。由于他用的都是真是存在的同龄人身份,简历上印的学历也是真实的,所以他能够找到工作赚钱生活下去。在这个过程中,他还弄到了一叠盖好章的出生证明!这意味着将来他想换身份就更容易了,直接把想要克隆的身份填写到出生证明上面就可以了,然后去考驾照和申请补发社会保障号码卡。

第 4 部分讲 Kevin 最终被捕的过程。Kevin 一直做足安全措施,包括使用克隆的手机号码,拨号时利用先前已经黑下来的电话公司交换机设置多重呼叫转移使得拨号源头难以跟踪,为此 FBI 一直都抓不到他。最后他黑了安全专家 Tsutomu Shimomura。对方是个高傲的黑客,因为被黑而决定帮助 FBI 抓住 Kevin,FBI 为此也给了他很多平民在合法程序上不可能获得的特权,例如监听电话和网络。最终 Kevin 被捕,同时公诉人再次想往他头上套上最严重的罪名,结果反而导致了 Free Kevin 活动的活跃。

媒体最初一直把 Kevin Mitnick 渲染为大黑客,说他只要对着电话吹口哨就能发射核导弹。现在由于政府玩大了,风向就发生了变化,人们开始支持 Free Kevin——不是说他无罪,而是说他应该在合理的范围内承担他的罪名。他盗窃了大量的源代码和信用卡信息,但是他没有利用这些信息赚钱,所以造成的损失也就是各大公司少赚了一份源代码授权费用而已,因此应该根据这部分损失来做出判决。

公诉人和法官当然不这样想,但 Kevin 竟然找到了一份极具参考价值的判例——一位税局官员曾经处于好奇心而获取了众多议员的税务信息,最终因为他没有因此获利而被轻判,理由是他只是好奇心驱使而已。这个判例使得 Kevin 获得了合理的对待,他只需要赔偿源代码授权费的损失就可以了,而且赔偿金额还要根据他未来几年的赚钱能力调整。考虑到假释条款上规定他不能使用电脑、不能接触电话、不能做技术咨询……基本上他就只能去麦当劳煎牛肉了,法官判他 $4000 多一些的赔偿。

假释期满后,Steve Wozniak 亲自给他送了一台最新款的 PowerBook G4,然后他又回到了他的黑客世界。只不过这次他是开咨询公司,在道德的范围内,在客户的要求下,去黑客户的公司,帮助客户找出他们的漏洞。

总的来说,如果你对社会工程学有点好奇心,或者想知道美国政府是如何不小心把 Kevin Mitnick 搞成头号黑客的,Ghost in the Wires 能够很好地满足你的好奇心。里面有技术细节,但非常少,就算你完全看不懂也不影响对情节的理解。

P.S. 这本书里面的每一章开头都有一段密文,有兴趣的人可以自己尝试去破解,懒得破解的可以搜索相关的文章。

2011年12月31日 星期六

Jawbone UP 测评

感谢 Peter Fang 和他朋友的帮助,我星期一晚上拿到了我的 Jawbone UP。之前我一直对健康类的数码产品观望,最终 UP 引起了我的兴趣,因为它的设计理念足够好——只要你把它戴在手腕上,它自己就能够工作,你根本不需要注意到它的存在。这种纯被动式的产品往往能带来较好的体验,至少不会需要你去折腾。尽管它也有不完美的地方,例如不支持 Fitbit 那样的无线同步,因此每次同步时还是要脱下来的。

UP 发布后,各大网站对它的评价都不错,例如说 The VergeSummer Tomato。随之而来的是,用户反馈说他们的 UP 变砖了(尽管它的形状看起来无论如何都不像块砖),这确实影响了我对 UP 的信心。不过我想,既然变砖的都可以找 Jawbone 免费更换,那买一个回来试试也好嘛,如果真的出问题了再看看谁方便帮忙拿去更换咯。$100 对于一个健康数码设备来说还是可以接受的,于是我就下决心要买一个。

UP 到手后第一件事情自然是拆包装。iPhone 的 app 我早就装好了,但不把 UP 接上 app 就拒绝你使用。UP 的包装设计得还可以,没有任何难以拆开的地方,附件只有一本说明说和一个充电转换头。我把 UP 插到 iPhone 的耳机口后,app 就允许我注册了。首先需要注册一个账号,然后填写你的性别、生日、身高、体重,顺便让你设置你的目标(例如一天睡 7 个小时,走 5000 步,吃 2 餐能让你充满能量的)。完成向导后,你就可以开始使用 UP 了。首次同步时我的 UP 有 49% 的电量,所以我也没去充电,先用一晚上再说。

硬件

UP 的设计很简单,只有一个硬件按钮和两个指示灯,指示灯也仅仅在状态切换时亮一下,以表示你切换到什么状态了。第一天晚上我为了测试运动模式,在睡觉前使用 GAIN Fitness 运动了半小时。在运动前,先双击并按住按钮,UP 就会提示你它切换到运动模式了。接下来的运动过程 UP 会记录下来,不过由于它是戴在手腕上的,那意味着它只能记录上肢有活动幅度的运动,如果你戴着它做俯卧撑它就记录不了什么了。由于 GAIN Fitness 提供的是室内运动项目,包括热身、几组练习、放松,UP 的设计使得它认为热身是最激烈的部分,而放松(主要是瑜伽动作)是最轻松的部分。

UP 的另一个模式是睡眠模式,点击并按住按钮 UP 就会显示蓝色月亮,并且震动提示你模式切换成功。在睡眠模式里,它会跟踪你的上肢运动以便跟踪你是否在深度睡眠。智能闹钟能够利用这点避免在你深度睡眠时叫醒你,而且唤醒的方式也十分有趣——就是让 UP 震动起来。唯一不足是这个闹钟不支持贪睡模式,一旦点击按钮结束震动,就算你继续睡 UP 也不会再尝试叫醒你。

最后,UP 的一般模式只会跟踪你的步数。由于它不像 Fitbit 一样夹在衣服上,所以在你刷牙时它会记录非常多的步数,而在你端着一杯水走时我猜它会记录不到步数。这 UP 为便利性牺牲准确性的设计,不过我觉得如果它能成功刺激你多走动,效果就已经达到了,我那 $100 也不是白花了。此外,UP 的活动提醒功能也能够刺激你多活动。在每天指定的时段内(通常就是坐在办公室的时段),只要你连续若干分钟不活动(我设置为 30 分钟),UP 就震动一下提醒你起来活动活动。

软件

UP 的软件设计显然不如硬件优雅。尽管界面和交互设计得还可以,不过细节处理得还不是很好,性能尤其不行。如此简单的一个界面,在后台打开应用程序多时滚动起来还会卡,我实在想不明白为什么它需要如此多的资源。当然,如果你愿意为了 UP 而关闭所有后台应用的话,这就不是个问题了。

UP 的主界面显示你今天的目标完成度。例如说,如果你设置了一天要走 5000 步,它就会告诉你 4000 步只完成了 80%。在这个界面点击一下,或者旋转屏幕,UP 就会切换到时间轴界面,显示 UP 跟踪的历史记录。

UP 本地会缓存一天到两天的历史记录,再往前滚动的话就需要重新获取数据进行绘图。在这个图表里面,你可以清楚看到自己的活动分布是怎样的——在什么时候进食,在什么时候运动,在什么时候走的路比较多。通过这个图表,你可以反思自己的生活是否合理,然后调整自己的生活习惯。在这个图表上点击某一个时间段的话,可以查看该时间段的详细信息。

睡眠记录把你的睡眠时间划分为深度睡眠、浅层睡眠、清醒三个状态,通常进入睡眠模式后都会有一段时间的清醒然后才能入睡。睡眠质量分数我猜想是跟深度睡眠时间长短有关的,至于比例是否有所影响我暂时不确定。

运动记录同样是分成三个状态:剧烈、中等、轻度。我的猜想是,这跟你手臂活动的幅度和频率有关系,但暂时没有通过实验去证明,因为我不想把 UP 脱下来专门做实验并且污染我的数据。

其他

尽管 UP 说自己有一米的防水深度,并声称洗澡的时候也不用脱下来,但考虑到那么多 UP 变砖的事件我还是尽量让它不碰水。洗手的时候我尽量不会弄湿它,洗澡的话干脆就把它先脱下来。

UP 还有带 GPS 的运动功能,不过 GPS 实际上由 iPhone 提供,只是把 UP 的数据一起合并到 app 里面去。考虑到北京的空气质量如此糟糕,我是肯定不会进行任何户外活动的,还是等我回广州后去滨江跑步时再测试这个功能吧。

更新:实测表明,UP 的电力确实能够支撑至少 10 天。我充满后用了整整 10 天,电量还有 20% 剩余。

2011年12月14日 星期三

前端工程师的职业发展路线在哪?

我猜想国内很多前端工程师都想过这个问题吧。前端工程师往往属于产品研发团队,但却很容易被边缘化——后端工程师觉得自己才是主力,没有后端工程师产品就不存在了,但没有前端工程师产品还能有,只是界面非常糟糕而已。这时候前端工程师就开始感觉自己像是个外包似的,只是来帮别人完成一些任务而已,对产品没有归宿感。这时候前端工程师的职业发展路线在哪?成为一个更好的外包吗?

要做关键任务

我觉得,要别人重视你的工作,不仅仅是你做得好就行了,还要求你的工作对别人来说足够重要。这跟产品定位有关——例如说对搜索引擎来说,前端对产品的影响不会非常大,用户只要能搜索到自己想要的结果就行了。搜索引擎最复杂的交互可能就是搜索框的自动完成了,但有自动完成和无自动完成的区别到底有多大呢?跟准确率和召回率相比,有没有自动完成实在没有多重要。况且,自动完成的结果本身也依赖于准确率和召回率,所以后端工程师比前端工程师重要得多。

因此,前端工程师在选择工作时首先要选择前端足够重要的工作。重要用什么来衡量?务实的话,是钱;务虚的话,是产品。如果一个功能只能在前端实现,并且这个实现能够提高多少的转化率,使得多少原本不产生利润的点击产生利润,那么前端对这个产品来说一定十分重要。可惜往往跟钱相关的事情不由前端工程师来研究和决定,所以这部分工作还是安心交给产品设计师来做吧,让他们来决定怎么样的产品能赚钱,然后由你来完成这个产品的实现,这时候你的目标就是把产品做好。

回到刚才的问题,有些产品更依赖于后端,例如搜索引擎,当然也有些产品更依赖于前端。什么样的产品更依赖于前端?就是后端难以建立起技术壁垒的产品。这类产品要抄袭一个功能差不多的并不难,因此只有细节做得最好的能够获得足够多的用户。这类产品在 iOS App Store 上很常见——有很多 app 拥有相似的功能,而其中只有一个交互设计得最好的能够获得绝大多数的用户。尽管 app 不存在 HTML + CSS + JS 这个前端,不过道理是一样的。当年 Tweetie 能够取代老牌的 Twitterrific 成为主流 Twitter 客户端,靠的就是交互上的创新,外加不差的性能和稳定性。如果交互对于一个 web app 来说十分重要,这个 web app 自然也就需要十分优秀的前端工程师。

总结一下,由于前端工程师的价值在于实现复杂的前端细节,因此如果可以选择的话尽量选择一个细节决定成败的产品。如果产品的成败已经由后端工程师决定了,例如某某数据规模要么能做要么不能做,那么这个产品就没你什么事了。

要懂核心业务

每一个公司,每一个项目,都有它的官方语言。不是指普通话,也不是指 C++,我指的是大家围绕什么问题来展开项目,什么问题的讨论能让大家为之兴奋。举个例子来说,百度的官方语言就是搜索,跟搜索没有关系的产品也会使用「准确率」、「召回率」这样的术语用来做比喻。前端工程师有多少知道什么是「准确率」、「召回率」的?估计不多,因为前端根本没有这样的概念。这时候前端工程师要跟后端工程师沟通也就不容易了。久而久之,你对人家很兴奋在讨论的什么 O(1) 还是 O(n) 不感兴趣,人家也不理解你的 {} != {} 是什么意思,你就被边缘化了。

如果不想被边缘化,就算前端不是公司的核心业务,你也必须懂公司的核心业务,然后说着官方语言,而不是前端的方言。这就意味着,如果你在一家后端技术很强大的公司,你最好也懂后端技术。我知道国内有很多前端工程师并不是计算机系毕业的,就算是国内的教育也不怎么样,这时候你只能恶补相关的基础知识了。如果你不懂这些,就算你能把整本《JavaScript 权威指南》背下来,你说的还是方言,说官话的人还是会鄙视你。如果公司主要服务于某个垂直领域的话,你必须对这个垂直领域十分了解,随时能用这个领域的行话来沟通。

总结一下,由于每个人已经熟悉的领域都不一样,所以没办法说哪个领域更适合前端工程师。如果你原本已经有某个领域的从业经验,进入服务于该领域的技术公司总是有显著优势的。如果你进入了一个自己不熟悉的领域,那就一定要补充相关基础知识,否则你对这个领域不感兴趣,这个领域也不会对你的前端工作感兴趣。

实际例子

为什么我选择加入豌豆荚?主要考虑的还是上面两点。

我在百度的时候一直就在想,既然前端对搜索引擎来说不重要,那对什么类型的应用来说比较重要呢?当时看到 Facebook 做得不错,所以觉得社区会需要复杂的交互,而如果复杂交互做不好则会影响用户使用,因此前端对社区来说应该十分重要。现在看来,也不完全是这样子。前端对社区来说确实重要,但 Facebook 并不是一个典型的例子,它是一个前端做得尤其优秀的例子。

在我了解到豌豆荚 Windows 客户端的实现方式时,我立即意识到它可以通过我的第一个判别标准——前端对它来说是关键任务。它使用 Webkit 做了一个容器,然后把所有的交互都通过 web app 的形式做在里面,然后通过一组接口跟 native 进行交互。如果一个应用决定要这样做了,那么前端就能影响到它的成败,因为这时候前端后端的分隔线已经很明确了。如果一项功能应该由前端来做那就必须由前端来做,后端基本不可能成为实现此项功能的备选方案,这时候前端就具备了无可替代的位置。

至于第二个判别标准——豌豆荚的核心业务是什么?我觉得豌豆荚做的很多事情都是以产品设计为起点的,而这至少是我感兴趣并且也有点感觉的东西。从细节上来说,就是大家喜欢谈论的事情是一致的,例如产品如何做一些很智能的设计,最新的技术方案如何能够巧妙地帮助这些设计得以实现。Junyu 说「设计就是创造性地解决问题」,这是我喜欢的解决问题方式。这个世界上能够把逻辑转化为代码的人非常多,同时有一定数学和计算机专业基础的人也不少,因此要拼谁的解决方案更好的话那还要加上创造力。

我知道国内有很多产品设计师,在考虑产品时首先想到的是百万千万级用户量,这样无论从单个用户身上赚到的钱多么的少,最终产品还是能赚大钱。百度曾经就属于这种思维方式,但这不是我喜欢的风格,因为没有明确的目标用户定位。我知道国内由很多工程师,在编写代码时用尽各种技巧以展示自己过人的才智,但是这样的代码还有可复用性吗?除了作者本人没有人能够维护啊。不同的人有不同的品味,能够跟品味一致的人一起工作是一件幸福的事情。

额外信息

这个话题到此就结束了吗?其实不是的。关于前端工程师的职业发展,我还有很多可以说的。不过我觉得找到一份让自己满意的工作必然是其中的第一步,因为你必须对工作充满兴趣,然后才能把事情做好,所以我把这部分内容放在最前面并且先发出来了。如果你不想错过后继讨论的话,欢迎订阅本博客。

此外,豌豆荚现在还招前端工程师,包括全职和实习,有兴趣的可以联系我:catchen@catchen.me。对于全职的前端工程师,我期望你熟悉 web app 的开发与调试,如果我让你手写一个 HTTP GET 请求你连个大概都写不出来,那我就要怀疑你平时都有多少时间对着 debug console 调试 AJAX 代码了。对于实习生,我期望你至少有扎实的 web page 基础,能够用简洁的代码实现符合语义的页面。至于豌豆荚提供什么?就是我前面所说的,但还有个前提——至少我们要有一致的品味。

2011年12月12日 星期一

看对的书 (Part 1 - Tribal Leadership)

很久很久之前(准确来说是两年前),我开始了一个系列叫做「看对的书」,然后只写了一篇 intro 就没有写下去。背后的原因包括,我确实越来越懒了,同时写书评也不容易——剧透太多了,大家觉得没必要去看原书;剧透太少了,大家觉得看不出书的吸引力在哪里。如何把握剧透多少,怎么剧透才能吸引大家取看原书,我还没有想好,不过我可以先尝试着写。

我今天想要推荐的书是《Tribal Leadership》,也就是《部落领导力》,不过这本书暂时没有中文版。尽管这是一本讲「领导力」的书,但里面说的并不是你要做什么事情才能拥有领导力。书中说得更多的是部落——只有在你理解到部落的 5 个阶段后,你才能够使用你的能力把你的部落往更高的阶段迁移。那到底什么是部落了?不知道是否还有人记得「为什么早期手机只能存储 150 个联系人」的问题,答案是 150 是 Dunbar's number邓巴数),这个数是一个人能够维持的熟人数量。而一个部落,就是有 20 到 150 个熟人组成的组织。如果一个组织的人数超过 150 人,它会自动分裂为多个部落,从而形成一个由部落组成的部落。

在部落形成后,你就可以尝试去识别他们到底处于哪一个阶段。这本书的作者发现了一个有效识别部落所处阶段的方法,就是看部落成员说话的方式。例如说,如果你在医院的电梯里听到 3 个医生在交谈——第一个医生说「你看到我在《新英格兰医学杂志》上发表的文章了吗?」第二个医生用讽刺的语气说「看到了,印象深刻。然而在你忙于做研究的时候,我完成的手术比楼层内的任何一个人都要多。」大家笑了一笑,第三个医生接着说「当你忙于码字的时候,当你忙于切肉的时候,我教授的新人比本院的任何人都要多。」大家又笑了,互相拍拍肩膀走出电梯。这 3 个医生所说的话,背后的意思都是一致的——「我比你牛,而且我有数据支撑。」这时候,你就可以认定这是一个处于第 3 阶段的部落了。

部落的 5 个阶段对应的话语都很有特色,所以要鉴别一个部落处于哪个阶段并不难:
  1. 「人生就是个悲剧」——第 1 阶段的人认为没有谁的生活是好过的。
  2. 「我就是个悲剧」——第 2 阶段的人看得到别人生活美好的一面,只是觉得自己的生活很糟糕。
  3. 「我很牛,但你不是」——第 3 阶段的人觉得自己的生活不错,因为自己很牛,但觉得自己身边的人都不如自己牛,也不愿意给予自己更多支持,因此自己没办法完成更大的目标。
  4. 「我们很牛,但你们不是」——第 4 阶段的人觉得自己的部落很牛,而且竞争对手都不如自己。
  5. 「世界很美好」——第 5 阶段的人只专注于做一些很崇高的事业,无视竞争对手的存在。
就美国而言,有一半的人处于第 3 阶段的部落,而且绝大多数都是专业化的知识工作者,例如医生、律师,当然也包括工程师。由于现代教育体系奖励很牛的小朋友,而不是奖励很会跟别人合作的小朋友,所以大多数专业人士到达第 3 阶段后就难以获得突破,除非他们能够获得顿悟。顿悟的来源通常有两种:要么是明白到自己想要追求的目标实在是太大,无论自己有多牛都不可能实现,所以必须放弃单干的幻想,学会通过支持别人来换取自己的目标得以实现;要么是觉得自己足够牛了,获取更多的个人成就已经变得没意思了,那还不如帮助别人实现他们的目标。只有在不看重个人成就高低的前提下,部落才可能进入第 4 阶段。

此外,美国还有四分之一的人处于第 2 阶段。他们往往有一个处于第 3 阶段的老板,这些老板为了保证自己能够获得最好的职业发展而只雇佣能力比自己低的第 2 或第 3 阶段下属。第 2 阶段的人往往就在这样的老板手下痛苦挣扎,看着老板的生活很好,同时感觉自己生活很悲催,因此发誓将来成为老板的话一定不能这样子。但是,等第 2 阶段的人成为老板并且过上好生活后,他们会进入第 3 阶段,成为他们过去痛恨的老板。这时候,他们就不想放弃晋升带来的冲劲了,希望能够更好地证明自己也是很牛的,同时迅速往上爬。

这背后的道理是,人的行为变更是不可能跨越阶段进行的。书中甚至把每一个阶段分成了 3 个子阶段——初期、中期、末期。你不能指望一个人在脱离第 2 阶段后能够立即拥有第 4 阶段的意识,放弃个人成长优势而去帮助他人。因此,如果你想要成为一个部落领袖的话,你需要能够鉴别部落所处的阶段,同时你自己要能穿梭于不同的阶段,去到部落所处的阶段然后把部落带到更高级的阶段。至于每一个子阶段的特征是什么,以及如何能够帮助部落迁移到更高的阶段,我在这里就不详细解释了,大家去看书中的内容吧。如果不愿意看书,到 Tribal Leader 官网上下载有声读物也可以,而且还是免费的,只不过需要注册一下。如果买书的话,我推荐买 Kindle 版,因为看起来方便,不用想方设法把一本进口书弄回国内。

我之所以选择推荐这本书,是因为我很认同里面所说的部落阶段划分。我经历过不少第 3 阶段的部落——名校、名企大多如此。我知道,自己再牛能做的事情也有限;但是,我不知道如何能够突破。这本书解释清楚了一个问题——个人要突破,必须先放下职业发展的得失。然后还要推动整个部落突破,这需要部落拥有核心价值观和高尚的目标,同时制定达成高尚目标的战略。我相信国内也会有很多优秀的工程师卡在第 3 阶段找不到突破口,所以我建议大家读读这本书看看有什么启发。

2011年12月4日 星期日

工具:开发者使用,企业埋单

我喜欢写一些小工具来简化我的工作,通常是一些小组件。利用这些小工具我可以提高自己代码的可读性,同时维持我的 DRY (Don't Repeat Yourself) 洁癖。工具对我来说很重要,因为时间对我来说很宝贵。能够用工具自动化完成的事情绝对不手工反复操作,能够用工具避免人为错误的地方一定让工具来确保质量。使用工具节省下来的时间用于玩游戏的话,绝对是值得的!

过去我主要做工具给自己用,或者是给自己所在的团队使用,所以觉得自己就是用户,自己设计产品给自己用肯定是没问题的。然而做工具给更多人用呢?看起来就不那么容易了。之前在百度经历过 Tangram 的需求分析与设计阶段,每个产品线都派出了前端工程师代表参与需求评审,通过多轮的辩论来决定哪些需求是要覆盖到的,哪些需求是可以忽略的。最后做出来的产品?只获得了一部分产品线的认可。推广也得不到公司层面的支持。

工具的需求难道不是来自使用者吗?使用者就是坐在自己隔壁并且做在同一领域开发的团队,这样都可能出现需求理解偏差吗?这个问题我一直没有想明白。

后来我到了 Yahoo,在对 YUI 表示恶心一端时间后,我开始理解到恶心背后的原因。无论是 Yahoo YUI 还是 Google Closure,目的都是一样的——通过工程性来保证,就算工程师素质很低,产出的代码质量很差,项目也能交付。我当时只是觉得大公司为了交付而造出一些压制工程师创意的工具是很恶心的事情,而没有考虑到这是否跟我要思考的问题相关。

到了豌豆实验室后,我开始着手构建一些前端工具,这一次我终于明白到需求源自于哪里了。工具确实是给开发者使用的,而需求来源于企业。企业在不同时期可能有不同的需求。企业刚刚成立时,肯定希望尽快把产品概念做出来,所以要求工具能够快速实现产品原型。等到产品成功发布后,企业又希望能够拓宽产品的市场,于是要求工具能保证产品复杂度增加的同时维护成本可控。随后企业会同时开发多个产品,这时候就会要求工具能够帮助降低产品共有的成本。

在文化相对开放的企业,工具的选择看起来是由开发者决定的。开发者爱用什么工具就用什么工具,企业不干预。实际上,企业不是因为文化相对开放而不干预,而是缺乏干预的动机。如果提供足够的动机,例如使用某种工具能够使得利润翻倍,任何企业都会愿意为之而干预。不干预的企业,往往只是没看清楚工具能给它的成本造成什么影响而已。

此外,在企业决定干预开发者使用什么工具时,企业肯定不会在乎工具使用起来爽不爽。开发者不爽是一种成本,但把这项成本计算在内后,如果特定的工具还是能降低企业成本,企业还是会选择这种工具。至于开发者不爽的成本,是将来可以通过优化工具来避免的。

回顾之前百度 Tangram 推广不利的问题,答案就显而易见了。为什么 Tangram 缺乏百度自顶而下的支持?为什么作为同类产品的 YUI 却能得到 Jerry Yang 的支持并在整个 Yahoo 得到推广?因为 Tangram 覆盖了开发者的需求,但没有很好地考虑到百度作为企业的需求,尤其是没有考虑到这种需求在百度成长中的变化。

Tangram 在设计初期,考虑的是如网页搜索这样的产品线的需求,要求 JavaScript 尽可能优化,少占用带宽。于是 Tangram 设计为支持函数级别的依赖项。然而等 Tangram 做出来时,贴吧则决定直接使用 jQuery。这是因为网页搜索和贴吧的成长进入了两个不同的阶段,网页搜索要求在细节上继续优化,而贴吧要求通过快速迭代尝试不同的产品概念。需求差异如此之大,一个工具又怎么可能都覆盖到?既然 Tangram 没办法在多个产品线证明它能有效降低成本,百度又如何下决心为它埋单?

这同时解释了一个问题:为什么企业内部工具应该由架构师来主导。(YUI 及后来的 Cocktails 都是由架构师主导的。)因为架构师的职位能让他更好地解企业的需求,同时他的位置也更靠近老板。此外,架构师在一家企业内的资历也会对此有帮助。跟随这家企业成长的经历能让他更好地看到这家企业将来的需求。因此,企业内部工具不是随随便便挑一个技术足够好的人来主导就行的,他的职位会影响到他是否胜任此项工作。

对此我可以补充一点信息。我见过 Tangram 推广所用的幻灯片,里面堆满了对开发者有用的信息——Tangram 在 gzip 前后跟其他库的体积比较、能够覆盖到多少功能点、在百度产品线的代码覆盖率能够达到多少。(代码覆盖率还曾经是 Tangram 的执行目标。)然而里面缺乏对老板有用的信息——使用 Tangram 能够省多少钱。由于位置的缘故,做 Tangram 的团队确实缺乏获取相关信息的途径,也更没办法获取到老板对不同产品线的长远规划。

再来看看 Yahoo 架构师 Bruno 是怎么介绍 Cocktails 项目的。幻灯片里面没有一点开发者关心的信息,前半部分说的都是 Yahoo 接下来的战略——必须同时抢占桌面、平板、移动 3 个平台,后半部分讲的是如果为这 3 个平台独立开发应有成本有多高,而使用 Cocktails 的话能够缩减多少成本。我听 Bruno 讲完后,就立即明白到这其实是用来向老板要钱的幻灯片,只是顺带再讲一遍以便让开发者知道公司战略而已。

老板实际上不明白也不在乎开发者对工具的需求是什么,幻灯片上他能看懂的就是那一堆 $ 后面跟着的数字。如果你给不出这堆数字,老板对工具的态度就会是既不支持也不反对。因此,找一个足够了解公司现有数字同时知道如何利用技术去改变这些数字的人,对内部工具项目来说尤其重要。这个人往往就是个架构师。