邵珠庆の日记 生命只有一次,你可以用它来做很多伟大的事情–Make the world a little better and easier


184月/13

JavaScript跨域总结与解决办法

发布在 邵珠庆

本文来自网络(http://f2e.me/200904/cross-scripting/,该网址已不能访问),仅作个人读书笔记之用,并稍作修改和补充。

什么是跨域

JavaScript出于安全方面的考虑,不允许跨域调用其他页面的对象。但在安全限制的同时也给注入iframe或是ajax应用上带来了不少麻烦。这里把涉及到跨域的一些问题简单地整理一下:

首先什么是跨域,简单地理解就是因为JavaScript同源策略的限制,a.com 域名下的js无法操作b.com或是c.a.com域名下的对象。更详细的说明可以看下表:

URL 说明 是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下 允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口 不允许
http://www.a.com/a.js
https://www.a.com/b.js
同一域名,不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js
域名和域名对应ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同 不允许
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名 不允许
特别注意两点:
第一,如果是协议和端口造成的跨域问题“前台”是无能为力的,
第二:在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。
“URL的首部”指window.location.protocol +window.location.host,也可以理解为“Domains, protocols and ports must match”。

接下来简单地总结一下在“前台”一般处理跨域的办法,后台proxy这种方案牵涉到后台配置,这里就不阐述了,有兴趣的可以看看yahoo的这篇文章:《JavaScript: Use a Web Proxy for Cross-Domain XMLHttpRequest Calls

1、document.domain+iframe的设置

对于主域相同而子域不同的例子,可以通过设置document.domain的办法来解决。具体的做法是可以在http://www.a.com/a.html和http://script.a.com/b.html两个文件中分别加上document.domain = ‘a.com’;然后通过a.html文件中创建一个iframe,去控制iframe的contentDocument,这样两个js文件之间就可以“交互”了。当然这种办法只能解决主域相同而二级域名不同的情况,如果你异想天开的把script.a.com的domian设为alibaba.com那显然是会报错地!代码如下:

www.a.com上的a.html

document.domain = 'a.com';
var ifr = document.createElement('iframe');
ifr.src = 'http://script.a.com/b.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
    var doc = ifr.contentDocument || ifr.contentWindow.document;
    // 在这里操纵b.html
    alert(doc.getElementsByTagName("h1")[0].childNodes[0].nodeValue);
};

script.a.com上的b.html

document.domain = 'a.com';

这种方式适用于{www.kuqin.com, kuqin.com, script.kuqin.com, css.kuqin.com}中的任何页面相互通信。

备注:某一页面的domain默认等于window.location.hostname。主域名是不带www的域名,例如a.com,主域名前面带前缀的通常都为二级域名或多级域名,例如www.a.com其实是二级域名。 domain只能设置为主域名,不可以在b.a.com中将domain设置为c.a.com。

问题:
1、安全性,当一个站点(b.a.com)被攻击后,另一个站点(c.a.com)会引起安全漏洞。
2、如果一个页面中引入多个iframe,要想能够操作所有iframe,必须都得设置相同domain。

2、动态创建script

虽然浏览器默认禁止了跨域访问,但并不禁止在页面中引用其他域的JS文件,并可以自由执行引入的JS文件中的function(包括操作cookie、Dom等等)。根据这一点,可以方便地通过创建script节点的方法来实现完全跨域的通信。具体的做法可以参考YUI的Get Utility

这里判断script节点加载完毕还是蛮有意思的:ie只能通过script的readystatechange属性,其它浏览器是script的load事件。以下是部分判断script加载完毕的方法。

js.onload = js.onreadystatechange = function() {
    if (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') {
        // callback在此处执行
        js.onload = js.onreadystatechange = null;
    }
};

3、利用iframe和location.hash

这个办法比较绕,但是可以解决完全跨域情况下的脚步置换问题。原理是利用location.hash来进行传值。在url: http://a.com#helloword中的‘#helloworld’就是location.hash,改变hash并不会导致页面刷新,所以可以利用hash值来进行数据传递,当然数据容量是有限的。假设域名a.com下的文件cs1.html要和cnblogs.com域名下的cs2.html传递信息,cs1.html首先创建自动创建一个隐藏的iframe,iframe的src指向cnblogs.com域名下的cs2.html页面,这时的hash值可以做参数传递用。cs2.html响应请求后再将通过修改cs1.html的hash值来传递数据(由于两个页面不在同一个域下IE、Chrome不允许修改parent.location.hash的值,所以要借助于a.com域名下的一个代理iframe;Firefox可以修改)。同时在cs1.html上加一个定时器,隔一段时间来判断location.hash的值有没有变化,一点有变化则获取获取hash值。代码如下:

先是a.com下的文件cs1.html文件:

function startRequest(){
    var ifr = document.createElement('iframe');
    ifr.style.display = 'none';
    ifr.src = 'http://www.cnblogs.com/lab/cscript/cs2.html#paramdo';
    document.body.appendChild(ifr);
}

function checkHash() {
    try {
        var data = location.hash ? location.hash.substring(1) : '';
        if (console.log) {
            console.log('Now the data is '+data);
        }
    } catch(e) {};
}
setInterval(checkHash, 2000);

cnblogs.com域名下的cs2.html:

//模拟一个简单的参数处理操作
switch(location.hash){
    case '#paramdo':
        callBack();
        break;
    case '#paramset':
        //do something……
        break;
}

function callBack(){
    try {
        parent.location.hash = 'somedata';
    } catch (e) {
        // ie、chrome的安全机制无法修改parent.location.hash,
        // 所以要利用一个中间的cnblogs域下的代理iframe
        var ifrproxy = document.createElement('iframe');
        ifrproxy.style.display = 'none';
        ifrproxy.src = 'http://a.com/test/cscript/cs3.html#somedata';    // 注意该文件在"a.com"域下
        document.body.appendChild(ifrproxy);
    }
}

a.com下的域名cs3.html

//因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);

当然这样做也存在很多缺点,诸如数据直接暴露在了url中,数据容量和类型都有限等……

4、window.name实现的跨域数据传输

文章较长列在此处不便于阅读,详细请看 window.name实现的跨域数据传输

5、使用HTML5 postMessage

HTML5中最酷的新功能之一就是 跨文档消息传输Cross Document Messaging。下一代浏览器都将支持这个功能:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 。 Facebook已经使用了这个功能,用postMessage支持基于web的实时消息传递。

otherWindow.postMessage(message, targetOrigin);
otherWindow: 对接收信息页面的window的引用。可以是页面中iframe的contentWindow属性;window.open的返回值;通过name或下标从window.frames取到的值。
message: 所要发送的数据,string类型。
targetOrigin: 用于限制otherWindow,“*”表示不作限制

a.com/index.html中的代码:

b.com/index.html中的代码:

参考文章:《精通HTML5编程》第五章——跨文档消息机制https://developer.mozilla.org/en/dom/window.postmessage

6、利用flash

这是从YUI3的IO组件中看到的办法,具体可见http://wiht.link/YUI-intro
可以看在Adobe Developer Connection看到更多的跨域代理文件规范:ross-Domain Policy File SpecificationsHTTP Headers Blacklist

174月/13

git 分支管理 branch

发布在 邵珠庆

Git的分支管理是Git的神器。拥有了它就会使我么管理代码更加游刃有余。那么什么是Git的分支管理?为什么要使用Git的分支管理?Git分支管理怎么用?
     在集中式版本控制中,冲突的合并是可怕的,是令人恶心的。所以很多版本控制软件通过加锁来拒绝多个人同时访问一个文件;而有的版本管理软件,则不是通过加锁的方式,第一个提交的人会很顺畅,但是如果第二个人提交,那么面临它的将是恶心的冲突解决。
    而在分布式管理软件中,冲突解决、合并、衍合,则是一种容易的事情,它是版本管理中的常态。
     而合并、衍合的主体就是分支。
    分支其实就是指向某种代码状态的一个指针。而合并其实就是将两种代码状态合并到另一种代码状态中。
     在Git中,正确的使用方法中,无处不在使用分支。比如,提交实际上就是本地分支合并到远程分支,更新实际上就是将远程分支合并到本地分支,在开发过程中,每加入一个功能或特性,都加入一个分支,当实验成功后合并到主分支...
    为什么要使用分支管理?
     我们来设想下面几种情况:1、我们在基于一个稳定的版本在进行开发,突然在稳定版本上有一个紧急的bug需要我们解决。2、我们在软件中加入了一个小的特性,但是开发到一半的时候,发现开发组的另一个的想法更有创意,所以我们想废弃自己的更改。3、自己想在软件中同时加入多个特性,但是希望并行开发开发,而不是依次开发。
     如果采用单分支形式的话,以上可能也可以实现,但是实现的复杂度可能就会加大。而应用多分支管理时情况就变的简单了。
     如果我们开发新功能时是基于一个新的分支的话,如果稳定版本有一个紧急bug需要处理,那么我们就可以切换到稳定版本的分支,然后修改bug,修改之后,我们再次切换到原先的分支继续工作,最后我们将该分支合并到稳定分支即可。如果我们想废弃正在开发的某个特性,如果该特性在一个单独的分支上,只需要简单的删除该分支即可。如果我们想并行开发多个特性,我们可以创建多个分支,分别开发,然后将每个分支都合并到稳定分支上即可。
     多分支管理,我们可以维护一个稳定的分支,然后某些特性或实验性的开发可以单独作为一个分支,这样开发过程就不会影响到稳定的版本。而且Git中分支的创建和切换基本上没有多少消耗。
    Git如何进行分支管理?
     1、创建分支
     创建分支很简单:git branch <分支名>
     2、切换分支
     git checkout <分支名>
     该语句和上一个语句可以和起来用一个语句表示:git checkout -b <分支名>
     3、分支合并
     比如,如果要将开发中的分支(develop),合并到稳定分支(master),
     首先切换的master分支:git checkout master。
     然后执行合并操作:git merge develop。
     如果有冲突,会提示你,调用git status查看冲突文件。
     解决冲突,然后调用git add或git rm将解决后的文件暂存。
     所有冲突解决后,git commit 提交更改。
     4、分支衍合
     分支衍合和分支合并的差别在于,分支衍合不会保留合并的日志,不留痕迹,而 分支合并则会保留合并的日志。
     要将开发中的分支(develop),衍合到稳定分支(master)。
     首先切换的master分支:git checkout master。
     然后执行衍和操作:git rebase develop。
     如果有冲突,会提示你,调用git status查看冲突文件。
     解决冲突,然后调用git add或git rm将解决后的文件暂存。
     所有冲突解决后,git rebase --continue 提交更改。
     5、删除分支
     执行git branch -d <分支名>
     如果该分支没有合并到主分支会报错,可以用以下命令强制删除git branch -D <分支名>

174月/13

数据分析这点事

发布在 邵珠庆

 

 先声明一下,按照传统的定义,我还真不是数据分析高手,各种关联算法,只会最简单的一种(话说不少场合还算管用);各种挖掘技术,基本上一窍不通;各种牛逼的数据分析工具,除了最简单的几个免费统计平台之外,基本上一个都不会用。所以,各种高手高高手请随意BS,或自行忽略。这里说点高手不说的。

       从微博段子说起,微博上关于数据分析有两个段子,我经常当作案例讲,第一个段子,说某投资商对某企业所属行业有兴趣,要做背景调查,甲是技术流,一周分析各种网上数据,四处寻找行业材料,天天熬夜,终于写出一份报告;乙是人脉流,和对方高管喝了次酒,请对方核心人员吃了顿饭,所有内幕数据全搞定,问谁的方法是对的;第二个段子,某电商发现竞争对手淘宝店,周收入突然下降了30%,但是隔周后又自然恢复,中间毫无其他异常现象,于是老板让分析师分析,苦逼的分析师辛苦数日,做各种数学模型,总算找到勉强的理由自圆其说,老板读毕,虽说不能让人信服,却也没有更合理的解释,某日,见对手老板,闲聊此事,“你们某段时间怎么突然收入下降?”“嗨,别提了,丈母娘去世了,回家奔丧,公司放羊了。”老板恍然大悟。

       两个段子,第一个段子,微博上一边倒的说,苦逼分析没有人脉有用;第二个段子类似,一边倒的认为,人脉的消息比苦逼分析管用多了。但是我想说的是,这个解读绝对是错的!

       先说第一个段子,其实网络不乏这种“人脉达人”,特别是媒体圈,一些所谓的“IT名记”或者“著名评论家、分析师”和各种互联网大佬称兄道弟,天天秘闻不断,但是呢?他们从不研究产品,不分析用户,所以,他们知道了数据,却不懂数据背后是什么,更不知道什么是重要的,什么是次要的,我有时会批评身边这样的朋友,别天天觉得自己知道几个互联网大佬的花边新闻,就当自己是资深业内人士了,正因为掌握这些东西又觉得炫耀,才反而忽视了真正有价值的信息和有价值的数据。这就是为什么混网络媒体的,见过市面的各种达人,在互联网创业浪潮里,几乎没有成功几率的真实原因,自以为人脉广泛,无所不知,其实正因为缺乏最基本的数据背景分析,所以才是看上去什么都懂,细究下其实什么都不懂。请记住一点,除非你是富二代,官二代,衔着金钥匙出生,那不在我的讨论范围里,否则,没有苦逼的经历,就没有牛逼的成就。

      我常订阅一些著名分析师的微博,他们透露的数据往往是很有价值的(这是我订阅的原因),但是他们的解读通常是惨不忍睹的,这就是只看表象的恶果,而且随便翻看一下他们的数据解读,可以说他们的数据感和数据认知贫乏到可笑,甚至缺乏最基本的数据校核和考证的能力,他们拿到了某公司核心数据又怎样?没经历过苦逼的分析,他们其实什么都看不到。

      第二个段子同理,如果不是持续有效的数据跟踪,怎么能得出下降30%的结论,这一数据结论与人脉得到的消息相互验证,才会得到完整真实的结果,否则仅仅是闲聊,你怎能知道对方企业管理对业绩影响的范畴,苦逼的分析也许一时没有人脉的消息管用,但是你所得到的对数据的认知和积累,是人脉永远不会给你的。

      所以,再次强调,基本的数据跟踪和日常的数据感养成,绝不是可以忽略和无视的。人脉情报可以成为数据解读重要的信息来源,但是绝不能喧宾夺主,替代基本的数据分析工作。

 

     下面说一下数据感,什么是数据感?就是别人说一个数据出来,你会琢磨一下这个是否符合常理,与你日常的数据观测经验是否一致,如果不一致,那么可能的理由是哪些? 比如12306号称一天几十亿次点击,如果你有数据感,第一眼就会质疑这个“点击”定义的合理性;比如曾经有人说某国内图片分享网站一天多少亿访问量,第一眼就知道这个“访问量”定义是有歧义的,(事后官方解释是图片加载量,这个和访问量差异几十倍。) 数据感需要不断的培养,和基本的逻辑(比如你应该知道中国有多少网民,每天有多少人上网,一个大概什么类型,什么排名的网站会覆盖网民的比例是多少),以及善于利用各种工具,我以前在巨头公司,得益于公司巨大的数据资源,可以看到很多互联网的核心数据;但是离开后,才发现,其实互联网上公开可获取的数据途径是非常多的,而且善于利用的话非常有效。每天去查询一些感兴趣的数据,经过一段时间积累,想没有数据感都难。

      作为公司或团队负责人,怎么培养员工的数据感,我其实也有一个建议,平时可以搞一些小的竞猜,比如团队集体竞猜新产品或产品改版上线后的日活跃用户,或者pv数字,或者收入数据,等等;然后看谁的最准,一种是惩罚制,最不准的请最准的喝奶茶,吃冰淇淋;另一种不惩罚,最准的累计积分后公司可以发一些奖品鼓励,这样下去大家的数据感就会在日常培养起来,而且对团队的气氛培养也有帮助。

       数据感之后,谈数据分析的方法,我的建议是,不炫技,不苛求技术复杂度,最简单的数据,所包含的信息往往是最有价值的,而很多人恰恰这一步都没做好,就总想着弄一堆挖掘算法;数据的价值在于正确的解读,而不是处理算法的复杂度,切不可喧宾夺主。 大公司的kpi制度,往往会产生偏差,比如技术工程师的评定,要讲究“技术复杂度”、“技术领先性”,直接导致简单的事情没人肯做,最基本的工作不认真做!所以往往是大公司的分析工程师,为了评高级工程师,非要简单问题复杂化,四则运算就搞定的事情一定要弄一套诡异的算法,最终非但浪费了资源,消耗了时间,而且往往由于工程师对业务理解的漠视,对应的产品人员又对算法的陌生,导致了严重的理解歧义,从而出现各种误读。

 

       下面说关键,数据解读,正确的数据解读,是所有数据分析工作最关键的一步,这一步错了,前面的所有努力都是白搭,然后,往往很多人简单的以为“数据会说话”,他们认为把数据处理完一摆就ok了,所以我看到很多知名分析师拿着正确的数据信口胡诌;而更有甚者,显然是故意的行为,一个非常非常著名的、口碑极佳的跨国企业,曾经就同一份很酷的数据,在不同的场合下,为了市场公关的需求,做出不同的解读;这简直就是道德问题了。

      数据解读,不能是为了迎合谁,要遵循数据的本质,要遵循科学的逻辑,要有想象力(配合求证),可能有时候也需要依赖人脉关系所获得的情报,(这个也有很多典型范例),这个具体再怎么说可能我也说不清楚,说几个反面例子也许更容易理解。

      1、因果关联错误,或忽略关键因素,A和B的数据高度相关,有人就片面认为A影响了B,或者B影响了A;但是,有时候真实原因是C同时影响了A和B,有时候C被忽略掉了。

      2、忽略沉默的大多数,特别是网上投票,调查,极易产生这种偏差,参与者往往有一定的共同诉求,而未参与者往往才是主流用户。

      3、数据定义错误,或理解歧义,在技术与市场、产品人员沟通中产生信息歧义,直接导致所处理的数据和所需求的数据有偏差,结果显著不正确。

      4、强行匹配;不同公司,不同领域的数据定义可能不一致,在同一个公司内或领域内做对比,往往没有问题,大家对此都很习惯,却有评论家不懂装懂,强行将不同定义的数据放在一起对比做结论,显著失真

94月/13

HTML5 开发者需要了解的技巧和工具汇总

发布在 邵珠庆

HTML5现在已经成为了Web开发中的热门话题,大多数现代浏览器(Safari、Chrome,Firefox,IE10和移动设备)都支持HTML5。即使HTML5的规范还没有制定完成,但许多开发者已经将其作为Web开发项目中的主要技术。一些网站巨头,如Google、Facebook、Twitter和YouTube等,都建立在HTML5基础上。 



HTML5中最令人兴奋的功能莫过于画布(canvas)和强大的表单功能,画布功能已经可以在大部分浏览器中完美体验(除了IE),但对于新表单元素的支持还不是太好。对Web开发者来说,是时候开始HTML5开发了。 



要进行HTML5开发,本文中的一些技巧、工具可以让你缩短学习的时间,提高开发的效率。 



一、HTML5支持测试列表 



在开始之前,你需要了解现代的浏览器以及移动平台对于HTML5的支持情况。 

 

二、让HTML5元素可用 



老版本的IE浏览器不能识别新的HTML元素。但是,可以使用一些JavaScript或CSS解决方案来弥补这个缺陷。 

 

  • HTML5Shiv:此脚本可以使IE浏览器识别HTML5元素。
  • HTML5 Enabler:功能与HTML5Shiv类似。
  • Modernizr:它使得开发者可以在支持HTML5和CSS3的浏览器中充分利用HTML5和CSS3的特性进行开发,同时又不会牺牲其他不支持这些新技术的浏览器的控制。
  • HTML5 Reset:它提供了一组HTML、CSS文件,让你能够以最少的时间来启动一个新的项目。它使用modernizr来支持HTML5 和 CSS3。

三、浏览器插件 



下面是一些JavaScript插件,可以弥补一些浏览器对HTML5的支持问题。 



1. VideoJS 



VideoJS是一个HTML5的视频播放器,可以在所有浏览器中使用,包括IE6和移动设备。对于不支持HTML5的浏览器则自动使用Flash播放器来播放。 

 





2. AudioJS 



HTML音频播放器。用来让HTML5 的 <audio> 标签可以在各种浏览器上使用,包括移动设备。 

 





3. HTML5Widget 



HTML5的表单模块,包括日历,调色板,滑动部件,客户端验证等。 

 





4. Webforms2 



HTML5 表单属性的支持,例如pattern、required和autofocus。 

 





5. LimeJS 



LimeJS是HTML5的游戏框架,用于为现代触摸设备和桌面浏览器创建快速、本地化的游戏。 

 





6. FlexieJS 



支持CSS3弹性盒子模型(Flexible Box Model)。 

 





四、在线工具 



此外,还有一些在线工具,可以帮助开发者加快HTML5项目的开发。 



1. HTML5 Boilerplate 



HTML5Boilerplate 是一个HTML / CSS /JS模板,是实现跨浏览器正常化、性能优化、稳定的可选功能如跨域Ajax和Flash的最佳实践。开发者称之为技巧集合,目的是满足你开发一个跨浏览器,并且面向未来的网站的需求。 

 





2. Switch to HTML5 



非常有用的在线工具,可以根据你的喜好生成HTML5文档结构。 

 





3. Initializr 



Initializr是一个HTML5模板生成器,以帮助你开始HTML5项目的开发 。它建立在HTML5 Boilerplate之上。 

 





4. HTML5 Visual 速查表 

 





5. HTML5 Canvas 速查表 

 





6. HTML5 笔记 

 





五、其他 



你可以通过下面的链接来跟踪HTML5的更新。 



HTML5追踪 



你可以通过下面的链接获得HTML5网站的设计灵感。这个网站库中包含了大量的使用HTML5技术的网站。 



HTML5Gallery 



VIA http://www.queness.com/post/9375/tips-tricks-and-tools-you-will-need-to-start-using-html5-today 

34月/13

营销名词解释

发布在 邵珠庆

1. 4C

Customer(客户)、Cost(成本)、Convenience(便利性)、Communication(沟通)

2、4Ps营销理论

产品(Product)、价格(Price)、渠道(Place)、促销(Promotion);策略(Strategy);

3. 6W1H

6W:who-when-where-why-for whom-what

1H:how

283月/13

55张图看懂这个世界

发布在 邵珠庆

1男人眼中只有女人的下半身
 
 
 
2.面具人生
 
 
3.男人被束缚了自由,女人对他即使再好,他也是厌烦。
 
图片
 
4.你们当初剪下我的翅膀,如今却要我飞翔。
 
 
5.安逸使人死亡
 
 
6.不知不觉已经虚度了许多时光
 
 
7.长大的标志不是我们穿上了成年人的衣服、鞋子,是生理上的。
 
 
 

 

 
 
 
8.长大了,我们就不再是纯粹的人…

 
 
9.当你在算计别人的时候,别人也在算计你。
 
 
10.当生活一团乱麻时,请关掉自己的心情。
 
 
11.地球在慢慢蜕变。
 
 
12.富人的钱,永远是从劳动者那里获取。
13.高处不胜寒!官位、地位越高越容易撞见鬼。
14.孩子不能输在起跑线上。
15.很多人只靠着一张脸活着,却不知早已如同行尸走肉一般苟且。
16.靠男人的女人,总有摔倒的一天。
17.渴望自由的孩子仰望星空,眼睛却看不见真实。
18.理想很丰满,现实很苍白。
19没有永恒的朋友,只有永恒的利益。

.

20.美好的表面之下,隐藏的总是丑陋和肮脏。
 
 
21.你真的真的 能承受所有人异样的眼光吗?
22.你知道在你身边的人是天使还是恶魔吗?
23.年龄不是问题,关键是拥有一颗年轻的心。
24.女人,你永远不懂。
25.其实天使就在身边,可你却听了恶魔的话。
26.人生就像舞台,不到谢幕,永远不会知道自己有多精彩。
27.人一大,“胃口”也大了。
28.学校跟监狱实质上区别不大。
29.要是我看不到真实,那么我宁愿把我的眼睛给缝起来。
图片
30.人越大,“心”越小……
31.社会是不公平的,只能靠自己的双手去赚的。
32.双膝之间的爱情,是你要的爱情么?

33.外表美丽,内心早已腐朽。

34.网络时代,深陷其中。
35.为了友情,真的可以舍去一切吗?
36.我把心交给了你,你却用冷漠的眼光杀死了我。
37.我会杀死每个凌驾在我之上的东西,哪怕是温顺的兔子。

38.我想要一个安全的生活,可是生活不会给我。
39.无法看清自己。
39.邪恶的内心,远比邪恶的外表可怕得多。
40.兄弟,你的镰刀out了。
41.已经有很多伤口了,但是还是在不断受伤。
42.要想受到膜拜,那么就同样的不怕受到唾弃。
43.男人藏在背后的往往是鲜花,而女人多数是利刃。
44.有谁看见过小丑的悲伤呢?
45.拥有一个诚实的敌人,好过一个虚伪的朋友。
46.有时候,你不知道自己的话有多伤人。
47.有危险在靠近,跟你最亲近的全然不知,而被你抛弃的心知肚明,却无法告诉你。
48.再忙也给家里打个电话。
49.至诚的外表和诚敬的行为演示一颗魔鬼般的心。
50.众人皆醉唯我独醒。
51.周几最高兴,自己懂得。

 

 
52.左脑掌控理性,右脑掌控感性,天才的精神世界都是很奇异。


53.有些人就是拥有的太多才不懂得珍惜。

图片
54.你说无论我变成什么样子,你都不会离开我,于是我摘下了面具,却看到了落荒而逃的你。
图片

273月/13

编写可读代码的艺术

发布在 邵珠庆

代码为什么要易于理解

“Code should be written to minimize the time it would take for someone else to understand it.”

日常工作的事实是:

  • 写代码前的思考和看代码的时间远大于真正写的时间
  • 读代码是很平常的事情,不论是别人的,还是自己的,半年前写的可认为是别人的代码
  • 代码可读性高,很快就可以理解程序的逻辑,进入工作状态
  • 行数少的代码不一定就容易理解
  • 代码的可读性与程序的效率、架构、易于测试一点也不冲突

整本书都围绕“如何让代码的可读性更高”这个目标来写。这也是好代码的重要标准之一。

如何命名

变量名中应包含更多信息

使用含义明确的词,比如用download而不是get,参考以下替换方案:

1
2
3
4
send -> deliver, dispatch, announce, distribute, route
 find -> search, extract, locate, recover
start -> lanuch, create, begin, open
 make -> create,set up, build, generate, compose, add, new

避免通用的词

tmpretval这样词,除了说明是临时变量和返回值之外,没有任何意义。但是给他加一些有意义的词,就会很明确:

1
2
3
tmp_file = tempfile.NamedTemporaryFile()
...
SaveData(tmp_file, ...)

不使用retval而使用变量真正代表的意义:

1
sum_squares += v[i]; // Where's the "square" that we're summing? Bug!

嵌套的for循环中,ij也有同样让人困惑的时候:

1
2
3
4
for (int i = 0; i < clubs.size(); i++)
 for (int j = 0; j < clubs[i].members.size(); j++)
 for (int k = 0; k < users.size(); k++) if (clubs[i].members[k] == users[j])
 cout << "user[" << j << "] is in club[" << i << "]" << endl;

换一种写法就会清晰很多:

1
if (clubs[ci].members[mi] == users[ui]) # OK. First letters match.

所以,当使用一些通用的词,要有充分的理由才可以。

使用具体的名字

CanListenOnPort就比ServerCanStart好,can start比较含糊,而listen on port确切的说明了这个方法将要做什么。

--run_locally就不如--extra_logging来的明确。

增加重要的细节,比如变量的单位_ms,对原始字符串加_raw

如果一个变量很重要,那么在名字上多加一些额外的字就会更加易读,比如将string id; // Example: "af84ef845cd8"换成string hex_id;

1
2
3
4
Start(int delay) --> delay → delay_secs
 CreateCache(int size) --> size → size_mb
ThrottleDownload(float limit) --> limit → max_kbps
 Rotate(float angle) --> angle → degrees_cw

更多例子:

1
2
3
4
password -> plaintext_password
 comment -> unescaped_comment
 html -> html_utf8
 data -> data_urlenc

对于作用域大的变量使用较长的名字

在比较小的作用域内,可以使用较短的变量名,在较大的作用域内使用的变量,最好用长一点的名字,编辑器的自动补全都可以很好的减少键盘输入。对于一些缩写前缀,尽量选择众所周知的(如str),一个判断标准是,当新成员加入时,是否可以无需他人帮助而明白前缀代表什么。

合理使用_-等符号,比如对私有变量加_前缀。

1
2
3
4
5
6
7
8
var x = new DatePicker(); // DatePicker() 是类的"构造"函数,大写开始
var y = pageHeight(); // pageHeight() 是一个普通函数
 
var $all_images = $("img"); // $all_images 是jQuery对象
var height = 250; // height不是
 
//id和class的写法分开
<div id="middle_column" class="main-content"> ...

命名不能有歧义

命名的时候可以先想一下,我要用的这个词是否有别的含义。举个例子:

1
results = Database.all_objects.filter("year <= 2011")

现在的结果到底是包含2011年之前的呢还是不包含呢?

使用minmax代替limit

1
2
3
4
5
6
7
CART_TOO_BIG_LIMIT = 10
 if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
 Error("Too many items in cart.")
 
MAX_ITEMS_IN_CART = 10
 if shopping_cart.num_items() > MAX_ITEMS_IN_CART:
 Error("Too many items in cart.")

对比上例中CART_TOO_BIG_LIMITMAX_ITEMS_IN_CART,想想哪个更好呢?

使用firstlast来表示闭区间

1
2
3
4
print integer_range(start=2, stop=4)
# Does this print [2,3] or [2,3,4] (or something else)?
 
set.PrintKeys(first="Bart", last="Maggie")

firstlast含义明确,适宜表示闭区间。

使用beiginend表示前闭后开(2,9))区间

1
2
3
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")
 
PrintEventsInRange("OCT 16 12:00am", "OCT 16 11:59:59.9999pm")

上面一种写法就比下面的舒服多了。

Boolean型变量命名

1
bool read_password = true;

这是一个很危险的命名,到底是需要读取密码呢,还是密码已经被读取呢,不知道,所以这个变量可以使用user_is_authenticated代替。通常,给Boolean型变量添加ishascanshould可以让含义更清晰,比如:

1
2
SpaceLeft() --> hasSpaceLeft()
bool disable_ssl = false --> bool use_ssl = true

符合预期

1
2
3
4
5
6
7
public class StatisticsCollector {
 public void addSample(double x) { ... }
 public double getMean() {
 // Iterate through all samples and return total / num_samples
 }
 ...
}

在这个例子中,getMean方法遍历了所有的样本,返回总额,所以并不是普通意义上轻量的get方法,所以应该取名computeMean比较合适。

漂亮的格式

写出来漂亮的格式,充满美感,读起来自然也会舒服很多,对比下面两个例子:

1
2
3
4
5
6
7
8
9
10
11
12
class StatsKeeper {
 public:
 // A class for keeping track of a series of doubles
 void Add(double d); // and methods for quick statistics about them
 private: int count; /* how many so far
 */ public:
 double Average();
 private: double minimum;
 list<double>
 past_items
 ;double maximum;
};

什么是充满美感的呢:

1
2
3
4
5
6
7
8
9
10
11
12
// A class for keeping track of a series of doubles
// and methods for quick statistics about them.
class StatsKeeper {
 public:
 void Add(double d);
 double Average();
 private:
 list<double> past_items;
 int count; // how many so far
 double minimum;
 double maximum;
};

考虑断行的连续性和简洁

这段代码需要断行,来满足不超过一行80个字符的要求,参数也需要注释说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PerformanceTester {
 public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator(
 500, /* Kbps */
 80, /* millisecs latency */
 200, /* jitter */
 1 /* packet loss % */);
 
 public static final TcpConnectionSimulator t3_fiber = new TcpConnectionSimulator(
 45000, /* Kbps */
 10, /* millisecs latency */
 0, /* jitter */
 0 /* packet loss % */);
 
 public static final TcpConnectionSimulator cell = new TcpConnectionSimulator(
 100, /* Kbps */
 400, /* millisecs latency */
 250, /* jitter */
 5 /* packet loss % */);
}

考虑到代码的连贯性,先优化成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PerformanceTester {
 public static final TcpConnectionSimulator wifi =
 new TcpConnectionSimulator(
 500, /* Kbps */
 80, /* millisecs latency */ 200, /* jitter */
 1 /* packet loss % */);
 
 public static final TcpConnectionSimulator t3_fiber =
 new TcpConnectionSimulator(
 45000, /* Kbps */
 10, /* millisecs latency */
 0, /* jitter */
 0 /* packet loss % */);
 
 public static final TcpConnectionSimulator cell =
 new TcpConnectionSimulator(
 100, /* Kbps */
 400, /* millisecs latency */
 250, /* jitter */
 5 /* packet loss % */);
}

连贯性好一点,但还是太罗嗦,额外占用很多空间:

1
2
3
4
5
6
7
8
9
10
11
12
public class PerformanceTester {
 // TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
 // [Kbps] [ms] [ms] [percent]
 public static final TcpConnectionSimulator wifi =
 new TcpConnectionSimulator(500, 80, 200, 1);
 
 public static final TcpConnectionSimulator t3_fiber =
 new TcpConnectionSimulator(45000, 10, 0, 0);
 
 public static final TcpConnectionSimulator cell =
 new TcpConnectionSimulator(100, 400, 250, 5);
}

用函数封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Turn a partial_name like "Doug Adams" into "Mr. Douglas Adams".
// If not possible, 'error' is filled with an explanation.
string ExpandFullName(DatabaseConnection dc, string partial_name, string* error);
 
DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, "Doug Adams", &error)
 == "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, " Jake Brown ", &error)
 == "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection, "No Such Guy", &error) == "");
assert(error == "no match found");
assert(ExpandFullName(database_connection, "John", &error) == "");
assert(error == "more than one result");

上面这段代码看起来很脏乱,很多重复性的东西,可以用函数封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CheckFullName("Doug Adams", "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown ", "Mr. Jake Brown III", "");
CheckFullName("No Such Guy", "", "no match found");
CheckFullName("John", "", "more than one result");
 
void CheckFullName(string partial_name,
 string expected_full_name,
 string expected_error) {
 // database_connection is now a class member
 string error;
 string full_name = ExpandFullName(database_connection, partial_name, &error);
 assert(error == expected_error);
 assert(full_name == expected_full_name);
}

列对齐

列对齐可以让代码段看起来更舒适:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CheckFullName("Doug Adams" , "Mr. Douglas Adams" , "");
CheckFullName(" Jake Brown ", "Mr. Jake Brown III", "");
CheckFullName("No Such Guy" , "" , "no match found");
CheckFullName("John" , "" , "more than one result");
 
commands[] = {
 ...
 { "timeout" , NULL , cmd_spec_timeout},
 { "timestamping" , &opt.timestamping , cmd_boolean},
 { "tries" , &opt.ntry , cmd_number_inf},
 { "useproxy" , &opt.use_proxy , cmd_boolean},
 { "useragent" , NULL , cmd_spec_useragent},
 ...
};

代码用块区分

1
2
3
4
5
6
7
8
9
10
11
12
13
class FrontendServer {
 public:
 FrontendServer();
 void ViewProfile(HttpRequest* request);
 void OpenDatabase(string location, string user);
 void SaveProfile(HttpRequest* request);
 string ExtractQueryParam(HttpRequest* request, string param);
 void ReplyOK(HttpRequest* request, string html);
 void FindFriends(HttpRequest* request);
 void ReplyNotFound(HttpRequest* request, string error);
 void CloseDatabase(string location);
 ~FrontendServer();
};

上面这一段虽然能看,不过还有优化空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class FrontendServer {
 public:
 FrontendServer();
 ~FrontendServer();
 // Handlers
 void ViewProfile(HttpRequest* request);
 void SaveProfile(HttpRequest* request);
 void FindFriends(HttpRequest* request);
 
 // Request/Reply Utilities
 string ExtractQueryParam(HttpRequest* request, string param);
 void ReplyOK(HttpRequest* request, string html);
 void ReplyNotFound(HttpRequest* request, string error);
 
 // Database Helpers
 void OpenDatabase(string location, string user);
 void CloseDatabase(string location);
};

再来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Import the user's email contacts, and match them to users in our system.
# Then display a list of those users that he/she isn't already friends with.
def suggest_new_friends(user, email_password):
 friends = user.friends()
 friend_emails = set(f.email for f in friends)
 contacts = import_contacts(user.email, email_password)
 contact_emails = set(c.email for c in contacts)
 non_friend_emails = contact_emails - friend_emails
 suggested_friends = User.objects.select(email__in=non_friend_emails)
 display['user'] = user
 display['friends'] = friends
 display['suggested_friends'] = suggested_friends
 return render("suggested_friends.html", display)

全都混在一起,视觉压力相当大,按功能化块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def suggest_new_friends(user, email_password):
 # Get the user's friends' email addresses.
 friends = user.friends()
 friend_emails = set(f.email for f in friends)
 
 # Import all email addresses from this user's email account.
 contacts = import_contacts(user.email, email_password)
 contact_emails = set(c.email for c in contacts)
 
 # Find matching users that they aren't already friends with.
 non_friend_emails = contact_emails - friend_emails
 suggested_friends = User.objects.select(email__in=non_friend_emails)
 
 # Display these lists on the page. display['user'] = user
 display['friends'] = friends
 display['suggested_friends'] = suggested_friends
 
 return render("suggested_friends.html", display)

让代码看起来更舒服,需要在写的过程中多注意,培养一些好的习惯,尤其当团队合作的时候,代码风格比如大括号的位置并没有对错,但是不遵循团队规范那就是错的。

如何写注释

当你写代码的时候,你会思考很多,但是最终呈现给读者的就只剩代码本身了,额外的信息丢失了,所以注释的目的就是让读者了解更多的信息。

应该注释什么

不应该注释什么

这样的注释毫无价值:

1
2
3
4
5
6
7
8
9
10
// The class definition for Account
class Account {
 public:
 // Constructor
 Account();
 // Set the profit member to a new value
 void SetProfit(double profit);
 // Return the profit from this Account
 double GetProfit();
};

不要像下面这样为了注释而注释:

1
2
3
4
// Find a Node with the given 'name' or return NULL.
// If depth <= 0, only 'subtree' is inspected.
// If depth == N, only 'subtree' and N levels below are inspected.
Node* FindNodeInSubtree(Node* subtree, string name, int depth);

不要给烂取名注释

1
2
3
// Enforce limits on the Reply as stated in the Request,
// such as the number of items returned, or total byte size, etc.
void CleanReply(Request request, Reply reply);

注释的大部分都在解释clean是什么意思,那不如换个正确的名字:

1
2
// Make sure 'reply' meets the count/byte/etc. limits from the 'request'
void EnforceLimitsFromRequest(Request request, Reply reply);

记录你的想法

我们讨论了不该注释什么,那么应该注释什么呢?注释应该记录你思考代码怎么写的结果,比如像下面这些:

1
2
3
4
5
6
7
// Surprisingly, a binary tree was 40% faster than a hash table for this data.
// The cost of computing a hash was more than the left/right comparisons.
 
// This heuristic might miss a few words. That's OK; solving this 100% is hard.
 
// This class is getting messy. Maybe we should create a 'ResourceNode' subclass to
// help organize things.

也可以用来记录流程和常量:

1
2
3
4
5
6
7
// TODO: use a faster algorithm
// TODO(dustin): handle other image formats besides JPEG
 
NUM_THREADS = 8 # as long as it's >= 2 * num_processors, that's good enough.
 
// Impose a reasonable limit - no human can read that much anyway.
const int MAX_RSS_SUBSCRIPTIONS = 1000;

可用的词有:

  • TODO : Stuff I haven’t gotten around to yet
  • FIXME : Known-broken code here
  • HACK : Adimittedly inelegant solution to a problem
  • XXX : Danger! Major problem here

站在读者的角度去思考

当别人读你的代码时,让他们产生疑问的部分,就是你应该注释的地方。

1
2
3
4
5
6
7
struct Recorder {
 vector<float> data;
 ...
 void Clear() {
 vector<float>().swap(data); // Huh? Why not just data.clear()?
 }
};

很多C++的程序员啊看到这里,可能会想为什么不用data.clear()来代替vector.swap,所以那个地方应该加上注释:

1
2
// Force vector to relinquish its memory (look up "STL swap trick")
vector<float>().swap(data);

说明可能陷阱

你在写代码的过程中,可能用到一些hack,或者有其他需要读代码的人知道的陷阱,这时候就应该注释:

1
void SendEmail(string to, string subject, string body);

而实际上这个发送邮件的函数是调用别的服务,有超时设置,所以需要注释:

1
2
// Calls an external service to deliver email. (Times out after 1 minute.)
void SendEmail(string to, string subject, string body);

全景的注释

有时候为了更清楚说明,需要给整个文件加注释,让读者有个总体的概念:

1
2
// This file contains helper functions that provide a more convenient interface to our
// file system. It handles file permissions and other nitty-gritty details.

总结性的注释

即使是在函数内部,也可以有类似文件注释那样的说明注释:

1
2
3
4
5
# Find all the items that customers purchased for themselves.
for customer_id in all_customers:
 for sale in all_sales[customer_id].sales:
 if sale.recipient == customer_id:
 ...

或者按照函数的步进,写一些注释:

1
2
3
4
5
6
7
8
def GenerateUserReport():
 # Acquire a lock for this user
 ...
 # Read user's info from the database
 ...
 # Write info to a file
 ...
 # Release the lock for this user

很多人不愿意写注释,确实,要写好注释也不是一件简单的事情,也可以在文件专门的地方,留个写注释的区域,可以写下你任何想说的东西。

注释应简明准确

前一个小节讨论了注释应该写什么,这一节来讨论应该怎么写,因为注释很重要,所以要写的精确,注释也占据屏幕空间,所以要简洁。

精简注释

1
2
3
4
// The int is the CategoryType.
// The first float in the inner pair is the 'score',
// the second is the 'weight'.
typedef hash_map<int, pair<float, float> > ScoreMap;

这样写太罗嗦了,尽量精简压缩成这样:

1
2
// CategoryType -> (score, weight)
typedef hash_map<int, pair<float, float> > ScoreMap;

避免有歧义的代词

1
// Insert the data into the cache, but check if it's too big first.

这里的it's有歧义,不知道所指的是data还是cache,改成如下:

1
// Insert the data into the cache, but check if the data is too big first.

还有更好的解决办法,这里的it就有明确所指:

1
// If the data is small enough, insert it into the cache.

语句要精简准确

1
# Depending on whether we've already crawled this URL before, give it a different priority.

这句话理解起来太费劲,改成如下就好理解很多:

1
# Give higher priority to URLs we've never crawled before.

精确描述函数的目的

1
2
// Return the number of lines in this file.
int CountLines(string filename) { ... }

这样的一个函数,用起来可能会一头雾水,因为他可以有很多歧义:

  • ”” 一个空文件,是0行还是1行?
  • “hello” 只有一行,那么返回值是0还是1?
  • “hellon” 这种情况返回1还是2?
  • “hellon world” 返回1还是2?
  • “hellonr crueln worldr” 返回2、3、4哪一个呢?

所以注释应该这样写:

1
2
// Count how many newline bytes ('n') are in the file.
int CountLines(string filename) { ... }

用实例说明边界情况

1
2
3
// Rearrange 'v' so that elements < <span class="wp_keywordlink"><a href="http://blog.jobbole.com/24057/" title="Pivot — 创业者最重要的本领" rel="nofollow" target="_blank">Pivot</a></span> come before those >= <span class="wp_keywordlink"><a href="http://blog.jobbole.com/24057/" title="Pivot — 创业者最重要的本领" rel="nofollow" target="_blank">Pivot</a></span>;
// Then return the largest 'i' for which v[i] < pivot (or -1 if none are < pivot)
int Partition(vector<int>* v, int pivot);

这个描述很精确,但是如果再加入一个例子,就更好了:

1
2
3
// ...
// Example: Partition([8 5 9 8 2], 8) might result in [5 2 | 8 9 8] and return 1
int Partition(vector<int>* v, int pivot);

说明你的代码的真正目的

1
2
3
4
5
6
7
8
void DisplayProducts(list<Product> products) {
 products.sort(CompareProductByPrice);
 // Iterate through the list in reverse order
 for (list<Product>::reverse_iterator it = products.rbegin(); it != products.rend();
 ++it)
 DisplayPrice(it->price);
 ...
}

这里的注释说明了倒序排列,单还不够准确,应该改成这样:

1
2
// Display each price, from highest to lowest
for (list<Product>::reverse_iterator it = products.rbegin(); ... )

函数调用时的注释

看见这样的一个函数调用,肯定会一头雾水:

1
Connect(10, false);

如果加上这样的注释,读起来就清楚多了:

1
2
3
4
def Connect(timeout, use_encryption): ...
 
# Call the function using named parameters
Connect(timeout = 10, use_encryption = False)

使用信息含量丰富的词

1
2
3
4
// This class contains a number of members that store the same information as in the
// database, but are stored here for speed. When this class is read from later, those
// members are checked first to see if they exist, and if so are returned; otherwise the
// database is read from and that data stored in those fields for next time.

上面这一大段注释,解释的很清楚,如果换一个词来代替,也不会有什么疑惑:

1
// This class acts as a caching layer to the database.

简化循环和逻辑

流程控制要简单

让条件语句、循环以及其他控制流程的代码尽可能自然,让读者在阅读过程中不需要停顿思考或者在回头查找,是这一节的目的。

条件语句中参数的位置

对比下面两种条件的写法:

1
2
3
4
5
if (length >= 10)
while (bytes_received < bytes_expected)
 
if (10 <= length)
while (bytes_expected > bytes_received)

到底是应该按照大于小于的顺序来呢,还是有其他的准则?是的,应该按照参数的意义来

  • 运算符左边:通常是需要被检查的变量,也就是会经常变化的
  • 运算符右边:通常是被比对的样本,一定程度上的常量

这就解释了为什么bytes_received < bytes_expected比反过来更好理解。

if/else的顺序

通常,if/else的顺序你可以自由选择,下面这两种都可以:

1
2
3
4
5
6
7
8
9
10
11
if (a == b) {
 // Case One ...
} else {
 // Case Two ...
}
 
if (a != b) {
 // Case Two ...
} else {
 // Case One ...
}

或许对此你也没有仔细斟酌过,但在有些时候,一种顺序确实好过另一种:

  • 正向的逻辑在前,比如if(debug)就比if(!debug)
  • 简单逻辑的在前,这样ifelse就可以在一个屏幕显示 – 有趣、清晰的逻辑在前

举个例子来看:

1
2
3
4
5
6
7
8
9
if (!url.HasQueryParameter("expand_all")) {
 response.Render(items);
 ...
} else {
 for (int i = 0; i < items.size(); i++) {
 items[i].Expand();
 }
 ...
}

看到if你首先想到的是expand_all,就好像告诉你“不要想大象”,你会忍不住去想它,所以产生了一点点迷惑,最好写成:

1
2
3
4
5
6
7
8
9
if (url.HasQueryParameter("expand_all")) {
 for (int i = 0; i < items.size(); i++) {
 items[i].Expand();
 }
 ...
} else {
 response.Render(items);
 ...
}

三目运算符(?:)

1
2
3
4
5
6
7
8
time_str += (hour >= 12) ? "pm" : "am";
 
Avoiding the ternary operator, you might write:
 if (hour >= 12) {
 time_str += "pm";
 } else {
 time_str += "am";
}

使用三目运算符可以减少代码行数,上例就是一个很好的例证,但是我们的真正目的是减少读代码的时间,所以下面的情况并不适合用三目运算符:

1
2
3
4
5
6
7
return exponent >= 0 ? mantissa * (1 << exponent) : mantissa / (1 << -exponent);
 
if (exponent >= 0) {
 return mantissa * (1 << exponent);
} else {
 return mantissa / (1 << -exponent);
}

所以只在简单表达式的地方用。

避免使用do/while表达式

1
2
3
do {
 continue;
} while (false);

这段代码会执行几遍呢,需要时间思考一下,do/while完全可以用别的方法代替,所以应避免使用。

尽早return

1
2
3
4
5
public boolean Contains(String str, String substr) {
 if (str == null || substr == null) return false;
 if (substr.equals("")) return true;
 ...
}

函数里面尽早的return,可以让逻辑更加清晰。

减少嵌套

1
2
3
4
5
6
7
8
9
10
11
if (user_result == SUCCESS) {
 if (permission_result != SUCCESS) {
 reply.WriteErrors("error reading permissions");
 reply.Done();
 return;
 }
 reply.WriteErrors("");
} else {
 reply.WriteErrors(user_result);
}
reply.Done();

这样一段代码,有一层的嵌套,但是看起来也会稍有迷惑,想想自己的代码,有没有类似的情况呢?可以换个思路去考虑这段代码,并且用尽早return的原则修改,看起来就舒服很多:

1
2
3
4
5
6
7
8
9
10
11
12
if (user_result != SUCCESS) {
 reply.WriteErrors(user_result);
 reply.Done();
 return;
}
if (permission_result != SUCCESS) {
 reply.WriteErrors(permission_result);
 reply.Done();
 return;
}
reply.WriteErrors("");
reply.Done();

同样的,对于有嵌套的循环,可以采用同样的办法:

1
2
3
4
5
6
7
8
9
for (int i = 0; i < results.size(); i++) {
 if (results[i] != NULL) {
 non_null_count++;
 if (results[i]->name != "") {
 cout << "Considering candidate..." << endl;
 ...
 }
 }
}

换一种写法,尽早return,在循环中就用continue:

1
2
3
4
5
6
7
8
for (int i = 0; i < results.size(); i++) {
 if (results[i] == NULL) continue;
 non_null_count++;
 
 if (results[i]->name == "") continue;
 cout << "Considering candidate..." << endl;
 ...
}

拆分复杂表达式

很显然的,越复杂的表达式,读起来越费劲,所以应该把那些复杂而庞大的表达式,拆分成一个个易于理解的小式子。

用变量

将复杂表达式拆分最简单的办法,就是增加一个变量:

1
2
3
4
5
6
if line.split(':')[0].strip() == "root":
 
//用变量替换
username = line.split(':')[0].strip()
if username == "root":
 ...

或者这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (request.user.id == document.owner_id) {
 // user can edit this document...
}
...
if (request.user.id != document.owner_id) {
// document is read-only...
}
 
//用变量替换
final boolean user_owns_document = (request.user.id == document.owner_id);
if (user_owns_document) {
 // user can edit this document...
}
...
if (!user_owns_document) {
 // document is read-only...
}

逻辑替换

  • 1) not (a or b or c) <–> (not a) and (not b) and (not c)
  • 2) not (a and b and c) <–> (not a) or (not b) or (not c)

所以,就可以这样写:

1
2
3
4
if (!(file_exists && !is_protected)) Error("Sorry, could not read file.");
 
//替换
if (!file_exists || is_protected) Error("Sorry, could not read file.");

不要滥用逻辑表达式

1
assert((!(bucket = FindBucket(key))) || !bucket->IsOccupied());

这样的代码完全可以用下面这个替换,虽然有两行,但是更易懂:

1
2
bucket = FindBucket(key);
if (bucket != NULL) assert(!bucket->IsOccupied());

像下面这样的表达式,最好也不要写,因为在有些语言中,x会被赋予第一个为true的变量的值:

1
x = a || b || c

拆解大表达式

1
2
3
4
5
6
7
8
9
10
11
12
var update_highlight = function (message_num) {
 if ($("#vote_value" + message_num).html() === "Up") {
 $("#thumbs_up" + message_num).addClass("highlighted");
 $("#thumbs_down" + message_num).removeClass("highlighted");
 } else if ($("#vote_value" + message_num).html() === "Down") {
 $("#thumbs_up" + message_num).removeClass("highlighted");
 $("#thumbs_down" + message_num).addClass("highlighted");
 } else {
 $("#thumbs_up" + message_num).removeClass("highighted");
 $("#thumbs_down" + message_num).removeClass("highlighted");
 }
};

这里面有很多重复的语句,我们可以用变量还替换简化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var update_highlight = function (message_num) {
 var thumbs_up = $("#thumbs_up" + message_num);
 var thumbs_down = $("#thumbs_down" + message_num);
 var vote_value = $("#vote_value" + message_num).html();
 var hi = "highlighted";
 
 if (vote_value === "Up") {
 thumbs_up.addClass(hi);
 thumbs_down.removeClass(hi);
 } else if (vote_value === "Down") {
 thumbs_up.removeClass(hi);
 thumbs_down.addClass(hi);
 } else {
 thumbs_up.removeClass(hi);
 thumbs_down.removeClass(hi);
 }
}

消除变量

前一节,讲到利用变量来拆解大表达式,这一节来讨论如何消除多余的变量。

没用的临时变量

1
2
now = datetime.datetime.now()
root_message.last_view_time = now

这里的now可以去掉,因为:

  • 并非用来拆分复杂的表达式
  • 也没有增加可读性,因为`datetime.datetime.now()`本就清晰
  • 只用了一次

所以完全可以写作:

1
root_message.last_view_time = datetime.datetime.now()

消除条件控制变量

1
2
3
4
5
6
7
8
boolean done = false;
while (/* condition */ && !done) {
 ...
 if (...) {
 done = true;
 continue;
 }
}

这里的done可以用别的方式更好的完成:

1
2
3
4
5
6
while (/* condition */) {
 ...
 if (...) {
 break;
 }
}

这个例子非常容易修改,如果是比较复杂的嵌套,break可能并不够用,这时候就可以把代码封装到函数中。

减少变量的作用域

我们都听过要避免使用全局变量这样的忠告,是的,当变量的作用域越大,就越难追踪,所以要保持变量小的作用域。

1
2
3
4
5
6
7
8
9
10
11
12
class LargeClass {
 string str_;
 void Method1() {
 str_ = ...;
 Method2();
 }
 void Method2() {
 // Uses str_
 }
 // Lots of other methods that don't use str_
 ... ;
}

这里的str_的作用域有些大,完全可以换一种方式:

1
2
3
4
5
6
7
8
9
10
class LargeClass {
 void Method1() {
 string str = ...;
 Method2(str);
 }
 void Method2(string str) {
 // Uses str
 }
 // Now other methods can't see str.
};

str通过变量函数参数传递,减小了作用域,也更易读。同样的道理也可以用在定义类的时候,将大类拆分成一个个小类。

不要使用嵌套的作用域

1
2
3
4
5
6
7
8
9
# No use of example_value up to this point.
if request:
 for value in request.values:
 if value > 0:
 example_value = value
 break
 
for logger in debug.loggers:
 logger.log("Example:", example_value)

这个例子在运行时候会报example_value is undefined的错,修改起来不算难:

1
2
3
4
5
6
7
8
9
example_value = None
if request:
 for value in request.values:
 if value > 0: example_value = value
 break
 
if example_value:
 for logger in debug.loggers:
 logger.log("Example:", example_value)

但是参考前面的消除中间变量准则,还有更好的办法:

1
2
3
4
5
6
7
8
9
def LogExample(value):
 for logger in debug.loggers:
 logger.log("Example:", value)
 
 if request:
 for value in request.values:
 if value > 0:
 LogExample(value) # deal with 'value' immediately
 break

用到了再声明

在C语言中,要求将所有的变量事先声明,这样当用到变量较多时候,读者处理这些信息就会有难度,所以一开始没用到的变量,就暂缓声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
def ViewFilteredReplies(original_id):
 filtered_replies = []
 root_message = Messages.objects.get(original_id)
 all_replies = Messages.objects.select(root_id=original_id)
 root_message.view_count += 1
 root_message.last_view_time = datetime.datetime.now()
 root_message.save()
 
 for reply in all_replies:
 if reply.spam_votes <= MAX_SPAM_VOTES:
 filtered_replies.append(reply)
 
 return filtered_replies

读者一次处理变量太多,可以暂缓声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
def ViewFilteredReplies(original_id):
 root_message = Messages.objects.get(original_id)
 root_message.view_count += 1
 root_message.last_view_time = datetime.datetime.now()
 root_message.save()
 
 all_replies = Messages.objects.select(root_id=original_id)
 filtered_replies = []
 for reply in all_replies:
 if reply.spam_votes <= MAX_SPAM_VOTES:
 filtered_replies.append(reply)
 
 return filtered_replies

变量最好只写一次

前面讨论了过多的变量会让读者迷惑,同一个变量,不停的被赋值也会让读者头晕,如果变量变化的次数少一些,代码可读性就更强。

一个例子

假设有一个页面,如下,需要给第一个空的input赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<input type="text" id="input1" value="Dustin">
<input type="text" id="input2" value="Trevor">
<input type="text" id="input3" value="">
<input type="text" id="input4" value="Melissa">
...
var setFirstEmptyInput = function (new_value) {
 var found = false;
 var i = 1;
 var elem = document.getElementById('input' + i);
 while (elem !== null) {
 if (elem.value === '') {
 found = true;
 break;
 }
 i++;
 elem = document.getElementById('input' + i);
 }
 if (found) elem.value = new_value;
 return elem;
};

这段代码能工作,有三个变量,我们逐一去看如何优化,found作为中间变量,完全可以消除:

1
2
3
4
5
6
7
8
9
10
11
12
13
var setFirstEmptyInput = function (new_value) {
 var i = 1;
 var elem = document.getElementById('input' + i);
 while (elem !== null) {
 if (elem.value === '') {
 elem.value = new_value;
 return elem;
 }
 i++;
 elem = document.getElementById('input' + i);
 }
 return null;
};

再来看elem变量,只用来做循环,调用了很多次,所以很难跟踪他的值,i也可以用for来修改:

1
2
3
4
5
6
7
8
9
10
11
var setFirstEmptyInput = function (new_value) {
 for (var i = 1; true; i++) {
 var elem = document.getElementById('input' + i);
 if (elem === null)
 return null; // Search Failed. No empty input found.
 if (elem.value === '') {
 elem.value = new_value;
 return elem;
 }
 }
};

工程师就是将大问题分解为一个个小问题,然后逐个解决,这样也易于保证程序的健壮性、可读性。如何分解子问题,下面给出一些准则:

  • 看看这个方法或代码,问问你自己“这段代码的最终目标是什么?”
  • 对于每一行代码,要问“它与目标直接相关,或者是不相关的子问题?”
  • 如果有足够多行的代码是处理与目标不直接相关的问题,那么抽离成子函数

来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
ajax_post({
 url: 'http://example.com/submit',
 data: data,
 on_success: function (response_data) {
 var str = "{n";
 for (var key in response_data) {
 str += " " + key + " = " + response_data[key] + "n";
 }
 alert(str + "}");
 // Continue handling 'response_data' ...
 }
});

这段代码的目标是发送一个ajax请求,所以其中字符串处理的部分就可以抽离出来:

1
2
3
4
5
6
7
var format_pretty = function (obj) {
 var str = "{n";
 for (var key in obj) {
 str += " " + key + " = " + obj[key] + "n";
 }
 return str + "}";
};

意外收获

有很多理由将format_pretty抽离出来,这些独立的函数可以很容易的添加feature,增强可靠性,处理边界情况,等等。所以这里,可以将format_pretty增强,就会得到一个更强大的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var format_pretty = function (obj, indent) {
 // Handle null, undefined, strings, and non-objects.
 if (obj === null) return "null";
 if (obj === undefined) return "undefined";
 if (typeof obj === "string") return '"' + obj + '"';
 if (typeof obj !== "object") return String(obj);
 if (indent === undefined) indent = "";
 
 // Handle (non-null) objects.
 
 var str = "{n";
 for (var key in obj) {
 str += indent + " " + key + " = ";
 str += format_pretty(obj[key], indent + " ") + "n"; }
 return str + indent + "}";
};

这个函数输出:

1
2
3
4
5
6
7
8
9
10
11
{
 key1 = 1
 key2 = true
 key3 = undefined
 key4 = null
 key5 = {
 key5a = {
 key5a1 = "hello world"
 }
 }
}

多做这样的事情,就是积累代码的过程,这样的代码可以复用,也可以形成自己的代码库,或者分享给别人。

业务相关的函数

那些与目标不相关函数,抽离出来可以复用,与业务相关的也可以抽出来,保持代码的易读性,例如:

1
2
3
4
5
6
7
8
9
10
11
business = Business()
business.name = request.POST["name"]
 
url_path_name = business.name.lower()
url_path_name = re.sub(r"['.]", "", url_path_name)
url_path_name = re.sub(r"[^a-z0-9]+", "-", url_path_name)
url_path_name = url_path_name.strip("-")
business.url = "/biz/" + url_path_name
 
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

抽离出来,就好看很多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CHARS_TO_REMOVE = re.compile(r"['.']+")
CHARS_TO_DASH = re.compile(r"[^a-z0-9]+")
 
def make_url_friendly(text):
 text = text.lower()
 text = CHARS_TO_REMOVE.sub('', text)
 text = CHARS_TO_DASH.sub('-', text)
 return text.strip("-")
 
business = Business()
business.name = request.POST["name"]
business.url = "/biz/" + make_url_friendly(business.name)
business.date_created = datetime.datetime.utcnow()
business.save_to_database()

简化现有接口

我们来看一个读写cookie的函数:

1
2
3
4
5
6
7
8
var max_results;
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
 var c = cookies[i];
 c = c.replace(/^[ ]+/, ''); // remove leading spaces
 if (c.indexOf("max_results=") === 0)
 max_results = Number(c.substring(12, c.length));
}

这段代码实在太丑了,理想的接口应该是这样的:

1
2
set_cookie(name, value, days_to_expire);
delete_cookie(name);

对于并不理想的接口,你永远可以用自己的函数做封装,让接口更好用。

按自己需要写接口

1
2
3
4
5
6
7
ser_info = { "username": "...", "password": "..." }
user_str = json.dumps(user_info)
cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE)
encrypted_bytes = cipher.update(user_str)
encrypted_bytes += cipher.final() # flush out the current 128 bit block
url = "http://example.com/?user_info=" + base64.urlsafe_b64encode(encrypted_bytes)
...

虽然终极目的是拼接用户信息的字符,但是代码大部分做的事情是解析python的object,所以:

1
2
3
4
5
def url_safe_encrypt(obj):
 obj_str = json.dumps(obj)
 cipher = Cipher("aes_128_cbc", key=PRIVATE_KEY, init_vector=INIT_VECTOR, op=ENCODE) encrypted_bytes = cipher.update(obj_str)
 encrypted_bytes += cipher.final() # flush out the current 128 bit block
 return base64.urlsafe_b64encode(encrypted_bytes)

这样在其他地方也可以调用:

1
2
user_info = { "username": "...", "password": "..." }
url = "http://example.com/?user_info=" + url_safe_encrypt(user_info)

分离子函数是好习惯,但是也要适度,过度的分离成多个小函数,也会让查找变得困难。

代码应该是一次只完成一个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var place = location_info["LocalityName"]; // e.g. "Santa Monica"
if (!place) {
 place = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles"
}
if (!place) {
 place = location_info["AdministrativeAreaName"]; // e.g. "California"
}
if (!place) {
 place = "Middle-of-Nowhere";
}
if (location_info["CountryName"]) {
 place += ", " + location_info["CountryName"]; // e.g. "USA"
} else {
 place += ", Planet Earth";
}
 
return place;

这是一个用来拼地名的函数,有很多的条件判断,读起来非常吃力,有没有办法拆解任务呢?

1
2
3
4
var town = location_info["LocalityName"]; // e.g. "Santa Monica"
var city = location_info["SubAdministrativeAreaName"]; // e.g. "Los Angeles"
var state = location_info["AdministrativeAreaName"]; // e.g. "CA"
var country = location_info["CountryName"]; // e.g. "USA"

先拆解第一个任务,将各变量分别保存,这样在后面使用中不需要去记忆那些繁长的key值了,第二个任务,解决地址拼接的后半部分:

1
2
3
4
5
6
7
// Start with the default, and keep overwriting with the most specific value. var second_half = "Planet Earth";
if (country) {
 second_half = country;
}
if (state && country === "USA") {
 second_half = state;
}

再来解决前半部分:

1
2
3
4
5
6
7
8
9
10
var first_half = "Middle-of-Nowhere";
if (state && country !== "USA") {
 first_half = state;
}
if (city) {
 first_half = city;
}
if (town) {
 first_half = town;
}

大功告成:

1
return first_half + ", " + second_half;

如果注意到有USA这个变量的判断的话,也可以这样写:

1
2
3
4
5
6
7
8
9
var first_half, second_half;
if (country === "USA") {
 first_half = town || city || "Middle-of-Nowhere";
 second_half = state || "USA";
} else {
 first_half = town || city || state || "Middle-of-Nowhere";
 second_half = country || "Planet Earth";
}
return first_half + ", " + second_half;

要把一个复杂的东西解释给别人,一些细节很容易就让人产生迷惑,所以想象把你的代码用平实的语言解释给别人听,别人是否能懂,有一些准则可以帮助你让代码更清晰:

  • 用最平实的语言描述代码的目的,就像给读者讲述一样
  • 注意描述中关键的字词
  • 让你的代码符合你的描述

下面这段代码用来校验用户的权限:

1
2
3
4
5
6
7
8
9
10
11
$is_admin = is_admin_request();
if ($document) {
 if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
 return not_authorized();
 }
} else {
 if (!$is_admin) {
 return not_authorized();
 }
}
// continue rendering the page ...

这一段代码不长,里面的逻辑嵌套倒是复杂,参考前面章节所述,嵌套太多非常影响阅读理解,将这个逻辑用语言描述就是:

1
2
3
4
有两种情况有权限:
1、你是管理员(admin)
2、你拥有这个文档
否则就没有权限

根据描述来写代码:

1
2
3
4
5
6
7
8
if (is_admin_request()) {
 // authorized
} elseif ($document && ($document['username'] == $_SESSION['username'])) {
 // authorized
} else {
 return not_authorized();
}
// continue rendering the page ...

最易懂的代码就是没有代码!

  • 去掉那些没意义的feature,也不要过度设计
  • 重新考虑需求,解决最简单的问题,也能完成整体的目标
  • 熟悉你常用的库,周期性研究他的API

还有一些与测试相关的章节,留给你自己去研读吧,再次推荐此书:

203月/13

邵珠庆博客 12年通过justgive为中国儿童慈善捐款$250美金

发布在 邵珠庆

183月/13

创业公司的股权分配

发布在 邵珠庆

导读:创业公司应该如何分配股权?一般标准是谁最重要谁就该最多股份。比如:产品型团队,工程师拿最多股份。业务型,销售合伙人拿最多。商业模式型,那会融资的就应该拿最多股份。



这几天网上流传一篇文章,叫初创企业如何分配股份期权,里面说到硅谷一般原则如下:

1、外聘CEO :5%到8%;

2、副总0.8%到1.3%;

3、一线管理人员 0.25%;

4、普通员工 0.1%;

5、外聘董事会董事 0.25%;

6、期权总共占公司15%到20%股份。

期权工作1年后开始兑现,4年完毕。利益分配公平,才能形成有战斗力的创业团队。


很明显,硅谷的分配原则不符合中国的实际情况。别说外聘CEO根本不靠谱,初创企业你试试看找个不管是CTO还是CMO,只给0.8%到1.3%看看;估计没人跟你干。市场的行情是,CTO往往没10个点左右,根本找不到合适的人才。实际上,你给10个点,也未必有牛逼的人愿意跟你干。

而期权,基本上除了企业发展到一定阶段要改造成股份企业,并准备上市,对普通员工来讲,那基本如同废纸。还不如老板多发点奖金,或者老板在全公司员工面前颁发一个最佳员工管用。


那么中国式股权究竟应该如何分配呢?其实原则很简单,创业者要搞清楚,谁是公司除创始人之外最重要的人?谁最重要谁就拿最多股份。如果该公司是产品驱动型团队,工程师产品经理就应该拿最多股份。如果是业务型,销售合伙人拿最多。商业模式型,需要前期烧钱的,那会融资的就应该拿最多股份。

产品型团队最典型的就是网络游戏。做游戏的,几乎不用动任何其他心思,完全看产品。产品好,用户爱玩,渠道会主动来找你联运。开发游戏,人多了无用,资金多了也无用。把钱砸在办公室,场面上都无用。反而不如在一个三室两厅七八个兄弟一起干能成事。弹弹堂的CEO曹凯,在游戏已经过月收入千万后,还每天工作到12点,看游戏中存在的各种问题。

销售型团队往往是要靠客户订单才能维持下去。即使是想转型为产品型团队,往往前期也是要靠外包才能活下去。在这种情况下,谁能带来销售谁就最牛逼。牛逼的销售死人都能说成活人,用户没有需求也能说出需求。在中国典型的官商勾结的回扣文化中,牛逼的销售真的是一个人顶一万人;分最多的利益无可厚非。

商业模式型团队,往往前期要烧钱,甚至烧钱不是几百万,而是几千万,上亿的。动辄货币单位不是人民币而是美金的;那谁能融钱,谁就应该拿大的利益。举个例子,一个朋友帮兄弟的互联网公司在去年资本市场已经很差的情况下从温州老板手里面募了800万人民币,估值咂舌的达到了1个亿人民币,产品甚至都没上线。兄弟给了他给了15个点股份,不用全职。朋友全职工作仍然是投融资,维护好和温州投资人的关系。


很多初创公司的创始人,心怀大志,团队既没有关羽和张飞这样独当一面的人才,又想做平台级产品。出来委托融资顾问时,只愿意给很少的利益;最后融资不成,公司倒闭。这真应了红楼梦那句话:机关算尽太聪明,反误了卿卿性命。

企业发展过程中,所需人才不同,需要随时调配利益。最典型的你看刘备打天下,初期是关羽张飞;后期诸葛亮就获得更大的权益。为了保持平衡,关羽被赋予了一方诸侯的使命。这就类似初创公司发展大了以后,把某块业务划给某个团队的老成员经营,以换得他在总公司管理上的出局。这些议题不在本文讨论之中。


结语:中国是个草莽江湖;创业公司要的就是野蛮性增长。美国那一套细化的股权分配方式在企业大到一个规模前往往不管用。创业公司最重要的是有自知之明,分配利益有的放矢。

 

创业公司如何公平分配股权?

创业网友提问:

我有一个新的社会网络应用的构想。我并不期望这个应用可以获得巨大成功,但我想还是有些潜力的。我找过一些好友和同事交流过这个想法,他们都十分喜欢这个构想。还有些朋友甚至提出想作为合作伙伴加入一起进行开发,把想法变为一个可用的软件。

我无法用自己的钱给他们支付工资(他们也不期望那样),而且大家都打算把这项工作作为我们平时晚上或周末的业余项目。因为我认为这个想法有潜力可以变为成功的企业,我想从目前这个阶段开始,就解决企业所有权/薪酬的问题,免得将来因为没界定清楚导致真正的问题。我倾向于把公司所有权在我们三个创始人当中平分,而且基于这个分配规则来确定将来盈利后如何分配。这个选择对吗?如果对,怎么才算公平的股权分配?这是我自己提出的想法而且已花了不少时间做规划(而且我很确定我将自己负担所有开发过程中发生的费用),所以我感觉我应该理所当然持有更大的股权。这个要求是否合理?

我还同时企图想办法按付出的努力来激励我的合伙人。我不担心有人加入后什么都不干,但我的确认为我们当中有一个或多个人也许将十分努力付出,比其他人付出更多。如果情况是这样,我想这类合伙人应该获得更多的股份。在规划股权架构方面,您有何建议?

Joel Spolsky回答:

这个问题实在是太普遍了,我打算对这个为题给出这个世界上最详细的回答。我希望,将来如果这个坛子上有人问到类似的问题,大家只需要引用我的回答。

最重要的(股权分配)原则:公平,而且可感知到的公平,比真正拥有大的股份更有价值

在一个创业公司,几乎所有可能会出错的地方都会出错,而且会出错的问题当中最大最大的问题是创始人之间巨大的、令人气愤的、吵到面红耳赤的关于“谁更努力工作”的争论,谁拥有更多股份,谁提出的想法等等。这也是我总会与一个朋友50-50平分一个新公司的股权,而不是坚持自己拥有60%的股权,因为“这是我的想法”,或者因为“我比你更有经验”,或者任何其它原因。为什么呢?因为如果我把股权拆分为60-40,公司将在我们(创始人)不断争吵当中走向失败!如果你只是说,“去他妈的,我们永远也无法知道正确的股权分配比例,我们还是像哥们儿那样50-50平分”,你们将继续是朋友而且公司将生存下去。

所以,我郑重向大家推出:Joel的适用于任何创业公司创始人完全公平划分股权的秘笈!

为简单起见,我将假设你们不打算拿风险投资,而且你们将不会有外来的投资人。随后,我再解释如何处理风险投资,但目前我们暂时假设没有投资人。同样为简单起见,我们临时假设所有创始人都辞掉了他们的全职工作,而且同时开始全职为新公司工作。随后,我再解释如何处理后来加入的创始人。

来啦,原则是这样的:随着你们公司的成长,你们将一层一层/一批一批地加入新员工。公司的首批员工就是第一个创始人(或者第一批创始人)。也许有1个,2个,3个或者更多,但你们都同时开始在新公司工作,而且你们要冒一样的风险……例如辞掉你们的工作加入一个未被市场认可的新公司。

第二批进来的人就是首个(批)真正的员工。当你聘任这批人时,你已从某个来源获得现金(投资人或者客户,这个无所谓)。这些人不需要冒多大风险因为他们从工作的第一天开始就拿了工资,而且,老实说,他们不是公司的创始人,他们是加入公司打工的。

第三批的人是更后来加入到员工。他们加入公司时,公司已运作得不错。

对于很多公司而言,每隔大约1年将进来一“批”员工。当你的公司规模大到可以卖给谷歌或上市或是其它,你公司员工也许已经有了6批:创始人1批,员工大约5批。每一批员工人数都比上一批更多。也许有2个创始人,第二批当中有5名最早的员工,第三批有25名员工,而第四批有200名员工。越迟加入公司的员工需要冒的风险越低。

好啦,你将这样利用上述信息:创始人应该最终拿整个公司大约50%的股份;首层下面的5层员工的每一层最终都分别分到大约10%的公司股份,每一层的员工都将平分这10%的股份。


例子:

2个创始人启动公司。他们每人拿2500份股份。公司总市值按5000股算,所以每个创始人拿一半。

第一年,他们聘用了4名员工。这4名员工每人拿250份股份。公司总市值按6000股算。

第二年,他们又聘用了一批20名员工。这些员工每人拿50份股份。他们获得更少股份因为他们要承受的风险更少。因为公司给每一批员工派发的股份是1000股,所以他们每人拿到50股。

直到公司员工有了6批,你已给出10000股。每个创始人最终持有公司25%的股份。每个员工“层级”持有10%的股份。所有员工当中,最早进入公司的员工,因为他们与迟来的相比要承担的风险最大,在所有员工中持有最多股份。


靠谱吗?你不必严格按照这个公式来规划股份,但基本思路是:你设立不同的资历“层”,最高的层级中的员工承受最大的风险,最低层层级的员工承担最少的风险,而每个“层”的员工平分公司分配给这个层级的股份,这个规则神奇地让越早加入到员工获得越多的股份。

使用“层级”的一个稍微不同的方式是“资历”。你的顶部层级是公司创始人,再下一层,你需要预留一整层给将来招聘牛逼哄哄并坚持需要10%股份的CEO;再下一层是给那些早期进来的员工以及顶级经理人的,等等。无论你如何组织你的层级,它们应该是设计清晰明了,容易理解,不容易产生纷争。


现在,你搞定了一个公平的份股系统,但还有一个重要的原则:你必须执行“股份绑定”(vesting)。

股份绑定期最好是4到5年。任何人都必须在公司做够起码1年才可持有股份(包括创始人)。好的股份绑定计划一般是头一年给25%,然后接下来每个月落实2%。否则,你的合作创始人将加入公司3个星期后跑掉,然后7年后又出现,并声称他拥有公司的25%的股份。没有“股份绑定”条款,你派股份给任何人都是不靠谱的!没有执行“股份绑定”是极其普遍的现象,后果可以十分严重。你看到有些公司的3个创始人没日没夜地工作了5年,然后你发现有些混蛋加入后2个星期就离开,这混蛋还以为他仍然拥有公司25%的股份,就因为他工作过的那2个星期。


好了,让我们清理一下整个设计蓝图中没搞定的小问题。如果你的公司融资了,股份如何分割?投资可以来自任何方向,一个天使投资人,一个风险投资公司,或者是某人的老爸。基本上,回答很简单:新的投资将“稀释”所有人的股份。

沿用上面的例子,我们有2个创始人,我们给了自己每人2500股股份,所以我们每人拥有公司的50%股份,然后我们找了个风投,风投提出给我们100万换取1/3的公司股份。公司1/3的股份 = 2500股。所以,你发行2500股给了风投。风投持有1/3公司股份,而你和另外一个创始人各持1/3。就这么多。如果并不是所有早期员工都需要拿工资,怎么办?很多时候,有些公司创始人有不少个人积蓄,她决定公司启动后的某个阶段可以不拿工资。而有些创始人则需要现金,所以拿了工资。很多人认为不拿工资的创始人可以多拿一些股份,作为创业初期不拿工资的回报。问题是,你永远不可能计算出究竟应该给多多少股份(作为初期不拿工资的回报)。这样做将导致未来的纷争。千万不要用分配股权来解决这些问题。其实,你只需要针对每位创始人拿的工资做好记帐:不拿工资创始人就给她记着工资“欠条”。当公司有了足够现金,就根据这个工资欠条补发工资给她。接下来的几年中,当公司现金收入逐步增加,或者当完成第一轮风险投资后,你可以给每一位创始人补发工资,以确保每一位创始人都可从公司得到完全一样的工资收入。


创业构想是我提出的,难道我不应该多拿股份吗?不拿。构想基本上是不值钱的。仅仅因为提出创业构想就获得更多股权,因此导致纷争是不值得的。如果你们当中有人首先提出的创业构想,但你们都同时辞工并同时开始创业,你们应该拿同等的股份。为公司工作才是创造价值的原因,而你洗澡的时候突发奇想的“创业点子”根本不值什么钱。

如果创始人之一不是全职投入创业公司工作,该怎么办?那么,他(们)就不能算是“创始人”。在我的概念中,如果一个人不全职投入公司的工作就不能算是创始人。任何边干着他们其它的全职工作边帮公司干活的人只能拿工资或者工资“欠条”,但是不要给股份。如果这个“创始人”一直干着某份全职工作直到公司拿到风投,然后辞工全职过来公司干活,他(们)和第一批员工相比好不了多少,毕竟他们并没有冒其他创始人一样的风险。

如果有人为公司提供设备或其它有价值的东西(专利、域名等),怎么处理?很好啊。按这些东西的价值支付现金或开个“欠条”咯,别给股份。你准确算一下他给公司带来的那台电脑的价值,或者他们自带的某个聪明的字处理专利的价格,给他们写下欠条,公司有钱后再偿还即可。在创业初期就用股权来购买某些公司需要的东西将导致不平等,纷争和不公平。

投资人、创始人和雇员分别应该拥有多少股份?这都要看市场情况来确定。现实地看,如果投资人最终获得超过50%的公司股权,创始人将感觉自己不重要而且会丧失动力,所以好的投资人也不会这样干(拿超过50%的股权)。如果公司能依赖自我积累来发展而不依靠外来投资,创始人和员工一起将拥有公司100%的股权。有趣的是,这样的安排将给未来投资人带来足够的压力,以平衡投资人与创始人/员工。一条老经验是:公司上市时(当你雇佣了足够的员工而且筹集了足够的投资后),投资人将拥有50%股份,创始人+员工将拥有50%股份,但是就2011年热门的网络公司而言,他们的投资人最终拥有的股份都比50%少得多。


结论

虽然创业公司股权分配原则这个问题没有一刀切的解决方案,但是你得尽可能让它简单化,透明化,直接了当,而最重要的是:要公平。只有这样你的公司才更有可能成功。

183月/13

如何在nginx服务器下防盗链

发布在 邵珠庆

 

盗链是指一个网站的资源(图片或附件)未经允许在其它网站提供浏览和下载。
尤其热门资源的盗链,对网站带宽的消耗非常大,本文通过nginx的配置指令location来实现简单的图片和其它类型文件的防盗链。

Nginx 的配置文件 :

location ~ .(jpe?g|png|gif)$ {
     valid_referers none blocked mysite.com *.mysite.com;
     if ($invalid_referer) {
        return   403;
    }
}

用 (“|”) 来分隔你想保护的文件的扩展名。

valid_referers指令包含允许访问资源的网站列表,不在列表中请求的返回403。下面是valid_referers指令参数的解释 :

none - 匹配没有Referer的HTTP请求(Matches the requests with no Referer header).
blocked - 请求有Referer ,但是被防火墙或者代理服务器修改,去掉了https://或http:// (Matches the requests with blocked Referrer header).
*.mydomain.com - 匹配mysite.com的所有二级域名(Matches all the sub domains of mydomain.com. Since v0.5.33, * wildcards can be used in the server names).

除了使用location对文件访问进行限制,也可以对特定目录进行限制,下面的配置会禁止访问images目录下所有文件

location /images/ {
     valid_referers none blocked mysite.com *.mysite.com;
     if ($invalid_referer) {
        return   403;
    }
}

以上配置都是简单通过验证请求头来实现防盗链,如果盗链的网站通过伪造来路的http请求时不能屏蔽。