编程的精进之法

编程,众所周知被定义为知识工作。所有的知识工作,从业者和门外汉都喜欢把它神秘化,将整个过程以不可知论的风格来解释。理由往往非常简单粗暴,无非是“照搬体力工作时代的工作方式会在知识工作中失败。”

这其实没什么靠谱的理论。偶有几个人能写写理论与理由,也跟癔症者的呓语无甚差别。我个人是反对将知识工作神秘化的,我是科学管理原理的忠实粉丝,尽管科学管理原理的具体案例都过时了,但泰勒的研究方法依然是有效的,只是研究者和被研究者发生了一些微妙的变化。

彼得德鲁克在《21世纪的管理挑战》中提到,“知识工作者需要自我管理”,那么很明显不是体力工作时代形成的方法不能用在知识工作中,而是不能指望个体之外的人对个体进行简单的粗暴的分析和命令,不要妄想以此产生很高的生产效率。这个分析和下命令的人必须是知识工作者自己,我们需要自己纪录自己的行为,然后分析、优化,才能得到生产力的提升,任何向外诉求都会很快的撞上一个“天花板”而无法提升。如果非要寻求外部干预,那么我们只能说,对于新时代的管理者定位,老板更像老师,以引导和帮助训练为主,真正的效率提升主要还是靠自己。

1-floor

那么书归正传,追求知识工作的一种——编程的效率,是本文关注的重点。但我们首先要声明,本文不会给一个可以直接产生高效率的方法,而会给一个可以将生产效率可视化,并从中发现瓶颈的方式。至少在不改变质量的前提下,可以极大的提升你的效率,如果使用得当,可能会得到质量和效率的双提升。

本文引入的方法也并不新鲜,简单说来,就是任务列表法+PDCA的一套组合使用而已。大道至简,坚持者寡,而坚持下来的人往往都可以获得数倍的效率提升。

任务列表法

我们做任何事情都应该划出任务列表,按照列表一项项去完成,这不是什么特别稀奇的工作方式。然而,很多人的问题在于,列出的问题列表不能达到完全穷尽,各自独立。

2-todolist

完全穷尽是什么意思呢?

当我开始做事情的时候,如果不能把所有的事情穷尽,我列出的列表跟我做的事情是不完全等价的,这说明我们的工作行为非常混沌且不可视,哪怕是对自己。

有时,事情看起来在大面上穷尽了,但是做的时候,又会发现新的任务。那说明每一项任务的输入和输出没有想清楚,因此每当发现有所欠缺,就需要输入新的任务作为补充。于是任务列表就增加了,这也是一种没有穷尽。

各自独立是什么意思呢?

意味着,每一项任务都可以单独做完,而不需要先做完其中一项任务,才能做另一项。 假如我有三项任务:任务1、任务2、任务3。

我做的时候,必须把任务2做完,任务1才能做完;任务3做完,任务2才能做完。结果我就从任务1开始一路做到任务3,最后再逐步回到任务1,整个过程非常混乱,那就不是各自独立的。

在现实生活中想做到各项任务都独立,挑战还是比较大的,但是在编程的世界里就轻松多了。优秀的设计都是要求解耦的,如果做不到,基本等于活儿比较烂。

3-fly

当我们做到“任务的完全穷尽与各自独立”之后,我们的任务列表法才算达标,这之后才能高效的工作,然而达到这一点并不是一蹴而就的,没有谁可以一上来就做到任务划分的完全穷尽、各自独立,需要不停的刻意练习。所以我们称之为编程的精进之法。

PDCA

PDCA是Plan-Do-Check-Action四个词的组合。这是著名的戴明环。讲究从计划开始、经过实践、反思、产生的改进行动再纳入下一轮计划的持续改进过程。

当我们把这一套从工业领域搬过来的时候,我们对计划的理解还是工业领域那一套。如果用在个人提升方面,我们应该把PDCA微观化,这之后就有两个问题需要被解答,一个是Plan是什么?一个是Check什么?

第一个问题的答案很显然,我们前面讲的任务列表法就是在形成这个Plan。

第二个问题本身是一个母问题,每当我们对这个问题进行回答,都要回答一个衍生出来的子问题:我们要做点什么才能在需要Check的时候能够Check。

常用的套路有两个:

  • Plan的时候估计一个时间,然后开始做,做的时候计时,做完就要Check这个时间是否达标,无论快了还是慢了(通常是比较明显的差距才能引起反思,比如20%以上的差距),Check都要反思并产生Action,纳入到未来的Plan中去。
  • 估计的任务列表和实际做的任务列表是否是一样多的?往往是会多出来,这时就要反思,自己在哪里有不足导致了这个差别。

这些反思往往是发现自己的问题,比如自己不熟悉的知识点、方法,甚至业务知识,最后的Action也往往都是通过刻意练习来提升生产效率,比如反复练类似题目。有时也会借助一些工具来提升效率,比如抽取live template,使用快捷键,只是效率工具的使用往往也需要刻意练习。有时也可以通过复用技术(其实live template已经是复用技术了)来提升生产效率,然而可复用模式的识别与抽取本身也是需要练习的,否则在那里纠结浪费的时间更长。

有些同学会感觉到,记录了时间却不知道哪里有问题,这个时候可以跟TDD相结合,把时间划分为写测试的时间,写实现的时间和测试通过的时间。其实除去这几种时间,还有其他时间消耗,比如调研的时间。不管怎么划分,将时间消耗结构化掉,一部分一部分的追求最高效率是一种可行的办法。

举例

我们做一个简单的修改用户信息功能的API。那么我们在某一个Java技术栈上可能的任务列表是长这样的:

  1. 写UserController (10分钟)
  2. 写UserDAO (15分钟)

当你真正开始做的时候,会碰到两种主要的意外:

  1. 任务列表扩张
  2. 时间估计不准

下面就这个例子,就讲一讲当我们遇到这两种意外,该怎么反思和处理。

任务列表扩张

任务列表扩张,顾名思义,就是指我们所估计的任务数量会随着我们开始工作变的比预想的多,可能有两种主要原因:

  1. 技术原因
  2. 业务原因

技术原因:

比如在这个案例里面,第二项任务是“写UserDAO”,就是一个没想清楚的事情。我们还需要建数据库表,我们在一个有migration脚本支持的技术栈设计上工作,我们还需要写初始化脚本和回滚脚本。也许这是我的第一个表,所以我还得配置数据库,搞不好还要把ORM的基础代码都写完,所以这些导致了我可能任务估少了。

再比如,项目规范要求我们Controller不能直接调DAO,要在中间加一个Service,尽管我个人觉得这是一件很二的规范,然而规范就是规范,我对项目技术规范不熟悉,导致我的计划缺少了一些必要的任务。再比如,我们的项目采用了Jersey,根本没有Controller这么一个东西,那么不了解技术框架导致我的任务表从根本上就列错了。

这种情况属于我对技术了解不足,通过对任务列表扩张的原因进行Check,我会得出一些Action:去了解技术规范、项目的技术架构、现有的代码,以防止以后的任务画错。

业务原因:

也比如在这个例子里,在更新用户的API里不能更新密码,所以我们还需要一个专门修改密码的API。再比如,这是一个遗留系统,用户信息的修改会触发数据库里的一系列触发器,进而修改系统的其他数据,然而有些修改是有前提的,那么我就需要更多的任务去处理这些前提条件;或者当数据变化时,要求我去修改系统里的其他数据,那么我就需要更多的任务去完成这些工作。

这种情况属于我对整个系统的业务了解不足,通过对任务列表扩张原因的Check,我会得出一些Action:通读数据库表、通读代码、更全面的阅读需求,或者跟需求方更多的沟通,以了解业务。

时间估计不准

时间估计不准就简单很多,在这个例子里,可能的主要原因也有三个:

  1. 任务列表扩张了,但是我没意识到。比如UserDAO写起来没有我想的那么简单,所以多花了时间;
  2. 单纯的技术不熟练;
  3. 花了太多时间在纠结上;

对于隐藏的任务列表扩张,不准确的时间估计给了我们一个很好的线索去发现。一旦发现了,可以如前文所述去处理,也就不再赘述。

4-time

对于单纯的技术不熟练,正如前文所述,要设计刻意练习。比如我就曾设计过针对数据库的增删改查训练以提升自己的速度,使我即便使用TDD依然保持一个极高的速度。我们或许不曾意识到,基础能力的薄弱对于我们的高级能力的限制有多严重,这种体验也只有基础能力已经熟练的人去教基础能力不熟练的人一些高级技能的时候才会发现。这种视而不见的收益,使得大多数人都会轻视基本功的练习。哪怕已经获得收益的人,也容易鼓吹要更多的启发而忽略了基本功的价值。

对于花了太多时间在纠结上,这其实也是一种不熟练,是对设计知识和能力的不熟练。之前看的设计知识只能有一个大概的感觉,对于每个知识的边界、使用之后的发展、如何从一种设计过渡为另一种设计了解不清,从而害怕在那一刻犯错。实际上真正值得纠结的部分没有那么多,大多是自己吓自己,或者引入了过度设计。

当然也有一种情况是暴露出了架构级的问题,比如我们对于应该提出的原则性规范没有提出,导致我们每个地方都要现想,大家可以想象在没有RESTful之前设计Web API,我们可能真的是每一个API都现想的,有了它之后,我们的纠结时间就变少了。这种情况下,通过本方法,架构师也算是有了相应的数据支持,那么架构师也就有了发现问题的一种工具。

结论

总的来说,任务列表法+PDCA式工作法形成的组合方法,是一个通过逐渐提升个人能力以实现高效工作的方法。这两种方法单独拿出来用,都会由于各自的局限而触碰到各自的天花板,只有有机结合才能真正突破这个天花板。

刚开始使用时,很多人会感觉到一些痛苦,这一点上我只能说,提升就是痛苦的,而新的习惯一旦养成,痛苦也就不翼而飞,所以美国心理学之父威廉詹姆士说,“我们需要在尽可能早的时候,让尽可能多的有用动作变成自动的和习惯的……一段痛苦的艰难时期之后就是自由的时光”。当我们的基础能力达到一个极高的水平之后,会发现争取自由的筹码会变得更多。

5-freedom

更多精彩内容,请关注微信公众号:ThoughtWorks

Share

结对编程的正确姿势,你会了吗?

极限编程的各个实践已经广为人知,也颇具争议,我听到最多的话题当属结对了:

“我的小伙伴总拿着键盘不放,只听过麦霸,来到骚窝竟然还有键霸!”

“我总算明白为什么面前会有两个键盘子了,如果再给我一次机会(请用湖南话脑补)”

“我不知道我的小伙伴在做什么,我跟不上,很沮丧,要不玩会手机算了”

“我的小伙伴特别忙,有时候一天也找不到,我怎么办…”

“我的小伙伴是个急性子,总说‘XX你做的太慢了,客户着急要,还是我来做吧’”……

那么结对时,你的小伙伴碰到这样的问题怎么办呢?当你遇到键霸、手机哥、上网君的时候怎么破呢?

在展开之前让我们先来回顾一下结对编程的前世今生是什么。

结对编程

极限编程是由当时Smalltalk领域的大师级人物Kent Beck在1996年受聘领导克莱 斯勒公司的一个综合工资项目开发 C3(Chrysler Comprehensive Compensation)中首次采用, 并于1999年10月出版的《解析极限编程》一书中正式提出了这一软件开发方法,而我们今天要讨论的结对编程则是其中一项核心实践。

极限编程中的“极限”(Extreme)是指将我们认同的有效软件开发原理和实践应用到极限, 如: “如果集成测试很重要,那就要在一天中进行多次集成,并且反复进行回归测试”,所以我们要做持续集成。结对编程在提出时更多的是强调 “如果代码评审很好,那么我们就一直进行代码评审”,所以我们要做结对编程。简单讲,结对编程就是由两个程序员用同一台电脑完成同一个任务,由一个人负责编写代码,另一个负责审查代码,从而能够时时刻刻的进行代码评审。

但问题来了,原先一个人工作,现在两个人了。“原来我自己写就好了,现在多一个人我还要给他讲,多浪费时间啊”;“我也讲不明白,我就是喜欢写代码而已,别逼我”(请自动脑补一个新人和一个不耐烦的老手一起编程的感觉)。这让我想起了美国AT&T公司贝尔实验室的Bjarne Stroustrup博士说过的一句话:

设计和编程都是人的活动。忘记这一点,将会失去一切。- Bjarne Stroustrup

从极限编程诞生到今天的26年历史中,如果说持续集成是应用最广泛的一个实践,那么我认为结对编程则是最具争议的实践(没有之一)。其实这也间接印证了当时Kent Beck提出的关于“极限编程是一种社会性变革”的说法。在现代互联网如此多变和快速响应的软件行业趋势下,事实已经证明软件从此不再是一个人单打独斗的工作,而是要求越来越多的多角色多任务的协作。软件行业的工作者必须要有比以往更多的沟通和协作技巧。而这对于习惯了一个人的软件开发者而言是一个巨大的挑战,必然要有一个改变和适应的过程。这也是为什么结对编程会成为最具争议的实践。文章开头的那些现象都是一个人改变做事行为的必然过程,不信你去问一问那些老一点的ThoughtWorker,哪一个不是“从抢不到键盘到键霸,又从键霸到键盘无键,键在心中”的一个过程。

那么结对编程除了审查代码提升代码质量,还给我们带来哪些好处呢?

结对编程的好处

第一:培养新人,促进沟通,提升团队整体能力。

通过结对,年轻的团队成员可以向其他小伙伴学习,包括快捷键、算法、语法、SQL、设计、解决问题的思路、做事方式等等,1对1面对面师傅带徒弟式的学习是新技能get最快的方式之一。

第二:更好的知识共享和信息交流,促进团队协作。

结对中可以互相分享代码的上下文,交换对代码的理解,促进质量改进和团队协作,同时也使得代码集体所有制成为可能,减少团队对某些成员的依赖,降低团队风险。

第三:促进团队成员的沟通,提升团队凝聚力。

通过结对,成员间彼此熟悉,增深了解,从而能够更好的协作完成任务。

如何进行结对?

为了达到结对的目的,保持结对有趣持续的进行,通常根据结对的双方经验不同和场景分为如下多种角色和合作模式:

pair-mode

1.领航员和驾驶员(Driver-Navigator) – 键霸出没请小心。

驾驶员编写实现当前任务的代码,而领航员需要引领代码的编写并负责审查代码。除此之外,领航员通常还要考虑当前的实现方法是否正确,是否有别的做法,它是否会影响到其它功能模块,下一步是什么。驾驶员的主要任务是跟上领航员,负责完成代码的编写,保证代码质量。需要特别指出的是,微小的语法错误,多一个空行等错误,IDE会帮助我们纠正,是驾驶员关注的职责,而领航员只需进行提醒,结对时无需将此作为主要关注点。

合作场景:适应于各种组合,尤其一老一新组合。

2.乒乓模式

这里需要提及极限编程的另一实践:测试驱动测试。结对双方可以一个人编写失败的测试,一个人写实现通过测试;然后交换角色,不断循环。对于结对双方经验相当的情况下,由于交互和交换的频率很快,就如打乒乓一般,所人们戏称这种方式为结对的乒乓模式。

合作场景:适用于各种组合,尤其双方经验相当的场景。乒乓模式由于它的角色分工清晰,交换频率相对较快,所以乒乓模式可以帮助精力不集中的小伙伴快速融入,也是避免键霸出现的一个很好的方式。

3.鼠标和键盘模式

这是驾驶员和领航员的一种具体表现方式,其中一方使用鼠标,是领航员;另一方使用键盘完成代码的编写,是驾驶员。

合作场景:适用于一老一新组合。

有统计结果显示,好的结对工作效率是大于单兵作战的,能用较少的时间产生高质量的代码。那么为了保证结对的高效和高质量,我们还需要注意哪些呢?

几点Tips

1.多沟通。

由一个人的工作变成了两个人的事,小伙伴们就要彼此尊重,多沟通。如果有其他的任务要暂时离开,请及时告诉你的小伙伴,以便彼此更好的安排工作,保证效率。

2.确定开发任务列表(Tasking)

结对除了沟通,另一个挑战就是如何保持结对双方共同的开发节奏:一个小伙伴在做A功能,另一个小伙伴要做B功能。结对双方通过协商开发任务列表,能够提高对开发任务理解的一致性,确保开发节奏顺利进行。

3.定期交换小伙伴

定期交换小伙伴可以使得知识得到充分分享,每个小伙伴都有机会充当不同的角色,了解不同的知识上下文。与此同时,新的小伙伴的加入往往可以激发新的解题思路,或帮助发现问题,同时也增加结对的乐趣。

4.可持续的结对工作

真正的结对会比一人工作更专注,紧凑,所以一天8小时的结对会很累,因此结对需要定时的休息,保持合理的节奏。可与结对的小伙伴一起协商休息时间,比如一个小时或两个小时休息一次,从而保证可持续的工作。

5.多给新人机会

与新加入的小伙伴结对,需要耐心,多给予她/她上手的时间与空间。通常建议开始时多讲解,多展示,给她/他学习的机会;比如一开始可以由熟悉代码的小伙伴写测试,而新加入的写实现;随后可采用鼠标键盘方式或者乒乓结对方式。

6.勇敢加勇敢

对于新加入的小伙伴,如果跟不上怎么办?要勇敢的叫停,打断结对的小伙伴,弄懂这个问题,这样做才是达到了结对的目的。曾经有人说我记下来回家去弄懂,我更建议及时弄清楚。就如前面提到的,结对是一个快速让自己学习和成长的机会;而且你的小伙伴通过讲解也会梳理自己的思路,能够更深入的理解这个问题或技术,互助互学。如果这个问题发现项目中其它成员也不懂的,那么我们还可以将这个对话扩展开来,分享给整个团队,提升团队的战斗力,所以更推荐及时解决,当然深度需要适当的把握。

如果结对的时候遇到键霸怎么办?作为新人自带消除键霸光环,勇敢的把鼠标默默的递过去,把键盘牢牢的握在自己的手中,“亲,辛苦了,让我试一下,我来!”。

7.反馈

就如戴明环一样,做事情的环要闭合,有始有终,有序循环螺旋式改进。而反馈往往是最后一环,也是最有效的一环,是帮助自己和结对小伙伴的必要工具之一,温暖的“小黑屋”是可以经常光顾的。

8.不是所有的场景都适合结对

对于那些结果需要维护,能够促进沟通、知识传递等价值的开发行为都建议结对,而诸如方案调研、一些非常简单的问题(微小的缺陷修复如拼写错误)等是可以不用结对的。

总结

结对并不阻止个人的独立思考,它带来了诸多软件协作的好处,但结对也不是所谓的坐在一起就可以了。结对不是一成不变的,需要根据目前的任务灵活确定是否适合结对。 我认为想要做好结对,首要的是有效沟通。

一首打油诗说的好:

好结对成长快,互相监督与学习,感情信任日日增。

坏结对伤害大,手机上网人心离,团队早晚要散伙。

新人们不要怕,键盘牢牢握手中,勇气反馈早成长。

老人们不着急,系统把控在心中,沟通分享影响大。

有了这些姿势,文章开头那些问题你是不是已经有答案了呢?

题图来自网络,其它均由禚娴静提供。

Share

C#读书雷达

大家都知道,ThoughtWorks的技术雷达每年都会发布两到三次,它不但是业界技术趋势的标杆,更提供了一种卓有成效的方法论,即打造自己的技术雷达。在这种思想的驱动下,我们诞生了自己的读书雷达。但这份雷达略显high level,缺乏某一具体领域的详细书单。又由于最近很多同事都跟我讨论过C#书籍的问题,于是突发奇想,“滥竽充数”地搞了这份C#读书雷达,权当是读书雷达于C#这一领域的补充。

跟技术雷达和读书雷达一样,C#读书雷达也是非常主观的。并且我只会列出我读过(或粗略翻看过)的书籍,所以难免会有疏漏(如果有其他好书,欢迎各位补充)。

这份雷达同样分为四个象限:语言、应用、底层和规范,并且包含三个环,分别为初级、中级、高级。

imageC#ReadingRadar.001

基础

这个象限下的书籍都是不错的入门读物,从C#基本语法到高级应用(ASP.NET、WCF),但都是泛泛而谈,要想了解更深入的内容,还是应该阅读相关的书籍。

这三本都是大部头,适合放到案边以供随时查阅。个人觉得有其中一本就足够了。

技巧

本象限的书籍介绍C#的一些高级技巧,是学习C#的进阶读物。读了这些书,你就能写出高效优雅的C#代码。

前两本是Bill Wagner的大作,大名鼎鼎的Effective家族中的两位重量级成员。其内容包括语言习惯、资源管理、表达设计、动态特性、泛型、多线程、LINQ等等。两本书共包含100个行之有效的改善C#代码的tips,是便携C#代码的最佳实践,相信它们一定能帮助我们提升代码质量,增强可维护性。虽然这两本中文版书名容易让人迷惑,但只要记住它们的英文名(Effective C#和More Effective C#)就可以了。

《深入理解C#》是我打算重点推荐的,它是我阅读过的最好的C#书籍没有之一。它不但介绍了从C# 1开始到C# 5的所有特性,还解释了设计者为什么要这样设计。比如像迭代器块这样的特性,其他书籍很可能一笔带过,但本书却花整整一章去深入探讨,并且在补充材料中介绍了编译之后的状态机。

底层

说来奇怪,关于JVM的书籍如百花齐放,也不乏一些国产好书,但跟CLR有关的却凤毛麟角,翻来覆去就只有《CLR via C#》。但话说回来,这样的书有此一本也就足够了。本书涵盖CLR基础、类型系统、语言特性、核心机制和线程处理,常看常新,是居家旅行的必备。

如果您常逛.NET社区,一定知道赵三本,即著名.NET程序员老赵推荐的三本.NET书籍(均在本雷达中)。不过这“三兄弟”其实还后续了一位“四弟”,也就是《Pro .NET Performance》。这是我读过的第二本(第一本是《CLR via C#》)深入介绍.NET类型系统和垃圾回收的书,并且它的关注点在性能,这个领域的书籍也确实偏少。本书国内已经引进,正在翻译,不出意外的话明年应该能买到。

规范

前三个象限(基础、技巧、底层)逐渐提高和深入,有点像台阶。而“规范”这个象限有点像扶手,帮助我们更好地提高。

相比其他领域.NET(或C#)也非常缺乏规范类的书籍,这大概也是因为有《.NET设计规范》这本标杆在。本书的作者来自微软.NET Framework团队,讲述了他们在设计.NET时的一些决策过程,包括命名规范、类型设计,同时还有经验丰富的框架设计师、业界专家及用户给出的评注,为书中的许多规范增色不少。并且,它不仅对于.NET的代码规范,对于其他语言来说,也大有裨益。

《C#语言规范》是一本免费书(严格地说应该是文档而不是书),就安安静静地躺在我们安装的.NET Framework文件夹里。它是C#语言的说明文档,是以上所有书籍的最终依据。我以前会打印出来装订好,放在手边当字典翻阅。

以上就是我的C#读书雷达。

Share

后现代的系统编程语言——C++

C++作为一门经典的编程语言,从上世纪八十年代起至今一直被广泛应用在系统开发和高性能计算领域。近几年来随着各种编程语言和范式的兴起,C++的身影渐渐淡出了人们的视野。但是作为一个仍在不断进步的语言,C++在最近几年飞速发展,已经具备了现代语言应有的特性,而且也有了许多已有的和正在进行的新的拓展。

经典的C++

作为C语言的超集,一方面,C++集成了C在系统编程优点,能够精确的控制内存中的每一个bit;另一方面,提供了丰富的抽象机制和编程范式,引入了面向对象、泛型编程和函数式编程等风格。因为这一点,C++拥有了与C媲美的运行时性能,另一方面,也简化了C语言带来的领域建模的难度。但是因为C++的整体设计结合了多种风格,几乎相当于嵌套了几个小语言的一个庞大的系统,这也使得C++的整体易学性和易用性上有些差劲。同时,由于标准库更新跟不上需求,在诸如Concurrency/Network等应用层的软件设计方面逐渐被Java等后来者取代。而且,各个C++厂商对编译器的实现并没有完全参考ISO标准,也造成了很多跨平台可移植性和兼容性问题。

现代C++

C++在最近几年进行了几次探索和蜕变,让整个语言变得更具备现代化的特色。

资源管理

RAII(Resource Aquiration is Initialization,资源获取即初始化)作为C++的特色之一,被广泛地应用到C++的程序中。RAII通过堆对象的生命周期来控制资源(包括堆内存、文件句柄、网络连接等)的生命周期,使得资源管理变得更加自动化,同时也避免了引入垃圾回收带来的运行时负担。但这种模式有一个很重要的问题,就是当需要对资源进行共享时,需要做更多额外的工作来进行检查和同步等工作。

作为更现代的资源管理方式,C++11中引入了两种智能指针,std::shared_ptrstd::unique_ptr。前者拥有线程安全的引用计数,后者则是通过所有权(owenrship)转移来控制资源的生存周期。C++11中也引入了右值引用和移动语义,来避免资源传递的过程中的不必要的复制。

与Rust中的生命周期(Lifetime)和所有权(Ownership)的概念类似,C++的std::unique_ptr在每一次值传递的时候将自身持有的资源转移到赋值的目标,同时结合移动语义,将赋值过程进一步地优化。

Lambda

Functor作为C++ STL的一个重要组件,也是C++中被使用很多的一个功能。一个Functor其实就是一个重载了operator()的类的实例对象,这种对象配合C++模版的行为,可以被简单看成一个函数来调用,所以被称为Functor(函子)。但是,由于C++对于匿名类和内部类支持并不够好,使用Functor必须提前进行设计。一方面不方便使用,另一方面,定义和使用分离,对代码的组织和理解也造成了一定的困难。

首先,lambda作为Functor的替代品,解决了不能即时定义并使用的问题。配合STL中的容器和算法,lambda也能将C++的函数式风格发挥到极致。其次,出于C++一贯对性能和抽象的考虑,引入了lambda capture的概念,使得对象的生命周期能够绑定到lambda表达式,也就能够构建出闭包对象(closure)。另外,C++14中加入的generic lambda,增强了lambda的类型推导算法,在不损失类型安全特性的基础上,让组合式编程(Combinatorbased Programming)更加易于实现。

并发

C++设计的初期,并发并未作为核心的语言特性考虑在内。并且,线程等并发模型在不同平台之上也有各种不同的实现,构建一个统一的并发模型也很困难。

C++11中重新设计了C++的内存模型,在保持原有兼容性的基础之上加入了并发的内容。同时标准库中也加入了线程(<thread>)、信号量(<condition_variable>)、互斥锁(<mutex>)和原子操作(<atomic>)等内容。同时也在此基础上封装了future/promise模式和async等操作。

元编程

C++自身对元编程提供了良好的支持。作为主要组件之一的模版,提供了编译时的数值计算和类型计算。但一方面由于使用模版减慢编译速度,另一方面,在使用模版的时候,非常难以调试和排错,这让很多人望而却步,甚至对基于模版的STL组件也有一种畏惧感。

C++11中对元编程支持做了加强。首先是把type traits作为标准库引入,能够给模版提供一套直观的约束,也让类型作为C++中的第一类值(first-class value)存在;另外constexpr的引入简化了编译时的值运算,配合用户自定义字面量(user-defined literals)以及可变参数模版(varadic template/parameter pack)等特性,让C++能够更方便地定义内部DSL。

Bright Future

作为一门经典的编程语言,C++至今还在不断地更新着。即将到来的C++17中,正在筹备着这些重要的特性:

-更丰富的标准库:C++中对File System、Network等重要的组件进行了标准化的支持,

-Module TS:模块化提案,用于替代继承自C语言的头文件,简化C++的编译模型和模块依赖,

-Concepts TS:用于增强类型约束和类型推导,同时也简化模版的用法,

-Reflection TS:提供编译期静态反射的支持,简化和增强type traits,提供更丰富的元编程功能。

Conclusion

可以看到C++发展至今一直都走在时代的前列线上。一方面,增加了更多适合应用和系统开发的组件,另一方面,通过语言特性的扩充来简化抽象复杂度。作为这样一个兼具新生特性和历史责任的编程语言,足以预见其应用的广度;同样,更多的系统级开源项目,像Mesos等,也选择C++作为主要的编程语言。有足够的理由让我们相信,C++正在重获新生。

Share

也谈响应式编程

太长不读;

本文将会围绕reactive extension介绍reactive programming的起源,其要解决的问题。

编程范式的演进

最近几年,reactive programming这个词语的热度迅速提升,下面的 google trends的这个图表很能说明问题。

rp-trends

自从高级编程语言被发明以来,各种编程范式的编程语言层出不穷,命令式编程(如C)
面向对象编程(如Java,Ruby),函数式编程(如Clojure, Scala,Haskell)都曾经或者正在软件开发领域占有一席之地。

面向对象编程

上世纪九十年代前,命令式编程仍然在软件开发领域占有主导地位。随着软件规模的不断增大,面向对象编程以其封装性,可重用性受到开发者和组织的青睐。

进入多核时代

随着摩尔定律的失效,单核CPU的计算能力几乎达到了极限,CPU进入了多核时代,程序员转而通过并发编程,分布式系统来应对越来越复杂的计算任务。

然而并发编程并不是银弹,做为一种基于共享内存的并发编程,多线程编程有常见的死锁线程饥饿),race condition等问题,而且多线程bug的以其难以重现定位臭名昭著。

函数式编程的兴起

近年来逐渐火爆的functional programming以其提倡的:

  • 函数是编程语言的一等公民(function as first-class citizen)
  • 不可变量(immutable variable)
  • 无副作用的函数(no side-effect/reference transparency)
  • 可组合的函数(composable functions)

顺利地解决了因可变量mutabble variable被多个线程共享,修改等而导致可能的多线程的bug。

并发编程的痛点仍然存在

然而,functional programming就是现代的完美编程范式了么?远远不是。

即使使用了functional programming, 程序员总会需要处理异步任务或者事件,并且总有一些IO或者计算密集型的任务,这些任务可能还会阻塞其他活动线程,而且,处理异常,失败,线程任务之间的同步都比较困难而且容易出错。程序员需要不断地询问一个线程的运算结果(在Java中以Future<T>表示,T表示运算结果的类型)是否可用。我们来考虑一下下面两个例子:

有三个线程t1, t2, t3,他们的运算结果分别为f1, f2, f3
有一个线程t4依赖于这三个线程的运行结果,而且每个线程都有有可能执行失败。
我们该如何编写线程t4的代码?

GUI程序中一次拖动操作中光标的位置就可被表示为Future<List<Position>>, (使用Future是因为这些Position的值是在未来的时间点生成的)。

如果我们希望在第一个Position可用时(拖动时间的开始位置)就能够在这Position所对应的位置画点,而不是等所有的Position都可用是一次性把光标的运行轨迹画出来。即我们希望程序能够尽快对输入进行响应。

即程序要及时,非阻塞地对输入响应。

上面的两个例子就是reactive programming尝试解决的问题,而Reactive Extension做为这个问题的答案,应运而生了。

Reactive Extension

Reactive Extension 这个概念最早出现在.net社区的Rx.net,一个提供处理异步事件的程序库,其核心概念是Observable,表示有限或者无限多个现在或者将来到达的事件。Observable提供了onNextonErroronCompleted供开发者定制新元素到达,出现错误,或者流结束时的程序的行为。
并提供了List上类似的操作,如mapfilterreduce,大大降低了异步事件编程的复杂度。

因为这些概念是如此的强大,以至于很多编程语言,如javarubyjavascript很快就有了各自的reactvie extension

关于reactive extension的技术细节可以在我的这篇博客里找到。这个视频详细地介绍了为什么需要reactive extension,以及reactive extension的是如何被发明出来的。

Reactive Manifesto

Wikipedia上对reactive programming解释如下:

reactive programming is a programming paradigm oriented around data flows and the propagation of change.

举个例子,在命令式编程下,表达式a = b + c,a的值在这个表达式执行完毕之后就是确定的,即使bc的值发生变化,a的值也不会改变。然而在响应式编程的语境下,a的值与bc的值是绑定的,上述表达式其实建立的是abc之间的一种依赖,a的值会随bc的变化而变化。

我们称之为能够响应输入变化的事件(event)

然而现在来看,上述定义已经不能囊括reactive programming的含义了。随着软件系统的非功能需求要求越来越高,reactive已不仅局限于响应事件(event)的传递,也表示程序能够响应负载(load),系统运行时出现的错误(failure)

发布于2014年9月份的Reactive Manifesto以宣言的形式提供了能够满足这些需求的软件系统架构设计的指导原则。

Reactive Architecture

在笔者看来,reactive programming可以从语言和架构两种层面上来理解,近年来层出不穷的各种语言的reactive extention就是语言层面的代表,而在架构层面上,也有遵循了reactive manifesto的类库(如akka)出现,笔者暂且称之为reactive architecture

在后续的文章中,笔者将会带着大家理解一个reactive architecture是如何做到reactive的。

Share