2012年2月8日 星期三

如何帮助别人改变坏习惯

最近为了写一个 slides 而重新看了一遍 The Mystery Method,然后又重新学习了一遍我已经学习过并且忘记掉的知识。其中让我觉得比较有意义的一条是关于如何训练别人的行为模式的,尽管背后的道理源自对动物行为模式的训练。

如果你的狗总喜欢跳到你身上来,你该怎么办?你可以打它骂它,但它无法理解你的情绪,所以它只会把这理解为关注,也就是你在乎它,因此它会继续跳到你身上来,以便获取更多的关注。你可以把硬币放在一个罐子里,然后摇晃那个罐子让狗因为恐惧而走开,但久而久之它会理解到你和罐子存在关系,因此把你和厌恶联系在一起,这显然也不是你想要的结果。正确的做法是,先通过摇晃罐子让它从你身上下来,然后让它做一个简单容易做到的动作,例如跟它说「sit」,等它坐下来后在拍拍它给它一些奖励。这样子它就会把厌恶和错误的行为联系起来,但不会把厌恶跟你联系起来,因为你还是会奖励它的。

尽管 The Mystery Method 把这个方法用于训练别人的服从性,不过如果你想帮助身边的朋友改掉坏习惯这也是适用的。可能你觉得,既然大家都是朋友那么平等理性地沟通就可以了,只要对方知道这是个缺点他就可以改掉。然而事实不是这样的,可能沟通并不一定能够很好地传达你的意思,让对方有足够的决心去改变;可能对方一早就知道这个缺点的存在,但是他的意识无法扭转无意识的行为习惯,久而久之他就会合理化自己的行为,为自己无意识的行为辩论。

关于意识和无意识,我喜欢用 The Happiness Hypothesis 里面的那个比喻——无意识是大象,有力量,能够做重复性劳动,但是智慧不是很高;意识是骑象人,力量比不过大象,而且还懒惰,但智商足够高。这个比喻的一个要点就是,人永远不能跟象较劲。象看到了香蕉就要冲过去,而人看到了香蕉就在悬崖边上,这时候人想拉住象是不可能的。人应该使用自己的聪明才智防止这种情况的发生,例如训练象看到香蕉不要那么冲动。

显然你不希望在帮助对方改掉坏习惯的过程中让对方讨厌你,尤其是无意识讨厌你同时意识到应该感激你。前面已经说了,人是不能跟象较劲的,所以如果你有幸让一个人无意识讨厌你,那么无论他多么明白事理,他也无法让自己喜欢上你。为此,当你需要帮助别人改变习惯的时候,不一定需要跟对方的象骑者沟通。如果对方足够聪明,也足够信任你,他会允许你直接训练他的象,并且他也能理解你在做什么。

2012年1月31日 星期二

Google Search Plus 把用户选择放到了内容质量的前面

Google 发布了 Search, plus Your World,简称 Google Search+,带来最大的改变包括 3 点:

  1. 跟个人相关的搜索结果:你跟 Google+ 上好友发表过的链接和照片都会出现在搜索结果内,包括通过 Circle 限制为非公开的。
  2. Google+ Profile:如果搜索人名,自动完成和搜索结果都会出现对应的 Google+ Profile。
  3. 相关 Google+ Profile:如果 Google+ 上有 Profile 跟你搜索的关键字相关,会出现在搜索结果页右侧。

其中最直接影响搜索结果排名的是第 1 点,后面两点都只是往搜索结果里插入少数 Google+ Profile 条目而已。这第 1 点可以说是把用户选择放到了内容质量前面来,使得过去完全针对内容的搜索引擎优化手段变得需要同时考虑针对用户做优化,也就是引入营销手段。

在过去,搜索引擎优化考虑的是内容质量。理论上来说,如果内容质量足够高,别人就会频繁引用你的页面,这样你的页面自然应当排到前面去。但是有了 Google Search+ 之后游戏规则彻底改变了,就算你的页面内容十分小众,只要它被某人发布到 Google+ 上去了,它就会出现在此人好友的搜索结果上。这时候,被多少人 +1 就显得比内容如何更重要。

这个变化可以说是由作者投票变成了读者投票。过去 PageRank 是以对待论文的方式来对待页面,被引用的次数越多,引用来源越重要,这篇论文也就越重要。然而,能够影响论文引用的只有论文作者,有多少读者读过一篇论文后受益是不知道的,除非他为此再写一篇论文。现在 Google Search+ 的做法可以说是把页面对读者的影响力也计算在内了。

跟作者投票不一样的是,读者投票的影响是跟个人相关的。只有离你近的读者投票才会影响到你的搜索结果,离你远的读者投票对你影响不明显(除非你指定搜索他的信息)。因此,如果要让你的内容通过搜索引擎展示给特定的受众看,你不一定再需要把内容优化为其他作者会认同的样子,只要你有营销手段让受众中的一部分先认同你的内容,搜索引擎就会让这种认同扩散出去。

尽管已经有很多社会化营销手段涉及到 Facebook 和 Twitter,Google Search+ 暂时还不支持这两者,所有社会化信息仅来自 Google+。无论这是 Google 有意的恶性竞争行为,还是技术上和商务上暂时没把对 Facebook 和 Twitter 的支持做好,现在你要影响 Google 搜索结果中的社会化成分就必须通过 Google+。估计很快网站就会重视页面上嵌入的 +1 按钮,并且使用各种方式去鼓励用户点击那个按钮。

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 基础,能够用简洁的代码实现符合语义的页面。至于豌豆荚提供什么?就是我前面所说的,但还有个前提——至少我们要有一致的品味。