乐者为王

Do one thing, and do it well.

10x开发者不是神话

英文原文:http://www.ybrikman.com/writing/2013/09/29/the-10x-developer-is-not-myth/

更新:你可以在这里找到本文的西班牙语翻译

昨天晚上,我在Twitter发布了以下内容:

我被“10x”或者“摇滚明星开发者”是神话的说法所迷惑。明星运动员、艺术家、作家,或者摇滚明星是神话?

— Yevgeniy Brikman (@brikis98) September 29, 2013

我收到了大量的回复和问题,但Twitter是个糟糕的讨论媒介,所以我写这篇博文作为补充。

有大堆的文章[1234]声称10x开发者不存在。反对的理由通常分成3种:

  1. 原来的10x数字来自单个的有缺陷的研究(Sackman,Erikson和Grant(1968))。
  2. 生产率是个模糊的事情,很难测量,所以我们不能做出任何10x的声明。
  3. 人才有分布,但没有单个工程师可以做10倍的工作。

我不同意所有这些。让我们逐个地检查这些理由。

这不是一个研究

虽然Twitter和Hacker News上的空谈科学家喜欢严厉谴责已被同行评审的研究,但在那种情况下的证据是相当有吸引力的,不仅限于单项研究。请允许我引用这个问题在Stack Overflow上的讨论的最顶部的回复:

...发现个体编程生产力的巨大差异的原始研究是在20世纪60年代末由Sackman、Erikson和Grant(1968)进行的。他们研究了平均7年经验的专业程序员,发现最佳和最差程序员之间的初始编码时间的比例约为20:1;调试时间超过25:1;程序大小5:1;并且程序执行速度约为10:1。他们发现程序员的丰富经验与代码质量或生产率之间没有任何关系。

对Sackman、Erikson和Grant的发现的详细检查显示出其方法论的一些缺陷。但是,即使考虑到这些缺陷,他们的数据仍然显示出最佳和最差程序员之间的差距超过10倍。

在原始研究之后的几年中,“程序员之间存在数量级差距”的一般性发现已经被许多其它对专业程序员的研究所证实(Curtis 1981、Mills 1983、DeMarco和Lister 1985、Curtis等人1986、Card 1987、Boehm和Papaccio 1988、Valett和McGarry 1989、Boehm等人2000)...

想知道更多可以看这里这里

如果你不能测量它,你仍然可以推理它

即使你忽略上面的研究,并宣称“编程生产力”很难测量——它是的——我们仍然可以讨论10x程序员。只是因为某些东西难以测量并不意味着我们无法推理它。

例如,你是如何为最近的项目挑选编程语言的?你有没有查阅“证明”这门语言比其它语言更有效的研究?就个人而言,我不需要实验来证明,Ruby比起C在构建网站时会是有一个数量级的更有生产力的选择。你可以胡乱拼凑些粗略的指标(库可用性、社区支持、文档),但现实是,大多数人基于直觉推理而不是双盲研究来做出这类语言决策。尽管缺乏过硬的数据,但我敢打赌,大多数时候,采用Ruby而不是C进行网站开发都将是正确的决策。

当然,编程不是唯一这样的事情:什么“指标”可以告诉你一位作家、艺术家、老师或者哲学家比另外一位更好?仅仅观察他们,我不能给出表明莎士比亚、纳博科夫或奥威尔比普通作家好一个数量级的“生产力指标”,但是绝大多数人都会同意这点。

编程不是体力劳动

抗拒10x程序员的最大问题是有些人认为编程是手工劳动,而程序员是装配线工人。有些程序员比其他程序员好些,但可以肯定的是,单个程序员不可能持续接近其他程序员的10倍!10个人的团队总是胜过单个编码者!9个女人不能在1个月内生产婴儿!

上面的逻辑听起来像是编程生产力只关乎打字速度。仿佛10x程序员只是能够生产平均水平的10倍代码。这种推理无视编程是创造性行业,不是体力劳动:解决同样问题的方法有很多种。停止简单地类比,更多地考虑罪案解决的类比:10名普通侦探与夏洛克·福尔摩斯。谁能更快地解决罪案?

10x开发者具有洞察力,能找到普通程序员永远不会找到的解决方案。他们将避免花费普通程序员大量时间的整类问题。编写正确代码的单个工程师绝对可以胜过编写错误代码的10个工程师。

编程是关于选择

考虑构建单个软件产品(如网站)的决策有多少:你用哪种语言?什么Web框架?你用什么数据存储?你用什么缓存?你在哪里托管站点?你怎么监控它?你如何推进新的变化?你如何存储代码?你安排什么样的自动化测试?

10个普通程序员将在每个步骤中做出“平均”质量的决策,这些决策的成本或收益将倍增。想象下流量以指数级增长,而这个普通的团队维护着普通的网站,数据存储引擎难以分片,没有足够冗余的主机,版本控制没有正确备份,没有CI环境,也没有监控。如果他们花费所有的时间去灭火,这10个编码者的效率如何?

如果程序员可以以减少一个数量级的工作量的方式对问题进行建模,那么单个程序员可以胜过这个10人团队。从多年的经验来看,伟大的程序员会知道,以后修复错误要花费更多。通过在前面作出良好的决策,10x程序员可以避免几个月的工作。

它不是编写更多的代码,它是编写正确的代码。成为10x程序员不是通过做一个数量级的更多工作,而是通过做出比平常一个数量级的更好的决策。

这不是说10x程序员根本不犯错误。程序员每天都要做出很多选择,而伟大的程序员做出正确的选择比普通程序员多得多。

编程不是唯一这样的事情。你愿意有10名普通科学家还是艾萨克·牛顿?10名普通科学家没有提出运动定律、重力理论、二项式系列、微积分等,艾萨克·牛顿做到了。你愿意让你的球队有迈克尔·乔丹还是10名普通球员(注意:乔丹获得了NBA平均薪水的10倍)?你愿意让史蒂夫·乔布斯(Steve Jobs)或者伊隆·马斯克(Elon Musk)经营一家公司还是把钥匙交给10个普通企业家?

10x程序员很少见

重要的是以正确的角度看待事情。明星的程序员、运动员、作家和科学家极其少见。我不推荐围绕只雇佣“摇滚明星”来制定招聘策略,它会使你看起来很愚蠢和孤独。不要让完美成为好的敌人:聘请你能得到的最好工程师,并给他们充足的机会去发展和变得更好。

但是,不要陷入所有程序员生而平等的谬误。任何创造性行业都有广泛的能力。一方面是会让组织陷入困境的雇员类型,用他们编写的每行代码积极增加技术债务。另一方面,有些人可以编写带来更多可能的代码,并且具有比平均水平大一个数量级的影响。

神秘的10x程序员

英文原文:http://antirez.com/news/112

在编程神话中,10x程序员就是,一位程序员可以完成另一位普通程序员的10倍工作量,我们可以想象普通程序员擅长完成他们的工作,但没有10x程序员的神奇能力。实际上,为了更好地刻画“普通程序员”,更合理的说法是,在本学科的专业人员中,具有平均编程输出的程序员。

编程社区对这种怪物的存在与否处于两极分化:有人说不存在10x程序员这样的东西,有人说他实际上不仅存在,而且如果你知道去哪里寻找,甚至有100x程序员。

如果将编程视为一门“线性”学科,很明显10x程序员看起来像是非理性的可能。一个跑步者怎么可能比另一个跑步者快10倍?或者一位建筑工人在同一时间内的建造速度是另一位工人的10倍?然而,编程是一门非常特殊的设计学科。即使程序员不参与程序的实际架构设计,实施的行为仍然需要实现策略的子设计。

因此,如果程序的设计和实现不是线性能力,在我看来,就像经验、编码能力、知识、无用部分的识别不仅仅是线性优势,它们在创建程序的行为中以一种倍增的方式工作。当程序员既可以处理程序的设计又可以处理程序的实现时,这种现象当然也会发生的更多。“目标导向”的任务越多,潜在的10x程序员就越能利用他/她的能力,以便用更少的努力达成目标。当手头的任务更死板,具体指导使用什么工具以及如何实现这些任务时,10x程序员在较短时间内执行大量工作的能力就会被削弱:他仍然可以利用“局部的”设计可能性做些更好的工作,但是不再能够以更深刻的方式改变实现目标的途径,这可能包括甚至可能完全从项目中删除部分规范,以便达成几乎是同样的目标,但达成这一目标的努力因为某个大的因素而被减少。

作为程序员的20年里,我观察过与我一起工作的其他程序员,作为同事,由我指导以达成给定的目标,向Redis和其它项目提供补丁。同时,很多人告诉我,他们相信我是一个非常快的程序员。考虑到我远不是工作狂,我也将自己作为快速编码的参考。

以下是我认为在程序员生产力上最大差异的品质列表。

纯粹的编程能力:完成子任务

程序员最显著的限制或优势之一是处理实际实现程序部分的子任务:一个函数、一个算法或者其它任务。令人惊讶的是,根据我的经验,非常有效地使用基本的命令式编程结构来实现某些功能的能力并不像人们想象的那么普遍。在一个团队中,有时我观察到非常无能的程序员,甚至不知道一个简单的排序算法,和那些在理论上非常有能力,但实现解决方案的实践非常差的刚毕业的程序员相比,他们不能完成更多的工作。

经验:模式匹配

根据经验,我的意思是一系列已经探索过的用于某些重复任务的解决方案。有经验的程序员最终知道如何处理各种子任务。这样既避免了很多的设计工作,而且反过来也是针对简洁性的最大敌人之一——设计错误的非常强大的武器。

专注:实际时间 vs 假设时间

如果不考虑时间质量的话,编写代码的时间是无关紧要的。内部和外部因素都可能导致缺乏专注。内部因素是拖延,对手边的项目缺乏兴趣(你不能做好你不喜欢的事情),缺乏锻炼/健康,睡眠质量差或者睡觉不足。外部因素是频繁的会议,没有独立办公室的工作环境,同事经常打扰等等。自然,尝试改善专注和减少中断对编程生产力将产生非边际影响。有时为了获得专注,需要采取极端措施。例如,我只会不时地阅读电子邮件,并且不回复它们中的大多数。

设计的牺牲:删减5%而获得90%

当不愿意承认项目的非基本目标造成很大的设计复杂性,或者正在使另一个更重要的目标难以达成时,往往会产生复杂性,因为在基本功能和非基本功能之间存在着设计矛盾。设计师认识到设计中不容易实现的所有部分是非常重要的,因为努力和优势之间没有比例。为了最大限度地实现产出而执行的项目将精确地集中在可以在合理的时间内实现的方面。例如,当设计消息代理Disque时,某些时候我意识到,通过为消息提供力所能及的排序,项目的所有其它方面可以大大改善:可用性、查询语言和客户端交互、简洁性以及性能。

简洁性

这是显而易见的观点,意味着全有或全无。为了明白什么是简洁性,检查复杂性是如何产生的是值得的。我认为复杂性的两个主要驱动因素是,不愿意进行设计的牺牲,也不愿意在设计活动中累积错误。

如果你在设计过程中思考,每次追求错误的路径,我们将越来越远离最佳解决方案。在错误的手中产生的初始设计错误,不会产生同一系统的重新设计,而是会导致另一个复杂解决方案的设计,以便应对初始错误。因此,这个项目在每个错误的步骤变得更多复杂和更少效率。

实现简洁性的方式是从小块“概念证明”的方面思考,从看起来最可行和直接的解决方案开始工作,以便在程序员脑中探索大量简单的设计。随后,经验和个人设计能力将有助于改进设计,并为需要解决的子设计找到合理的解决方案。

不管怎样,每次需要复杂的解决方案时,只有在没有更好的可能性,甚至考虑过完全不同的替代方案后,才能继续这一方向,重要的是要长时间地思考如何避免复杂性。

完美主义:如何扼杀你的生产力和影响你的设计

完美主义有两种变体:在程序中达到最佳可衡量性能的工程文化,以及一种人格特质。这两种情况我认为是程序员快速交付的最大障碍之一。完美主义和对外部判断的恐惧带入的设计偏见会导致选择不佳,即仅仅根据心理或简单可衡量的参数就改进设计,其中诸如鲁棒性、简洁性、及时交付的能力常常不被考虑。

知识:某些理论会有帮助

在处理复杂任务时,关于数据结构的知识,计算的基本限制,非常适合于某些任务模式的非凡算法将对找到合适设计的能力产生影响。成为每件事的超级专家不是必需的,但是肯定至少要知道一个问题的众多潜在解决方案。例如,运用设计的牺牲(接受某些错误百分比)并且意识到概率集基数估计器可以组合在一起,以避免复杂、缓慢和记忆效率低的用于计算流中唯一条目的解决方案。

底层:了解机器

即使是使用高级语言的时候,程序中的很多问题也是由于对计算机如何执行给定任务的误解而产生的。这甚至可能导致需要从头开始重新设计和重新实现项目,因为被使用的工具或算法存在根本问题。良好的C语言能力,了解CPU如何工作,以及关于内核如何运转和系统调用如何实现的清晰思路,可以避免糟糕的后期意外。

调试技巧

寻找缺陷很容易花费大量的工作时间。善于获取缺陷的状态,以便用一组合理的步骤修复缺陷,以及编写不可能包含太多缺陷的简单代码的态度,这三者对程序员的效率有很大的影响。

看到程序员的上述品质如何能够对输出产生10倍的影响,我并不奇怪。结合起来,他们允许的从可行模式开始的良好设计实现,可以比替代方案简单几倍。有一种方式被用来强调简洁性,我喜欢称之为“机会主义编程”。基本上在每个开发步骤中,要选择一系列要实现的功能,以便用最少的努力,最大程度地影响程序的用户基础。

费曼技巧:最好的学习方法

英文原文:https://www.farnamstreetblog.com/2012/04/learn-anything-faster-with-the-feynman-technique/

费曼技巧有4个简单的步骤,我将在下面解释它们:

  • 选择一个概念
  • 把它教给某个小孩
  • 识别薄弱环节,回到原始材料
  • 回顾和简化(可选)

如果你不学习就会固步自封。那么,学习新主题并识别现有知识的薄弱环节的最好方式是什么?

两种类型的知识

两种类型的知识,我们大多数人关注错误的那种。第一类知识注重知道某事物的名称。第二类注重知道某事物。它们不是一回事。著名的诺贝尔物理学奖获得者理查德·费曼(Richard Feynman)明白知道某事物和知道某事物的名称之间的差异,这是他成功的最重要的原因之一。事实上,他创造了一个学习公式,确保他比其他人更明白某些东西。

这被称为费曼技巧,它将帮助你更快更明白地学到东西。最重要的是,它极其容易实现。

一个人如果说他知道他在想些什么,却表达不出来,通常是他其实并不知道自己在想些什么。——莫提默·艾德勒

费曼技巧

费曼技巧有4个步骤。

步骤1:把它教给某个小孩

拿出一张白纸,在顶部写下你想要学习的主题。写出你对这个主题的了解,好像你正在把它教给某个小孩。不是你聪明的成年朋友,而是一个8岁的小孩,他刚好有足够的词汇和注意力来涵盖基本的概念和关系。

很多人倾向于使用复杂的词汇和行话来掩盖他们不明白的东西。问题是我们仅仅愚弄自己,因为我们不知道我们不明白。另外,使用行话会掩盖周围的人对我们的误解。

当你自始至终都用孩子可以理解的简单的语言写出某个想法时(提示:只用最常见的单词),你迫使自己在更深的层次上去理解这个概念,并简化想法之间的关系和连接。如果你努力,你会清楚地知道自己在哪里还有薄弱环节。这种压力很好——它预示着学习的机会。

步骤2:回顾

在第一步中,你不可避免地会遇到你的知识的薄弱环节,你忘记了某些重要的东西,或者不能解释它,或者只是很难把重要的概念联系起来。

这是宝贵的反馈,因为你已经发现你的知识的边缘。胜任力是知道你能力的极限,你刚刚已经识别出一个!

这是学习开始的地方。现在你知道在哪里会遇到困难,回到原始材料并重新学习,直到你可以用基本的术语去解释它们。

识别你的理解的边界也限制了你可能犯的错误,并增加了在应用知识时成功的机会。

步骤3:整理和简化

现在你有一套手工制作的笔记。检查它们以确保你没有错误地从原始材料中借用任何行话。将它们组织成一个丰满的简单的故事。

把它们大声地朗读出来,如果解释不直白或者听起来很混乱,这表明你在该领域的理解仍需要做些工作。

步骤4(可选):传播

如果你真的想要确保自己的理解没有任何偏差,那就把它告诉别人(理想状态是这个人对该主题知之甚少,或者就找个8岁的小孩)。对你的知识的最终考验是你将其传达给另一个人的能力。

这不仅是学习的一个妙诀,它也是一种不同的思维方式的窗口,允许你将想法分解,然后从头开始重建。(Elon Musk称它为从第一个原则思考)。这会导致对想法和概念的更深入的理解。重要的是,以这种方式解决问题,你可以在别人不知道他们自己在说什么的情况下理解这个问题。

费曼的方法直观地认为智力是一个成长的过程,这与卡罗尔·德韦克(Carol Dweck)的工作非常吻合,卡罗尔·德韦克漂亮地描述了固定型和成长型思维之间的区别

100%代码覆盖率的悲剧

英文原文:http://labs.ig.com/code-coverage-100-percent-tragedy

有趣的是,我对测试的观点正在发生变化。十五年来,我一直在宣扬TDD(测试驱动开发,或者被称为测试先行方法),或至少让开发者写些单元测试。不过,最近我发现自己更经常地说,“你为什么要写测试?”而不是“你应该写测试”。

怎么回事?

在办公室四处走走时,开发者要求我帮助他进行单元测试。看来他在使用Mockito测试以下代码时遇到了麻烦:

initialise-method

我想他是非常惊讶于我的回应:“你不需要测试。”

“但我不得不测啊!”他说。“否则如何知道这段代码是正常的?”

“这段代码很明显。没有条件,没有循环,没有转换,没有任何东西。它们只是一些普通的旧式胶水代码。”

“但没有测试,任何人都可以来修改和破坏这段代码呀!”

“看,如果那个虚构的邪恶/无知的开发者来了,破坏了这些简单的代码,如果相关的单元测试中断,你认为他会做什么?他只会删除它。”

“但是如果非要写测试怎么办?”

“在这种情况下,我将这样写测试:”

initialise-test

“但是你没有使用Mockito啊!”

“那又怎么样呢?Mockito没有帮助你。恰恰相反:它会妨碍你,并且它也不会使测试变得更易读或更简单。”

“但是我们决定使用Mockito进行所有测试!”

我:“……”

后来我碰到他,他自豪地说,他已经设法用Mockito写了测试。我明白让测试代码正常运行的心理满足感,但尽管如此,这种解决方案让我难过。

另一个例子

我加入的某个开发团队,他们对新应用程序的高代码覆盖率以及对BDD(行为驱动设计)的新发现感到兴奋。查看代码,可以发现如下的Cucumber测试:

cucumber-test

如果你以前使用过Cucumber,你就不会震惊于它所需的支持代码的数量:

cucumber-support

cucumber-support2

和所有要测试的代码:

cucumber-code

是的,一个简单的地图查找。我和这个开发者有足够的信任去直言不讳地说,“这是在浪费时间。”

“但我的老板希望我能为所有的类写测试,”他回答。

“代价是什么?”

“费用?”

“无论如何,这些测试与BDD无关。”

“我知道,但是我们决定使用Cucumber进行所有测试”

我:“……”

我明白按照自己意愿改造工具的心理满足感,但尽管如此,这种解决方案让我难过。

悲剧在哪里?

悲剧是两位聪明的开发者(我需要带他们去团队面试)浪费时间写那种测试,测试是毫无意义的,但需要后来的IG开发者维护。

悲剧是不使用正确的工具,因为没有特别好的理由,我们决定坚持不懈地使用错误的工具。

悲剧是一旦某个“良好实践”成为主流,我们似乎就忘记它是怎么来的,它的好处是什么,最主要的是,使用它的代价是什么。

如果我们只是机械地应用它而没有太多的思考,这通常意味着我们最终得到最平庸的结果,失去大部分的好处,但支付所有(甚至更多)的成本。根据我的经验,编写好的单元测试并非易事。

那么100%的代码覆盖率值得追求吗?

是的,每个人都应该实现它……在一个项目中。我认为你必须用极端的手段去了解限制是什么。

我们已经有了一个极端的大量经验:0个单元测试的项目,所以我们知道在这上面工作的痛苦。我们通常缺乏的是在另一个极端的经验:强制100%代码覆盖率和一切都是TDD的项目。单元测试(尤其是测试先行方法)是一个非常好的实践,但我们应该学习哪些测试是有用的,哪些是适得其反的。

要记住没有什么是免费的,没有什么是银弹。使用工具前请停下来想一想。

关于作者

Daniel Lebrero在IG的大数据团队担任技术架构师。拥有超过15年的Java经验和4年的Clojure经验,他现在是函数式编程的大力倡导者。可以在TwitterLinkedIn或者他的个人博客找到他。

学习新编程语言的非传统方式

英文原文:https://hackernoon.com/unconventional-way-of-learning-a-new-programming-language-e4d1f600342c

现在已经有500多种编程语言。因此,开始学习新的编程语言对你来说是很正常的。你可能知道C++和Java,但是你的工作需要Python;或者你精通Python,但是需要用Java编写代码;或者也许你想要学习这种很酷的语言只是为了扩展你的编程技能。

如果你想学习新的编程语言,你会选择哪种方式?

  • 从若干在线教程中学习
  • 或者从若干在线课程(MOOC)中学习

有些人甚至可能认为,学习新语言的最佳方式应该是这样的:

  • 学习这门新的编程语言的语法
  • 再用这门语言构建一些个人项目

有道理!这样可以确保你能够应用学习语言的语法而获得的知识。

我开发过20多个迷你项目,同时学习不同的语言。相信我,当你为个人项目编写代码的时候,不管这些项目是周末项目还是紧急快速补丁,你编写代码都只是为了完成任务。你只会关注——“我的代码是否工作?”你几乎不关心代码的质量。

任何傻瓜都能写出计算机可以理解的代码。好的程序员能写出人类可以理解的代码。——Martin Fowler

那么,你是如何学习你正在尝试学习的新的编程语言的良好实践呢?

向该语言的开源项目贡献代码

惊讶吗?有些人可能在想——“等等,开源是很难的。只有当我们是该语言的专家时,我们才能为开源项目贡献代码,对吗?”答案是不。

让我给你们讲个故事。

去年,我收到Booking.com全职工作的邀请,而且我知道我将使用Perl(这是该公司后端使用的主要语言)工作。2016年6月,当我完成大学学位后,我开始学习Perl,以便为自己在大学毕业后的首份工作做准备。因为我会在7月的第二周入职,所以我差不多有1个月的时间。

我开始阅读Perl的语法,并开始理解这门语言的一些常见模式。现在,我真的想使用Perl构建一些东西,以便我可以应用我的这门语言的知识和实践这门语言的各种概念。当我在寻找使用Perl构建某些东西的想法时,我在GitHub偶遇DuckDuckGo的开源组织。我注意到这个组织的某些开放项目是用Perl写的。我浏览这些项目的Issues发现有很多“新手”问题。我立即开始去解决它们,并提交了几个pull request。到今天为止,我已经是该组织的几个开放项目的主要贡献者之一,也是DuckDuckGo的20个开源社区领袖之一。

故事的寓意——通过向用Perl编写的开源项目贡献代码我学会了Perl。

为什么这种方法奏效呢?

就在我学会Perl的语法之后,我开始向开源项目贡献代码。当这样做的时候,我总是习惯看看现有的模块。我经常留意在Perl中使用的模式。此后,我开始在自己的代码中吸收这些良好的实践,它帮助我学习如何使用Perl编写好的代码。

这并不是偶然。让我给你们讲个另外类似的故事。

最近,当我在Booking.com工作的时候,我挑选了一些任务,包括给用Go语言编写的服务之一添加新功能。以下是我和队友的对话:

我:我真的喜欢这项任务。我想做它。你怎么看?

他:是的,它的确很有意思。但是,它需要Go的知识。你知道Go吗?

我:不知道。

他:你想学习Go吗?

我:是的!

他:😊 那就去吧!

我去了,那也是我学习另外一门编程语言——Go的起点!

我开始阅读Go的语法,并在它的官方网站上发现了一个非常棒的初学者语言教程。它足以让我熟悉该语言的所有基本概念。

再次地,我开始寻找含有“新手”或“易于修复”问题的Go开源项目。我发现了一个Google的项目,它基本上是GitHub的REST API的Go包装器。

在我开始学习Go的2天后,我有了这个项目的第一个PR。下图是我过去1年的贡献图表

contribution-graph

开源是如何帮助的?

现在你可能会疑惑给开源贡献代码如何帮助你学习一门语言的良好实践。它有多个方面。让我们来逐个讨论。

代码质量

大多数良好的开源项目都有严格的编码指导原则,你必须遵守它们才能使你的代码被合并。参与开源将帮助你适应这些指导原则,从而编写优质的代码,即使你只是在学习这门语言。

不仅如此,你还有机会查看其余的代码,学习别人是如何写代码和/或写文档的。

代码审查

给开源贡献代码的最好部分是代码审查。当你推送代码时,你将获得与该项目相关的专家的反馈,因此可以让你有机会提升对语言的理解。

这就像获得了关于如何编写好代码的一次免费的个人指导。

赞赏

下图是我在Go语言上的第一个PR的首个评论

go-appreciation-comment

作为软件开发者,我们的工作真的需要得到赞赏。而开源社区能够确保这些。在我的整个开源贡献经历中,我从来没有收到过甚至一条侮辱或者挫伤的评论。每个人都善于鼓舞和乐于助人。

下图是DuckDuckGo社区中另个人的评论:

duckduckgo-appreciation-comment

所以,下次你想学习一门新语言,只管去学!找个开源项目贡献代码,在学习这门语言和它微妙之处的道路上奋勇前进吧;)

务必让我知道这种非传统方式是否对你有效。另外,如果你认为这种方式对某人有用,请推荐(❤)这篇文章。

如果有任何其它有效的方法也请告诉我。可以在Twitter上关注我@sahildua2305

GitHub对软件职业生涯的影响

英文原文:https://medium.com/@sitapati/the-impact-github-is-having-on-your-software-career-right-now-6ce536ec0b50

在未来的12-24个月里——换句话说,即2018到2019年间——程序员的聘用方式将彻底改变。

2004-2014年间,我任职于Red Hat,世界上最大的开源软件工程公司。2004年7月,在我工作的第一天,我的上司Marty Messer对我说:“你在这里所做的一切工作都是开源的。在将来,你不再需要简历,人们可以直接Google你。”

在那时,它是在Red Hat工作的一个独特之处:我们有机会在开源社区创立自己的个人品牌和声誉。我们通过邮件列表、缺陷追踪器以及提交源代码到Mercurial、Subversion和CVS版本库 来和其他软件工程师进行交流。所有这些都是公开的,并且可以被Google索引。

快进到2017,我们生活的这个世界已经被开源软件所吞噬。

有两个因素可以让你真切地感受到开源时代的到来:

  1. 微软——曾经是闭源私有软件的典型代表和反对开源的圣战士——已经全心全意地拥抱开源软件,成立.NET基金会(Red Hat是其中的一员)和加入Linux基金会。现在.NET已经作为开源项目进行开发。
  2. GitHub已经成为一个奇特的社交网络,它把问题追踪和分布式代码控制捆绑在一起。

对于来自主要是闭源背景的软件开发者来说,刚刚发生了什么还不是很清楚。对他们来说,开源等于“在业余时间免费工作”。

然而,对于我们这些在过去10年里建成一个10亿美元开源软件公司的人来说,为开源工作没有什么免费或业余时间。并且,为开源工作的好处和结果是显而易见的:你的声誉是你的,而且在公司间是可携带的。GitHub是一个社交网络,在那里,你的社会资本,通过你的提交和对你正在工作的任何技术的全球交流的贡献创造的,是你的——不会绑定到你正在临时工作的公司。

聪明人会利用这个优势——他们会向他们日常工作中使用的语言和框架贡献补丁、问题和评论——TypeScript、.NET、Redux。

他们同样会提倡并创造性地安排他们的工作尽可能地以公开的方式完成——即使那只是他们对私有版本库的贡献图。

GitHub是一个很好的均衡器。你可能不能从印度找到一份澳大利亚的工作,但没有什么阻止你在印度利用GitHub与澳大利亚人进行合作。

在过去的十年里,从Red Hat获取一份工作的方式是显而易见的。你只要开始与Red Hat的工程师一起协作开发他们的一些开源项目,然后作出有价值的贡献并且得到他们的认可,你就可以申请一份工作。或者他们会找你。

现在,同样的途径对每个人都开放,不过仅限于技术职位。随着世界被开源所吞噬,同样的求职方式在各个地方开始流行起来。

最近的访谈中,Linux和Git的发明者Linus Torvalds(在GitHub上有4.9万关注者)这样说道:

你提交大量的小补丁,直到项目的维护者信任你,到那时你会成为信任网络的一部分,而不仅仅是个发送补丁的家伙。

你的声誉是你在信任网络中的定位。当你换公司时,它们会减弱并且有所丢失。如果你生活在一个小镇,并且已经在那里很长一段时间,那么小镇里所有的人都了解你。如果你去了其他国家,那么你最终到了一个没人了解你的地方——更糟糕的是,没人知道有谁了解你。

你已经丢失了你的第一度和第二度,甚至可能是第三度连接(译者:不明白什么是“度”的可以搜索六度分隔理论)。除非你已经通过在会议上演讲或者其它一些重要的事情建立品牌,否则你通过与其他人合作以及给企业内部版本库提交代码建立的信任将会不复存在。

但是,如果这些工作一直都在GitHub上完成,它就不会消失。它是可见的。它连接到了一个可见的信任网络。

首先发生的事情之一是弱势群体将开始利用这个优势。学生、新毕业生、移民,他们将利用这个优势搬到澳大利亚。

并且这也将改变整个软件开发的生态环境。以前的特权开发者会突然发现他们的网络被破坏了。开源的原则之一是精英政治——最好的想法胜出,最多的提交胜出,通过测试最多的胜出,最好的实现胜出,等等。

它并不完美(没有什么是完美的)。并且它不会让成为一个好同事的努力废除或打折。在Red Hat,我们解雇过一些摇滚明星工程师,他们只是不能很好地与其他人一起工作——这样的事情不会出现在GitHub,因为大部分开发者都在与其他贡献者互动。

正如有些人用稻草人谬误描述它一样,GitHub不仅仅是代码版本库和原始提交数字的列表。它是一个社交网络。这么说吧:

它不是你的代码在GitHub上的计数——它是其他人在GitHub上谈及你的代码的计数。

那是你的可携带声誉。在未来的12-24个月里,由于一些开发者开发这种声誉而其他开发者不,它将成为一个鲜明的区分因素。就像有电子邮箱和没电子邮箱(现在每个人都有电子邮箱)、有蜂窝电话和没蜂窝电话(现在每个人都有蜂窝电话)。最终绝大多数将会以开源的方式工作,它将再次是区别于其它因素的一个公平竞争的领域。

但现在,开发者的职业生涯空间正在被GitHub破坏。

如何修改Android的hosts文件

由于「你懂的」的原因,某些时候我们需要修改Android的hosts文件。Android的hosts文件路径是/system/etc/hosts,在修改该文件前首先需要Android手机获取root权限。至于如何root你的手机,这里就不加详述,可以自行在网络上查找,很多也很简单。

本文将要阐述的是如何在命令行下通过adb程序访问root过的手机,把hosts拖到电脑上修改,然后再复制回手机来实现修改hosts的方法。

下面就开始具体的步骤:

1
2
C:\tools>adb pull /system/etc/hosts hosts.mod
[100%] /system/etc/hosts

上面的命令是把手机上的hosts文件拖到电脑上,[100%]表明文件已经传输完成,可以修改hosts文件了。在修改完成后就使用以下命令上传到手机:

1
2
C:\tools>adb push hosts.mod /system/etc/hosts
adb: error: failed to copy 'hosts.mod' to '/system/etc/hosts': Read-only file system

从回显的消息可以看到,文件系统是只读的,所以不能直接上传。

试试以下的命令看能不能成功?!

1
2
3
C:\tools>adb root  # 帮助文档说该命令可以让adbd守护进程获得root权限
C:\tools>adb push hosts.mod /system/etc/hosts
adb: error: failed to copy 'hosts.mod' to '/system/etc/hosts': Read-only file system

还是不行,看来要重新挂载/system目录才可以。

1
2
C:\tools>adb remount
remount failed: Operation not permitted

没有权限?这是必须祭起shell大法的节奏啊!

1
2
3
4
5
C:\tools>adb shell
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
shell@maguro:/ $ ls -al /system/etc/hosts
-rw-r--r-- root     root           25 2013-08-14 07:00 hosts

从上面最后一行可以看出hosts这个文件只有它的拥有者能写入,对于其他人来说都是只读的。要想让其他人也能做修改,必须使用以下命令进行提权,再改变hosts文件的属性才行。

1
shell@maguro:/ $ su

如果是第一次执行这个命令,手机会亮起,SuperSU应用会提示你是否同意权限的分配。这里当然是要同意的!接着你就可以看到终端下的提示符从$变成了#,@前的字符也由shell变成了root。然后我们就可以修改hosts文件的权限属性了。

1
2
3
4
root@maguro:/ # chmod +666 /system/etc/hosts
Bad mode
root@maguro:/ # chmod 666 /system/etc/hosts
Unable to chmod /system/etc/hosts: Read-only file system

又是Read-only file system!输入以下命令看看/system目录的文件系统详情呢。

1
2
root@maguro:/ # mount | grep system
/dev/block/platform/omap/omap_hsmmc.0/by-name/system /system ext4 ro,seclabel,relatime...

看到ext4后面的ro了吗?它是read only的缩写,即只读的意思。这说明/system目录是只读的。接下来我们要把它改成可以读写。

1
root@maguro:/ # mount -o rw,remount /system

上面的-o用于指定加载文件系统时的选项。这些选项包括:

1
2
3
remount 重新加载设备。通常用于改变设备的设置状态。
ro 以只读模式加载。
rw 以可读写模式加载。

再次查看,可以看到原来ro的位置已经变成rw了。

1
2
root@maguro:/ # mount | grep system
/dev/block/platform/omap/omap_hsmmc.0/by-name/system /system ext4 rw,seclabel,relatime...

不过到这里我们还不能向手机拷贝hosts文件,因为hosts文件的权限属性还没被改过,如果强行上传的话,会得到如下的错误消息:

1
2
C:\tools>adb push hosts.mod /system/etc/hosts
adb: error: failed to copy 'hosts.mod' to '/system/etc/hosts': Permission denied

运行以下命令:

1
root@maguro:/ # chmod 666 /system/etc/hosts

然后查看hosts文件的属性。

1
2
root@maguro:/ # ls -al /system/etc/hosts
-rw-rw-rw- root     root           25 2013-08-14 07:00 hosts

可以看到所有人都可以读写hosts文件了。

既然一切都准备就绪,那就再来试试上传修改后的hosts文件吧。

1
2
C:\tools>adb push hosts.mod /system/etc/hosts
adb: error: failed to copy 'hosts.mod' to '/system/etc/hosts': Read-only file system

怎么回事,为什么还是拷贝失败呢?不是已经把文件系统改为可读写了吗?

并且在查找原因的过程中还发现一个奇怪的事情。在root模式下/system目录是可读写的,但在shell模式下/system却是只读的。

1
2
3
4
5
root@maguro:/ # mount | grep system
/dev/block/platform/omap/omap_hsmmc.0/by-name/system /system ext4 rw,seclabel,relatime...
root@maguro:/ # exit
shell@maguro:/ # mount | grep system
/dev/block/platform/omap/omap_hsmmc.0/by-name/system /system ext4 ro,seclabel,relatime...

而且在手机上的Terminal Emulator中把/system目录mount成可读写之后,在adb shell的root模式下查看/system的状态仍然显示为只读。

在网上找啊找啊找啊,都快要绝望了,终于找到可能之问题所在。就是这个帖子:mount in shell as user or root with different output。有个回答提到mount namespace这样东西。然后才知道:

A mount namespace is the set of filesystem mounts that are visible to a process.

每个进程的挂载点对其它进程是不可见的。Terminal Emulator中mount后的挂载点属于该进程,而adb shell中shell模式和root模式的挂载点分别属于各自的进程。这就是前面root模式下修改/system目录为可读写后在shell模式下仍显示为只读的原因。

知道问题的原因了,那如何解决呢?

在SuperSu应用的设置中有个mount namespace separation的选项,如下图所示:

mount-namespace-separation

把勾选取消,然后mount的挂载点就是全局性的了,不再为mount它们的进程所独有。不过要记住的是,只有在重启手机后该修改才有效。

下面是取消mount namespace separation后在上传的结果:

1
2
C:\tools>adb push hosts.mod /system/etc/hosts
[100%] /system/etc/hosts

可以看到[100%]的回显,说明文件已经上传完成。

查看hosts文件的属性:

1
2
root@maguro:/ # ls -al /system/etc/hosts
-rw-rw-rw- root     root       137679 2017-02-16 00:20 hosts

文件的大小已经由25变成137679,说明文件已经替换完成。

下面就是恢复手机到原先的状态:

1
2
3
4
root@maguro:/ # chmod 644 /system/etc/hosts
root@maguro:/ # ls -al /system/etc/hosts
-rw-r--r-- root     root       137679 2017-02-16 00:20 hosts
root@maguro:/ # mount -o ro,remount /system

至此,修改hosts文件的工作就算大功告成。

象棋残局大师开发实录(2)

界面的实现主要是绘制棋盘和棋子,有纯代码绘制和使用图片两种方式。因为担心图片缩放引起图片质量问题,以及害怕根据缩放计算棋子落点的麻烦,打算采取纯代码绘制方式。不过在编写了部分代码后发觉这不是个好主意。

纯代码绘制棋盘需要画纵横线、斜线、炮兵座线、文字“楚河汉界”以及中文数字两套坐标,部分线条需要加粗,河界区的竖线不需要画,文字绘制时是以baseline为Y坐标的。这些因素导致文字和棋子在视图中的坐标位置都需要经过细细地计算。如果再考虑让“楚河汉界”这几个字躺着显示,那就更是麻烦。即便这样,绘制完的棋盘背景也只是白色,素素的不是很好看。就这还是没有考虑绘制棋子的结果。当然,好处也是有的,棋子所在的交点坐标很容易计算得到(这个其实不算优点,只是我没深入思考的结果,图片的方式也很容易计算棋子的落点坐标),棋盘也不需要考虑缩放问题,总是适配当前运行的机器的。

既然纯代码绘制的方式问题多多且只有无需屏幕适配这个优势,那采用图片的方式就是必然。

图片方式的实现也有两种,一种是使用View或者SurfaceView显示图片;还有一种是把棋盘和棋子当作ImageView控件处理。使用Layout.addView(View)和Layout.deleteView(View)就可以很容易地放置和消除棋子。经过简单的比较我选用SurfaceView作为绘制的视图。SurfaceView的内容这里不做介绍,因为这不是我们要考虑的重点。以下是SurfaceView的代码骨架:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class GameView extends SurfaceView implements SurfaceHolder.Callback {
    private DrawThread drawThread;
    private SurfaceHolder surfaceHolder;

    public GameView(Context context, AttributeSet attrs) {
        super(context, attrs);

        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);

        // TODO: 加载棋盘棋子图片资源
    }

    // 自定义的绘制方法
    private void doDraw(Canvas canvas) {
        // TODO: 绘制棋盘和棋子
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // TODO: 游戏交互
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (drawThread == null) {
            drawThread = new DrawThread();
            drawThread.start();
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (drawThread != null) {
            drawThread.stopThread();
        }
    }

    private class DrawThread extends Thread {
        private boolean isRunning = false;

        public DrawThread() {
            isRunning = true;
        }

        public void stopThread() {
            isRunning = false;
            boolean retry = true;
            while (retry) {
                try {
                    this.join();    // 保证run方法执行完毕
                    retry = false;
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void run() {
            while (isRunning) {
                Canvas canvas = null;
                try {
                    canvas = surfaceHolder.lockCanvas();
                    synchronized (surfaceHolder) {
                        if (canvas != null) {
                            doDraw(canvas);
                        }
                    }
                } finally {
                    if (canvas != null) {
                        surfaceHolder.unlockCanvasAndPost(canvas);
                    }
                }
            }
        }
    }
}

用图片在视图上绘制棋盘需要考虑不同屏幕尺寸的适配问题,官方推荐的做法是单图片多分辨率,即将不同分辨率的同个图片放在特定的资源目录下。这种做法的缺点是需要维护多套图片,且绘制的棋盘也不可能正好完全匹配屏幕。使用单分辨率图片的话,如果图片尺寸过小,那么在绘制时就必须放大,容易变得模糊;如果图片尺寸太大的话,又会导致资源太大进而引起应用安装包过大的问题。这里使用的是558 * 620像素的图片,既不是太大,又不是太小,恰到好处。其中,棋盘格子是57 * 57像素的正方形,河界的高度和棋盘格子的边长相等,所以两条边线的距离是57 * 8 = 456像素,底线距离是57 * 9 = 513像素。

marked-chess-board

棋子图片的原始大小是55 * 55像素。连同上面棋盘的那些值可以设置成以下的常量供以后使用:

1
2
3
4
5
6
7
public class ChessBoard {
    public static final int RAW_IMAGE_WIDTH = 558;
    public static final int RAW_IMAGE_HEIGHT = 620;
    public static final int RAW_TILES_WIDTH = 456;
    public static final int RAW_TILES_HEIGHT = 513;
    public static final int RAW_TILE_SIZE = 57;
    public static final int RAW_CHESS_SIZE = 55;

因为使用的是单张图片,所以在绘制之前还需要知道屏幕的大小,以便在绘制时对图片做等比例的缩放,使图片在填满屏幕的前提下,最大程度地保证图片的缩放效果,确保图片不变形。缩放代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 计算屏幕的最佳缩放比例
private float calcBestScale() {
    DisplayMetrics dm = getResources().getDisplayMetrics();
    int screenWidth  = dm.widthPixels;
    int screenHeight = dm.heightPixels;

    float scaleX = (float)screenWidth / RAW_IMAGE_WIDTH;
    float scaleY = (float)screenHeight / RAW_IMAGE_HEIGHT;
    return Math.min(scaleX, scaleY);
}

private Bitmap resizeBitmap(Bitmap bitmap, float bestScale) {
    Matrix matrix = new Matrix();
    matrix.postScale(bestScale, bestScale);
    return Bitmap.createBitmap(bitmap, 0, 0,
            bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

在保证能得到最佳效果的缩放图片后,就可以加载棋盘和棋子的图片资源了。

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
private void initResources() {
    board = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.board)), bestScale);

    chesses[0] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_jiang)), bestScale);
    chesses[1] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_shi)), bestScale);
    chesses[2] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_xiang)), bestScale);
    chesses[3] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_ma)), bestScale);
    chesses[4] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_ju)), bestScale);
    chesses[5] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_pao)), bestScale);
    chesses[6] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.black_zu)), bestScale);
    chesses[7] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_shuai)), bestScale);
    chesses[8] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_shi)), bestScale);
    chesses[9] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_xiang)), bestScale);
    chesses[10] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_ma)), bestScale);
    chesses[11] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_ju)), bestScale);
    chesses[12] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_pao)), bestScale);
    chesses[13] = resizeBitmap(BitmapFactory.decodeStream(
            getResources().openRawResource(+R.drawable.red_bing)), bestScale);
}

注意,加载图片资源要用openRawResource()配合decodeStream()才能得到原始大小的图片,如果使用decodeResource()的话,得到的图片大小则是原始大小 * 手机密度 / 160。至于R前面的+号则是因为图片在drawable目录下时Android Studio会提示警告,不想加的话就必须要把图片放到raw目录中。

有了棋盘和棋子的图片资源后,我们就可以绘制象棋界面了。当然,在这之前还要先确定表示棋局状态的数据结构,我们用一个10行9列的二维数组来描述。

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
public class ChessBoard {
    /*
     * 无子(0)
     * 黑将(1) 黑士(2) 黑象(3)  黑马(4)  黑车(5)  黑砲(6)  黑卒(7)
     * 红帅(8) 红仕(9) 红相(10) 红馬(11) 红車(12) 红炮(13) 红兵(14)
     */
    private int[][] chessPoints = {
        /*   1 2 3 4 5 6 7 8 9   */
        {5, 4, 3, 2, 1, 2, 3, 4, 5},
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {0, 6, 0, 0, 0, 0, 0, 6, 0},
        {7, 0, 7, 0, 7, 0, 7, 0, 7},
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        /*       楚河 汉界       */
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {14, 0, 14, 0, 14, 0, 14, 0, 14},
        {0, 13, 0, 0, 0, 0, 0, 13, 0},
        {0, 0, 0, 0, 0, 0, 0, 0, 0},
        {12, 11, 10, 9, 8, 9, 10, 11, 12}
        /* 九 八 七 六 五 四 三 二 一 */
    };

    public int[][] getChessPoints() {
        return chessPoints;
    }

    public int getChess(int row, int col) {
        return chessPoints[row][col];
    }

    public boolean hasChess(int row, int col) {
        return chessPoints[row][col] != 0;
    }

接着,我们需要根据适配时图片缩放的比例计算各个棋子绘制时的偏移坐标(假设棋盘的偏移坐标是[0, 0])。先看下图:

chess-piece-offset

通过观察可以知道,两个棋子左边框之间的距离等于棋盘格子的边长。如果把左上角棋子相对棋盘图片边界偏移的变量分为称为chessBaseLeft和chessBaseTop的话,那么它们的值可以通过以下的公式计算得到:

1
2
3
4
5
tileSize = ChessBoard.RAW_TILE_SIZE * bestScale;    // 棋盘格子缩放后的大小
chessBaseLeft = (ChessBoard.RAW_IMAGE_WIDTH - ChessBoard.RAW_TILES_WIDTH
        - ChessBoard.RAW_CHESS_SIZE) / 2 * bestScale;
chessBaseTop = (ChessBoard.RAW_IMAGE_HEIGHT - ChessBoard.RAW_TILES_HEIGHT
        - ChessBoard.RAW_CHESS_SIZE) / 2 * bestScale;

得到棋子相对棋盘的偏移坐标后,我们就可以开始真正的绘制棋盘和棋子了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
canvas.drawColor(Color.WHITE);

// 绘制棋盘
canvas.drawBitmap(board, 0, 0, paint);

// 根据points数组绘制棋子
for (int row = 0; row < chessBoard.getChessPoints().length; row++) {
    for (int col = 0; col < chessBoard.getChessPoints()[row].length; col++) {
        if (chessBoard.hasChess(row, col)) {
            float left = chessBaseLeft + col * tileSize;
            float top = chessBaseTop + row * tileSize;
            int index = chessBoard.getChess(row, col) - 1;
            canvas.drawBitmap(chesses[index], left, top, paint);
        }
    }
}

至此,象棋界面的绘制就算完成。以下是最终实现的界面截图:

cmm-rendering

象棋残局大师开发实录(1)

小时候经常在街头看到有人摆象棋残局,红方无论赢棋还是和棋都算破解成功,否则就是失败。作为象棋低手,也曾尝试过好多次,但从来没有真正破解过哪个残局。有次甚至默记住某个残局的走法去破解,结果在解完所有圈套后还是在接下来的走棋中输掉。

一晃许多年过去了,象棋也很长时间没有再玩过。最近偶尔在网上接触到象棋残局,又再次把思绪给挑动起来。同时也了解到那些街头残局的最终结果通常都是和棋,并且由于残局往往会包含多个圈套,所以破解成功的可能性非常非常之低。因为在解套阶段,你必须步步走对才能把所有的圈套解除,否则只要有一步走错便是输。解完套之后,你仍然得小心翼翼地走棋,要是误走一着也很可能会输掉。正所谓:一着不慎,满盘皆输。

残局棋谱通常会有参考走法,按照这些走法逐步执行就可以破解成功。但目前我在网上碰到的有些残局的参考走法明显有错误。比如,红方在某个关键步骤时有不遵循参考走法的走法会让红方对黑方形成必杀,又或者黑方在某一步时换个走法就会使和棋的结局变成黑方必胜。当然,这些错误或正确的走法也可能是我想当然,因此我也不能确定我的想法是否真的正确。为解决这个问题,我决定开发一款结合人工智能的象棋残局软件。毕竟现在的人工智已经非常强大了,上半年的AlphoGo都已经把围棋这个难题给攻克,所以用人工智能来探索象棋残局还是非常合适的。

象棋游戏属于完全信息博弈游戏,所以它至少应具备如下几个部分:

  1. 某种在机器中表示棋局的方法,能够让程序知道博弈的状态。
  2. 产生合法走法的规则,以使博弈公正地进行,并可判断棋手是否乱走。
  3. 从所有合法的走法中选择最佳走法的技术。
  4. 一种评估局面优劣的方法,用以同上面的技术配合作出智能的选择。
  5. 用户界面。有了它,程序才可以用。

但我要开发的程序主要是用于研究象棋残局的,所以它还需要一些特殊的辅助功能。在最理想的情况下,残局的人机博弈走法应该和参考走法相同。但有时候我们会发现黑方会走出不在参考走法上的步骤。因为参考走法是经过多年的演变而来,通常是最优的,所以我们需要能对黑方的走棋进行纠错。当然,最好的做法是提高博弈算法的能力,但智能的改进不是一蹴而就的,纠错功能的提供只是作为这种无奈的补充。假如黑方的走法确实比参考走法更优,并且我们发现黑方这样走以后红方似乎找不到可以赢棋或者和棋的走法,这时就需要机器帮助我们计算出最佳走法。此外,轮到红方走棋时可能会发现有貌似比参考走法上更优的走法需要验证,这时我们需要在验证失败后能够回退到不同走法的分叉点上。

带有这些特殊需求的游戏界面线框图如下所示:

cmm-wireframe

ANTLR 4权威参考读书笔记(33)

ANTLR的语法分析器可以生成错误信息,也可以从大量不同情况的错误中恢复。我们还可以自定义错误信息以及将它们重定向到不同的错误监听器。所有这些功能都被封装在指定ANTLR错误处理策略的对象中。接下来我们就将详细研究该策略,以学习更多关于自定义语法分析器如何响应错误的知识。

更改ANTLR的错误处理策略

默认的错误处理机制工作得很好,但在有些非典型的情况下我们可能要更改它。首先,我们可能由于运行时开销而想要禁用一些内联错误处理。其次,我们可能希望在出现第一个语法错误时就退出语法分析器。例如,当为类似bash这样的shell解析命令行时,没必要试图从错误中恢复。无论如何我们不能冒险执行该命令,所以语法分析器可以在一遇到麻烦时就推出。

要了解错误处理策略,请查看接口ANTLRErrorStrategy及其具体的实现类DefaultErrorStrategy。这个类持有与默认错误处理行为相关联的一切。ANTLR语法分析器指示该对象报告错误并恢复。例如,下面是在每个ANTLR生成的规则函数里面的捕获块:

1
2
_errHandler.reportError(this, re);
_errHandler.recover(this, re);

_errHandler是一个持有DefaultErrorStrategy实例的引用的变量。方法reportError()和recover()表示错误报告和同步和返回功能。reportError()根据抛出的异常类型将错误报告委托给3个方法的其中之一。

回到第一种非典型的情况,让我们来降低语法分析器上的错误处理在运行时的负担。看看下面这段ANTLR为语法Simple中member+子规则生成的代码:

1
2
3
4
5
6
7
8
_errHandler.sync(this);
_la = _input.LA(1);
do {
    setState(22); member();
    setState(26);
    _errHandler.sync(this);
    _la = _input.LA(1);
} while ( _la==6 );

对于可以安全地假设输入语法是正确的应用程序,比如网络协议,我们最好避免检测并从错误中恢复的开销。我们可以通过继承DefaultErrorStrategy并用空方法覆写sync()来做到这点。Java编译器可能会内联并消除_errHandler.sync(this)调用。我们将在下个例子中阐述如何通知语法分析器使用不同的错误策略。

另一种非典型的情况是在出现第一个语法错误时就退出语法分析器。为使它工作,我们必须覆写3个关键的恢复方法,如下面的代码所示:

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
import org.antlr.v4.runtime.*;

public class BailErrorStrategy extends DefaultErrorStrategy {
    /** Instead of recovering from exception e, rethrow it wrapped
     *  in a generic RuntimeException so it is not caught by the
     *  rule function catches. Exception e is the "cause" of the
     *  RuntimeException.
     */
    @Override
    public void recover(Parser recognizer, RecognitionException e) {
        throw new RuntimeException(e);
    }

    /** Make sure we don't attempt to recover inline; if the parser
     *  successfully recovers, it won't throw an exception.
     */
    @Override
    public Token recoverInline(Parser recognizer) throws RecognitionException {
        throw new RuntimeException(new InputMismatchException(recognizer));
    }

    /** Make sure we don't attempt to recover from problems in subrules. */
    @Override
    public void sync(Parser recognizer) { }
}

对于测试平台,我们可以重复使用我们典型的样板代码。除了创建并启动语法分析器,我们需要创建一个新的BailErrorStrategy实例,并告诉语法分析器使用它替换默认的策略。

1
parser.setErrorHandler(new BailErrorStrategy());

当我们处理这个问题的时候,我们也应该在出现第一个词汇错误时退出。要做到这点,我们必须重写Lexer中的recover()方法。

1
2
3
4
5
6
7
public static class BailSimpleLexer extends SimpleLexer {
    public BailSimpleLexer(CharStream input) { super(input); }

    public void recover(LexerNoViableAltException e) {
        throw new RuntimeException(e);    // Bail out
    }
}

让我们先尝试一个词法错误,通过在输入开头插入一个#字符,词法分析器会抛出异常并从控制流中返回到主程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ antlr Simple.g
$ compile Simple TestBail
$ run TestBail
# class T { int i; }
EOF
line 1:1 token recognition error at: '#'
Exception in thread "main"
java.lang.RuntimeException: LexerNoViableAltException('#')
at TestBail$BailSimpleLexer.recover(TestBail.java:9)
at org.antlr.v4.runtime.Lexer.nextToken(Lexer.java:165)
at org.antlr.v4.runtime.BufferedTokenStream.fetch(BufferedT...Stream.java:139)
at org.antlr.v4.runtime.BufferedTokenStream.sync(BufferedT...Stream.java:133)
at org.antlr.v4.runtime.CommonTokenStream.setup(CommonTokenStream.java:129)
at org.antlr.v4.runtime.CommonTokenStream.LT(CommonTokenStream.java:111)
at org.antlr.v4.runtime.Parser.enterRule(Parser.java:424)
at SimpleParser.prog(SimpleParser.java:68)
at TestBail.main(TestBail.java:23)
...

语法分析器也会从第一个语法错误中退出(在这里是缺少类名)。

1
2
3
4
5
6
$ run TestBail
class { }
EOF
Exception in thread "main" java.lang.RuntimeException:
org.antlr.v4.runtime.InputMismatchException
...

我们将通过下面更改语法分析器报告错误的方式来演示ANTLRErrorStrategy接口的灵活性。要改变标准的消息“noviable alternative at input X,”,我们可以覆盖reportNoViableAlternative()并将消息更改为其它不同的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.antlr.v4.runtime.*;

public class MyErrorStrategy extends DefaultErrorStrategy {
    @Override
    public void reportNoViableAlternative(Parser parser, NoViableAltException e)
        throws RecognitionException {
        // ANTLR generates Parser subclasses from grammars and
        // Parser extends Recognizer. Parameter parser is a
        // pointer to the parser that detected the error
        String msg = "can't choose between alternatives";    // nonstandard msg
        parser.notifyErrorListeners(e.getOffendingToken(), msg, e);
    }
}

到这里,我们已经涵盖了ANTLR内所有重要的错误报告和恢复设施。因为ANTLRErrorListener和ANTLRErrorStategy接口,我们在错误消息发生的地方具有很大的灵活性:这些消息是什么,以及语法分析器如何从错误中恢复。