抽象的不同层次

一个关于抽象的故事 抽象能力应该是程序员所需要具备的、再如何强调都不为过的、最重要的能力了。通过抽象,我们可以从纷纭错乱的、独立的、看似互不干涉的大量问题中解脱出来,找到一个方案,然后一举解决一类问题。 由于抽象是提取了众多具体事物的某种特征并进行了简化的结果,抽象的概念有时候反而会阻碍我们的思考(大概人的大脑更喜欢具体的事物吧)。这时候我们与需要具体的实例来帮助理解。也就是说,在思维活动中,不论是代码编写,还是方案设计时需要不断的从实例中提取概念并形成抽象,又需要不时的将抽象具体化成实例来验证理论。Bret Victor曾将这个过程比作抽象的梯子是非常有见地的 – 你需要不断的爬上爬下,从高处看一看概览,然后回到地上做一些实际而具体的动作。 另一个有趣的事实是,抽象可以有多个层次,当我们终于脱离了众多细节,建立起更高层的概念时,这些高级(high-level)的概念又变成了新的细节,而我们又可以基于这些细节作出新的抽象。就好比我们现在使用JavaScript或者Python来写程序的时候,基本上不需要考虑诸如寄存器的编码含义,机器指令长度等等细节,也无需关注内存的分配和释放等等细节,而只需关注业务功能即可。 这么说起来太过于抽象,我们来看一个具体的例子吧。 背景介绍 在这篇文章中,我希望通过对一个模拟项目的自动化测试(使用cypress)的重构来描述一些代码编写/重构时的模式。其中涉及到的重构方法比较常规,并没有高深的技巧,不过我觉得通过详细解析一个从可以工作的代码演进为较为整洁且便于理解/修改的测试用例,整个过程还是比较有趣的,希望你也可以有所收获。 所谓模拟项目,是对我从2019年1月到10月所经历的一个项目的模拟。纯粹从业务来说,该项目的业务规则/逻辑并不是十分复杂,不过技术细节上有很多值得反思的地方。我们的自动化测试也是经历了N个版本的迭代,最后我也成功将这篇文章中描述的模式应用到了该项目中,效果也是比较令人满意的。 这里的模拟应用Questionnaire是这样一个服务:通过类似调查问卷的方式来输入用户技能,角色,技术水平,项目偏好等等,根据后台的规则引擎来返回一些最适合用户的offer列表。后台的计算逻辑我们这里刻意将其淡化,这里只关注信息的采集部分。 应该注意的是,这里的调查问卷是动态的,比如如果对问题Q1选择了答案A,则下一个问题会变成Q303,如果选B的话则需要回答问题Q1024等。也就是说,问卷的路线有可能是多条的,而且每一条都是同等重要的(至少在测试里需要完整走通一遍)。 从界面上来看,我将整个问卷简化成了3步,实际场景里则可能是10+步,而且每一步中需要回答的问题长短不等,有些问题展现/隐藏还会依赖于前面某一步/某几步用户的答案等。用户只有完成了当前所在的步骤的所有必选项之后,才可以进入下一步(Next按钮才会可用)。 抽象101 - 函数 从功能测试编写的角度来看,将每个步骤视为一个独立的单元是一个合理的做法。每一个步骤需要做的事情也非常类似: 验证该步骤的标题是正确的 填写该步骤的所有必选项 点击下一步按钮 在写一个section的测试时,直观的通过cypress提供的API的结果大致为(假设这里的所有data-test标记我们已经在应用代码中打好了桩): it('Verify the basic information section', () => { cy.get('[data-test="step-title"]').contains('Basic information'); cy.get('[data-test="email-address"] input').type('juntao.qiu@gmail.com'); cy.get('[data-test="assignment"] input[value="assigned"]').check(); cy.get('button[data-test="next-button"]').click(); }); 第二个section的流程大同小异,都是先用selector找到页面元素,如果可以找到的话,通过cypress的API来模拟用户的实际操作: it('Verify the details section', () => { cy.get('[data-test="step-title"]').contains('More details'); cy.get('[data-test="ps-role"]').click(); cy.get('[data-value="dev"]').click(); cy.get('[data-test="developer"] input[value="frontend"]').check(); cy.get('[data-test="rating"] [for="rating-4"]').click(); cy.get('button[data-test="next-button"]').click(); }); 如果逐字对比的话,每行代码几乎都不一样,但是如果仔细看又会发现很多重复。消除这些重复显然可以让代码干净一些,可读性也可以得到提高。一个立即可以想到的重构方法是抽取函数,将验证标题和点击下一步抽取如下: const checkStepTitle = (title) => { cy.get("[data-test="step-title"]").contains(title); } const goToNextStep = () => { cy....

July 9, 2020 3 min

<del>程序员</del>英语学习指南

程序员英语学习指南 这篇文章从我个人的经验出发,总结了英语学习中一些比较重要的关键点。简单来说,就是尝试投入专门的时间,集中精力突破7,000到8,000的核心词汇,然后在这个基础上逐项提升英文的输入/输出能力(听、说、读、写)。一个省时省力的实践是:听和说可以一起练习,读和写往往也连在一起。如果可以在听的同时练习说,以及读的时候同时写,那么显然可以事半功倍,并促进形成比较积极的反馈,从而激励自己投入更多的时间和精力来提高英文能力。 在进入正题之前,我们需要看一些数据和事实,来帮助我们认清把英语学好是一件什么规模/规格的任务。 语言学习是困难的 语言的学习和很多生活中其他工作/任务有很大的不同,这些差异往往会导致学习者的挫败感。首先,学习外语的反馈周期往往很长,即使你每天坚持扩充词汇量、阅读文章、甚至与采用外语来写作,而从开始投入到看到成效往往需要较长时间。其次,自然语言中的规则往往不固定,比如英文中很多动词的过去式和过去分词都是不规则的,这些不规则使得学习者在熟记“公式”外,还需要记忆很多变体(比如单词seek的过去式和过去分词都是sought)。最后,语言通常总是和文化关联在一起,这也意味着它无处不在却又难以描述,而且这些关联又非常离散,难以集中突破。 虽然个体之间会有各式各样的差异,但是总的来说,这些客观的挑战对于每个外语学习者都是存在的。如果你咨询任何一个外语比较好的同事,大约会得到很多不同版本的经验或者秘笈。我这里想要分享的自然只是我自己的经验,未必系统,当然更不可能全面,但是我相信可能对你也有效。 简而言之,无论是在听、说、读、写四大元素的任何一个方面,要想取得比较大的进步,首先要做的是认真对待词汇量。丰富的词汇量是学习更近一步内容的基石,一方面,她可以帮助你更好的理解其他人的意图,捕捉到细微的感情差异。另一方面,她可以帮助更精确,更快速有效的将自己的思想传递给他人。 重中之重:词汇量 词汇量的重要性 开宗明义,永远不要“妄图”把英语说的和native一样好。这个目标既不切合实际,也完全没有必要。我估计不是每个人都同意这个观点,为了让你放弃这种想法,我们可以先看一下native speaker的平均词汇量: 就近50万的受访者的统计显示,50%的人在8岁时会有10,000的词汇量,而在15岁时这个数字会增长到20,000,而这个数字大约会在成年后到达30,000并基本保持一个平稳的状态。作为对比,我国的CET4级考试词汇量大约为4,000,基本相当于4岁的native,而雅思则需要近8,000的词汇量,约相当于7岁的native的平均水平。 不过好消息是,如果你愿意花一些时间来攻克7,000-8,000高频词汇的话,你就可以读懂大约80%-90%的普通材料(大部分新闻,公告,大部分技术书籍,一些简单的儿童读物等),而且你又可以通过这些材料进一步将自己的词汇量扩充到更高。 英语的另一个有趣的(可恨的)特点是可以通过非常简单的基础词汇+介词形成丰富的短语,如果可以把这些短语正确的应用,你甚至无需花费太多时间来学习更高级的词汇即可应对日常生活学习工作中的各种场景。把这一点用到极致的应该是词汇量相当于8岁儿童平均水平的懂王特朗普了,不过话又说回来了,这可能也是一项非凡的技能,比如可以和动词stand搭配形成短语并具备独立含义的结构就是十多个。 一个微小的例子 为了更好的说明上面的问题,这里有一个小例子。下面这段话是The Economist(经济学人)里的某篇最新文章中的两段。如果你的词汇量在8,000以下,可能读起来会比较吃力。如果有12,000,则读起来不会有太大障碍。而要像读中文那样顺畅的读懂大部分The Economist中的文章,则需要20,000以上的词汇量。 Many a regime has been toppled by a plague, but so far covid-19 is having the opposite effect. Most leaders have seen their approval ratings rise, even as the disease has killed at least 250,000 people. Morning Consult, a pollster, has found that a group of ten politicians have enjoyed an average gain of nine percentage points since the World Health Organisation declared a pandemic on March 11th....

May 13, 2020 3 min

一个输入框你要做一周?

How long does it take for adding a InputBox? When the Product Owner told you it’s a small change, don’t trust her. An estimation session After iteration planing meeting, after all the stories had been walked through, PO turned to you, pretending it was just a impromptu chat, asking how long does it take for adding a input box to one page. What he described was a “simple” input box for user to enter his/her address along with other personal information and persist to backend....

June 17, 2019 11 min

7个你需要知道的结对礼仪

7个你需要知道的结对礼仪 结对编程远远不是两个程序员坐在一起写代码那么简单。 — 鲁迅(没有说过) 结对编程 结对编程可能算是比测试驱动开发更具有争议的敏捷实践了,事实上,仅有很少的团队可以很好的实施它并从中受益,对于更多的团队来说,即使在践行敏捷的团队中,也常常会分为旗帜鲜明的两个阵营。提倡者往往会强调结对编程在“传递领域知识”,“减少潜在缺陷”,“降低信息孤岛的形成”等方面的作用;而反对者则认为结对编程在很多时候都是在浪费时间:开发者在实践中很多时候都难以聚焦,容易产生分歧,另外个人的产出往往也难以度量。 这篇文章不打算讨论结对编程对效率的影响,也不讨论要不要进行结对编程,当然也不会涉及以何种力度来执行结对。这里的假设是团队决定采用结对编程,但是对于如何实施存在一些疑虑;或者已经在采用结对编程的团队里,发现很多时候结对编程并没有很好的发挥作用,需要一些更有实践意义的指导。 结对编程远远不是两个开发者坐在一起写代码那么简单。作为一种科学且充满趣味性(才不是)的工程实践,事实上它是有一些基本原则的,团队里的开发者需要正确的实施这些原则,结对编程才有可能为团队带来实际的受益。 假设你找到了另一个程序员,并且已经准备好一起来编码实现一个具体的业务需求。在开始着手开始结对之前,你首先需要setup环境。 硬件设置 工欲善其事,必先利其器。首先你和你的peer需要一个大的外接显示器和有一个可以调节高度的桌子(当然也可以用纸箱子DIY一个低配版)。这一点往往被初学者忽视,而事实上它可以直接决定结对能不能作为一个可持续的团队工程实践。如果你的颈椎长时间处于不舒服的状态,你的注意力很快会退散,精神会变得难以集中,而一个处于病态的身体是无法支撑长时间的编码工作的。 在开始之前,请将屏幕调整到舒适的高度,以不仰头不低头为度(身高不一样的两个人可以通过调整椅子的高度达到基本一致)。此外还要注意看屏幕的角度,如果你需要抻着脖子才能看清楚屏幕,那么当时间稍长一点时,你的颈椎也会非常吃力。因此,如果条件允许的话,你和你的peer可以使用双屏来进行结对。 当然,还有一些常见的其他关于硬件准备的细节,比如: VGA/HDMI转接头 充足的电源 键盘/鼠标转接头 便签和笔 纸和笔永远是你(们)的好朋友,在实际动手写代码之前,请拿出纸笔来将要做的事情划分成更细粒度,可以验证的任务列表,贴在显示器的下边缘。最后,另外记得将手机调成震动模式,利人利己。 软件设置 硬件准备好之后就可以进行软件的配置了。软件问题远比硬件复杂,因为大部分开发者都有自己钟爱的工具集,小到curl/wget,大到Vim/Emacs,不要期望在这个问题上和peer达成一致,这是可遇而不可求,可望而不可即的。 根据我自己的经验而言,高级一些的IDE比如JetBrains出品的Intellij,WebStorm等都可以随意切换快捷键集合(keymap)。如果你和peer就快捷键的使用上无法达成一致,在轮到你输入代码的时候,你可以切换到自己熟悉的Keymap,反之亦然。 在Intellij/WebStorm里,你可以通过 ctrl+`` 来切换各种设置,比如选择3`就可以切换不同的Keymap,然后在下一步的窗口中按照自己的喜好进行切换预设的Keymap。某项目的王晓峰(Vim党)和张哲(Emacs党)尤其擅长此技,他们两人结对时,可以做到在抢键盘的瞬间将keymap切换到自己的挚爱而对方无察觉的地步。 这种技法事实上仅仅对于结对双方都有很高超的键盘操作能力的前提下,也唯有这种场景下,双发可以互不妥协。对于另一种场景(可能在实际中更为常见),比如结对一方键盘技巧和操作效率低下,影响结对流畅程度的场景,则需要效率低下的一方自己摒弃陋习(使用鼠标而不是键盘),快速赶上。 和别人结对之前,你需要至少熟悉一个IDE/编辑器,比如通过纯键盘的操作完成 按照名称查文件 按照内容搜索 定位到指定文件的指定行/指定函数 选中变量,表达式,语句等 可以快速执行测试(可以在命令行,也可以在IDE中) 熟常基本Shell技能和常用命令行工具的使用,可以完成诸如 文件搜索 网络访问 正则表达式的应用 查找替换文件中的内容 等操作,这样在结对时可以大大提高效率。这些都是稍加练习就可以掌握的技能,并没有多少技术含量在内,而且学会了可以收益很久。 当知识不对等时 好了,铺垫了这么多,终于到了正题部分。最理想的结对状态是,双方的技能水平相当,知识储备基本类似,可以非常流畅的进行交流,在结对过程中可以完全专注在需要解决的问题本身上,讨论时思想激烈碰撞,编码时键动如飞,不知日之将夕。这种场景下完全不需要任何技巧,随心所欲,自由发挥即可。与此对应的另一种场景是双方都没有任何储备,技能也无法胜任,这种情况我们需要在项目上完全避免。 这两种极端的情况之外,就是不对等的场景了,这也是现实中最为常见的case:很多时候,结对双方会有一个人比较有经验,而另一个人则在某方面需要catchup。比如一个老手带一个新手,或者一个擅长业务的开发和另一个该领域的新人结对等。 一般而言,需要双方有一个人来做主导,另一个人来观察,并在过程中交互,答疑解惑,共同完成任务。与传统的教与学不同的是:结对需要的是两个智慧头脑的碰撞,而不是单方面的灌输。因此观察者不是单方向的被动接受,主导者也并非完全讲述。事实上结对是一个会有激烈交互的过程。 主导者 对于主导者来说,千万不要太投入,而无视peer的感受。这种场景非常常见,我自己有时候也会不自觉的忽略掉peer,自顾自的写代码,很多时候把peer当成了小黄鸭。这时候你的peer会有强烈的挫败感,也很难跟上你的节奏,从而影响结对的效果。作为主导者,需要更耐心一些,不断的和自己的peer交互。 另一个极端是,主导者太热心的coach,而忽视了给新人实际锻炼的机会。这时候需要主导者给peer更多的实践机会:比如在带着新人编写了一个小的TDD循环(红绿重构)之后,可以抑制住自己接着写的冲动(我知道这个非常困难),然后将键盘交给你的peer,让他模仿你刚才的做法来完成下一个。 有时候,当你看到peer正在用一个不好的做法来完成任务时,你可以即使让他停下来,并通过问问题的方式来启发他: 还有更好的做法吗? 如果peer仍然在迟疑的话,你可以进一步提示: 你觉得XXX会不会更好? 一个实际的例子是,你们在写一段JS代码来迭代一个列表,你的peer正在用for循环来操作一个数组,而你可以提议使用Array.map。有些时候,你的peer会给你一些惊喜的回答!他的回答甚至比你预想中的更加出色,你也可以通过这种方式来向他学习。 观察者 另一方面,作为观察者而言,结对毋庸置疑是一种特别好的学习机会,你应该抓住一切可能的机会来向你的peer学习。包括快捷键的使用,命令行工具参数的应用,良好的编程习惯等等。保持你的专注力和好奇心,比如你看到peer神器的通过快捷键删除了花括号(block)中的所有代码,或者将curl的返回值以prettify过的样式打印到控制台,或者通过命令行merge了一个PR等等。 在实践的时候,可以采取Ping-Pang的方式来互换主导者和观察者的角色。比如,A写一个测试,B来写实现,A来重构,然后换B来写测试,A来实现,B来做重构等等。开始时,可能会由一个人来主导,随着合作越来越顺畅,你们可以提高交换的频次。 保持专注 在选定了要两人一起解决的问题之后,你们需要一起完成任务划分。这样可以确保你们可以永远关注在单一任务上,避免任务切换带来的损耗。 在做完一项任务后,用mark笔轻轻将其从纸上划掉(或者打钩)。千万不要小看这个小动作的威力,它既可以将你们的工作进度很好的表述出来,也可以在任何时候帮助你们回到正在做的事情上(特别是在吃完午饭之后),另外这个微小的具有仪式感的动作是对大脑的一个正向反馈,促进多巴胺的分泌(代码写的这么开心,还要什么女朋友?)。 很多时候,我们需要暂时搁置争议,保持前行。 无法统一的意见 如果你遇到了一个固执己见的同事,而不凑巧的是你也是一个难以被说服的人,那么如何处理那些无法避免的争论呢?特别是那些没有对错之分的技术问题。比如那种编程语言更适合Web开发,比如如何践行TDD(比如自顶向下的TDD和自底向上的TDD)等等。...

December 7, 2018 1 min

从三明治到六边形

本文首先发表于ThoughtWorks洞见。 软件项目的套路 如果你平时的工作是做各种项目(而不是产品),而且你工作的时间足够长,那么自然见识过很多不同类型的项目。在切换过多次上下文之后,作为程序员的你,自然而然的会感到一定程度的重复:稍加抽象,你会发现所有的业务系统都几乎做着同样的事情: 从某种渠道与用户交互,从而接受输入(Native App,Mobile Site,Web Site,桌面应用等等) 将用户输入的数据按照一定规则进行转换,然后保存起来(通常是关系型数据库) 将业务数据以某种形式展现(列表,卡片,地图上的Marker,时间线等) 稍加简化,你会发现大部分业务系统其实就是对某种形式的资源进行管理。所谓管理,也无非是增删查改(CRUD)操作。比如知乎是对“问题”这种资源的管理,LinkedIn是对“Profile”的管理,Jenkins对构建任务的管理等等,粗略的看起来都是这一个套路(当然,每个系统管理的资源类型可能不止一种,比如知乎还有时间线,Live,动态等等资源的管理)。 这些情况甚至会给开发者一种错觉:世界上所有的信息管理系统都是一样的,不同的仅仅是技术栈和操作的业务对象而已。如果写好一个模板,几乎都可以将开发过程自动化起来。事实上,有一些工具已经支持通过配置文件(比如yaml或者json/XML)的描述来生成对应的代码的功能。 如果真是这样的话,软件开发就简单多了,只需要知道客户业务的资源,然后写写配置文件,最后执行了一个命令来生成应用程序就好了。不过如果你和我一样生活在现实世界的话,还是趁早放弃这种完全自动化的想法吧。 复杂的业务 现实世界的软件开发是复杂的,复杂性并不体现在具体的技术栈上。如Java,Spring,Docker,MySQL等等具体的技术是可以学习很快就熟练掌握的。软件真正复杂的部分,往往是业务本身,比如航空公司的超售策略,在超售之后Remove乘客的策略等;比如亚马逊的打折策略,物流策略等。 用软件模型如何优雅而合理的反应复杂的业务(以便在未来业务发生变化时可以更快速,更低错误的作出响应)本身也是复杂的。要将复杂的业务规则转换成软件模型是软件活动中非常重要的一环,也是信息传递往往会失真的一环。业务人员说的A可能被软件开发者理解成Z,反过来也一样。 举个例子,我给租来的房子买了1年的联通宽带。可是不多过了6个月后,房东想要卖房子把我赶了出来,在搬家之后,我需要通知联通公司帮我做移机服务。 如果纯粹从开发者的角度出发,写出来的代码可能看起来是这样的: public class Customer { private String address; public void setAddress(String address) { this.address = address; } public String getAddress() { return this.address; } } 中规中矩,一个简单的值对象。作为对比,通过与领域专家的交流之后,写出来的代码会是这样: public class Customer { private String address; public void movingHome(String address) { this.address = address; } } 通过引入业务场景中的概念movingHome,代码就变得有了业务含义,除了可读性变强之外,这样的代码也便于和领域专家进行交流和讨论。Eric在领域驱动设计(Domain Drvien Design)中将统一语言视为实施DDD的先决条件。 层次架构(三明治) All problems in computer science can be solved by another level of indirection, except of course for the problem of too many indirections....

August 21, 2017 2 min