HTML5 设计原理

李松峰老师翻译,Jeremy Keith 在 Fronteers 2010 上的主题演讲

今天我想跟大家谈一谈HTML5的设计。主要分两个方面:一方面,当然了,就是HTML5。我可以站在这儿只讲HTML5,但我并不打算这样做,因为如果你想了解HTML5的话,你可以Google,可以看书,甚至可以看规范。

实际上,确实有人会谈到规范的内容。史蒂夫·福克纳(Steve Faulkner)会讲HTML5与可访问性。而保罗·艾里什(Paul Irish)则会讲HTML5提供的各种API。因此,我今天站在这里,不会光讲一讲HTML5就算完事了。

说老实话,在正式开始之前,我想先交待清楚我所说的HTML5到底是什么意思。这话听起来有点搞笑:这会子你一直在说HTML5,难道我们还不知道什么是HTML5吗?大家知道,有一个规范,它的名字叫HTML5。我所说的HTML5,指的就是这个规范。但问题是,有些人所说的HTML5,指的不仅仅是这个规范,还有别的意思。比如说,用HTML5来代指CSS3就是一种常见的叫法。我可不是这样的。我所说的HTML5,不包含CSS3,就是HTML5。

类似的术语问题以前也有过。Ajax本来是一种含义明确的技术,但过了不久,它的含义就变成了“用JavaScript来做一切好玩的东西”。这就是Ajax,对不对?今天,HTML5也面临同样的问题,它本来指的是一个特定的规范,但如今含义却成了“在Web上做一切好玩的事。”我说的不是这种HTML5,不是这种涵盖了最近刚刚出现的各种新东东的HTML5。我说的仅仅是规范本身:HTML5。
刚才已经说了,我今天想要讲的内容不多,也没有打算介绍HTML5都包含什么。今天我要讲的是它的另一方面,即HTML5的设计。换句话说,我要讲的不是规范里都包含什么,而是规范里为什么会包含它们,以及在设计这个规范的时候,设计者们是怎么看待这些东西的。

设计原理

设计原理本质上是一种信念、一种想法、一个概念,是你行动的支柱。不管你是制定规范,还是制造一种有形的物品,或者编写软件,甚至发明编程语言。你都能找到背后的一个或者多个设计原理,多人协作的任何成果都是例证。不仅仅Web开发领域是这样。纵观人类历史,像国家和社会这样大规模的构建活动背后,同样也有设计原理。

就拿美国为例吧,美国的设计原理都写在了《独立宣言》中了。

我们认为这些真理是不言而喻的,人人生而平等,造物主赋予了每个人不可剥夺的权利,包括生存、自由和追求幸福。

这里有一句口号:生存、自由和追求幸福。这是被写进宪法中的核心理念,它关系到我们所有人的一切,也就是我们构建自己社会的原则。

还有一个例子,就是卡尔·马克思(Karl Marx),他的著作在20世纪曾被奉为建设社会主义的圭臬。其基本思想大致可以归结为下面这条设计原理:

各尽所能,各取所需。

这其实就是一种经济体系背后的设计原理。
还有一个例子,比前面两个的历史更久远一些,不过大同小异:

人人为我,我为人人。

这个极为简单的设计原理,是两千年前的拿撒勒犹太人耶稣基督提出来的。而这条原则成为了后来许多宗教的核心教义。原理与实践有时候并不是同步的。

下面是小说中的一个例子。英国小说家乔治·奥威尔(George Orwell)笔下的《动物庄园》,就是在一条设计原理的基础上构建起来的虚拟社会。这条设计原理是:

四条腿的都是好人,两条腿的都是坏蛋!

《动物庄园》中有意思的是,随着社会的变迁——变得越来越坏,这条设计原理也跟着发生了改变,变成了“四条腿的都是好人,两条腿的就更好了。”最关键的是,即使是在虚构的作品里,设计原理都是存在的。

还有一套虚构的作品是以三条设计原理为基础构建起来的,那就是美国著名小说家艾萨克·阿西莫夫(Issac Asimov)的机器人经典系列。阿西莫夫发明了机器人学这个术语,并提出了机器人学三大法则,然后在这三个简单的设计原理基础上创作了一系列经典作品——大约有50本书。无论作品的情节如何变化,实际上都是从不同的角度来阐释这三大设计原理。我想,在座各位对机器人三大法则都不应该陌生。

机器人不得伤害人类,或袖手旁观人类受伤害。
机器人必须服从人类命令,除非命令违反第一法则。
机器人必须自卫,只要不违背第一和第二法则。

这些恐怕是第一次出现在小说中的针对软件的设计原理了。虽然基于这三个设计原理的软件运行在虚构的机器人的“正电子脑”中,但我想这应该是软件设计原理的事实开端。从此以后,我们才看到大量优秀软件背后的设计原理。

蒂姆·伯纳斯-李(Tim Berners-Lee),Web的发明者,在W3C的网站上发表过一份文档,其中有一个URL给出了他自己的一套设计原理。这些设计原理并不那么容易理解,不仅多,而且随着时时间推移,他还会不断补充、修改和删除。不过我还是觉得把自己认同的设计原理写出来放在某个地方真是个不错的主意。

实际上,CSS的发明人之一伯特·波斯(Bert Bos),也在W3C的网站上放着一份文档,其中讲的都是基本的设计原理,比如怎样设计并构建一种格式,无论是CSS还是其他格式。推荐大家看一看。

只要你在W3C的站点中随便找一找,就可以发现非常多的这种设计原理,包括蒂姆·伯纳斯-李个人的。当然,你还会看到他从软件工程学校里借用的一些口号:分权(decentalisation)、容忍(tolerance)、简易(simplicity)、模块化(modularity)。这些都是在他发明新格式的时候,头脑中无时无刻不在想的那些关键词。

在座各位对蒂姆·伯纳斯-李的贡献都是非常熟悉的,因为大家每天都在用。他发明了Web,与罗伯特·卡里奥(Robert Cailliau)共同发明了Web,而且在发明Web的同时,也发明了我们每天都在Web上使用的语言。当然,这门语言就是HTML:超文本标记语言。

HTML

HTML最早是从2.0版开始的。从来就没有1.0版。如果有人告诉你说,他最早是从HTML 1.0开始使用HTML的,那他绝对是在忽悠你。从前确实有一个名叫HTML Tags的文档,其中的部分标签一直用到现在,但那个文档并非官方的规范。

使用标签、尖括号、p或h1,等等,并不是蒂姆·伯纳斯-李首创的想法。当时的SGML里就有了这些概念,而且当时的CERN(Conseil Europeen pour la Recherche Nucleaire,欧洲核子研究委员会)也在使用SGML的一个特定的版本。也就是说,即便在那个时代,他也没有白手起家;这一点在HTML后来的发展过程中也体现了出来:继往开来、承前启后,而不是另立门户、从头开始。

换句话说,这篇名为HTML Tags的文档可以算作HTML的第一个版本,但它却不是一个正式的版本。第一个正式版本,HTML 2.0,也不是出自W3C之手。HTML 2.0是由IETF,因特网工程任务组(Internet Engineering Task Force)制定的。在W3C成立之前,IETF已经发布了不少标准。但从第三个版本开始往后,W3C,万维网联盟(World Wide Web Consortium)开始接手,并负责后续版本的制定工作。

20世纪九十年代HTML有过几次快速的发展。众所周知,在那个时代要想构建网站,可是一项十分复杂的工程。浏览器大战曾令人头疼不已。市场竞争的结果就是各家浏览器里都塞满了各种专有的特性,都试图在专有特性上胜人一筹。当时的混乱程度不堪回首,HTML到底还重不重要,或者它作为Web格式的前景如何,谁都说不清楚。

从1997年到1999年,HTML的版本从3.2到4.0到4.01,经历了非常快的发展。问题是到了4.01的时候,W3C的认识发生了倒退,他们说“好了,这个版本就这样了,HTML也就这样了;HTML 4.01是HTML的最后一个版本了,我们用不着HTML工作组了。”

W3C并没有停止开发这门语言,只不过他们对HTML不再感兴趣了。在HTML 4.01之后,他们提出了XHTML 1.0。虽然听起来完全不同,但XHTML 1.0与HTML 4.01其实是一样的。我的意思是说,从字面上看这两个规范的内容是一样的,词汇表是一样的,所有的元素是一样,所有的属性也都是一样的。唯一一点不同之处,就是XHTML 1.0要求使用XML语法。也就是说,所有属性都必须使用小写字母,所有元素也必须使用小写字母,所有属性值都必须加引号,你还得记着使用结束标签,记着对img和br要使用自结束标签。

从规范本身的内容来看,实际上是相同的,没有什么不同。不同之处就是编码风格,因为对浏览器来说,读取符合HTML 4.01、HTML 3.2,或者XHTML 1.0规范的网页都没有问题,对浏览器来说这些网页都是一样的,都会生成相同的DOM树。只不过人们会比较喜欢XHTML 1.0,因为不少人认同它比较严格的编码风格。

到了2000年,Web标准项目(Web Standards Project)的活动开展得如火如荼,开发人员对浏览器里包含的那些乱七八糟的专有特性已经忍无可忍了。大家都很生气,就骂那些浏览器厂商“遵守个规范就他妈的真有那么难吗?”当时CSS有了长足的发展,而且与XHTML 1.0结合得也很紧密,CSS加XHTML 1.0基本上就可以算是“最佳实践”了。虽然在我看来HTML 4.01与XHTML 1.0没有本质上的不同,但大家都接受了。专业的开发人员能做到元素全部小写,属性全部小写,属性值也全部加引号:由于专业人员起到了模范带头作用,越来越多的人也都开始支持这种语法。

我就是一个例子!过去的10年,我一直都使用XHTML 1.0文档类型,原因是这样一来验证器就能给我帮上很大的忙,对不对?只要我写的是XHTML 1.0,然后用验证器测试,它就能告诉我是不是忘了给属性值加引号,是不是没有结束某个标签,等等等等。而如果我写的是HTML 4.01,同样的问题就变成了有效的了,验证器就不一定会提醒我了。

这就是我一直使用XHTML 1.0的原因。我估计很多人都……使用XHTML 1.0的朋友,请把手举起来。好的。HTML 4.01呢?人少多了。一直没有举手的呢,大声点,你们用什么?HTML5,也很好!更早的呢,还有人使用更早的文档类型吗?没有了?

10年来我一直使用XHTML 1.0,就是因为验证器能够真正帮到我。有人用XHTML 1.1吗?你知道有人用吗?请举手,别放下。有人把网页标记为XML文档吗?有吗?那你们使用的就不是XHTML 1.1。

这就是个大问题。XHTML 1.0之后是XHTML 1.1,只是小数点后面的数字加了一个1,而且从词汇表的角度看,规范本身没有什么新东西,元素也都相同,属性也都相同。但对XHTML 1.1来说,唯一的变化是你必须把自己的文档标记为XML文档。在使用XHTML 1.0的时候,还可以把文档标记为HTML,而我们也正是这样做的,否则把文档标记为XML没准真会把人逼疯的。

为什么这么说呢?首先,把文档标记为XML后,Internet Explorer不能处理。当然,IE9是可以处理了。恐怕有人会讲“真是太可爱了”,他们到现在居然都没有忘了这件事。这艘船终于靠岸了!不过那时候,作为全球领先的浏览器,IE无法处理接收到的XML文档类型的文档,而规范又要求你以XML文档类型来发送文档,这不把人逼疯才怪呢。

所以说XHTML 1.1有点脱离现实,而你不想把文档以XML格式发送给那些能够理解XML的浏览器,则是因为XML的错误处理模型。XML的语法,无论是属性小写,元素小写,还是始终要给属性值加引号,这些都没有问题,都很好,事实上我也喜欢这样做,但XML的错误处理模型却是这样的:解析器如果遇到错误,停止解析。规范里就是这么写的。如果你把XHTML 1.1标记为XML文档类型,假设你用Firefox打开这个文档,而文档中有一个和号(&)没有正确编码,就算整个页面中就这一处错误,你看到的也将是黄屏,浏览器死掉了。Firefox会说:“没戏了,页面中有一个错误,你看不到这个网页了。”根据XML规范,这样处理是正确的,对Firefox而言,遇到错误就停止解析,并且不呈现其他任何内容是严格按照XML规范做的。因为它不是HTML,HTML根本就没有错误处理模型,但根据XML规范,这样做没错。

这就是为什么你不会把文档标记为XML的另一个原因。接下来,新的版本是XHTML 2,大家注意后面没有日期,因为这个规范并没有完成。

现在就说说XHTML 2,我很愿意把问题说清楚,XHTML 2实际上真是一个非常非常好的规范,确实非常好……从理论的角度来说。我的意思是说,制定这个规范的人都是非常非常有头脑的。直说吧,领导制定这个规范的家伙是斯蒂芬·彭伯顿(Stephen Pemberton),他应该是本地人,是一个聪明过人的家伙。规范本身也很了不起,如果所有人都同意使用的话,也一定是一个非常好的格式。只不过,还不够实际。

首先,XHTML 2仍然使用XML错误处理模型,你必须保证以XML文档类型发送文档;这一点不言自明:没人愿意这样做。其次,XHTML 2有意不再向后兼容已有的HTML的各个版本。他们甚至曾经讨论过废除img元素,这对每天都在做Web开发的人来说确实有点疯了的味道。但我们知道,他们之所以这样做,理论上确实有充足的理由——使用object元素可能会更好。

因此,无论XHTML 2在理论上是多么完美的一种格式,但却从未有机会付诸实践。而之所以难以将其付诸实践,就是因为像你我这样的开发人员永远不会支持它,它不向后兼容。同样,浏览器厂商也不会,浏览器厂商必须要保证向后兼容。

为什么XHTML 1.1没有像XML那样得到真正广泛地应用,为什么XHTML 2从未落到实处?因为它违反了一条设计原理,这条设计原理就是著名的伯斯塔尔法则(Postel’s Law)。大家都知道:

发送时要保守;接收时要开放。

没错,接收的时候要开放,而这也正是Web得以构建的基础。开发浏览器的人必须敞开胸怀,接收所有发送给浏览器的东西,因为它们过去一直都在接收那些不够标准的东西,对不对?Web上的很多文档都不规范,但那正是Web发展的动力。从某种角度讲,Web走的正是一条混沌发展之路,虽然混沌,但却非常美丽诱人。在Web上,格式不规范的文档随处可见,但那又怎样呢?如果所有人都能够写出精准的XML,所有文档的格式都十分正确,那当然好了。可是,那不现实。现实是伯斯塔尔法则。

作为专业人士,在发送文档的时候,我们会尽量保守一些,尽量采用最佳实践,尽量确保文档格式良好。但从浏览器的角度说,它们必须以开放的姿态去接收任何文档。

有人可能会说XML有错误处理模型,XHTML 1.1和XHTML 2都使用该模型,但那个错误处理模型太苛刻了。它绝对不符合接收时开放这个法则,遇到一个错误就停止解析怎么能叫开放呢?我们只能说它与健壮性法则(也就是伯斯塔尔法则)是对立的。

HTML5

之后,就到了HTML5,但HTML5并不是由W3C直接制定的。故事的经过是这样的,到20世纪末的时候,还没有HTML工作组,W3C内部的一些人就开始琢磨了,“HTML也许还可以更长寿一点,只要我们对它稍加扩展就行了。只要把我们放在XHTML上的时间和精力拿出一部分来,就可以提升一下HTML中的表单,可以让HTML更接近编程语言,就可以让它更上一层楼。”

于是,在2004年W3C成员内部的一次研讨会上,当时Opera公司的代表伊恩·希克森(Ian Hickson)提出了一个扩展和改进HTML的建议。他建议新任务组可以跟XHTML 2并行,但是在已有HTML的基础上开展工作,目标是对HTML进行扩展。W3C投票表决的结果是——“反对”,因为HTML已经死了,XHTML 2才是未来的方向。然后,Opera、Apple等浏览器厂商,以及其他一些成员说:“那好吧,不指望他们了,我们自已一样可以做这件事,我们脱离W3C。”他们成立了Web Hypertext Applications Technology Working Group(Web超文本应用技术工作组,WHATWG)——可巧的是,他们自称工作组,而不是特别小组(task force),这就为HTML5将来的命运埋下了伏笔。

WHATWG决定完全脱离W3C,在HTML的基础上开展工作,向其中添加一些新东西。这个工作组的成员里有浏览器厂商,因此他们不仅可以说加就加,而且还能够一一实现。结果,大家不断提出一些好点子,并且逐一做到了浏览器中。

WHATWG的工作效率很高,不久就初见成效。在此期间,W3C的XHTML 2没有什么实质性的进展。特别是,如果从实现的角度来说,用原地踏步形容似乎也不为过。

结果,一件有意思的事情发生了。那是在2006年,蒂姆·伯纳斯-李写了一篇博客,说:“你们知道吗?我们错了。我们错在企图一夜之间就让Web跨入XML时代,我们的想法太不切实际了,是的,也许我们应该重新组建HTML工作组了。”善哉斯言,后来的故事情节果真就是这样发展的。W3C在2007年组建了HTML5工作组。这个工作组面临的第一个问题,毫无疑问就是“我们是从头开始做起呢,还是在2004年成立的那个叫WHATWG的工作组既有成果的基础上开始工作呢?”答案是显而易见的,他们当然希望从已经取得的成果着手,以之为基础展开工作。于是他们又投了一次票,同意“在WHATWG工作成果的基础上继续开展工作”。好了,这下他们要跟WHATWG并肩战斗了。

第二个问题就是如何理顺两个工作组之间的关系。W3C这个工作组的编辑应该由谁担任?是不是还让WHATWG的编辑,也就是现在Google的伊恩·希克森来兼任?于是他们又投了一次票,赞成“让伊恩·希克森担任W3C HTML5规范的编辑,同时兼任WHATWG的编辑,更有助于新工作组开展工作。”

这就是他们投票的结果,也就是我们今天看到的局面:一种格式,两个版本。WHATWG的网站上有这个规范,而W3C的站点上同样也有一份。

如果你不了解内情,很可能会产生这样的疑问:“哪个版本才是真正的规范?”当然,这两个版本内容是一样的……基本上相同。实际上,这两个版本将来还会分道扬镳。现在已经有了分道扬镳的迹象了。我的意思是说,W3C最终要制定一个具体的规范,这个规范会成为一个工作草案,定格在某个历史时刻。

而WHATWG呢,他们还在不断地迭代。即使目前我们说的HTML5,也不能完全涵盖WHATWG正在从事的工作。最准确的理解是他们正在开发一项简单的HTML或Web技术,因为这才是他们工作的核心目标。然而,同时存在两个这样的工作组,这两个工作组同时开发一个基本相同的规范,这无论如何也容易让人产生误解。误解就可能造成麻烦。

其实这两个工作组背后各自有各自的流程,因为它们的理念完全不同。在WHATWG,可以说是一种独裁的工作机制。我刚才说了,伊恩·希克森是编辑。他会听取各方意见,在所有成员各抒己见,充分陈述自己的观点之后,他批准自己认为正确的意见。

W3C则截然相反,可以说是一种民主的工作机制。所有成员都可以发表意见,而且每个人都有投票表决的权利。这个流程的关键在于投票表决。从表面上看,WHATWG的工作机制让人不好接受。岂止是不好接受,简直是历史的倒退。相信谁都会认为“运作任何项目都不能采取这种方式!”

W3C的工作机制听起来让人很舒服。至少体现了人人平等嘛。但在实践中,WHATWG的工作机制运行得非常非常好。我认为之所以会这样,主要归功于伊恩·希克森。他的的确确是一个非常称职的编辑。他在听取各方意见时,始终可以做到丝毫不带个人感情色彩。

从原理上讲,W3C的工作机制很公平,而实际上却非常容易在某些流程或环节上卡壳,造成工作停滞不前,一件事情要达成决议往往需要花费很长时间。那到底哪种工作机制最好呢?我认为,最好的工作机制是将二者结合起来。而事实也是两个规范制定主体在共同制定一份相同的规范,我想,这倒是非常有利于两种工作机制相互取长补短。

两个工作组之所以能够同心同德,主要原因是HTML5的设计思想。因为他们从一开始就确定了设计HTML5所要坚持的原则。结果,我们不仅看到了一份规范,也就是W3C站点上公布的那份文档,即HTML5语言规范,还在W3C站点上看到了另一份文档,也就是HTML设计原理。而这份文档的一位编辑今天也来到了我们大会的现场,他就是安妮·奇泰丝(Anne Van Kesteren)。如果大家对这份文档有问题,可以请教安妮。

这份文档非常好,真的非常出色。这份文档,可以说见证了W3C与WHATWG同心协力共谋发展的历程。难道你们不觉得他们像是一对欢喜冤家吗?那他们还怎么同心同德呢?这份文档忠实地记录了他们一道做了什么,他们共同拥护什么。

接下来,我想要讲的就是这份文档。因为,既然他们能就这份文档达成共识,那么我相信,HTML5必将是一个伟大的规范,而他们已经认可这就是他们的共同行动纲领。为此,你才会看到诸如兼容性、实用性、互用性之类的概念。即便W3C与WHATWG之间再有多大的分歧——确实相当多——至少他们还有这份文档中记录的共识。这一点才是至关重要的。正因为他们有了共识,才有了这份基于共识描述设计原理的文档。

避免不必要的复杂性

下面我就给大家介绍一些这份文档中记载的设计原理。第一个,非常简单:避免不必要的复杂性。好像很简单吧。我用一个例子来说明。

假设我使用HTML 4.01规范,我打开文档,输入doctype。这里有人记得HTML 4.01的doctype吗?好,没有,我猜没有。除非……我的意思是说,你是傻冒。现场恐怕真有人背过,这就是HTML 4.01的doctype:

<!DOCTYPE html PUBLIC "-//W3C/DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

我不记这个两行代码,不然还要记事本、要Google、要模板有什么用呢?

要是我使用XHTML 1.0呢,这个规范我都已经用了10年了。有谁记得住这个doctype吗?没错,它的长度跟HTML 4.01的差不太多:

<!DOCTYPE html PUBLIC "-//W3C/DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

是不是,基本上相同。它要告诉浏览器的是:这个文档是XHTML 1.0的文档。那么在HTML 5中,省掉不必要的复杂性,doctype就简化成了:

<!DOCTYPE html>

仅此而已。好了,就连我也能过目不忘了。我用不着把这几个字符记在记事本里了。我得说,在我第一次看到这个doctype的时候——我当然以为这是一个HTML文档的doctype——被它吓了一跳:“是不是还少一个数字5啊?”我心里想:“这个doctype想告诉浏览器什么呢?就说这个文档是HTML吗?难道这是有史以来唯一一个HTML版本吗,这件事我得首先搞清楚,HTML今后永远不会再有新版本了吗?”好一副唯我独尊的架式!我错了,因为这个doctype并没有这个意思。为此,必须先搞清楚为什么文档一开头就要写doctype。它不是写给浏览器看的。Doctype是写给验证器看的。也就是说,我之所以要在文档一开头写那行XHTML 1.0的doctype,是为了告诉验证器,让验证器按照该doctype来验证我的文档。

浏览器反倒无所谓了。假设我写的是HTML 3.2文档,文档开头写的是HTML 3.2的doctype。而在文档中某个地方,我使用了HTML 4.01中才出现的一个元素。浏览器会怎么处理这种情况?它会因为这个元素出现在比doctype声明的HTML版本更晚的规范中,就不解释呈现该元素吗?不会,当然不会!它照样会解释呈现该元素,别忘了伯斯塔尔法则,别忘了健壮性。浏览器在接收的时候必须要开放。因此,它不会检查任何格式类型,而验证器会,验证器才关心格式类型。这才是存在doctype的真正原因。

而按照HTML5的另一个设计原理,它必须向前向后兼容,兼容未来的HTML版本——不管是HTML6、HTML7,还是其他什么——都要与当前的HTML版本,HTML5,兼容。因此,把一个版本号放在doctype里面没有多大的意义,即使对验器证也一样。

刚才,我说doctype不是为浏览器写的,这样说大多数情况下没有问题。在有一种情况下,你使用的doctype会影响到浏览器,相信在座诸位也都知道。但在这种情况下,Doctype并非真正用得其所,而只是为了达到某种特殊的目的才使用doctype。当初微软在引入CSS的时候,走在了标准的前头,他们率先在浏览器中支持CSS,也推出了自己的盒模型——后来标准发布了,但标准中使用了不一样的盒模型。他们怎么办?他们想支持标准,但也想向后兼容自己过去推出的编码方式。他们怎么知道网页作者想使用标准,还是想使用他们过去的方式?

于是,他们想出了一个非常巧妙的主意。那就是利用doctype,利用有效的doctype来触发标准模式,而不是兼容模型(quiks mode)。这个主意非常巧妙。我们今天也都是这样在做,在我们向文档中加入doctype时,就相当于声明了“我想使用标准模式”,但这并不是发明doctype的本意。这只是为了达到特殊的目的在利用doctype。

下面我出一道有奖抢答题,听好:“一分钟后开始,如果你手快的话,第一个在文档前面写完doctype html,然后我用Internet Explorer打开你的文档,会触发它的标准模式,还是会触发它的兼容模式?”

答案是,这是在Internet Explorer中触发标准模式的最少字符数目。我认为这也说明了HTML5规范的本质:它不追求理论上的完美。HTML5所体现的不是“噢,给作者一个简短好记的doctype不好吗?”,没错,简短好记是很好,但如果这个好记的doctype无法适应现有的浏览器,还不如把它忘了更好。因此,这个平衡把握得非常好,不仅理论上看是个好主意——简短好记的doctype,而且实践中同样也是个好主意——仍然可以触发标准模式。应该说,Doctype是一个非常典型的例子。

还有一个例子,同样可以说明规范是如何省略不必要的复杂性,避免不必要的复杂性的。如果前面的文档使用的是HTML 4.01,假设我要指定文档的字符编码。理想的方式,是通过服务器在头部信息中发送字符编码,不过也可以在文档这个级别上指定:

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

同样,我也不会把这行代码背下来。我还想省下自己的脑细胞去记点别的更有价值的东西呢。不过,如果我想指定文档使用UTF-8编码,只能添加这行代码。这是在HTML 4.01中需要这样做。要是你在XHTML 1.0指定同样的编码,就得多敲一下键盘,因为你还得声明meta元素位于一个开始的XML标签中。

<?xml version="1.0" encoding="UTF-8" ?>    
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

在HTML5中,你要敲的字符只有:

<meta charset="utf-8">

简短好记。我能背下来。

同样,这样写也是有效的。它不仅适用于最新版本的浏览器,只要是今天还有人在用的浏览器都同样有效。为什么?因为在我们把这些meta元素输入浏览器时,浏览器会这样解释它:“元数据(meta)点点点点点,字符集(charset)utf-8。”这就是浏览器在解释那行字符串时真正看到的内容。它必须看到这些内容,根据就是伯斯塔尔法则,对不对?

我多次提到健壮性原理,但总有人不理解。我们换一种说法,浏览器会想“好,我觉得作者是想要指定一个字符集……看,没错,utf-8。”这些都是规范里明文规定的。如今,不仅那个斜杠可以省了,而且总共只要写meta charset=”utf-8″就行了。

关于省略不必要的复杂性,或者说避免不必要的复杂性的例子还有不少。但关键是既能避免不必要的复杂性,还不会妨碍在现有浏览器中使用。比如说,在HTML5中,如果我使用link元素链接到一个样式表,我说了rel=”stylesheet”,然后再说type=”text/css”,那就是重复自己了。对浏览器而言,我就是在重复自己。浏览器用不着同时看到这两个属性。浏览器只要看到rel=”stylesheet”就够了,因为它可以猜出来你要链接的是一个CSS样式表。所以就不用再指定type属性了。你不是已经说了这是一个样式表了嘛;不用再说第二次了。当然,愿意的话,你可以再说;如果你想包含type属性,请便。

同样地,如果你使用了script元素,你说type=”text/javascript”,浏览器差不多就知道是怎么回事了。对Web开发而言,你还使用其他的脚本语言吗?如果你真想用其他脚本语言,没人会阻拦你。但我要奉劝你一句,任何浏览器都不会支持你。

愿意的话,你可以添加一个type属性。不过,也可以什么都不写,浏览器自然会假设你在使用JavaScript。避免-不必要的-复杂性。

支持已有的内容

支持已有的内容。这一点非常重要,因为很多人都认为HTML5很新,很闪亮;它应该代表着未来发展的方向,应该把Web推向一个新的发展阶段。这就是HTML5,对吗?显然,我们都会考虑让Web的未来发展得更好,但他们则必须考虑过去。别忘了W3C这个工作组中有很多人代表的是浏览器厂商,他们肯定是要考虑支持已有内容的。只要你想构建一款浏览器,就必须记住这个原则:必须支持已有的内容。

下面我们就来看一个HTML5支持已有内容的例子。

这个例子展示了编写同样内容的四种不同方式。上面是一个img元素,下面是带一个属性的段落元素。四种写法唯一的不同点就是语法。把其中任何一段代码交给浏览器,浏览器都会生成相同的DOM树,没有任何问题。从浏览器的角度看,这四种写法没有区别。因而在HTML5中,你可以随意使用下列任何语法。

<img src="foo" alt="bar" />
<p class="foo">Hello world</p>

<img src="foo" alt="bar">
<p class="foo">Hello world

<IMG SRC="foo" ALT="bar">
<P CLASS="foo">Hello world</P>

<img src=foo alt=bar><p class=foo>Hello world</p>

好了,看到这几段代码,恐怕有人会说“不对不对不对。其中只有一个是对的,另外三个——说不好。”不对,应该给属性值加引号!拜托,我们可是一直都给属性值加引号的!元素名大写对吗?这种做法10年不是就被抛弃了吗?

看到HTML5同时允许这些写法,我心里忍不住一阵阵想吐。我写了10年的XHTML 1.0,已经非常适应严格的语法了。但你必须明白,站在浏览器的角度上,这些写法实际上都是一样的。确实没有什么问题。

还有谁也感到不舒服了吗?有谁看到这些之后想“噢,这不是乱写嘛,这样做不对”?只有我这样想吗?还有别人吗?

但是,HTML5必须支持已经存在的内容,而已有的内容就是这个样子的。不是吗?根据伯斯塔尔法则,浏览器没有别的选择。

有人可能会说“这样不行。我觉得语言本身应该提供一种开关,让作者能够表明自己想做什么。”比如说,想使用某种特定的语法,像XHTML,而不是使用其他语法。我理解这些人的想法。但我不赞成在语言里设置开关。因为我们讨论的只是编码风格或者写作风格,跟哪种语法正确无关。对于像我们这样的专业人士,我认为可以使用lint工具(一种软件质量保证工具,或者说是一种更加严格的编译器。它不仅可以象普通编译器那样检查出一般的语法错误,还可以检查出那些虽然完全合乎语法要求,但很可能是潜在的、不易发现的错误),对其他技术我们不是也在使用lint工具嘛。

比如说对JavaScript使用lint工具。JavaScript同样也是比较混乱、不严谨的例子,但它非常强大,原因恰恰是它混乱、不严谨,而且有很多不同的编码方式。在JavaScript,你可以在每条语句末尾加上分号,但不是必需的,因为JavaScript会自动插入分号……是不是听起来有点不好接受?

正因为如此,才有了像JSlint这样的工具,在道格拉斯·克劳克福德(Douglas Crockford)的网站jslint.org上面。有个网页上写着“JSlint可能会伤害你的感情。”但这确实是个非常棒的工具,它可以把JavaScript代码变得完美无瑕。如果你通过JSlint运行JavaScript,它会告诉你“好,你的JavaScript代码有效,但写法不妥。你这种编码风格啊,我不喜欢。不赞成你这样写。这样写不好。”特别是对团队,对于要使用统一的编码风格的团队,JSlint是非常方便的工具。

我个人认为,不仅对团队来说,就算是你自己写代码,也要坚持一种语法风格。从浏览器解析的角度讲,不存在哪种语法比另一种更好的问题,但我认为,作为专业人士,我们必须能够自信地讲“这就是我的编码风格。”然而,我不认为语言里应该内置这种开关。你可以使用lint工具来统一编码风格。现在就来说说lint工具。大家可以登录htmllint.com,在其中运行你的HTML5文档,它会帮你检查属性值是否加了引号,元素是否小写,你还可以通过勾选复选框来设置其他检查项。

但这不意味着拒绝粗心大意的标记,做不做清理完全取决于你自己。我说过,因为浏览器必须支持已有的内容,HTML5自然也不能例外。归根结底还是伯斯塔尔法则。我们始终离不开伯斯塔尔法则。

解决现实的问题

HTML5的另一个设计原理是解决现实的问题。显而易见的是,解决各种问题的格式和规范已经比比皆是了,但是在我看来,那些格式和规范要解决的都是理论问题,而非现实问题。这条设计原理才是真正要解决今天的人们所面临的现实问题、令人头疼的问题。

下面我来举个例子。相信这个例子有不少人都遇到过。假设我使用HTML 4或XHTML 1,页面中已经有了一块内容,我想给整块内容加个链接,怎么办?问题是这块内容里包含一个标题,一个段落,也许还有一张图片。如果我想给它们全部都可以点击,必须使用3个链接元素。于是,我得先把光标放在标题(比如说h2元素)中,写一个链接标签,然后再选中所有要包含到链接里面来的文本。接着,再把光标放在段落里,写一个链接标签,然后把段落中的文本放在链接里……

<h2><a href="/path/to/resource">Headline text</a></h2>
<p><a href="/path/to/resource">Paragraph text.</a></p>

在HTML5中,我只要简单地把所有内容都包装在一个链接元素中就行了。

<a href="/path/to/resource">
    <h2>Headline text</h2>
    <p>Paragraph text.</p>
</a>

没错,链接包含的都是块级元素,但现在我可以用一个元素包含它们。这样太好了。因为我碰到过类似的情形,必须给几个块级元素加上相同的链接,所有能这样写就太好了。为此,我就非常欢迎HTML5这个新标准。

它解决了一个现实的问题。我敢说在座不少朋友都曾遇到过这个问题。

那这到底解决的是什么问题呢?浏览器不必因此重新写代码来支持这种写法。这种写法其实早就已经存在于浏览器中了,因为早就有人这样写了,当然以前这样写是不合乎规范的。所以,说HTML5解决现实的问题,其本质还是“你都这样写了很多年了吧?现在我们把标准改了,允许你这样写了。”

求真务实

在所有设计原理中,这一条恐怕是最响亮的了——求真务实。不知道大家有没有在公司里开会时听到过这种口号:“开拓进取,求真务实。”实际上,除了作为企业的口号,它还是一条非常重要的设计原理,因为求真务实对于HTML的含义是:在解决那些令人头痛的问题之前,先看看人们为应对这些问题都想出了哪些办法。集中精力去理解这些“民间的”解决方案才是当务之急。

HTML5中新的语义元素就是遵循求真务实原理的反映。新增的元素不算多,谈不上无限的扩展性,但却不失为一件好事。尽管数量屈指可数,但意义却非同一般。这些新元素涉及头部(header)、脚部(footer)、分区(section)、文章(article)……,相信大家都不会觉得陌生。我的意思是说,即便你不使用HTML5,也应该熟悉这些称呼,这些都是你曾经使用过的类名,比如class=”header”/“head”/“heading”,或class=”footer”/“foot”。当然,也可能是ID,id=”header”,id=”footer”。这些不都是我们已经司空见惯了的嘛。

好,举个例子吧,假设你今天写了下面这个文档。

<body>
    <div id="header">...</div>
    <div id="navigation">...</div>
    <div id="main">...</div>
    <div id="sidebar">...</div>
    <div id="footer">...</div>
</body>

这里有一个div使用了id=”header”,另一个div使用了id=”navigation”,……。怎么样,都轻车熟路了吧?在HTML5中,这些元素都可以换掉。说起新增的语义元素,它们价值的一方面可以这样来体现:“嘿,看啊,这样多好,用HTML5新增的元素可以把这些div都替换掉。”

<body>
    <header>...</header>
    <nav>...</nav>
    <div id="main">...</div>
    <aside>...</aside>
    <footer>...</footer>
</body>

当然了,你可以这样做。在文档级别上使用这些元素没有问题。但是,假如新增这些元素的目的仅仅是为了取代原来的div,那就真有点多此一举了。

虽然在这个文档中,我们用这些新元素来替换的是ID,但在我个人看来,将它们作为类的替代品更有价值。为什么这么说呢?因为这些元素在一个页面中不止可以使用一次,而是可以使用多次。没错,你可以为文档添加一个头部(header),再添加一个脚部(footer);但文档中的每个分区(section)照样也都可以有一个头部和一个脚部。而每个分区里还可以嵌套另一个分区,被嵌套的分区仍然可以有自己的头部和脚部,是这样吧?

这四个新元素:section、article、aside和nav,之所以说它们强大,原因在于它们代表了一种新的内容模型,一种HTML中前所未有的内容模型——给内容分区。迄今为止,我们一直都在用div来组织页面中的内容,但与其他类似的元素一样,div本身并没有语义。但section、article、aside和nav实际上是在明确地告诉你——这一块就像文档中的另一个文档一样。位于这些元素中的任何内容,都可以拥有自己的概要、标题,自己的脚部。

其中最为通用的section,可以说是与内容最相关的一个。而article则是一种特殊的section。Aside呢,是一种特殊的section。最后,Nav也是一种特殊的section。

好,即便是现在,你照样可以使用div和类来描述页面中不同的部分,就像下面这样:

<div class="item">
    <h2>...</h2>
    <div class="meta">...</div>
    <div class="content">...</div>
    <div class="links">...</div>
</div>

其中包含可能是有关内容作者的元数据,而下面会给出一些链接,差不多就这样。在HTML5中,我完全可以说这块内容就是一个文档,通过对内容分区,使用section或article或aside,我可以说“这一块完全是可以独立存在的。”因此,我当然可以使用header和footer。

<section class="item">
    <header><h1>...</h1></header>
    <footer class="meta">...</footer>
    <div class="content">...</div>
    <nav class="links">...</nav>
</section>

请注意,即便是footer,也不一定非要出现在下面,不是吗?这几个元素,header、footer、aside、nav,最重要的是它们的语义;跟位置没有关系。一想到footer这个词,我们总会不由自主地想,“噢,应该放在下面。”同样,我们把aside想象成一个侧边栏。可是,如果你看一看规范,就会发现这些元素只跟内容有关。因此,放在footer中的内容也可以是署名,文章作者之类的,它只是你使用的一个元素。这个元素并没有说“必须把我放在文档或者分区的下面。”

这里,请注意,最重要的还不是我用几个新元素替换了原来的div加类,而是我把原来的H2换成了H1——震撼吧,我看到有人发抖了。我碰到过不少职业的Web开发人员,多年来他们一直认为规范里说一个文档中只能有一个H1。还有一些自诩为万能的SEO秘诀同样说要这样。很多SEO的技巧其实是很教条的。所谓教条,意思就是不相信数据。过去,这种教条表现为“不行,页面中包含两个以上的H1,你就会死掉的。”在HTML5中,只要你建立一个新的内容块,不管用section、article、aside、nav,还是别的元素,都可以在其中使用H1,而不必担心这个块里的标题在整个页面中应该排在什么级别;H2、H3,都没有问题。

这个变化太厉害了。想一想吧,这个变化对内容管理是革命性的。因为现在,你可以把每个内容分区想象一个独立的、能够从页面中拿出来的部分。此时,根据上下文不同,这个独立部分中的H1,在整个页面中没准会扮演H2或H3的角色——取决于它在文档中出现的位置。面对这个突如其来的变化,也许有人的脑子会暂时转不过弯来。不要紧,但我可以告诉你,我认为这才是HTML5中这些新语义标记的真正价值所在。换句话说,我们现在有了独立的元素了,这些元素中的标题级别可以重新定义。

我的文档中可能会包含一个分区,这个分区中可能会嵌套另一个分区,或者一篇文章,然后文章再嵌套分区,分区再嵌套文章、嵌套分区,文章再嵌套文章。而且每个分区和文章都可以拥有自己的H1到H6。从这个意义上讲,H元素真可谓“子子孙孙,无穷匮也”了。但是,在你在编写内容或者内容管理系统的时候,它们又都是独立的,完全独立的内容块。这才是真正的价值所在。

实际上,这个点子并不HTML5工作组拍脑门想出来的,也不是W3C最近才提出来的。下面这几句话摘自蒂姆·伯纳斯-李1991年的一封邮件,邮件是发给丹·康纳利(Dan Connolly)的。他在邮件中解释了对HTML的理解,他说:“你知道……知道我的想法,我认为H1、H2这样单调地排下去不好,我希望它成为一种可以嵌套的元素,或者说一个通用的H元素,我们可以在其中嵌套不同的层次。”但后来,我们没有看到通用的H元素,而是一直在使用H1和H2——那是因为我们一直在支持已有的内容。20年后的今天,这个理想终于实现了。

平稳退化

下一条原理大家应该都很熟悉了,那就是平稳退化。毕竟,我们已经遵守这条规则好多年了。渐进增强的另一面就是平稳退化。

有关HTML5遵循这条原理的例子,就是使用type属性增强表单。下面列出了可以为type属性指定的新值,有number、search、range,等等。

    input type="number"
    input type="search"
    input type="range"
    input type="email"
    input type="date"
    input type="url"

最关键的问题在于浏览器在看到这些新type值时会如何处理。现有的浏览器,不是将来的浏览器,现有的浏览器是无法理解这些新type值的。但在它们看到自己不理解的type值时,会将type的值解释为text。

无论你写的是input type=”foo”还是input type=”bar”,现有的任何浏览器都会说:“嗯,也许作者的意思是text。”因而,你从现在开始就可以使用这些新值,而且你也可以放心,那些不理解它们的浏览器会把新值看成type=”text”,而这真是一个浏览器实践平稳退化原理的好例子。

比如说,你现在输入了type=”number”。假设你需要一个输入数值的文本框。那么你可以把这个input的type属性设置为number,然后理解它的浏览器就会呈现一个可爱的小控件,像带小箭头图标的微调控件之类的。对吧?而在不理解它的浏览器中,你会看到一个文本框,一个你再熟悉不过的文本框。既然如此,为什么不能说输入type=”number”就会得到一个带小箭头图标的微调控件呢?

当然,你还可以设置最小和最大值属性,它们同样可以平稳退化。这是问题的关键。

再看input type=”search”。你也可以考虑一下这种输入框,因为这种输入框在Safari中会被呈现为一个系统级的搜索控件,右边还有一个点击即可清除搜索关键词的X。而在其他浏览器中,你得到的则是一个文本框,就像你写的是input type=”text”一样,也就是你已经非常熟悉的文本框。那为什么还不使用input type=”search”呢?它不会有什么副作用,没有,对不对?

HTML5还为输入元素增加了新的属性,比如placeholder(占位符)。有人不知道这个属性的用处吗,没有吧?没错,就是用于在文本框中预先放一些文本。不对,不是标签(label)——占位符和标签完全不是一回事。占位符就是文本框可以接受的示例内容,一般颜色是灰色的。只要你一点击文本框,它就消失了。如果你把已经输入的内容全部删除,然后单击了文本框外部,它又会出现。

使用JavaScript编写一些代码当然也可以实现这个功能,但HTML5只用一个placeholder属性就帮我们解决了问题。

当然,对于不支持这个属性的浏览器,你还是可以使用JavaScript来实现占位符功能。通过JavaScript来测试浏览器支不支持该属性也非常简单。如果支持,后退一步,把路让开,乐享其成即可。如果不支持,可以再让你的JavaScript来模拟这个功能。

现在,我不得不提到另一个话题了:HTML5对Flash。也许你早听说过了,或者在哪里看到了这方面的讨论。说实话,我一点也不明白。我搞不懂人们怎么会仅仅凭自己的推测来展开争论。

首先,他们所说的HTML5对Flash,并不是指的HTML5,也不是指的Flash。而是指HTML5的一个子集和Flash的一个子集。具体来说,他们指的是视频。因此,不管你在哪里听到别人说“HTML5对Flash”,那很可能说的只是HTML5视频对Flash视频。

其次,一说HTML5对Flash,就好像你必须得作出选择一样:你站在哪一边?实际上不是这样的。HTML5规范的设计能够让你做到鱼和熊掌兼得。

好,下面就来看看这个新的video元素;真是非常贴心的一个元素,而且设计又简单,又实用。一个开始的video元素,加一个结束的video元素,中间可以放后备内容。注意,是后备内容,不是保证可访问性的内容,是后备内容。下面就是针对不支持video元素的浏览器写的代码:

<video src="movie.mp4">
    <!-- 后备内容 -->
</video>

那么,在后备内容里面放些什么东西呢?好,你可以放Flash影片。这样,HTML5的视频与Flash的视频就可以协同起来了。你不用作出选择。

<video src="movie.mp4">
    <object data="movie.swf">
     <!-- 后备内容 -->
    </object>
</video>

当然,你的代码实际上并没有这么简单。因为这里我使用了H264,部分浏览器支持这种视频格式。但有的浏览器不支持。

对不起,请不要跟我谈视频格式,我一听就心烦。不是因为技术。技术倒无所谓,关键是会牵扯到一大堆专利还有律师、知识产权等等,这些都是Web的天敌,对我建网站一点好处都没有。

可你实际上要做的,仅仅就是把后备内容放在那而已,后备内容可以包含多种视频格式。如果愿意的话,可以使用source元素而非src属性来指定不同的视频格式。

<video>
    <source src="movie.mp4">
    <source src="movie.ogv">
    <object data="movie.swf">
        <a href="movie.mp4">download</a>
    </object>
</video>

上面的代码中包含了4个不同的层次。

  1. 如果浏览器支持video元素,也支持H264,没什么好说的,用第一个视频。
  2. 如果浏览器支持video元素,支持Ogg,那么用第二个视频。
  3. 如果浏览器不支持video元素,那么就要试试Flash影片了。
  4. 如果浏览器不支持video元素,也不支持Flash,我还给出了下载链接。

不错,一开始就能考虑这么周到很难得啊。有了这几个层次,已经够完善了。

总之,我是建议你各种技术要兼顾,无论是HTML5,还是Flash,一个也不能少。如果只使用video元素提供视频,难免搬起石头砸自己的脚,我个人认为。而如果只提供Flash影片,情况也好不到哪去,性质是一样的。所以还是应该两者兼顾。

为什么要兼顾这两种技术呢?假设你需要面向某些不支持Flash的手持设备——只是举个例子——提供视频,你当然希望手持设备的用户能够看到视频了,不是吗?

至于为什么要使用不同的格式,为什么Flash视频和音频如此成功,我想可以归结为另一个设计原理,即梅特卡夫定律(Metcalfe’s Law):

网络价值同网络用户数量的平方成正比。

梅特卡夫的这个定律虽然是针对电话网提出来的,但在很多领域里也是适用的。使用网络的用户越多,网络的价值也就越大。人人都上Facebook,还不是因为人人都上Facebook嘛。虽然Facebook真正的价值不在于此,但只有人人都上才会让它的变得如此有价值。

梅特卡夫定律也适用于传真机。如果只有一个人购买了传真机,当然没有什么用处。但如果其他人也陆续购买了传真机,那么他的投资会就得到回报。

当然,面对竞争性的视频格式和不同的编码方式,你感觉不到梅特卡夫定律的作用,我也很讨厌以不同的方式来编码视频,但只向浏览器发送用一种方式编码的视频是行不通的。而这也正是Flash在视频/音频领域如此成功的原因。你只要把Flash影片发送给浏览器就好了,然后安装了插件的浏览器都能正常播放。本质上讲,Flash利用了梅特卡夫定律。

最终用户优先

今天我要讲的最后一个设计原理,也是我个人最推崇的一个,但没有要展示的代码示例。这个原理更有哲学的味道,即最终用户优先。

这个设计原理本质上是一种解决冲突的机制。换句话说,当你面临一个要解决的问题时,如果W3C给出了一种解决方案,而WHATWG给出了另一种解决方案,一个人这么想,另一个人那么想……这时候,有人站出来说:“对这个问题我们这样来解决。”

一旦遇到冲突,最终用户优先,其次是作者,其次是实现者,其次标准制定者,最后才是理论上的完满。

理论上的完满,大致是指尽可能创建出最完美的格式。标准制定者,指的是工作组、W3C,等等。实现者,指的是浏览器厂商。作者,就是我们这些开发人员,对吧?看看我们在这个链条里面的位置多靠上啊!我们的地位仅次于最终用户——事情本来就该这个样子。用户是第一位的。而我们的声音在标准制定过程中也同样非常非常重要。

Hixie(即Ian Hickson, Acid2、Acid3的作者及维护者,HTML5、CSS 2.1规范的制定者)经常说,在有人建议了某个特性,而HTML5工作组为此争论不下时,如果有浏览器厂商说“我们不会支持这个特性,不会在我们的浏览器中实现这个特性”,那么这个特性就不会写进规范。因为即使是把特性写进规范,如果没有厂商实现,规范不过是一纸空文,对不对?实现者可以拒绝实现规范。

而根据最终用户优先的原理,我们在链条中的位置高于实现者,假如我们发现了规范中的某些地方有问题,我们想“这样规定我们不能同意,我们不支持实现这个特性”,那么就等于把相应的特性给否定了,规范里就得删除,因为我们的声音具有更高的权重。我觉得这样挺好!本质上是我们拥有了更大的发言权,对吧?我认为开发人员就应该拥有更多的发言权。

我觉得这应该是最重要的一条设计原理了,因为它承认了你的权利,无论是设计一种格式,还是设计软件,这条原理保证了你的发言权。而这条原理也正道出了事物运行的本质。难道还不够明显吗?用户的权利大于作者,作者的权利大于实现者,实现者的权利大于标准制定者。然而,反观其他规范,比如XHTML2,你就会发现完全相反的做法。把追求理论的完满放在第一位,而把用户——需要忍受严格错误处理带来的各种麻烦的用户——放在了链条的最底端。我并没有说这种做法就是错误的,但我认为这是一种完全不同的思维方式。

因此,我认为无论你做什么,不管是构建像HTML5这样的格式,还是构建一个网站,亦或一个内容管理系统,明确你的设计原理都至关重要。

软件,就像所有技术一样,具有天然的政治性。代码必然会反映作者的选择、偏见和期望。

下面我们讲一个例子。Drupal社区曾联系马克·博尔顿(Mark Boulton)和丽莎·雷贺特(Leisa Reichilt)设计Drupal的界面。他们计划遵循一些设计原理。为此,他们并没有纸上谈兵,而是经过了一段时间的思考和酝酿,提出指导将来工作的4个设计原理:

简化最常见的任务,让不常见的任务不至于太麻烦。
只为80%设计。
给内容创建者最大的权利。
默认设置智能化。

实际上,我在跟马克谈到这个问题时,马克说主要还是那两个,即“只为80%设计。给内容创建者最大的权利。”这就很不错了,至少它表明了立场,“我们认为内容创建者比这个项目中的任何人都重要。”在制定设计原理时,很多人花了很多时间都抓不住重点,因为他们想取悦所有人。关键在于我们不是要取悦所有人,而是要明确哪些人最重要。他们认为内容创建者是最重要的。

另一条设计原理,只为80%设计,其实是一条常见的设计原理,也是一种通用模式,即帕累托原理(Pareto principle)。

帕累托是意大利经济学家,他提出这个比例,80/20,说的是世界上20%的人口拥有80%的财富。这个比例又暗合了自然界各个领域的幂律分布现象。总之,无论你是编写软件,还是制造什么东西,都是一样的,即20%的努力可以触及80%的用例。最后20%的用例则需要付出80%甚至更多的努力。因此,有时候据此确定只为80%设计是很合理的,因为我们知道为此只要付出20%的努力即可。

再比如,微格式同样也利用了帕累托原理,只处理常见用例,而没有考虑少数情形。他们知道自己不会让所有人都满意;而他们的目标也不是让所有人都满意。他们遵循的设计原理很多,也都非常有价值,但最吸引人的莫过于下面这条了:

首先为人类设计,其次为机器设计。

同样,你我都会觉得这是一条再明显不过的道理,但现实中仍然有不少例子违反了这条原理:容易让机器理解(解析)比容易让用户理解更重要。

所以,我认为平常多看一看别人推崇的设计原理,有助于做好自己手头的工作。你可以把自己认为有道理的设计原理贴在墙上。当然,你可以维护一个URL,把自己认为有价值的设计原理分享出来,就像Mozilla基金会那样,对不对,以下是Mozilla的设计原理:

Internet作为一种公共资源,其运作效率取决于互通性(协议、数据格式、内容)、变革及全球范围内的协作。
基于透明社区的流程有助于增进协作、义务和信任。

我觉得像这样的设计原理都非常好。而有了设计原理,我认为才更有希望设计出真正有价值的产品。设计原理是Web发展背后的驱动力,也是通过HTML5反映出来的某种思维方式。我想,下面这条原理你绝对不会陌生:

大多数人的意见和运行的代码。
对不对?这句话经常在我脑际回响,它囊括了Web的真谛,触及了HTML5的灵魂。

也许我该把这条原理打印出来贴到办公室的墙上,让它时刻提醒我,这就是Web的设计原理:大多数人的意见和运行的代码。

我想,今天的演讲就到这里了。如果大家有什么想法可以在twitter上通过@adactio找到我。有时候我也会在自己的博客,adactio.com上写写有关这个主题的文章。最后,可能还要顺便给我自己做个广告,我刚出了一本书,希望大家关注。

非常感谢大家。

[全文完]

程序员段子:等我敲完这行代码,就和你离婚!

工作是高端大气上档次,工资是低调奢华接地气!

我们叫做“程序猿”,也叫“攻城狮”!

但是往往城还没攻下来,我们的头发就先掉下来!

我们最喜欢听的一句话就是

图0:等我敲完这行代码,就和你离婚!

段子一

“等我敲完这行代码,就和你离婚!”

他头也不抬的说

听完之后,她心里暖暖的

她想,这可能是最长情的承诺

(因为深知永远敲不完代码)

–2017年度十大感动故事奖

段子二

“等我敲完这行代码,就陪你去吃饭”

听完之后,她的心拔凉拔凉的

她想,这可能是最婉转的分手了

(因为深知永远敲不完代码)

–2017年度十大感动故事提名奖

段子三

人生难得一知己,别人都不懂搞程序的我在想什么~终于有一天,我在山顶上与我的蓝颜知己相遇了。他光着头,穿着僧衣,莫名的,我两心心相惜。

我对他说:我放不下一些事,放不下一些人。

他说:这个世界上没有什么是放不下的。

我说:可我偏偏放不下。

他说:依我看,无非是你存储空间不足,要学会内部虚拟化,自然放得下。

我惊呼了,急忙问道:大师,你怎么这么懂。

他叹了一口气说:当初我就是不懂得内部虚拟化,费脑过多,头发掉光光,最后才来当和尚~

图1:等我敲完这行代码,就和你离婚!

段子四

一位搞程序的刚结婚不久,跟老板出去见客户,边喝酒边要说一些是人都听不懂的abcd代码,醉倒后不省人事。被抬回家后,老婆试着用各种方法给他醒酒,都无济于事,于是打电话询问他的同事。

同事说,我在现在做的系统发一个bug通知,突然老公手机短信微信邮箱钉钉同时震了,只见男人噌的一下从床上蹦起来,精神抖擞,大喊:“又TM的哪里有BUG”老公好惨!老公好辛苦!老公加油!

——致敬这一群拥有无与伦比的耐力、超越时代的智商、和横穿社会的苦逼

图2:等我敲完这行代码,就和你离婚!

段子五

男盆友是传说中的攻城狮,bug对他来说就是因为要修改,接着就是加班,然后就是没有夜生活~夜生活~夜生活~

图3:等我敲完这行代码,就和你离婚!

段子六

程序猿的世界你不懂~男朋友出差到上海做系统演讲,他说这是他长的这么大第一次走出广东省,屁溜溜地乘着火车滚了~途经长江,就想看到长江的宏伟壮大~而在程序猿的眼中,长江是这样滴~

图4:等我敲完这行代码,就和你离婚!

段子七

最烦男友说一句话就是:电脑在手,代码我有!

坐车就好好坐车,还发了图片证明自己坐车也不忘工作

一分钟不敲代码是不是就手痒,手痒我可以抽你

(我不就是想让你多休息休息)

图5:等我敲完这行代码,就和你离婚!

段子八

在IT这个行业里,程序猿都被称为“互联网精英”,其实他们是拿着“白菜”工资的“短命鬼”,正常上班时,他们在公司加班;正常休假时,他们在家加班,吃饭更没规律,只因他们要时时刻刻盯着,防着bug的骚扰。

连亲人和他见个面都要杀到机房才能见到他,不知道的人、不了解的人还以为你早出晚归,甚至夜不归宿是因为外面有“小五”

图6:等我敲完这行代码,就和你离婚!

做这行业的没有一个会脑呆,因为每天大脑都在高速运转,看不懂英语没关系,我看懂代码就知道上面那句翻译是什么,神思维~

段子九

记者问一个大爷:大爷,您保持亮丽的秘诀是什么?

大爷说:白天敲代码,晚上撸系统,姿势不要动,眼动手动就可以。

记者:啊?大爷您是做什么工作的?

大爷:敲代码的呀。

记者:那大爷您是本身就很喜欢光头的吗?

大爷:掉光的~

图7:等我敲完这行代码,就和你离婚!

段子十

如果你身边有搞程序的朋友,请多给他一点帮助。一天差不多有十几个小时坐在电脑前,保持一个姿势动也不动。有时间多带他出去见见溜溜,约他吃饭,喝酒吃肉各种消费时你来买单吧,不要跟他提钱了。

工作压力已经很大,请理解她、包容他、打牌也故意输给他。临走再塞个万儿八千的红包也行,让他感受到人间的温暖吧!请紧密陪伴他,生活是相互扶持的!不说了,前边有人扔鸡蛋!!!我要正面迎敌~

图8:等我敲完这行代码,就和你离婚!

看完以上的段子,你是不是感同身受,哭笑不得呢,虽然有一丢丢夸张搞笑的成分在,但是IT行业是真心不容易!每一个搞程序的人都是可亲可敬的超级英雄!

不可复制的“去IOE”

“IOE”并不当代表IBM、Oracle和EMC三家国际品牌的IT厂商,而是特指:“I”是IBM的缩写,指的是IBM小型机;“O”是Oracle的缩写,指的是Oracle数据库;“E”是EMC的缩写,指的是EMC存储设备。这里的“IOE”架构为针对传统行业企业关键应用而设计,基于向上扩展(Scale-up)技术高端设备以及围绕着它们开发的大型数据库和商业中间件。

对于绝大多数企业来说,不仅要了解自身的技术需求是否合适采用“去IOE”技术,还需要拥有庞大的技术团队,并具有自我试验的精神和决心,但这只是效仿阿里巴巴“去IOE”的必要不充分条件。

一旦企业用户效仿阿里巴巴选择分布式+自行开发开源系统,就意味着它将从此迈入孤独之旅,软件的开发将没有可以借鉴的经验,也没有战略合作伙伴。此外,貌似通过开源社区讨论对技术可控,但软件的可控性实际上要低于硬件的可控性,一旦开发核心人员发生变故,整套系统的开发成果都将有付诸东流的风险。

去IOE”到底是节省成本的命题还是成本转移的命题,也是值得企业用户推敲的。

最近,“去IOE”风声正劲。

阿里巴巴集团高调宣布今年“去IOE”成功,引发互联网行业甚至传统行业企业的热议:一、现在已经采用的IOE系统是否要效仿阿里巴巴进行替换?二、未来采用的系统是否不再优先考虑“IOE”架构?

企业用户要想获得这两大问题的个性化答案,其实还需要对这背后隐藏的若干潜在问题进行思路梳理。问题无外乎集中在如下几点:“去IOE”到底指的是什么?阿里巴巴为什么要“去IOE”?“去IOE”对于企业用户尤其是具有一定规模的企业来讲是否普遍适用?未来系统的选择是集中式还是分布式,商用系统还是开源系统?IOE系统的成本是否就高于非IOE系统,可控性是否就劣于后者?IBM、Oracle和EMC企业的产品是否代表的就是专有昂贵的集中式系统?

什么是“去IOE

当业界热议“去IOE”时,首先需要给“IOE”一个相对明确的定义。实际上,“IOE”并不当代表IBM、Oracle和EMC三家国际品牌的IT厂商,而是特指:“I”是代表IBM的缩写,指的是IBM小型机;“O”是Oracle的缩写,指的是Oracle数据库;“E”是EMC的缩写,指的是EMC存储设备。这里的“IOE”架构是针对传统行业企业关键应用而设计的,基于向上扩展(Scale-up)技术高端设备以及围绕着它们开发的大型数据库和商业中间件。

因此,如果将“去IOE”简单地理解成去掉三家国际品牌IT 厂商无疑是误读。这三家企业作为商用产品提供商,在互联网普遍推崇分布式与向外扩展(Scale-out)技术、开源软件、云服务中也一直处于活跃的态势。比如EMC的VMware是x86架构服务器云计算的基础,其公有云存储服务也开展得风生水起;开源分布式数据库MySQL实际上隶属于Oracle;IBM一直是开源软件的重要支持者与贡献者,其Power服务器也不再仅仅是拥有强大Scale-up能力的专有小型机的代名词。PowerLinux开始强调Scale-out分布式能力和对开放的系统的支持,而近期成立的OpenPOWER联盟更是开放了POWER内核IP授权,Google的加盟也使得Power未来在互联网行业的迅速推进成为可能。明年POWER8芯片的问世或将使得业界对Power服务器的变身刮目相看。

去IOE”的试验精神

阿里巴巴集团从2010年开始的“去IOE”运动耗时3年,经过1.7万名内部技术人员的努力,在今年高调宣布“去IOE”成功。据悉,除了支付宝完成了“去IE”目前依旧采用Oracle数据平台,阿里巴巴最大的现金流结算系统也完成了“去O”的工作,基本实现了“去IOE”的既定目标。

这里的一组数字值得关注,即耗时3年和1.7万名人员,阿里巴巴无疑将自身作为风险极高的“去IOE”创新试验品,下定决心才有了现在的成果。众所周知,在国外,Google、亚马逊等代表性互联网企业根本就不存在“去IOE”问题,因为它们构建系统之初从小规模起步日渐发展到超大规模,采用Scale-out的分布式系统是其“路径依赖”的结果。而“IOE”的系统架构则依据传统企业对IT的需求,基于Scale-up技术的高端设备以及围绕着它们开发的大型数据库和商业中间件。

阿里巴巴后来总结“去IOE”是“技术门槛很高、技术风险很大、水很深”的技术改革,敢冒如此风险的首要原因就是,考虑成本可控、技术可控等因素,不愿继续增加成熟商用系统以满足阿里巴巴特别是淘宝爆炸式业务增长的架构需求。由于其中的特殊性和特定性,这一过程虽然具有示范效应,但却有着太多不可复制的底层技术细节。比如互联网交易系统对数据一致性要求低于传统银行,但任何交易都存在数据复杂性与一致性的协调问题。因而虽然阿里巴巴采用分布式架构处理部分交易系统,但也需要对分布式开源数据库进行大量定制化改造。

一些具有一定技术规模的大型企业也曾经尝试“去IOE”,但在实施过程中出现技术反复,这其中甚至包括技术实力雄厚的电信运营商。因此绝大多数企业不仅要了解自身的技术需求是否合适采用“去IOE”技术,还需要拥有庞大的技术团队,并具有自我试验的精神和决心,但这只是“去IOE”的必要不充分条件。单凭这几点,企业效仿阿里巴巴将现在已经采用的“IOE”系统进行替换,就是风险极高的事。换句话说,阿里巴巴的“去IOE”运动是不可复制的。为此,多数企业对阿里巴巴“去IOE”运动思考落脚点开始集中在,未来将要采用的新系统是否不再优先考虑“IOE”系统?

去IOE”的实质

阿里巴巴为什么要“去IOE”?因为集中式部署很难适应互联网大规模应用对扩展性的要求,与其说是“去IOE”,更不如说其实质是分布式架构+开源系统替代了集中式架构+商用系统。

众所周知,IOE架构有效地支撑着绝大多数非互联网企业的关键业务。但大型企业自身技术的逐渐成熟,尤其是技术团队自主开发能力的增强,使得部分企业认为对大型IT厂商依赖过多,成本偏高,技术上逐渐产生依赖感,如何在未来新系统中实现技术可控与成本可控成为“去IOE”思想产生的重要原因。

分布式架构+开源系统是否就意味着技术可控值得推敲。原来的软件设计使得早期的IT系统强调单机可靠性和单机性能,但随着云计算的崛起,软件层面的可靠性、可扩展性设计降低了业务对底层服务器单机的可靠性和性能的要求。为此,IBM Power服务器也在不断变化。在拥有强大Scale-up技术的基础上,Power开始逐步淡化小型机形象,强调其在Scale-out上的分布式能力。实际上,IBM引以为傲的“Wston”系统就是由90台Power 750服务器构建的处理平台。而在最新一期HPC500强排行榜,就有16套Power系统上榜,其集群能力与x86服务器相比并无伯仲之分。

在开源系统和商用系统的博弈之中,企业需要考虑方法论问题,即需要考虑一个系统的功能性需求和非功能性需求。在企业新业务新系统的创新中容易首先考虑功能性需求,即创新的系统是否能够解决当下的问题。但当其满足需求之后,企业将很快面临非功能性需求的压力,即如何构建一套稳定的系统。有多少人员能够维护好开源系统,不断进行开源中的Bug修改,按照系统的新要求加入新的功能并不断优化。

这恰恰就是商用系统存在的价值,毕竟系统从特定条件下的“可用”到能够向其他企业推广商用,这其中的门槛很高,商用软件用户可以通过与商用系统厂商的战略合作,了解其他类似用户的做法,获取经验防患未然。因此,一旦企业用户效仿阿里巴巴选择分布式+自行开发开源系统,就意味着它将从此迈入孤独之旅,软件的开发将没有可以借鉴的经验,也没有战略合作伙伴。而且,貌似通过开源社区讨论对技术可控,但软件的可控性实际上要低于硬件的可控性,一旦开发核心人员发生变故,整套系统的开发成果都将有付诸东流的风险。

成本可控是“去IOE”的另一重要原因,但实际发生的也许只是成本的迁移。企业投资购置成熟高端设备和商用中间件,可以只关注业务的创新,而功能的实现、扩展、优化、安全的保障等都由商用系统厂商交付。如果企业采用低端分布式设备和自行开发开源软件,确实降低了初始投资,但却转移了成本。

阿里巴巴在“去IOE”中就谈到,原来只需要几十台小型机,现在却要面临数千台x86服务器,必须重新架构全新的运维体系把这种复杂性对上层进行“封装”。 如果企业此时又选择了自行开发开源软件,固然再次节省了软件投资,但实际成本又将转移到自身技术人员队伍的建设上,比如阿里巴巴就拥有1.7万人的庞大技术团队。因此,“去IOE”到底是节省成本还是成本转移,也是值得企业用户推敲的。

【编后语】

企业发展对信息技术的依赖性,信息技术手段的丰富和增强,促使企业有更多的技术方案和硬件平台选择,故去IOE化是信息化技术发展的趋势,也能提高企业信息化技术能力,也能降低信息化建设的成本,增强企业的技术研发实力和话语权,去IOE事件成为近年来行业内的热门话题。

若是企业选择去IOE的信息化建设道路,须弄清楚:去IOE概念,为什么要去IOE去IOE对企业投资成本的影响,那些业务场景适合非IOE技术架构,用什么技术和产品替代IOE平台,企业去IOE进程与力度等,换句话说企业必须弄清楚去IOE的本质和核心。

对于非互联网企业绝大多数情况下,IOE平台的时候会采购相应的维保服务,若是推动去IOE化,也会采购去IOE服务,不过比商业软硬件产品的维保费和License费用至少低40%。我们公司拥有一批互联网行业内实践过“去IOE”的技术精英,同时熟悉各行业的业务和流程,去IOE服务商-上海热璞网络科技有限公司愿为您企业的信息化建设保驾护航,企业文化:诚信是立足之本,口碑是客户认可的标志,技术、服务、信誉是核心竞争力。

 

33个网站足以使你成为一个天才

网络逐渐成为一个强有力的资源,利用网络资源每天都能学到新的知识。下面这些绝佳的网站绝对是你所需!

”我没有什么特殊的天赋,我只是保持一颗好奇心而已 “——爱因斯坦

抓住最佳的机会,利用下面的至少一个学习工具来使你成为更优秀的人。这里是一个新足以使你更加厉害的网站。

1.BBC — Future — 每天学习,足以使你更加聪明厉害。

2. 99U (YouTube) — 对生产力、组织和领导力的可行动的真知灼见,以帮助具有创造性的人实现他们的创意。

3. Youtube EDU — 这些教育片虽然没有可爱的猫在箱子中,但是它们解放了知识。

4. WikiWand — 一个具有华丽新界面的维基百科。

5. The long read (The Guardian) — 具有深度的报告、文章和资料。

6. TED —大量的视频,几乎包括了所有主题。

7. iTunes U —学习全球顶尖学府的公开课程。

8. CodeCloud—分享了很多有关编程学习和开发的干货和资源。

1-1poWxudGgu-zveMT7320jw

9. Cerego — 根据自己的特长和缺点制定适合自己的重获新知识的学习计划。

10. University of the People — 免费的网络大学,提供多种形式的高等教育课程。

11. OpenSesame — 在线培训市场,现在已经拥有22000多门课程。

12. CreativeLive — 参加全球顶尖专家的免费创意课程。

13. Coursera — 与美国顶尖大学一起合作,体统了大量的免费网络公开课程。

14. University of reddit — 免费主义的产品、知识共享的天堂。

15. Quora — 纯问答社区,汇聚了世界各领域的顶尖专家。

16. Digital Photography School— 提高摄影技巧的好网址。

17. Umano -探索真实世界的最大音频库。

Brain Pickings —很多对生活,艺术,科学,设计,历史,哲学等有远见的文章。

18. Peer 2 Peer University or P2PU, 一个开放教育项目,根据自己的节奏来学习。

19. MIT Open CourseWare 由MIT提供的免费在线课程目录和学习资源。

20. Gibbon— 这是学习的旗舰播放列表。

21. Investopedia — 了解你需要知道的投资、市场和个人金融的一切。

22. Udacity 提供交互式的在线课程和高等教育课程。

23. Mozilla Developer Network 为Web开发人员提供详细的文档和学习资源

24. Future learn — 学习来自世界顶级大学和顶尖专家的课程。

25. Google Scholar — 提供搜索文学、图书、论文摘要和论文的学术文献资源。

26. Brain Pump — 每天学习新技能的网站。

27. Mental Floss — 用有趣、脑筋急转弯、游戏、猜谜语等方式测试你的知识。

28. Learnist — 从专业网站、打印材料和视频内容学习。

29. DataCamp —在线R语言教程和数据科学课程。

30. edX — 参与全世界最好的大学的在线课程。

31. Highbrow —每天获取bite大小的课程

32. Coursmos — 任何时候、任何设备上学习微课程。

33. Platzi—直播流式的有关设计、市场和编程的课程。

    InsightfulQuestions (subreddit) — 具有启发性的问题讨论。

转:图灵的光环

原文地址:图灵的光环

仿佛全世界的人都知道,图灵(Alan Turing)是个天才,是他创造了计算机科学,是他破解了德国纳粹的Enigma密码。由于他的杰出贡献,计算机科学的最高荣誉,被叫做“图灵奖”。然而根据自己一直以来对图灵机等计算模型的看法,加上一些历史资料,我发现图灵本人的实际成就,相对于他所受到的崇拜,其实相差甚远。

由于二战以来各国政府对于当时谍报工作的保密措施造成的事实混淆,再加上图灵的不幸生世所引来的同情,图灵这个名字似乎拥有了一种扑朔迷离的光环。人们把很多本来不是图灵作出的贡献归结在他身上,把本来很平常的贡献过分地夸大。图灵的光环,掩盖了许多对这些领域做出过更加重要贡献的人。

图灵传

2012年,在图灵诞辰一百周年的时候,人们风风火火的召开各种大会,纪念这位“计算机之父”,很多媒体也添油加醋地宣传他的丰功伟绩。还有个叫Andrew Hodges的人,抓住这个时机推销自己写的一本传记,叫做《Alan Turing: The Enigma》。这本书红极一时,后来还被改编成了电影。

这本传记看似客观,引经据典,字里行间却可以感受到作者对图灵个人的膜拜和偏袒,他在倾心打造一个“天才”。作者片面地使用对图灵有利的证据,对不利的方面只字不提。仿佛图灵做的一切都是有理的,他做的不好的地方都是因为别人的问题,或者风水不好。提到别人做的东西,尽是各种缺陷和局限性,不是缺陷也要说成是缺陷;提到图灵的工作,总是史无前例,开天辟地的发明。别人先做出来的东西,生拉硬拽,硬要说成是受了图灵的“启发”,还怪别人没有引用图灵的论文。这让你感觉仿佛别人都在抄袭图灵伟大的研究成果,都在利用他,欺负他似的。如果你不想花钱买书,可以看看同一作者写的一个图灵简要生平,足以从中感受到这种倾向。

我写这篇文章的很大一部分原因,就是因为这本传记。作者对图灵贡献的片面夸大,对其他一些学者的变相贬低,让我感到不平。图灵在计算机界的名声,本来就已经被严重的夸大和美化,被很多人盲目的崇拜。现在出了这本传记和电影,又在人们心中加重了这层误解。所以我觉得有必要澄清一些事实,让人们不再被迷惑。

密码学

很多人提到二战Enigma密码的故事,就会把功劳一股脑地归到图灵头上,只字不提其他人。其实呢,破解Enigma密码是很多人共同努力的结果,图灵只是其中的一员。这些人缺少了任何一个,都可能是灾难性的后果。其中好些人的想法早于图灵,启发过图灵,贡献比图灵的大,设计的东西比图灵的先进,却很少有人听说过他们的名字。论智力和贡献,图灵在其中只是中等水平,最后说起来倒好像是他单枪匹马拯救了大家,这是不公平的。

最初破掉Enigma密码的,其实不是英国人,而是波兰人。波兰人不但截获并且仿造了德国人的Enigma机器,而且发现了其中微妙的漏洞,发明了一种用于解密的机器叫做BOMBA,以及一种手工破解的方法叫做Zygalski sheets。BOMBA可以在两个小时之内破解掉Enigma密码。波兰人一声不吭地窃听了德国人的通信长达六年半,最后在二战爆发前夕把这技术送给了英法盟友。

BOMBA的工作原理,其实就是模拟好几个Enigma机器,“并发”运转,这样可以加速猜出秘钥。最开头这样还行,但后来德国人改进了Enigma机器,把可选的齿轮从3个增加到了5个。5选3,有60种情况,这样秘钥的空间增大了60倍。理论上BOMBA只要运转60倍多的Enigma机器,就可以破解这增大的解空间,然而那已经超出了波兰的物资和人力。再加上德国人就要打过去,所以波兰只好请英法盟友帮忙。

图灵最重要的贡献,就是改进波兰人的BOMBA,设计了一个更好的机器叫BOMBE。BOMBE比起BOMBA,其实并没有质的飞跃,只不过BOMBE同时模拟的Enigma机器更多,转的更快。另外它加入了一些“优化”措施,尽早排除不可行的路径,所以速度快很多。图灵最初的设计,要求必须能够事先猜出很长的文本,所以基本不能用。后来Gordon Welchman发明了一种电路,叫做diagonal board,才使Bombe能够投入实用。关于Gordon Welchman的故事,你可以参考这个BBC纪录片

在Bombe能够投入使用之前,有一个叫John Herivel的人,发现了一种特殊的技巧,叫做Herivel tip,这种技术在Bombe投入使用之前几个月就已经投入实用,破解掉很多德军的消息,立下汗马功劳。如果Herivel tip没有被发明,盟军可能在1940年5月就已经战败,BOMBE也就根本没机会派上用场。

同时在Bletchley Park,还诞生了一台大型可编程电子计算机Colossus,它是由一个叫Tommy Flowers的工程师设计的。Colossus不是用来破解Enigma密码的,而是用于破解Lorenz SZ-40。那是一种比Enigma还要先进的密码机器,用于发送希特勒的最高指令。

德国人后来又改进了他们的通信方式,使用了一种具有四个齿轮的Enigma机器。这大大的增加了破解的难度,普通的Bombe机器也破不了它了。后来是Harold Keen设计了一个叫做Mammoth的机器,后来加上美国海军的帮助,制造了更快的Bombe,才得以破解。

所以你看到了,所有这些人的工作加起来,才改善了二战的局面。波兰人的BOMBA,已经包含了最重要的思想。图灵的工作其实更多是量的改进,而不是质的飞跃。现在很多人喜欢跟风,片面的夸大图灵在其中的作用,这是不对的。如果你对Enigma机器的技术细节感兴趣,可以参考这两个视频:[视频1][视频2]。

理论计算机科学

图灵被称为“计算机之父”,计算机科学界的最高荣誉,被叫做“图灵奖”(Turing Award)。然而如果你深入的理解了计算理论和程序语言理论就会发现,图灵对于理论计算机科学,其实并没产生长远而有益的影响。在某种程度上说,他其实帮了一个倒忙。图灵的理论复杂不堪,给人们造成很大的误导,阻碍了计算机科学的发展。而且他对于发表论文,对待研究的态度让我怀疑,我觉得图灵本人其实就是当今计算机学术界的一些不正之风的鼻祖。

图灵机和lambda演算

绝大部分计算机专业的人提到图灵,就会想起图灵机(Turing Machine)。稍微有点研究的人,可能知道图灵机与lambda演算在计算能力上的等价性。然而在“计算能力”上等价,并不等于说它们具有同样的价值,随便用哪个都无所谓。科学研究有一条通用的原则:如果多个理论可以解释同样的现象,取最简单的一个。虽然lambda演算和图灵机能表达同样的理论,却比图灵机简单,优雅,实用很多。

计算理论(Theory of Computation)这个领域,其实是被图灵机给复杂化了。图灵机的设计是丑陋,复杂,缺乏原则的。它的读写头,纸带,状态,操作,把本来很简单的语义搞得异常复杂。图灵机的读写两种操作同时发生,这恰好是编程上最忌讳的一种错误,类似于C语言的i++。图灵机是如此的复杂和混淆,以至于你很难看出它到底要干什么,也很难用它清晰地表达自己的意思。这就是为什么每个人上“计算理论”课程,都会因为图灵机而头痛。如果你挖掘一点历史,也许会发现图灵机的原型,其实是图灵母亲使用的打字机。用一台打字机来建模所有的计算,这当然是可行的,然而却复杂不堪。

相比之下,lambda演算更加简单,优雅,实用。它是一个非常有原则的设计。Lambda演算不但能清晰地显示出你想要表达的意思,而且有直接的“物理实现”。你可以自然的把一个lambda演算表达式,看成是一个电子线路模块。对于现实的编程语言设计,系统设计,lambda演算有着巨大的指导和启发意义。以至于很多理解lambda演算的人都搞不明白,图灵机除了让一些理论显得高深莫测,还有什么存在的意义。

历史的倒退

图灵机比起lambda演算来说,其实是一个历史的倒退。1928年,Alonzo Church发明了lambda演算(当时他25岁)。Lambda演算被设计为一个通用的计算模型,并不是为了解决某个特定的问题而诞生的。1929年,Church成为普林斯顿大学教授。1932年,Church在Annals of Mathematics发表了一篇论文,纠正逻辑领域里几个常见的问题,他的论述中用了lambda演算。1935年,Church发表论文,使用lambda演算证明基本数论中存在不可解决的问题。1936年4月,Church发表了一篇两页纸的“note”,指出自己1935年那篇论文可以推论得出,著名的Hilbert“可判定性问题”是不可解决的。

1936年5月,当时还在剑桥读硕士的图灵,也写了一篇论文,使用自己设计的一种“计算机器”(后来被叫做图灵机)来证明同一个问题。图灵的论文投稿,比Church最早的结论发表,晚了整整一年。编辑从来没见过图灵机这样的东西,而且它纷繁复杂,远没有lambda演算来得优雅。就像所有人对图灵机的第一印象一样,编辑很难相信这打字机一样的操作方式,能够容纳“所有的计算”。他无法确定图灵的论述是否正确,只好找人帮忙。Church恐怕是当时世界上唯一能够验证图灵的论文正确性的人。所以一番好心之下,编辑写了封信给Church,说:“这个叫图灵的年轻人很聪明,他写了一篇论文,使用一种机器来证明跟你一样的结果。他会把论文寄给你。如果你发现他的结果是正确的而且有用,希望你帮助他拿到奖学金,进入Princeton跟你学习。”

图灵就是这样成为了Church的学生,然而图灵心高气傲,恐怕从来没把Church当成过老师,反倒总觉得Church抢先一步,破坏了自己名垂青史的机会。跟Church的其它学生不一样,图灵没能理解lambda演算的精髓,却认为自己的机器才是最伟大的发明。进入Princeton之后,图灵不虚心请教,只是一心想发表自己的论文,想让大家对自己的“机器”产生兴趣,结果遭到很大的挫折。当然了,一个名不见经传的人,做了个怪模怪样的机器,说它可以囊括宇宙里所有的计算,你不被当成民科才怪呢!

1937年,在Church的帮助下,图灵的那篇论文(起名为《Computable Numbers》)终于发表了。Church还是很器重图灵的,他把图灵的机器叫做“图灵机”。不幸的是,论文发表之后,学术界对此几乎没有任何反响,只有两人向图灵索取这篇论文。图灵当然不爽了,于是后来就到处推销自己的图灵机,想让大家承认那是伟大的发明。有了一个锤子,看什么都是钉子。后来每到一个地方,每做一个项目(见下一节),他都想把问题往自己那篇论文和图灵机上靠,东拉西扯的想证明它的价值,甚至称别人发明的东西全都是受到了图灵机的启发…… 经过人们很长的时间的以讹传讹之后,他终于成功了。

图灵当年的作法,其实跟当今计算机学术界的普遍现象差不多。我想发表自己的想法A,结果别人已经发表了B,解决了A要解决的问题,而且还比A简单和清晰。怎么办呢?首先,我声明自己从没看过B的论文,这样就可以被称为“独立的发现”。然后,我证明A和B在“本质”上是等价的。最后,我东拉西扯,挖掘一下B的局限性,A相对于B在某些边沿领域的优势…… 这样反复折腾,寻找A的优势,总有一天会成功发表的。一旦发表成功,就会有人给我唱高调,没用的东西也要说成是有用的。他们会在A的基础上发展他们自己的东西,最后把我推崇为大师。那发表更早,更简单优美的B,也就无人问津了。胜利!

现在不得不说一下《图灵传》对此的歪曲。Church的论文发表,比图灵的论文投稿还早一年,而且Church使用了比图灵机更简单优雅的计算模型。Church的成果本来天经地义应该受到更多的尊重,到头来作者却说:“…and Turing was robbed of the full reward for his originality”(见第3节“The Turing machine”)。让人感觉貌似是Church用不正当的手段“抢走”了图灵的“原创性”一样。你本来没有什么原创性,还丑陋复杂,所以何谈抢走呢?我怎么觉得恰恰相反,其实是图灵抢走了Church的原创性?现在提起Hilbert可判定性问题,可计算性理论,人们都想起图灵,有谁还想得起Church,有谁知道他是第一个解决了这问题的人,有谁知道他用了更优美的办法?

Lambda演算与计算理论

由于图灵到处推销自己的理论,把不好的东西说成是好的,把别人发明的机器硬往自己的理论上面靠,说他们受到了图灵机的“启发”,以至于很多人被蛊惑,以为它比起lambda演算确实有优势。再加上很多人为了自己的利益而以讹传讹,充当传教士,这就是为什么图灵机现在被人们普遍接受作为计算模型。然而这并不能改变它丑陋和混淆的本质。图灵机的设计,其实是专门为了证明Hilbert的可判定性问题不可解决,它并不是一个用途广泛的计算模型。图灵机之所以被人接受,很大部分原因在于人的无知和愚蠢。很多人(包括很多所谓“理论计算机科学家”)根本没好好理解过lambda演算,他们望文生义,以为图灵机是“物理的”,实际可用的“机器”,而lambda演算只是一个理论模型。

事实恰恰相反:lambda演算其实非常的实用,它的本质跟电子线路没什么两样。几乎所有现实可用的程序语言,其中的语义全都可以用lambda演算来解释。而图灵机却没有很多现实的意义,用起来非常蹩脚,所以只能在计算理论中作为模型。另外一个更加鲜为人知的事实是:lambda演算其实在计算理论方面也可以完全取代图灵机,它不但可以表达所有图灵机能表达的理论,而且能够更加简洁和精确地表达它们。

很多理论计算机科学家喜欢用图灵机,仿佛是因为用它作为模型,能让自己的理论显得高深莫测,晦涩难懂。普通的计算理论课本,往往用图灵机作为它的计算模型,使用苦逼的办法推导各种可计算性(computability)和复杂性(complexity)理论。特别是像Michael Sipser那本经典的计算理论教材,晦涩难懂,混淆不堪,有时候让我都怀疑作者自己有没有搞懂那些东西。

后来我发现,其实图灵机所能表达的理论,全都可以用更加简单的lambda演算(或者任何一种现在流行的程序语言)来表示。图灵机的每一个状态,不过对应了lambda演算(或者某种程序语言)里面的一个“AST节点”,然而用lambda演算来表示那些计算理论,却可以比图灵机清晰和容易很多。在Indiana大学做计算理论课程助教的时候,我把这种思维方式悄悄地讲述给了上课的学生们,他们普遍表示我的这种思维方式更易理解,而且更加贴近实际的编程。

举一个很简单的例子。我可以用一行lambda演算表达式,来显示Hilbert的“可判定性问题”是无解的:

Halting(λm.not(Halting(m,m)), λm.not(Halting(m,m)))

完整的证明不到一页纸,请看我的另外一篇文章(英文)。这也就是图灵在他的论文里,折腾了十多页纸证明的东西。

我曾经以为自己是唯一知道这个秘密的人,直到有一天我把这个秘密告诉了我的博士导师,Amr Sabry。他对我说:“哈哈!其实我早就知道这个,你可以参考一下Neil Jones写的一本书,叫做《Computability and Complexity: From a Programming Perspective》。这本书现在已经可以免费下载

此书作者用一种很简单的程序语言,阐述了一般人用图灵机来描述的那些理论(可计算性理论,复杂性理论)。他发现用程序语言来描述计算理论,不但简单直接,清晰明了,而且在某些方面可以更加精确地描述图灵机无法描述的定理。得到这本书,让我觉得如获至宝,原来世界上有跟我看法如此相似,对事物洞察力如此之高的人!

在一次会议上,我有幸地遇到了Neil Jones,跟他切磋思想。当提到这本书的模型与图灵理论的关系,老教授谦虚地说:“图灵的模型还是有它的价值的……” 然而到最后,他其实也没能说清楚这价值何在。我心里很清楚,他只是为了避免引起宗教冲突,或者避免显得狂妄自大,而委婉其词。眼前的这位教授,虽然从来没有得过图灵奖,很少有人听说过他的名字,然而他对于计算本质的理解,却比图灵本人还要高出很多。

总的说来,图灵机也许不是一文不值,然而由于lambda演算可以更加清晰地解释图灵机能表示的所有理论,图灵机的价值相对来说几乎为零。Church在1937年给图灵论文写的Review指出,图灵机的优势,在于它可以让不懂很多数学,不理解lambda演算之类理论的人也可以看得懂。我怎么觉得图灵机对于不懂很多数学的人,理解起来其实更加痛苦呢?而且就算它真的对“外行”或者“笨人”的理解有好处,这价值貌似也不大吧?:P

电子计算机

很多“理论计算机科学家”喜欢说,大家现在用的计算机,只不过是一个“Universal Turing Machine”。就算你根本不知道图灵是谁,自己辛苦设计出一个机器或者语言,他们总喜欢说:“是图灵启发了你,因为你那东西是跟图灵机等价的,是图灵完备的……”

那么现在让我们来看看,图灵本人和他的理论,真正对电子计算机的发展起过多大的作用吧。如果一个人对一个行业起过重大的作用,那我们可以说“没有他不行”。然而事实却是,即使没有图灵,计算机技术会照样像今天一样发展,丝毫不会受到影响。看一看历史,你也许会惊讶的发现,图灵的理论不但没能启发任何计算机的设计,而且图灵亲自设计的唯一一个计算机(ACE),最后也以悲惨的失败告终。

什么是Universal Turing Machine(UTM)

ACE失败的一个重要原因,是因为图灵过度的看重他自己发明的Universal Turing Machine(UTM)。所以我想首先来解密一下,这个被很多人吹得神乎其神的,似乎什么都可以往上面扯的UTM,到底是什么东西。

说白了,UTM就是一个解释器,就像Python或者JavaScript的解释器一样。计算机的处理器(CPU)也是一个解释器,它是用来解释机器指令的。那这样说来,任何可编程,具有指令集的机器都是UTM了,所以图灵的理论启发了所有这些机器?你尽管跟我扯吧 :)

你应该知道,在图灵的UTM出现以前,Church的lambda演算里面早就有解释器的概念了,所以UTM根本不是什么新东西,而且它比起lambda演算的解释器,真是丑陋又复杂。而Church其实也不是第一个提出解释器这概念的人,像这类通用的概念,已经很难追溯是谁“发明”的了。也许并不是某一个人发明了它,而是历史上的很多人。

解释器这个概念的涵义实在是包罗万象,几乎无处不在。只要是“可编程”的机器,它本质上必然包含一个解释器。一个工程师在不知道解释器这概念的情况下,照样很有可能“不小心”设计出一个可编程的机器,所以如果你把这些全都归结成图灵或者Church的功劳,就太牵强了。

图灵与ACE的故事

事实上,最早的电子计算机,并不是图灵设计的,而是电子工程师跟其他一些数学家合作的结果。根据老一辈工程师的叙述,图灵的工作和理论,对于现实的电子计算机设计,几乎没有任何的正面作用。很多工程师其实根本不知道图灵是谁,图灵机是什么。他们只是根据实际的需求,设计和制造了那些电路。这就是为什么我们今天看到的电子计算机,跟图灵机或者图灵的其他理论几乎完全不搭边。

世界上最早的两台电子计算机,ENIAC和EDVAC,都是美国人设计制造的。其中EDVAC的设计报告,是冯诺依曼(von Neumann)参与并签署的。提到EDVAC的设计,《图灵传》有一段有趣的介绍,它基本是这样说的:“冯诺依曼在Princeton的时候,很了解图灵开天辟地的发明—UTM。UTM只有一根纸带,而EDVAC把指令和数据放在同一个存储空间,所以EDVAC的设计肯定是受了UTM的启发。然而EDVAC的设计报告,却只字不提图灵和UTM的名字,更没有引用图灵划时代的论文《Computable Numbers》……”

这其实是在含沙射影的说,冯诺依曼和EDVAC团队抄袭了图灵的研究成果。照这种歪理,我洗衣服的时候,袜子和内裤放在同一个桶里洗,也是受了图灵的启发了,就因为UTM只有一条纸带?这世界上的事物,还有啥不是受了UTM启发的?这让我想起某些全靠打专利官司赚钱的公司(patent troll)…… 冯诺依曼作为一代数学大师,比UTM重大的研究成果多得是了,他会在乎抄袭图灵的东西吗?其实人家恐怕是根本没把图灵和他的论文当回事。而且其他人(比如Church)早就有跟UTM等价的想法,而且还更好,更简单。之前抢了Church的风头,现在居然欺到冯诺依曼头上来了。哎,真受不了这种一辈子只想出过一个点子的人……

所以听说美国人造出了EDVAC,图灵开始各种羡慕嫉妒恨,感叹自己英才无用武之地。终于有一天,他的机会来了。在EDVAC诞生几个月之后,英国国家物理实验室(NPL)联系了图灵。他们想赶上美国的计算机技术发展,所以想招募图灵,让他帮忙山寨一个EDVAC的“英国特色版本”。图灵设计的机器叫做ACE(Automatic Computing Engine)。最初,图灵给NPL一个很宏伟的蓝图:ACE可以如此的强大,以至于整个英国只需要这样一台计算机就够了,我们可以把它叫做“英国国家计算机”…… 然而再大的口号,也难逃脱现实的检验,ACE项目最终以失败告终。

《图灵传》把ACE失败的责任,推托到NPL和其它人的“近视”和“官僚”,然而ACE失败的主要责任,其实在于图灵自己:他完全没有设计一台现实的计算机的基本技能,却总是自以为是,设立高大空的目标。图灵的设计跟当时(包括现在的)所有实用的计算机都有巨大的差别。不出你所料,他最初的设计思路,是根据自己之前的论文《Computable Numbers》里提到的“Universal Turing Machine”,不过从中去掉了一些不实际的设计,比如用一根纸带来存储数据。这一点改进貌似做对了,可是呢,他又加入了一些让工程师们无语的设计,美其名曰“极简设计”(minimalism)。比如,ACE的硬件只提供AND, OR, NOT之类的逻辑运算作为“基本操作”,其它的算数操作,包括加减乘除,全部用代码来实现。图灵大师啊,你知不知道有一种重要的指标,叫做“效率”?

这还不算…… 后来他更加异想天开,终于扯上了“思考机器”(thinking machines)—他想让ACE成为可以像人一样思考的机器,还想让这机器能够自己写自己的代码。按照图灵的原话:“在ACE的工作中,我对人脑建模的兴趣,比实际的计算应用更感兴趣。” 他显然已经把ACE当成了自己一个人的玩具,而不再是解决人们实际需求的工具。只要有人反对这想法,他就会嘲笑说,你是怕我的机器太聪明了,抢了你的饭碗吧?其实图灵对于实际的人脑工作原理所知甚少,基本处于初中生理卫生课本水平,然而他总喜欢对人说,人脑不过就是一个UTM。看吧,它有输入,输出,状态转换,就跟UTM一样…… 所谓“图灵测试”(Turing Test),就是那时候提出来的。当然了,因为他扯到了“thinking machine”,就有后人把他称为人工智能(AI)的鼻祖。其实呢,图灵测试根本就不能说明一个机器具有了人的智能,它只是在测试一些肤浅的表象。后来,“thinking machines”成为了一种通用的幌子,用于筹集大笔科研经费,最后全都血本无归。

图灵设计了这机器,NPL当时却没有能力制造它。于是他们求助于另外两位实现过计算机的工程师:F. C. WilliamsMaurice Wilkes(后来EDSAC计算机的设计者),请他们帮忙实现图灵的设计。可想而知,Williams和Wilkes都表示不喜欢ACE的设计,而且指出图灵的性格与自己的研究风格不匹配,不愿跟他合作,所以双双拒绝了NPL的邀请。最后,NPL新成立了一个电子部门,ACE的工程终于可以开始。然而,根据资深工程师们的讨论,觉得图灵提出的制造一个“电子人脑”和“智能机器”,并不是实际可行,或者在短期之内能派上用场的项目,所以决定做一些实际点的事情。图灵对此非常恼火,各种抱怨,说别人官僚啊,近视啊,没想象力啊之类的,然后开始公开的抵制NPL的决定。

最后工程师们和管理层都受不了他了,鉴于他名声在外,又不好意思开掉他,只好提出一个破天荒的提议:由NPL资助,让图灵回到剑桥大学去度年假(sabbatical),做一些纯数学的研究。于是ACE在图灵不在的情况下,终于开工了⋯⋯1950年,ACE运行了它的第一个程序。然而工程师们实现的ACE,完全偏离了图灵的设计,以至于实际的机器和图灵的设计之间,几乎没有任何相似性。一年之后,图灵还想回到NPL,继续影响ACE的设计,然而NPL的领导们却建议他继续留在大学里做纯理论的研究,并且让曼彻斯特大学给他一个职位。最后图灵接受了这个建议,这下大家伙儿都松了一口气…… 😛

图灵设计的唯一一个计算机ACE,终究以图灵完全退出整个项目而告终。今天回头看来,如果当时图灵留下来了,NPL真的按照图灵的意思来做,ACE恐怕直到今天都造不出来。由于图灵不切实际的设计和高傲的性格,NPL失去了最优秀的人的帮助。1949年,Maurice Wilkes按照EDVAC的思路,成功制造了EDSAC,速度是ACE的两倍以上,而且更加实用。所以你看到了,图灵并不是一个实干家,他的双脚飘在半空中。他的理论,他设计的机器,他的代码,全都停留在纸上。他并没有帮助造出任何一台实际可用的计算机,他对计算机的工程实现几乎没有任何有益的影响。可惜的是,有些人喜欢把实干家们千辛万苦造出来,真正可以用的东西,牵强附会地归功于某些高谈阔论的理论家,仿佛那是理论家的功劳似的。这也许就是为什么图灵被他们称为“计算机之父”吧。

如果对ACE和其它早期计算机感兴趣,你可以参考一下更详细的资料。你也可以看一看《图灵传》,虽然它观点荒唐,对图灵各种偏袒,然而图灵和其他人的通信,基本的史实,他应该不好意思篡改。

总结

我说这些是为了什么呢?我当然不是想否认图灵所做出的贡献。像许多的计算机工作者一样,他的某些工作当然是有意义的。然而那种意义并不像很多人所吹嘘的那么伟大,它们甚至不包含很多的创新。

我觉得很多后人给图灵带上的光环,掩盖了太多其它值得我们学习和尊敬的人,给人们对于计算机科学的概念造成了误导。计算机科学不是图灵一个人造出来的,图灵并不是计算机科学的鼻祖,他甚至不是在破解Enigma密码和电子计算机诞生过程中起最重要作用的人。

许许多多的计算机科学家和电子工程师们,是他们造就了今天的计算科学。他们的聪明才智和贡献,不应该被图灵的光环所掩盖,他们应该受到像跟图灵一样的尊敬。希望大家不要再神化图灵,不要再神化任何人。不要因为膜拜某些人,而失去向另一些人学习的机会。

关于烂代码的那些事 - 为什么每个团队存在大量烂代码

秦迪,微博研发中心技术专家,2013 年加入微博,负责微博平台通讯系统的设计和研发、微博平台基础工具的开发和维护,并负责微博平台的架构改进工作,在工作中擅长排查复杂系统的各类疑难杂症。爱折腾,喜欢研究从内核到前端的所有方向,近几年重点关注大规模系统的架构设计和性能优化,重度代码洁癖:以 code review 为己任,重度工具控:有现成工具的问题就用工具解决,没有工具能解决的问题就写个工具解决。业余时间喜欢偶尔换个语言写代码放松一下。

“一个人工作了几年、做过很多项目、带过团队、发了一些文章,不一定能代表他代码写的好;反之,一个人代码写的好,其它方面的能力一般不会太差。” —— 秦迪

最近写了不少代码,review 了不少代码,也做了不少重构,总之是对着烂代码工作了几周。为了抒发一下这几周里好几次到达崩溃边缘的情绪,我决定写一篇文章谈一谈烂代码的那些事。这里是上篇,谈一谈烂代码产生的原因和现象。

1、写烂代码很容易

刚入程序员这行的时候经常听到一个观点:你要把精力放在 ABCD(需求文档/ 功能设计/架构设计/理解原理)上,写代码只是把想法翻译成编程语言而已,是一个没什么技术含量的事情。

当时的我在听到这种观点时会有一种近似于高冷的不屑:你们就是一群傻 X,根本不懂代码质量的重要性,这么下去迟早有一天会踩坑,呸。

可是几个月之后,他们似乎也没怎么踩坑。而随着编程技术一直在不断发展,带来了更多的我以前认为是傻 X 的人加入到程序员这个行业中来。

语言越来越高级、封装越来越完善,各种技术都在帮助程序员提高生产代码的效率,依靠层层封装,程序员真的不需要了解一丁点技术细节,只要把需求里的内容逐行翻译出来就可以了。

很多程序员不知道要怎么组织代码、怎么提升运行效率、底层是基于什么原理,他们写出来的是在我心目中一堆垃圾代码。但是那一坨垃圾代码竟然能正常工作。

即使我认为他们写的代码是垃圾,但是从不接触代码的人的视角来看(比如说你的 boss),代码编译过了,测试过了,上线运行了一个月都没出问题,你还想要奢求什么?

所以,即使不情愿,也必须承认,时至今日,写代码这件事本身没有那么难了。

2、烂代码终究是烂代码

但是偶尔有那么几次,写烂代码的人离职了之后,事情似乎又变得不一样了。

想要修改功能时却发现程序里充斥着各种无法理解的逻辑、改完之后莫名其妙的 bug 一个接一个,接手这个项目的人开始漫无目的的加班,并且原本一个挺乐观开朗的人渐渐的开始无法接受了。

我总结了几类经常被鄙视到的烂代码:

2.1 意义不明

能力差的程序员容易写出意义不明的代码,他们不知道自己究竟在做什么。

就像这样:

public void save() {
    for(int i=0;i<100;i++) {
        //防止保存失败,重试100次
        document.save(); 
    }
}

对于这类程序员,建议他们尽早转行。

2.2 不说人话

不说人话是新手最经常出现的问题,直接的表现就是写了一段很简单的代码,其他人却看不懂。

比如下面这段:

public boolean getUrl(Long id) {
    UserProfile up = us.getUser(ms.get(id).getMessage().aid);
    if (up == null) {
        return false;
    }
    if (up.type == 4 || ((up.id >> 2) & 1) == 1) {
        return false;
    } 
    if(Util.getUrl(up.description)) {
        return true;
    } else {
        return false;
    }
}

还有很多程序员喜欢复杂,各种宏定义、位运算之类写的天花乱坠,生怕代码让别人一下子看懂了会显得自己水平不够。
很多程序员喜欢简单的东西:简单的函数名、简单的变量名、代码里翻来覆去只用那么几个单词命名;能缩写就缩写、能省略就省略、能合并就合并。这类人写出来的代码里充斥着各种 g/s/gos/of/mss 之类的全世界没人懂的缩写,或者一长串不知道在做什么的连续调用。

简单的说,他们的代码是写给机器的,不是给人看的。

2.3 不恰当的组织

不恰当的组织是高级一些的烂代码,程序员在写过一些代码之后,有了基本的代码风格,但是对于规模大一些的工程的掌控能力不够,不知道代码应该如何解耦、分层和组织。

这种反模式的现象是经常会看到一段代码在工程里拷来拷去;某个文件里放了一大坨堆砌起来的代码;一个函数堆了几百上千行;或者一个简单的功能七拐八绕的调了几十个函数,在某个难以发现的猥琐的小角落里默默的调用了某些关键逻辑。

这类代码大多复杂度高,难以修改,经常一改就崩;而另一方面,创造了这些代码的人倾向于修改代码,畏惧创造代码,他们宁愿让原本复杂的代码一步步变得更复杂,也不愿意重新组织代码。当你面对一个几千行的类,问为什么不把某某逻辑提取出来的时候,他们会说:

“但是,那样就多了一个类了呀。”

2.4.假设和缺少抽象

相对于前面的例子,假设这种反模式出现的场景更频繁,花样更多,始作俑者也更难以自己意识到问题。比如:

public String loadString() {
    File file = new File("c:/config.txt");
    // read something
}

文件路径变更的时候,会把代码改成这样:

public String loadString(String name) {
    File file = new File(name);
    // read something
}

需要加载的内容更丰富的时候,会再变成这样:

public String loadString(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}

之后可能会再变成这样:


public String loadString(String name) {
    File file = new File(name);
    // read something
}
public String loadStringUtf8(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}
public String loadStringFromNet(String url) {
    HttpClient ...
}
public Integer loadIntFromNet(String url) {
    HttpClient ...
}
public String loadString(String name) {
    File file = new File(name);
    // read something
}
public String loadStringUtf8(String name) {
    File file = new File(name);
    // read something
}
public Integer loadInt(String name) {
    File file = new File(name);
    // read something
}
public String loadStringFromNet(String url) {
    HttpClient ...
}
public Integer loadIntFromNet(String url) {
    HttpClient ...
}

这类程序员往往是项目组里开发效率比较高的人,但是大量的业务开发工作导致他们不会做多余的思考,他们的口头禅是:“我每天要做 XX 个需求”或者“先做完需求再考虑其他的吧”。

这种反模式表现出来的后果往往是代码很难复用,面对 deadline 的时候,程序员迫切的想要把需求落实成代码,而这往往也会是个循环:写代码的时候来不及考虑复用,代码难复用导致之后的需求还要继续写大量的代码。

一点点积累起来的大量的代码又带来了组织和风格一致性等问题,最后形成了一个新功能基本靠拷的遗留系统。

2.5 还有吗

烂代码还有很多种类型,沿着功能-性能-可读-可测试-可扩展这条路线走下去,还能看到很多匪夷所思的例子。

那么什么是烂代码?个人认为,烂代码包含了几个层次:

  • 如果只是一个人维护的代码,满足功能和性能要求倒也足够了。
  • 如果在一个团队里工作,那就必须易于理解和测试,让其它人员有能力修改各自的代码。
  • 同时,越是处于系统底层的代码,扩展性也越重要。

所以,当一个团队里的底层代码难以阅读、耦合了上层的逻辑导致难以测试、或者对使用场景做了过多的假设导致难以复用时,虽然完成了功能,它依然是垃圾代码。

2.6 够用的代码

而相对的,如果一个工程的代码难以阅读,能不能说这个是烂代码?很难下定义,可能算不上好,但是能说它烂吗?如果这个工程自始至终只有一个人维护,那个人也维护的很好,那它似乎就成了“够用的代码”。

很多工程刚开始可能只是一个人负责的小项目,大家关心的重点只是代码能不能顺利的实现功能、按时完工。

过上一段时间,其他人参与时才发现代码写的有问题,看不懂,不敢动。需求方又开始催着上线了,怎么办?只好小心翼翼的只改逻辑而不动结构,然后在注释里写上这么实现很 ugly,以后明白内部逻辑了再重构。

再过上一段时间,有个相似的需求,想要复用里面的逻辑,这时才意识到代码里做了各种特定场景的专用逻辑,复用非常麻烦。为了赶进度只好拷代码然后改一改。问题解决了,问题也加倍了。

几乎所有的烂代码都是从“够用的代码”演化来的,代码没变,使用代码的场景发生变了,原本够用的代码不符合新的场景,那么它就成了烂代码。

3、重构不是万能药

程序员最喜欢跟程序员说的谎话之一就是:现在进度比较紧,等 X 个月之后项目进度宽松一些再去做重构。

不能否认在某些(极其有限的)场景下重构是解决问题的手段之一,但是写了不少代码之后发现,重构往往是程序开发过程中最复杂的工作。花一个月写的烂代码,要花更长的时间、更高的风险去重构。

曾经经历过几次忍无可忍的大规模重构,每一次重构之前都是找齐了组里的高手,开了无数次分析会,把组内需求全部暂停之后才敢开工,而重构过程中往往哀嚎遍野,几乎每天都会出上很多意料之外的问题,上线时也几乎必然会出几个问题。

从技术上来说,重构复杂代码时,要做三件事:理解旧代码、分解旧代码、构建新代码。而待重构的旧代码往往难以理解;模块之间过度耦合导致牵一发而动全身,不易控制影响范围;旧代码不易测试导致无法保证新代码的正确性。

这里还有一个核心问题,重构的复杂度跟代码的复杂度不是线性相关的。比如有 1000 行烂代码,重构要花 1 个小时,那么 5000 行烂代码的重构可能要花 2、3 天。要对一个失去控制的工程做重构,往往还不如重写更有效率。

而抛开具体的重构方式,从受益上来说,重构也是一件很麻烦的事情:它很难带来直接受益,也很难量化。这里有个很有意思的现象,基本关于重构的书籍无一例外的都会有独立的章节介绍“如何向 boss 说明重构的必要性”。

重构之后能提升多少效率?能降低多少风险?很难答上来,烂代码本身就不是一个可以简单的标准化的东西。

举个例子,一个工程的代码可读性很差,那么它会影响多少开发效率?

你可以说:之前改一个模块要 3 天,重构之后 1 天就可以了。但是怎么应对“不就是做个数据库操作吗为什么要 3 天”这类问题?烂代码“烂”的因素有不确定性、开发效率也因人而异,想要证明这个东西“确实”会增加两天开发时间,往往反而会变成“我看了 3 天才看懂这个函数是做什么的”或者“我做这么简单的修改要花 3 天”这种神经病才会去证明的命题。

而另一面,许多技术负责人也意识到了代码质量和重构的必要性,“那就重构嘛”,或者“如果看到问题了,那就重构”。上一个问题解决了,但实际上关于重构的代价和收益仍然是一笔糊涂账,在没有分配给你更多资源、没有明确的目标、没有具体方法的情况下,很难想象除了有代码洁癖的人还有谁会去执行这种莫名 其妙的任务。

于是往往就会形成这种局面:

  • 不写代码的人认为应该重构,重构很简单,无论新人还是老人都有责任做重构。
  • 写代码老手认为应该迟早应该重构,重构很难,现在凑合用,这事别落在我头上。
  • 写代码的新手认为不出 bug 就谢天谢地了,我也不知道怎么重构。

4、写好代码很难

与写出烂代码不同的是,想写出好代码有很多前提:

  • 理解要开发的功能需求。
  • 了解程序的运行原理。
  • 做出合理的抽象。
  • 组织复杂的逻辑。
  • 对自己开发效率的正确估算。
  • 持续不断的练习。

写出好代码的方法论很多,但我认为写出好代码的核心反而是听起来非常 low 的“持续不断的练习”。这里就不展开了,留到下篇再说。

很多程序员在写了几年代码之后并没有什么长进,代码仍然烂的让人不忍直视,原因有两个主要方面:

  • 环境是很重要的因素之一,在烂代码的熏陶下很难理解什么是好代码,知道的人大部分也会选择随波逐流。
  • 还有个人性格之类的说不清道不明的主观因素,写出烂代码的程序员反而都是一些很好相处的人,他们往往热爱公司团结同事平易近人工作任劳任怨–只是代码很烂而已。

而工作几年之后的人很难再说服他们去提高代码质量,你只会反复不断的听到:“那又有什么用呢?”或者“以前就是这么做的啊?”之类的说法。

那么从源头入手,提高招人时对代码的质量的要求怎么样?

前一阵面试的时候增加了白板编程、最近又增加了上机编程的题目。发现了一个现象:一个人工作了几年、做过很多项目、带过团队、发了一些文章,不一定能代表他代码写的好反之,一个人代码写的好,其它方面的能力一般不会太差

举个例子,最近喜欢用“写一个代码行数统计工具”作为面试的上机编程题目。很多人看到题目之后第一反映是,这道题太简单了,这不就是写写代码嘛。

从实际效果来看,这道题识别度却还不错。

首先,题目足够简单,即使没有看过《面试宝典》之类书的人也不会吃亏。而题目的扩展性很好,即使提前知道题目,配合不同的条件,可以变成不同的题目。比如要求按文件类型统计行数、或者要求提高统计效率、或者统计的同时输出某些单词出现的次数,等等。

从考察点来看,首先是基本的树的遍历算法;其次有一定代码量,可以看出程序员对代码的组织能力、对问题的抽象能力;上机编码可以很简单的看出应聘者是不是很久没写程序了;还包括对于程序易用性和性能的理解。

最重要的是,最后的结果是一个完整的程序,我可以按照日常工作的标准去评价程序员的能力,而不是从十几行的函数里意淫这个人在日常工作中大概会有什么表现。

但即使这样,也很难拍着胸脯说,这个人写的代码质量没问题。毕竟面试只是代表他有写出好代码的能力,而不是他将来会写出好代码。

5、悲观的结语

说了那么多,结论其实只有两条,作为程序员:

  • 不要奢望其他人会写出高质量的代码
  • 不要以为自己写出来的是高质量的代码

如果你看到了这里还没有丧失希望,那么可以期待一下这篇文章的第二部分,关于如何提高代码质量的一些建议和方法。

转载请注明出处:代码说 » 关于烂代码的那些事 - 为什么每个团队存在大量烂代码

IK分词器原理与源码分析

引言

做搜索技术的不可能不接触分词器。个人认为为什么搜索引擎无法被数据库所替代的原因主要有两点,一个是在数据量比较大的时候,搜索引擎的查询速度快,第二点在于,搜索引擎能做到比数据库更理解用户。第一点好理解,每当数据库的单个表大了,就是一件头疼的事,还有在较大数据量级的情况下,你让数据库去做模糊查询,那也是一件比较吃力的事(当然前缀匹配会好得多),设计上就应当避免。关于第二点,搜索引擎如何理解用户,肯定不是简单的靠匹配,这里面可以加入很多的处理,甚至加入各种自然语言处理的高级技术,而比较通用且基本的方法就是靠分词器来完成,而且这是一种比较简单而且高效的处理方法。

分词技术是搜索技术里面的一块基石。很多人用过,如果你只是为了简单快速地搭一个搜索引擎,你确实不用了解太深。但一旦涉及效果问题,分词器上就可以做很多文章。例如, 在实我们际用作电商领域的搜索的工作中,类目预判的实现就极须依赖分词,至少需要做到可以对分词器动态加规则。再一个简单的例子,如果你的优化方法就是对不同的词分权重,提高一些重点词的权重的话,你就需要依赖并理解分词器。本文将根据ik分配器的原码对其实现做一定分析。其中的重点,主要3点,1、词典树的构建,即将现在的词典加载到一个内存结构中去, 2、词的匹配查找,也就相当生成对一个句话中词的切分方式,3、歧义判断,即对不同切分方式的判定,哪种应是更合理的。 代码原网址为:[https://code.google.com/p/ik-analyzer/](https://code.google.com/p/ik-analyzer/) 已上传github,可访问:[https://github.com/quentinxxz/Search/tree/master/IKAnalyzer2012FF_hf1_source/](https://github.com/quentinxxz/Search/tree/master/IKAnalyzer2012FF_hf1_source/)

词典

做后台数据相关操作,一切工作的源头都是数据来源了。IK分词器为我们词供了三类词表分别是:1、主词表 main2012.dic 2、量词表quantifier.dic 3、停用词stopword.dic。
Dictionary为字典管理类中,分别加载了这个词典到内存结构中。具体的字典代码,位于org.wltea.analyzer.dic.DictSegment。 这个类实现了一个分词器的一个核心数据结构,即Tire Tree。

Tire Tree(字典树)是一种结构相当简单的树型结构,用于构建词典,通过前缀字符逐一比较对方式,快速查找词,所以有时也称为前缀树。具体的例子如下。
tireTree.jpg

图1
从左来看,abc,abcd,abd,b,bcd…..这些词就是存在树中的单词。当然中文字符也可以一样处理,但中文字符的数目远多于26个,不应该以位置代表字符(英文的话,可以每节点包完一个长度为26的数组),如此的话,这棵tire tree会变得相当扩散,并占用内存,因而有一个tire Tree的变种,三叉字典树(Ternary Tree),保证占用较小的内存。Ternary Tree不在ik分词器中使用,所以不在此详述,请参考文章http://www.cnblogs.com/rush/archive/2012/12/30/2839996.html
IK中采用的是一种比方简单的实现。先看一下,DictSegment类的成员:

class DictSegment implements Comparable<DictSegment>{  
  
    //公用字典表,存储汉字  
    private static final Map<Character , Character> charMap = new HashMap<Character , Character>(16 , 0.95f);  
    //数组大小上限  
    private static final int ARRAY_LENGTH_LIMIT = 3;  
  
      
    //Map存储结构  
    private Map<Character , DictSegment> childrenMap;  
    //数组方式存储结构  
    private DictSegment[] childrenArray;  
  
  
    //当前节点上存储的字符  
    private Character nodeChar;  
    //当前节点存储的Segment数目  
    //storeSize <=ARRAY_LENGTH_LIMIT ,使用数组存储, storeSize >ARRAY_LENGTH_LIMIT ,则使用Map存储  
    private int storeSize = 0;  
    //当前DictSegment状态 ,默认 0 , 1表示从根节点到当前节点的路径表示一个词  
    private int nodeState = 0;    
    ……  

这里有两种方式去存储,根据ARRAY_LENGTH_LIMIT作为阈值来决定,如果当子节点数,不太于阈值时,采用数组的方式childrenArray来存储,当子节点数大于阈值时,采用Map的方式childrenMap来存储,childrenMap是采用HashMap实现的。这样做好处在于,节省内存空间。因为HashMap的方式的方式,肯定是需要预先分配内存的,就可能会存在浪费的现象,但如果全都采用数组去存组(后续采用二分的方式查找),你就无法获得O(1)的算法复杂度。所以这里采用了两者方式,当子节点数很少时,用数组存储,当子结点数较多时候,则全部迁至hashMap中去。在构建过程中,会将每个词一步步地加入到字典树中,这是一个递归的过程:

/** 
 * 加载填充词典片段 
 * @param charArray 
 * @param begin 
 * @param length 
 * @param enabled 
 */  
private synchronized void fillSegment(char[] charArray , int begin , int length , int enabled){  

     ……       
    //搜索当前节点的存储,查询对应keyChar的keyChar,如果没有则创建  
    DictSegment ds = lookforSegment(keyChar , enabled);  
    if(ds != null){  
        //处理keyChar对应的segment  
        if(length > 1){  
            //词元还没有完全加入词典树  
            ds.fillSegment(charArray, begin + 1, length - 1 , enabled);  
        }else if (length == 1){  
            //已经是词元的最后一个char,设置当前节点状态为enabled,  
            //enabled=1表明一个完整的词,enabled=0表示从词典中屏蔽当前词  
            ds.nodeState = enabled;  
        }  
    }  

}  

其中lookforSegment,就会在所在子树的子节点中查找,如果是少于ARRAY_LENGTH_LIMIT阈值,则是为数组存储,采用二分查找;如果大于ARRAY_LENGTH_LIMIT阈值,则为HashMap存储,直接查找。

词语切分

IK分词器,基本可分为两种模式,一种为smart模式,一种为非smart模式。例如原文:
张三说的确实在理
smart模式的下分词结果为:
张三 | 说的 | 确实 | 在理
而非smart模式下的分词结果为:
张三 | 三 | 说的 | 的确 | 的 | 确实 | 实在 | 在理
可见非smart模式所做的就是将能够分出来的词全部输出;smart模式下,IK分词器则会根据内在方法输出一个认为最合理的分词结果,这就涉及到了歧义判断。

首来看一下最基本的一些元素结构类:

public class Lexeme implements Comparable<Lexeme>{  
    ……  
  
    //词元的起始位移  
    private int offset;  
    //词元的相对起始位置  
    private int begin;  
    //词元的长度  
    private int length;  
    //词元文本  
    private String lexemeText;  
    //词元类型  
    private int lexemeType;  
     ……  

这里的Lexeme(词元),就可以理解为是一个词语或个单词。其中的begin,是指其在输入文本中的位置。注意,它是实现Comparable的,起始位置靠前的优先,长度较长的优先,这可以用来决定一个词在一条分词结果的词元链中的位置,可以用于得到上面例子中分词结果中的各个词的顺序。

/* 
* 词元在排序集合中的比较算法 
* @see java.lang.Comparable#compareTo(java.lang.Object) 
*/  
public int compareTo(Lexeme other) {  
//起始位置优先  
    if(this.begin < other.getBegin()){  
        return -1;  
    }else if(this.begin == other.getBegin()){  
     //词元长度优先  
     if(this.length > other.getLength()){  
         return -1;  
     }else if(this.length == other.getLength()){  
         return 0;  
     }else {//this.length < other.getLength()  
         return 1;  
     }  
       
    }else{//this.begin > other.getBegin()  
     return 1;  
    }  
}  

还有一个重要的结构就是词元链,声明如下

/** 
 * Lexeme链(路径) 
 */  
class LexemePath extends QuickSortSet implements Comparable<LexemePath> 

一条LexmePath,你就可以认为是上述分词的一种结果,根据前后顺序组成一个链式结构。可以看到它实现了QuickSortSet,所以它本身在加入词元的时候,就在内部完成排序,形成了一个有序的链,而排序规则就是上面Lexeme的compareTo方法所实现的。你也会注意到,LexemePath也是实现Comparable接口的,这就是用于后面的歧义分析用的,下一节介绍。
另一个重要的结构是AnalyzeContext,这里面就主要存储了输入信息 的文本,切分出来的lemexePah ,分词结果等一些相关的上下文信息。
IK中默认用到三个子分词器,分别是LetterSegmenter(字母分词器),CN_QuantifierSegment(量词分词器),CJKSegmenter(中日韩分词器)。分词是会先后经过这三个分词器,我们这里重点根据CJKSegment分析。其核心是一个analyzer方法。

public void analyze(AnalyzeContext context) {  
    …….  
          
        //优先处理tmpHits中的hit  
        if(!this.tmpHits.isEmpty()){  
            //处理词段队列  
            Hit[] tmpArray = this.tmpHits.toArray(new Hit[this.tmpHits.size()]);  
            for(Hit hit : tmpArray){  
                hit = Dictionary.getSingleton().matchWithHit(context.getSegmentBuff(), context.getCursor() , hit);  
                if(hit.isMatch()){  
                    //输出当前的词  
                    Lexeme newLexeme = new Lexeme(context.getBufferOffset() , hit.getBegin() , context.getCursor() - hit.getBegin() + 1 , Lexeme.TYPE_CNWORD);  
                    context.addLexeme(newLexeme);  
                      
                    if(!hit.isPrefix()){//不是词前缀,hit不需要继续匹配,移除  
                        this.tmpHits.remove(hit);  
                    }  
                      
                }else if(hit.isUnmatch()){  
                    //hit不是词,移除  
                    this.tmpHits.remove(hit);  
                }                     
            }  
        }             
          
        //*********************************  
        //再对当前指针位置的字符进行单字匹配  
        Hit singleCharHit = Dictionary.getSingleton().matchInMainDict(context.getSegmentBuff(), context.getCursor(), 1);  
        if(singleCharHit.isMatch()){//首字成词  
            //输出当前的词  
            Lexeme newLexeme = new Lexeme(context.getBufferOffset() , context.getCursor() , 1 , Lexeme.TYPE_CNWORD);  
            context.addLexeme(newLexeme);  

            //同时也是词前缀  
            if(singleCharHit.isPrefix()){  
                //前缀匹配则放入hit列表  
                this.tmpHits.add(singleCharHit);  
            }  
        }else if(singleCharHit.isPrefix()){//首字为词前缀  
            //前缀匹配则放入hit列表  
            this.tmpHits.add(singleCharHit);  
        }  
   ……  
}  

从下半截代码看起,这里的matchInMain就是用于匹配主题表内的词的方法。这里的主词表已经加载至一个字典树之内,所以整个过程也就是一个从树根层层往下走的一个层层递归的方式,但这里只处理单字,不会去递归。而匹配的结果一共三种UNMATCH(未匹配),MATCH(匹配), PREFIX(前缀匹配),Match指完全匹配已经到达叶子节点,而PREFIX是指当前对上所经过的匹配路径存在,但未到达到叶子节点。此外一个词也可以既是MATCH也可以是PREFIX,例如图1中的abc。前缀匹配的都被存入了tempHit中去。而完整匹配的都存入context中保存。
继续看上半截代码,前缀匹配的词不应该就直接结束,因为有可能还能往后继续匹配更长的词,所以上半截代码所做的就是对这些词继续匹配。matchWithHit,就是在当前的hit的结果下继续做匹配。如果得到MATCH的结果,便可以在context中加入新的词元。
通过这样不段匹配,循环补充的方式,我们就可以得到所有的词,至少能够满足非smart模式下的需求。

歧义判断

IKArbitrator(歧义分析裁决器)是处理歧义的主要类。
如果觉着我这说不清,也可以参考的博客:http://fay19880111-yeah-net.iteye.com/blog/1523740

在上一节中,我们提到LexemePath是实现compareble接口的。

public int compareTo(LexemePath o) {  
    //比较有效文本长度  
    if(this.payloadLength > o.payloadLength){  
        return -1;  
    }else if(this.payloadLength < o.payloadLength){  
        return 1;  
    }else{  
        //比较词元个数,越少越好  
        if(this.size() < o.size()){  
            return -1;  
        }else if (this.size() > o.size()){  
            return 1;  
        }else{  
            //路径跨度越大越好  
            if(this.getPathLength() >  o.getPathLength()){  
                return -1;  
            }else if(this.getPathLength() <  o.getPathLength()){  
                return 1;  
            }else {  
                //根据统计学结论,逆向切分概率高于正向切分,因此位置越靠后的优先  
                if(this.pathEnd > o.pathEnd){  
                    return -1;  
                }else if(pathEnd < o.pathEnd){  
                    return 1;  
                }else{  
                    //词长越平均越好  
                    if(this.getXWeight() > o.getXWeight()){  
                        return -1;  
                    }else if(this.getXWeight() < o.getXWeight()){  
                        return 1;  
                    }else {  
                        //词元位置权重比较  
                        if(this.getPWeight() > o.getPWeight()){  
                            return -1;  
                        }else if(this.getPWeight() < o.getPWeight()){  
                            return 1;  
                        }  
                          
                    }  
                }  
            }  
        }  
    }  
    return 0;  
}  
显然作者在这里定死了一些排序的规则,依次比较有效文本长度、词元个数、路径跨度…..

IKArbitrator有一个judge方法,对不同路径做了比较。

private LexemePath judge(QuickSortSet.Cell lexemeCell , int fullTextLength){  
    //候选路径集合  
    TreeSet<LexemePath> pathOptions = new TreeSet<LexemePath>();  
    //候选结果路径  
    LexemePath option = new LexemePath();  
      
    //对crossPath进行一次遍历,同时返回本次遍历中有冲突的Lexeme栈  
    Stack<QuickSortSet.Cell> lexemeStack = this.forwardPath(lexemeCell , option);  
      
    //当前词元链并非最理想的,加入候选路径集合  
    pathOptions.add(option.copy());  
      
    //存在歧义词,处理  
    QuickSortSet.Cell c = null;  
    while(!lexemeStack.isEmpty()){  
        c = lexemeStack.pop();  
        //回滚词元链  
        this.backPath(c.getLexeme() , option);  
        //从歧义词位置开始,递归,生成可选方案  
        this.forwardPath(c , option);  
        pathOptions.add(option.copy());  
    }  
      
    //返回集合中的最优方案  
    return pathOptions.first();  
}  

其核心处理思想是从第一个词元开始,遍历各种路径,然后加入至一个TreeSet中,实现了排序,取第一个即可。

其它说明

1、stopWord(停用词),会在最后输出结果的阶段(AnalyzeContext. getNextLexeme)被移除,不会在分析的过程中移除,否则也会存在风险。
2、可以从LexemePath的compareTo方法中看出,Ik的排序方法特别粗略,如果比较发现path1的词个数,比path2的个数少,就直接判定path1更优。其实这样的规则,并未完整的参考各个分出来的词的实际情况,我们可能想加入每个词经统计出现的频率等信息,做更全面的打分,这样IK原有的比较方法就是不可行的。
关于如何修改的思路可以参考另一篇博客,其中介绍了一种通过最短路径思路去处理的方法:http://www.hankcs.com/nlp/segment/n-shortest-path-to-the-java-implementation-and-application-segmentation.html

3、未匹配的单字,不论是否在smart模式下,最后都会输出,其处理时机在最后输出结果阶段,具体代码位于在AnalyzeContext. outputToResult方法中。

转载请注明出处:代码说 » IK分词器原理与源码分析

Joyent CTO谈容器在2016年亟需改变的问题

为了在云时代取得领先地位,Joyent 将赌注压在容器上。本文由 Joyent CTO Bryan Cantrill 接受 TechTarget 采访,深度介绍关于容器技术 2015 年的现状和亟需改变的问题。

Joyent 在过去的十年中一直在生产环境中实践 OS 容器技术,当 Docker 横空出世之时,Joyent 是最早投身这种技术的公司之一。去年年底,Joyent 把自己重新打造成了“原生容器架构”,从那时起,他们发布了多种产品,包括 Triton——用于管理 Docker 容器的容器基础设施;以及 Containerbuddy,可以把传统服务容器化,使它们可以在任何地方运行的。

Joyent 同时还是“原生云计算基金会”的创始成员之一,该基金会于 7 月成立,目的在于创造原生云应用和容器的参考架构。 Joyent 的 CTO Bryan Cantrill 是该基金会技术指导委员会成员。他最近和 SearchCloudComputing 谈到了容器技术的现状,他对未来一年期待,以及亟需改变的问题。

With containers there’s more opportunity and arguably more peril because they allow you to change the way you think about the problems.

Bryan Cantrill

CTO, Joyent Inc.,

问:容器在 2015 年持续受到很多关注,但是仍然存在不少问题需要解决,这方面你怎么看?

每个人都把容器看做未来,因此也引起了大量的关注。在十几年前的虚拟机革命过程中,人们大多时候不需要改变思维习惯,只需要把运行环境分成物理层及虚拟层来看。但对于容器来说,一方面机会更多,而同时存在着不少陷阱,因为容器改变之前十几年那种固化的思维习惯。

问:有些人会认为容器之所以能够受欢迎是因为它们有些类似于虚拟机?

没错,但是他们会发现容器只是和容器所在的环境水平相当。你有了能在生产中盛东西的容器并不代表你就有了服务发现。当我们把宠物转变成牲畜时,容器的意义可远远不止于改变基底。容器的真正意义在于更快速和简单地构建大规模系统,但是同时意味你需要解决如何有效地把现有系统转变为微服务的问题。

问:现在容器市场的成熟度如何?人们对容器技术的理解水平如何?

KubeCon(最近谷歌的 Kubernetes 社区举行的大会)提出的问题之一就是:“容器领域的状态接近峰值混乱了吗?” 有趣的是,每个和我交谈的人——无论他们是开发、运维还是云厂商—— 都认为我们没有进入峰值混乱。他们仍然期待成长,甚至有些人期待着加速成长。

我不知道我们应该期待容器技术在近期达到多大程度的稳定性,因为所有的技术都是开源的。容器领域存在着各种各样的框架和设计哲学,因此也存在较多混乱以及和简洁相悖的气质。直白一点说,这里有很多竞争白热化地带,在这种情况下,如果你故意想要进入尽量多的领域,那么区分每个区域的边界就会很难。

问:容器即服务(CaaS)是我们从许多云厂商那里看到的最新趋势,你是否担心某些厂商和容器带来的私有概念会让服务的跨平台可移植性大打折扣?

人们确实对华而不实的容器即服务抱有成见。他们想要自己使用容器来构建东西。我们的信念就是,我们需要原生容器基础设施—— 并不是在虚拟机内部供应,而是在纯粹的容器上供应。在这种情况下,如果容器有一个 IP 地址的话某些事情就会简单得多,而你也不需要完成任何愚蠢的映射特技了。

我的确认为有些容器服务,特别是那些期望将容器运行在 VM 内部的服务,只是现有的虚拟机专家想要在容器领域“到此一游”而已。他们并不理解容器革命背后隐含的经济学原理。

问:容器市场接下来需要发生什么样的变化?

概念的扩张需要放缓一些,或者我们需要沉淀一些东西,让这些技术在生产环境中做出有意义的东西来。但是这方面我们还一筹莫展……

你可以真正在生产环境部署 Triton。你可以用 Docker Compose 来支持适应性比较强的服务,但是除了 Triton 以外,很多技术感觉上还很稚嫩和原始,而这样的技术也招来了一些恶名。人们不应该再吹嘘自己收到多少代码提交,拥有多少贡献者以及下载量了。对于我来说,这样做从某种程度上代表着混淆视听。如果你面临着一两万个问题,你怎么能忍受项目上的任何东西以这样的速度迅速扩张呢?

到 2017 年,我们至少会在心里巩固某些概念,并不是说未来将会产生某些概念一统天下的情况,而是说我们将会更好地了解某些概念在哪些方面更合适,在哪些方面不合适。进一步的技术突变也很有可能会发生,尽管如此,我仍然相信我们将会看到更多对稳健性的增强,以及更少的不间断扩张。

更多「架构」容器文章

英文原文: http://searchcloudcomputing.techtarget.com/news/4500258178/Joyent-CTO-talks-Docker-containers-and-the-work-ahead-in-2016

转载请注明出处:代码说 » Joyent CTO谈容器在2016年亟需改变的问题

微博基于Docker的混合云平台设计与实践

王关胜,微博研发中心运维架构师。2011 年初加入新浪,一直负责微博平台&大数据等业务线的运维保障工作,包括产品稳定性,运维基础设施建设,工具建设等。致力于推进 Docker 在微博的应用,参与建设微博混合云平台 DCP。擅长大规模分布式系统集群的管理与运维,疑难问题分析,故障定位与处理等。对运维工具平台建设、监控、应用性能跟踪及分析、数据化运维等方面有深入的研究。

2011 年初,新浪微博进入快速发展期,同时也开启平台化的进程,服务器设备,及人力成本大量增加,业务的快速发展,促使运维团队建立了一套完整的运维平台。虽然已稳定运行了 3 年,但随着公有云的逐渐成熟,Docker Container 技术的兴起,一时间各大型企业纷纷开始升级内部运维系统,提供自动化能力的同时,更注重弹性调度。我们也于 2014 年底构建了第一版基于 Docker 的运维平台,并在元旦,春节,红包飞等大型活动中得到了考验。但是要想更好的应对微博的这种业务场景,系统局限性还很多,比如设备申请慢,业务负载饱和度不一,扩缩容流程繁琐且时间长,基于此出发点,2015 年技术团队设计与实现了一套基于 Docker 的混合云平台 DCP。

微博的业务场景及混合云背景

微博目前有 8 亿注册用户,单日活跃用户数达 1 亿多。微博总体分为端和后端平台,端上主要是 PC 端,移动端和第三方开发者,后端平台主要是 Java 编写的各种接口层,服务层及存储层。就前端来说,每日超过 600 亿次的 API 调用,超过万亿的 RPC 调用,产生的日志就达百 T+。对于这么大体量的业务系统,对于运维的要求也很严格,就接口层来说,SLA 必须达到 4 个 9,且接口平均响应时间不能高于 50ms。因此技术团队会面临下述各种各样的挑战。

  • 产品功能迭代快,代码变更频繁
  • 业务模块多,且依赖关系复杂
  • 突发的热点事件,如典型的#周一见# #马航370# #刘翔摔倒# #明星丑闻#
  • 大型活动及春节、元旦保障,如红包飞

下面具体看下春晚时的业务模型:


每年的元旦,春晚,红包飞会带来巨大的流量挑战,这些业务场景的主要特点是: 瞬间峰值高,互动时间短。基本上一次峰值事件,互动时间都会 3 小时以内,而明星突发新闻事件,红包飞这种业务,经常会遇到高达多倍的瞬间峰值。传统的应对手段,主要是靠提前申请足够的设备,保证冗余;降级非核心及周边的业务;生扛这三种手段。这么做,除了成本高外,对系统进行水平扩容时,耗费的时间也久。

除了来自业务的挑战,在应对峰值事件时,对于运维的挑战也蛮大的。遇到难点较多的环节包括:设备申请太麻烦,时间长;扩缩容流程繁琐。

要完成一次具体的扩容,首先基础运维从采购拿到新机器,录入 CMDB,再根据业务运维提的需求,进行上架到相应的 IDC,机架,操作系统安装,网络配置,最后分给相应的业务运维,就是一台完整的可以登录的机器了。其次,业务运维拿到机器,需要对机器进行初始化配置,并继续服务部署。服务部署好后,经过 check,再挂到负载均衡上,引入流量。设备坏了自动报修,过期下架或替换。具体可看下图

这种扩容的方式,除了流程繁琐外,还经常遇到各服务器间环境差异,无法充分利用服务器硬件资源,即使有多余的服务器也无法灵活调度。若采取新申请设备,在大一点的公司申请流程通常较长。一般设备申请是业务方及业务运维发起采购提案,然后相关方进行架构评审,评审通过,则由 IT 管委会评审,再由决策及成本部门评审,评审通过,进入采购流程,再上架,还经常遇到机房机架位不足,这些都导致交付周期变得很长。

除了业务扩容的繁琐,公司内设备利用率也不均衡,主要表现在三个地方:

  1. 各个业务组的服务器利用率各不相同,大家对利用率的理解不一致,导致有些设备未能得到充分利用,这也会导致更大的成本压力。
  2. 各个业务模型不同,比如有的业务高峰是在晚 22 – 23 点,有的业务则在中午。
  3. 在线业务与离线业务完全分离,导致成本也高,对于离线业务,可在低峰继续跑在线业务

正因为这些挑战,怎么能更好的解决,技术团队想到基于 Docker,及公有云来构建一套具有弹性伸缩的混合云系统。利用过去的私有云加公有云,以及公司内闲置的运算资源,混合云,兼具安全性与弹性扩展能力。其特点如下图

有了这套混合云系统后,不仅能很好的整合内部运算资源,解决内部的弹性需求外,当系统面临流量剧增的峰值事件时,也可以将过多的流量切入外部公有云,减轻内部系统的压力。

两种云内的设备如何使用呢?内部私有云设备安全性高,可控度也高,只要对硬件资源进行优化配置,用它来处理固定的工作负载。而公有云的设备则具有标准化,自动化的特性,可以快速因临时需求,在流量暴涨时,可以快速创建大量 ECS,扩容业务工作负载的能力。而对于公司,可以利用公有云这种按需付费的机制,减低硬件的运营成本。因此采用混合云架构,就可以兼具私有云及公有云的优点,可以同时拥有安全与弹性扩容能力,使业务工作负载可以在业务间进行漂移,低负载的业务就应该使用更少的设备,反之亦然。而基于 Docker 来做,对于上述情况来说,复杂度降低很多。

三大基础设施助力微博混合云

微博混合云系统不单只是一般的混合云,而是应用了 Docker,透过 Docker Container 快速部署的特性,解决海量峰值事件对微博系统带来的压力。过去公司在面对峰值事件,一般采取的作法是,首先评估需要多少额外设备,并向外部公有云申请机器分摊流量。然而,除了可能低估应付峰值事件所需的设备外,即使事先準备了足够的 VM,其部署时间成本也远高于 Docker,无法即时帮助公司分摊过多外部请求。

而微博 Docker Container 平台的混合云核心设计思想,主要是借鉴银行的运作机制:民众可以把钱存在银行,而需要使用金钱的时候,只需要提领一部分,剩余的存款银行可以拿去进行投资。而微博则借鉴银行的这一套运作模式,在内部设立一个运算资源共享池外,还导入了外部公有云。

而要微博实现高弹性调度资源的混合云架构,其中实现的关键则是 Docker。刚开始我们思考该要使用裸机还是 VM 架构,作为 Docker Container 的基础设施。后来,考量如果要采用 VM,内部机房架构要进行许多改造。所以,目前微博的内部私有云使用裸机部署,而外部公有云则采用阿里云弹性计算服务(ECS)的 VM 架构。等平台建设成熟后,还可以应用其他厂商公有云,如 AWS 等。

构建 Docker Container 平台基础架构的 3 大关键,包含 Docker 在裸机上的部署架构、改进版的 Docker Registry 以及负载均衡组件 Nginx Upsync 模块

第一是 Docker Container 的裸机部署方案,透过 IP 位址以及 Port 定义一个唯一的 Container 服务,而每台主机上则可以开启多个 Container,各个具有不同的功能。

每一个 Container 服务所产生的行为日志,会经由一个名为 Scribe 的 Container 集中收集。而集中后的数据则可进行用户行为分析。对于容器类监控数据,则是透过建立 CAdvisor Container,将 Container 运行产生的资料,传送至 ELK(Elasticsearch、Logstash 及 Kibana)开源监控软体,进行分析。对于业务数据,通过 Graphite,监控业务系统的运作状况。

第二则是 Docker Registry,我们使用 Docker 官方提供的 Docker Registry,构建了私有的 Registry Hub,并且透过这个私有调度 Docker Container 需要的镜像。

今年基于 V2 版的 Registry Hub,将存储引擎改为使用分布式开源存储平台 Ceph,并且在 Docker Registry 前端结合 Nginx,实作负载平衡功能。这个过程中,比较麻烦的是,在 Docker 版本升级过程中必须让系统能够兼容新旧版本的 Registry Hub,而前端 Nginx 可以分析系统需求,辨別要从新版本或是旧版本 Docker 仓库下载镜像。而外部公有云,则是透过镜像缓存 mirror,不必像私有云一样,部署完整的镜像仓库。

对于镜像服务,总共包含 3 层架构。包含最底层操作系统、中间层的运行环境如 Java、Tomcat,及最上层的 Container。而在调度 Container 时,透过使用 dockerignore 指令,减少不必要的文件、目录,借此减低映像档的容量。除此之外,在镜像标签命名上,我们则禁止使用「Latest」做为镜像标签。这是由于不同使用者对于标签的理解不一,可能会误以为是代表映像档最新的版本。

而第三则是独立研发的 nginx upsync 模块,去年刚开始使用 container 时,将container 挂到 nginx 后,必须通过重启或 reload 指令,使流量生效。而这个过过程中,nginx 对于特別大的并发流量会发生运行不稳定的情形,因此后来开发了 nginx upsync 模块,不通过 reload 指令重启,也可以保持系统稳定运作。针对两种模块进行比较,发现流量大时,用了 nginx upsync 模块,不做 reload,单机扛量多了 10% 的请求,且性能并不会降低。

Nginx upsync 的核心功能主要是:

  1. Fix Nginx reload时带来的服务抖动
  2. Fix Web Container的服务发现

项目已开源至 github 上:https://github.com/weibocom/nginx-upsync-module

其架构如下

微博混合云 DCP 系统设计核心:自动化,弹性调度

目前开发的 Docker Container 混合云平台,包含 4 层架构:主机层、调度层及业务编排层,最上层则是各业务方系统。底层的混合云基础架构则架构了专线,打通微博内部资料中心以及阿里云。

主要思想:来源于官方三驾马车(Machine + Swarm +Compose)

DCP 混合云系统的设计理念,总共包含 4 个核心概念:弹性伸缩、自动化、业务导向等。

DCP 系统,最核心 3 层架构:主机层、调度适配层及编排层。

主机层的核心是要调度运算资源。目前设计了资源共享池、Buffer 资源池,配额管理,及多租户管理机制,借此实现弹性调度资源。

而调度层则通过 API,把调度工具用 API 进行包装,而微博 4 种常用的调度工具组合包含:Docker、Swarm、Mesos 及自主开发的 Dispatch。

而最上层的则是负责业务编排及服务发现。编排层也包括了大数据工具Hadoop,进行大数据分析的业务场景。

对于调度来说,最重要的就是资源管理,Mesos 处理这个是最合适不过了,很多公司就专用其做资源管理,比如 Netflix 写了一个 Titan 调度服务,其底层资源管理则是通过 Mesos。在调度组件中,使用最多的还是 swarm,dispatch。

可以看下面这个场景:这也是私有云内部资源流转的最佳实践

当业务 A 多余的运算资源导入混合云共享池时,此时流量暴涨的业务 C,可从共享池调度业务 A 的运算资源,而峰值事件后,便可以把多余的运算资源归还至共享池。

DCP 对于物理主机的流转,基于资源共享池、Buffer 资源池,配额管理,及多租户管理机制,其实非常复杂,详情可以去看我在台湾 iThome 举办 Container summit 2015 技术大会上分享的内容。这里可以看一下一台物理主机的生命周期(状态流转图)

引入阿里云做为第三机房,实现弹性调度架构

起初,对于是否采用混合云的架构,技术团队也是做了一番考虑的。目前微博机房的部署架构总共分为 3 层,依次是最上层域名以及负载均衡层。中间则是Web 层,包含各种前端框架,RPC 层以及中间件(Middleware)。而最下层则包含 MySQL、Redis、HBase 等资源架构。

微博核心服务主要分布在 2 个机房,两者互相做为灾难备份用途,而第 3 个机房则采用外部阿里云。混合云架构总共有 2 种部署方案。

第 1 种部署方案,将阿里云视为数据中心,底层架构与微博内部机房一样部署。内部机房采用 nginx 做为负载均衡层,但外部公有云则 SLB 引入流量。其他阿里云的中间 Web 层则与内部机房架构一致。不过,出于存储数据需要考虑的因素较多,初期把阿里云做为应付大量峰值时的方案更为简单,存储永久性数据的MySQL、HBase暂不部署于阿里云。

而第 2 种部署方案则把应付峰值事件需要的弹性计算能力迁移到阿里云。第 2 种部署方案困难之处,需要把微博的内部业务进行改造,让微博中间 Web 层,直接对阿里云机房进行 RPC 调用。此种方案部署结构相较比较简单,也让混合云架构具有实现可行性。

而这 2 种方案都会依赖 VPC 网路,如果有没有专线,想实现公有云的弹性计算能力几乎是不可能。因为公网调度资源的延迟时间太高,无法应付微博大量的业务。因此,技术团队与跟阿里云合作,在两边建立了内部专线,让阿里云机房与微博的机房互通。

大规模集群操作自动化:设备申请,初始化,服务上线

微博 Docker Container 混合云 DCP 设计思想,核心目标就是透过自动化操作大规模集群,进行弹性调度资源的任务,要完成此任务,必须经过 3 个流程:设备申请、设备初始化及服务上线。

弹性扩容任务所需要的设备来源是内部的集群以及外部的阿里云。而申请到足够设备后,也必须对设备进行初始化、部署环境及配置管理。最后一步则是将服务上线,开始执行 Container 调度以及弹性扩容的任务。

第 1 步骤则是申请设备,而设备申请借鉴于银行机制的概念。私有云的设备来源,主要来自离线集群、低负载集群以及错峰时间空出的多余设备。具体来说:离线集群并不是时时刻刻都在执行运算,即使稍微减少计算资源,也不会对业务产生重大影响;而工作负载较低的集群,如果有多余的资源也可以进行调度。此外,微博有上百个业务线,每个业务线峰值出现的时间点都不一样,而在此种状况下,各业务也可以互相支援,共享多余的计算资源。

而不同业务的集群则各自独立,并且具有独立的资源池。集群内可以自由调度资源,例如,缩小 A 服务的规模,将多余的运算资源分派给 B 服务。若集群要调度外部资源,也有设计配额制度,控制每个集群分配到的资源。在集群外,必须看 Buffer 池是否有足够的资源,如果足够的话,可以直接将 Buffer 池内的设备初始化,进行使用。

反之,如果 Buffer 池内的资源不足,也必须查看是否有足够的配额,可以直接申请机器。当设备申请完成,直接分配给 Buffer 池后,随即被纳入 DCP 使用,所有可调度的主机只能通过集群自己的 Buffer 池。

第 2 步骤则是设备初始化;通过自己开发的工具系统,可以达到操作系统升级自动化、系统操作 API 化。而服务器所依赖的基础环境、需要的软体等环境配置,透过配置管理工具 Puppet,将需要的配置写成模块,并使用 RPM 机制打包,进行安装。

在初始化流程中,必须要支持软件反安装模式,例如,当A业务要调度设备时,其他业务在交付设备前,必须要先透过反安装程序,将要设备恢复为最原始的状态。

而目前业界中的一些做法也可以免去初始化的流程。例如导入为 Container 而生的容器操作系统,像 CoreOS、RancherOS 或 Red Hat Atomic。不过,虽然这样可以,但是此种做法的可维护度高,但是缺点在于迁移设备的成本较高。

第 3 步骤则是利用完成初始化的设备,开始进行服务上线。目前一线的运维人员,只要在系统页面上输入服务池名称、服务类型、Container 类型、需要 Container 数量以及镜像地址等参数,即可在 5 分钟内完成扩容,扩容完成后,系统也会产出完整的报告,列出扩容程序中,执行的步骤,以及每件任务的成功、失败状况。

总体来说:一键扩容流程如下

应用微博Docker混合云,不怕峰值事件

在用微博混合云 DCP 时,优先调度内部的共享池资源的运算资源。但是在红包飞、春晚时,则必须调度外部的运算资源。平日的正常状况下,阿里云只需提供几百个运算节点,并且在 5 到 10 分钟内完成部署任务。但是,面对春晚、红包飞的峰值流量,则要提供数千个节点。这对阿里云的机房也是有较高要求的。

目前 Docker 混合云 DCP 平台,去年 10 月完成第一版,Container 数量千级。截止发稿,基本上手机微博,微博平台,红包飞,在今年的春晚,都会基于此系统进行峰值应对。上线以来,达成了微博所设定每次水平扩容时间低于 5 分钟的目标。「有这样的弹性调度能力时,系统面对大型活动的峰值压力就小很多。」

转载请注明出处:代码说 » 微博基于Docker的混合云平台设计与实践

深入理解同步/异步与阻塞/非阻塞区别

「那谁」,codedump.info 博主,多年从事互联网服务器后台开发工作。

几年前曾写过一篇描写同步/异步以及阻塞/非阻塞的文章,最近再回头看,还存在一些理解和认知误区,于是重新整理一下相关的概念,希望对网络编程的同行能有所启发。

同步与异步

首先来解释同步和异步的概念,这两个概念与消息的通知机制有关。

举个例子,比如一个用户去银行办理业务,他可以自己去排队办理,也可以叫人代办,办完之后再告知用户结果。对于要办理这个银行业务的人而言,自己去办理是同步方式,而别人代办完毕再告知则是异步方式。

两者的区别在于,同步的方式下,操作者主动完成了这件事情;而异步方式下,调用指令发出后,操作马上就返回了,操作者并不能马上知道结果,而是等待所调用的异步过程(在这个例子中是帮忙代办的人)处理完毕之后,再通过通知手段(在代码中通常是回调函数)来告诉操作者结果。

 

在上图的异步 IO 模型中,应用程序调用完 aio_read 之后,不论是否有数据可读,这个操作都会马上返回,这个过程相当于这个例子中委托另一个人去帮忙代办银行业务的过程,当数据读完拷贝到用户内存之后,会发一个信号通知原进程告诉读数据操作已经完成(而不仅仅是有数据可读)。

阻塞与非阻塞

接着解释阻塞与非阻塞的概念。这两个概念与程序处理事务时的状态有关。

同样用前面的例子,当真正执行办理业务的人去办理银行业务时,前面可能已经有人排队等候。如果这个人一直从排队到办理完毕,中间都没有做过其他的事情,那么这个过程就是阻塞的,这个人当前只在做这么一件事情。

在上图中,应用程序发起 recvfrom 操作之后,要等待数据拷贝成功才能返回。整个过程中,不能做其它的操作,这个就是典型的阻塞 IO。

反之,如果这个人发现前面排队的人不少,于是选择了出去逛逛,过了一会再回来看看有没有轮到他的号被叫到,如果没有又继续出去逛,过一阵再回来看看……如此以往,这个过程就是非阻塞的。因为处理这个事情的人,在这整个过程中,并没有做到除了这件事之外不做别的事情,他的做法是反复的过来检测,如果还没有完成就下一次再次尝试完成这件事情。

上图与前面阻塞 IO 图的区别在于,当没有数据可读时,同样的 recvfrom 操作返回了错误码,表示当前没有可读数据。换言之,即使没有数据也不会一直让这个应用阻塞在这个调用上,这就是非阻塞 IO。

到了这里,可以先简单的小结一下这两组概念

  • 阻塞与非阻塞:区别在于完成一件事情时,当事情还没有完成时,处理这件事情的人除此之外不能再做别的事情;
  • 同步与异步:是自己去做这件事情,还是等别人做好了来通知有结果,然后再自己去拿结果。注意这里说的是拿结果,如果只是别人告诉你可以做某事,然后自己去操作,这种情况下也是同步的操作,在后面多路复用I/O中会进行阐述。

可见,两组概念不是一个维度的概念。我们把需要办理银行业务的人称为 A,把代办理的人称为 B,那么在 A 委托 B 办理业务的情况下,假设 A 在交代 B 帮忙办事之后,A 就去做别的事情,那么 A 并不存在针对办理银行业务这件事情而言是阻塞还是非阻塞,办理事务时阻塞与否,是针对真正需要办理这件事情的人,也就是这个例子里的 B。

与多路复用 I/O 的联系

前几年写这篇文章时,将多路复用 I/O 类的 select/poll 等和异步操作混为一谈,在这里特别做一些补充说明。

在以前笔者包括不少同行的理解中,就这个例子而言,列举下面情况,当去办理业务的人,需要排队时通常都会先去取号,拿到一个纸条的号码,然后等待银行叫号。在那个例子里面,曾经将银行叫号理解成 select 操作,把纸条比作向 select 注册的回调函数,一旦可以进行操作的条件满足,就会根据这个回调函数来通知办理人,然后办理人再去完成工作,因此 select 等多路复用操作是异步的行为。

但上面这种理解最大的错误在于,同步与异步的区别在于是不是要求办理者自己来完成,所有需要自己完成的操作都是同步操作,不管是注册了一个回调(这里的叫号小纸条)等待回调你,还是自己一直阻塞等待。

在上例中,虽然对需要办理业务的人而言,通过叫号小纸条,他可以等待银行的办理通知。等待的同时可以去别的事情,比如浏览手机上网,但只要可以办理该业务的条件满足,真正叫到号可以办理业务时,办理者是需要自己去完成办理的。

换言之,在完成一件事情时,需要区分处理两种状态:一是这个事情是不是可以做了(条件满足的消息,如 select 告诉你某个文件描述符可读),另外一个是否完成了这件事情(如调用 read/write 完成 IO 操作)。多路复用 IO 记录下来有哪些人在等待消息的通知,在条件满足时负责通知办理者,而完成这件事情还是需要办理者自己去完成。只要是自己去完成的操作,都是同步的操作

UNP 的 6.2 节中,最后对异步与同步做的总结非常准确。

POSIX defines these two terms as follows:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.

An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

转载请注明出处:代码说 » 深入理解同步/异步与阻塞/非阻塞区别