如何写程序入门哪位高手可以解释一下吗?

作为一个管理者,先跟大家说一个不是秘密的秘密,年度调薪时,拿到的总预算几乎是固定的,如何分?基本是按照二八原则,着重考虑前 20% 的程序员。所谓前 20% 的程序员,就是价值前 20% 的程序员。如何评估价值?从一个古老的故事说起。在,很久,很久以前,上大学前的谢师宴上,一个小学老师拉着我的手,语重心长地对我说了不下五遍:“你有内才,但重要的是也要有外才!”所谓外才,就是一个人的影响力:一个人“内才”10 分,被人看到的有几分?有多少人看到?博多·舍费尔的《财务自由之路》(千万别被书名误导,是一本不错的书),书中提到一个公式,我简化一下,就是我小学老师当初想跟我表达的:价值 = 能力 * (10 * 影响力)影响力有着最大的权重,这个很好理解,看看现在的网红经济就知道了,在网络时代,也许权重不再是 10,而是 100、1000……本文当然不谈网红经济,我们来说说,为什么质量对程序员如此重要?因为它可以提升你的能力和影响力。首先,质量是可以积累的能力我刚毕业时用的是如火如荼的 Delphi,如今几乎已经绝迹,程序员要学的东西太多了,而且年年都在变。大家可以看看 Github 上的一个项目:开发者路线图,或者看看技术雷达。上面提到的技术有多少是十年前存在的?又有多少十年后还会存在?一路狂奔的路上,技术来来走走,我们问问自己,在奔跑的路上积累了什么?我参加工作一两年后,才知道 1 Bytes = 8 bits, 基本功差到没朋友。然而,值得骄傲地是,我的绩效一向在团队里名列前茅,为什么?因为我对质量有执念,做的东西让人放心。我非常认真地对待自己的代码,这种责任心和产出质量,直到现在仍然是我的吃饭家伙。我是老程序员了,然而没有日本的寿司之神老。寿司之神 90 多岁了,打天下的还是两个字:质量。所以,一路狂奔中能够积累的,无非是基本功、质量,还有思维体系。最近很火的 996 话题,也可以从这个角度去思考,如果 996 无碍于自己的身心健康,也无碍于自己积累基本功和质量能力,倒不用太排斥,从心就好,毕竟工资高。但是如果用 996 的时间天天跟烂代码搏斗,什么积累都没有,趁早跳槽吧。其次,质量是影响力的发动机。当我们谈影响力的时候,我们首先要注意的是消除负影响力。什么是负影响力,质量差的代码就是负影响力。代码是程序员的产出,这个都知道;但代码是一种成本,这个往往被忽略。平均来说,一个程序员写代码的时间和读代码的时间之比是 1 : 9 。这意味着,你产出 1 小时代码时,也为别人(包含未来的自己)制造了 9 小时的读代码的成本。这个还不包含修 bug 的时间。有了代码是一种成本的思路,就知道,低质量代码的成本是高质量代码成本的 10 倍 50 倍,这就是一个工匠的产出是码农的产出的 10 倍 50 倍的真相:工匠不在于他们写的代码多,而在于他们的代码成本低。更为重要的是,作为团队的一员,我们的代码产生的成本是附加在别人身上的,通俗点说:如果老是挖坑给别人填,你的影响力可不就是负的吗?反过来说,当所有人都在跟烂代码搏斗时,你写出的代码如果能最小化别人的阅读成本和维护成本,继而 QA 的测试成本、用户的使用成本、团队的 Hotfix 成本,别人真的会对你感激涕零。这不就是影响力的起点吗?前两天看到一篇文章,标题是《最大的能力,是让人放心》。作为管理者的我深有体会,“你办事,我放心”是一个人能得到的最高评价,大概只有 10% 的人,他们做的东西,不需要别人去 Review 甚至收尾,这种人,我称之为工匠。2 什么是质量?了解了质量的意义,我们来明确一下质量的定义。寿司之神做的寿司,就算对一个对鱼或虾过敏的人或者不吃寿司的人,仍旧有精神上的意义,这是质量的最高境界:举世公认的卓越。神的境界可望不可即,也不是本 Chat 的内容,让我们回到人间。我最喜欢的对质量的定义之一,是大师温伯格 (Gerald M.Weinberg)说的:质量是对某些人的价值(Quality is Value to some person)。这个听上去朴实无华的实用主义标准,绝不是能轻松达成的。为了让用户觉得有价值,我们需要做出好的产品;而为了做出好的产品,我们要把事情做对;为了把事情做对,我们需要好的方法。为此,我基于 David A. Garvin 的五种质量模型,稍加调整,提出软件开发质量圈模型,如图:基于过程的质量学厨之人,必先切菜;练武之人,必扎马步;高尔夫明星老虎伍兹,每天练习挥杆。他们在练什么?练习基本功,练习过程。大家留意一下,现在很多的高档餐厅,厨房都是透明或者开放式的,我们可以看到大厨在里面井井有条的制作过程。一个制作过程都不敢公开示人的厨师,或者那种一顿操作猛如虎的厨师,你相信他做出来的菜是好吃的吗?基于产出的质量对程序员来说,最主要的直接产出就是代码了。代码对其它程序员的价值主要是高可读性和可维护性,对 QA 的价值是 Bug 少,对产品经理的价值是符合需求规格。除了代码,我想强调一下设计文档这个产出,尤其是一些基本的 UML 图,比如 Domain Model、Sequence Diagram 等。一图胜千言,它们在团队交流中起到很大的作用。不夸张地说,画图能力是通往架构师、技术管理等高级职位的必修课。由于篇幅关系,本 chat 不会对此展开。基于产品的质量如果说基于产出的质量是关于把事情做对,基于产品的质量则是关于做对的事情。如果需求是低效的甚至是错的,那么代码质量再高也没有意义。在这个层面思考,业务才是我们要解决的问题,技术或代码只是解决问题的一种方案。这就是程序员必须熟悉业务的原因,如果连要解决的问题都不了解,那你是在做什么呢?基于价值的质量这个是更高的层面,如果说产品是一种 output (产出),用户价值则是一种 outcome (结果)。有一句大家认为是亨利·福特(福特汽车创始人)说的话:如果你问客户要什么,他会告诉你要一匹更快的马。 by 福特没说过。我们可以按照客户的需求,给他一匹很快的马,这匹马就是我们提供的产品。它也确实对客户提供了价值,比如,让客户以后从上海出差到苏州更快捷了。然而,它对这种出差客户的价值会比一辆汽车更大吗?这就是乔布斯说下面这句话的意思:通常情况下,人们并不了解自己需要什么,直到你把产品呈现在他们面前。好,简单说完质量圈,下面由内而外,看看如何度量以及提升它们。3 如何提升基于过程的质量?今井正明的《现场改善》中提到,质量不仅仅是一种结果:质量指的是产品或服务的质量。然而从广义上说,质量也包括产出这些产品或服务的过程和工作的质量。我们可以称前者为“结果质量”,而称后者为“过程质量”。抽象地说,质量圈里的每个内圈相对于外圈来说都是过程,所以我们从最内圈开始,在讨论产出质量(对程序员来说主要是代码质量)之前,我们谈谈写代码的质量。一个追求好代码的程序员,他写代码的过程也一定是高质量的。我们来看看工匠的写代码实践。3.1 工匠如何写代码3.1.1 实践:代码不通过编译的时间间隔不超过 3 分钟码农方法这边写两行半,发现那边得先加一个方法,那边还没加好,又赶紧先把刚才 Ctrl + C 的内容随便找个地方先 Ctrl + V 下来,因为他要 Ctrl + C 另一个代码片段了。如此,代码很长时间处于无法编译的状态,好不容易通过编译,自己改了多少东西自己都忘了。如此反复几次,掐指一算,觉得应该没问题了,提交给 QA 测吧。工匠方法假设在做一个客户下单的功能,我们负责后端的实现。1. 先用 CRC 分析,大致写下来有几个类(Class),它们有哪些主要的属性和方法(Responsibility),它们需要跟哪些别的类交互(Collaborator)。
得到的几个 CRC 卡片,我为了不画图,用下面的伪代码表示://service layer, REST APIclass
BookOrderController//domain layer, business logic.//BookOrderService 先使用 CustomerQuery 来取得 Customer 的基本信息,然后用 BookOrderValidator 来验证数据,最后创建 Order 对象,交给 OrderRepository 来保存。class BookOrderServiceclass BookOrderValidatorclass CustomerQuery//data access layer, to save Orderclass OrderRepository2. 有了几个 CRC 小卡片后,最好找同事讨论一下,看看设计上有没有大的方向问题。这时你不用保证这些类的设计是全部正确的,因为这是一个迭代的过程。然后建议从最核心的负责业务逻辑的类开始,在上面的例子中,就是 BookOrderService。3. 有了 CRC ,你已经知道了 BookOrderService 的消费者是 BookOrderController,所以可以确定它的入参是 UI 上传回来的购物车里的东西,所以你决定加一个类来表示入参。4. 开始写这个入参类,先写好几个确定的属性即可。然后提交代码。public Class CartModel{
public List<CartItemModel> Items; }public Class CartItemModel{
public int ProductId;
public int Quantity; }5. 开始写 BookOrderService 类的方法,提交代码。public void BookOrder (int customerId, CartModel cart){ }6. 实现上面的方法时,第一步需要开始根据 customerId 取数据库里的 Customer 信息,所以需要一个 CustomerQuery 类。所以接下来的顺序是:写空的 CustomerQuery 类;写 BookOrderService 的构造方法,入参为 CustomerQuery ;继续实现 BookOrder 方法,用 CustomerQuery 查询 Customer。参加如下 step 1、2、3://step 1public Class CustomerQuery {
public Customer Query (int customerId)
{
} }public Class BookOrderService {
//step 2
public BookOrderService (CustomerQuery customerQuery)
{
_customerQuery = customerQuery;
}
public void BookOrder (int customerId, CartModel cart)
{
//step 3
var customer = _customerQuery.Query(customerId)
} }回顾一下这个过程。用 CRC 分析好主要的几个类,从核心类开始,然后进入一个循环:写到第一行时,发现它依赖一个类或接口,则先写好这个空的类或接口;如此反复,一行一行驱动。这样你的编译不通过的时间应该不会超过 1 分钟,而不是 3 分钟。当然,这是理想状况。现实中,你会写着写着发现自己思维开始乱了,不知道怎么往下写了,那么,抛弃所写的代码,重新思考。或者,把依赖的类写好以后,发现设计有问题,不应该这么依赖,这时,也抛弃所写的代码。没错,就是下面这个命令:git checkout .一行行驱动,驱动不出来就抛弃。这是把写代码的任务分解到极致的做法,只有这样,才能把写代码当做一个创作过程而不是复制粘贴过程,才能让自己对每一行代码负责,才有资格把代码交给 Reviewer 和 QA 时底气十足。3.1.2 实践:每 30 分钟提交一次代码码农方法写了一天的代码,改了几十个文件,才提交。提交时的 comment 也不知道该怎么写了,因为太多了。工匠方法做完一件事情就会提交一次,提交的 comment 会是很简单的一句话。比如重命名一个变量,抽一个方法,都可以单独提交。当然,熟练之后,不用提交得如此频繁,我的个人经验,1 小时是合适的,最长不超过 2 小时。这其实就叫持续集成。持续集成不是 Jenkins, 而是一个实践,它是指代码持续地集成在一起的过程:频繁提交,频繁跑自动化测试,程序员之间频繁地用代码交谈。这样做的好处有:1. 鼓励程序员之间的代码交流。下围棋又称手谈,我们码农也该码谈:我重命名了一个变量、或者重构了一个方法,我要尽快集成进去,以便于别人的代码能基于我的改动。2. 代码如果有问题或风险,可以尽快识别,需要时也方便 rollback 。3. 放下心理负担,便于被打断后或第二天快速继续。3.1.3 实践:频繁重构码农方法写代码以通过编译、实现功能为目标。重构成为一个稀有任务,某一天心血来潮才会说:这代码要重构一下了。工匠方法随时重构,就像呼吸一样自然。每做完一件事,就看一下有什么需要重构的。命名是否可读?写完的几行函数是否需要抽一个方法?工匠不仅重构自己刚写完的代码,而且重构别人写的代码,比如给 A 方法加功能时,虽然 A 方法是别人写的,但顺手也会把 A 方法的不好的变量命名改一改。这个叫 童子军法则:Leave the campground cleaner than you found it. 离开宿营地时,应该比到达宿营地时更干净。套用到写代码上,意思是提交的代码应该比签出时的代码更干净。如果行医的真谛是“有时去治愈,常常去帮助,总是去安慰”(To cure sometimes, to relieve often, to comfort always),我们程序员写代码的真谛应该是:有时写代码,常常去重构,总是在思考。行动!1. 使用 CRC 分析,用卡片,直到你不需要卡片。2. 不要等代码写完再找人 Review ,对代码有想法时就找人 Review。3. 阅读《重构》这本书。4. 熟练使用 IDE 的 重命名、抽取方法、抽取类、反转 if 等快捷键。这些应该是原子操作,不破坏代码的可编译性。5. 跟同事结对编程,把它当作一个技能来练。6. 代码超过 3 分钟不能编译时,抛弃所有修改,重来。7. 写代码时使用番茄工作法,25 分钟的番茄时间内,代码必须提交。8. 每次提交代码时,应用童子军法则,重构签出的代码。3.2 心法与启示3.2.1 过程质量心法:拆为了说明这个心法,特地搜了一下老虎伍兹的挥杆,对我们这种外行来说,挥杆就是一个动作,但是在伍兹这种高手的眼里,是很多个动作,每个动作都有要领并且刻意练习。1. 开球准备。要领有:球的位置,上半身倾斜角度,身体重心,背部挺直,等等。2. 上杆。要领有:双手始终在胸前,增加挥杆半径。3. 上杆顶点。要领有:左肩与下巴相对位置,髋部扭动幅度,重心,右膝微曲。4. 下杆。又分解为滑动、旋转、弹跳等动作。5. 触球。要领有:头部留在球的后方,左腿伸直完美支撑,双臂完全伸直,等等。这种分解动作并且刻意练习的方法,是成为高手的必经之路。我们写代码也是一样,它是程序员迈向高手的一项核心技能。代码是一行行写出来的,如果我们不能把要解决的问题拆解到一行行的代码,那只能做一个代码的批量搬运工了。3.2.2 管理者启示为什么要管理过程质量?西方有句谚语:如果你用同样的方法烤面包,你会得到同样的面包。你不改进烤面包的方式,你得到的面包也不会有变化。不改进写代码的方式,你得到的代码质量也不会有变化。结对编程不仅是对代码的实时反馈和改进,也是对程序员写代码过程的实时反馈和改进。如果你不相信结对编程,不要说出来,而是找个相信的人负责。日本有个流派:现场管理。就是到第一线去,看团队的做事过程和做事方法。在现场管理中不断总结,然后不断进行这三种活动:(简化)Simplify => (标准化)Standardize => (自动化)Automate。我对这三个单词印象深刻,是因为一个国外的同事发现,三个英文单词的首字母倒过来写,是一个很容易记的词 (Ass)。作为管理者,除了程序员的写代码过程,还有很多过程需要管理。比如产品经理收集需求的过程、团队写 User Story 的过程、会议过程、不同角色的合作过程等等。4 如何提升基于产出的质量前面说过不会对代码之外的产出进行展开,所以我们直接聚焦在代码质量上。4.1 代码质量度量首先明确一点,这边的代码是广义的定义,很多人对代码的理解就是指为了实现需求而写的代码,或者说用户用我们产品时会执行到的代码,一般我们叫它生产代码;除此,程序员写得最多的还有测试代码。而 DevOps 工程师写得多的有发布代码和基础设施代码(Infrastructure as code)等等。说的极端一点,测试代码的质量比生产代码的质量更重要,至少同等重要。毕竟它们不直接对用户产生价值,是纯成本,如果花时间写了又不把它们写好,简直是对用户犯罪。如何度量代码质量呢?这边我也做了一个度量圈图:4.1.1 度量之 QA 找到的 Bug 数很多制造型企业都有这样的价值观:下一道工序是客户。这是对自己这道工序的质量要求。软件行业也一样,程序员的下一道工序往往是 QA ,所以当我们把“写好”的功能交给 QA 时,想想有没有把他们当客户?很多软件交付给外部客户时,会附上 Known Issues (已知问题),程序员对自己的内部客户也应该如此。码农等 QA 告诉他们有什么 Bug,工匠则告诉 QA 有什么 Bug。我清楚地记得在我写代码的早年,有一天晚上睡到半夜,大脑半梦半醒中还在思考白天写的代码,突然,发现代码有个逻辑漏洞,赶紧醒过来,把这个漏洞写在笔记本上,第二天一早到公司就把它修复了。这种“非人"的日子过了一年,我找到了 Unit Test 这个利器,我把能想到的逻辑和场景都用 Unit Test 覆盖,从此过上了安心而幸福快乐的生活。所以,工匠需要责任心和自动化测试。我会和 QA 一起整理所有的测试用例,然后尽量地用单元测试覆盖它们。当我把功能交付给 QA 的时候,我们的谈话经常是这样的:“所有测试用例都覆盖到了,如果你测出 Bug,我请你吃饭。”或者这样的:“有一个测试用例我没覆盖到,因为测试数据太复杂,太难自动化了,这边可能有 Bug,你着重帮我验证一下。”瞧,这就是我告诉 QA 哪里有 Bug,而不是等他们告诉我。行动!1. 在写代码的过程中主动跟 QA 一起 Review Test Case,互相提醒可能漏了哪些。2. 主动告知 QA 你测了哪些,哪些还有风险。3. 发现 Bug 后,先用一个 Unit Test 重现,再修复它。4.1.2 度量之 Code Review 中的 WTF 数这个是很多人眼里代码质量的金标准,还是源于这个事实:咱们程序员读代码的时间比写代码的时间多得多。所以 Martin Fowler 说:任何一个傻瓜都能写出计算机可以理解的代码,惟有写出人类容易理解的代码,才是优秀的程序员。(Any fool can write code that a computer can understand. Good programmers write code that humans can understand.)有很多实践可以帮助你写出可读性更高的代码,这边举一些例子。实践:少写注释而不是很多人认为的多写注释,你有本事写注释,你有本事让代码自解释啊。关于注释,有两个重要建议:1. 给每个公有类和公有方法写 IDE 推荐的标准注释,这样别的地方在调用这些方法时,IDE 可以显示这些注释。2. Comment Why not What。比如下面的注释就是好的。//不要使用 global.isFinite()//因为它在 value 为 null 时返回 true。 Number.isFinite(value)而下面的注释则是和代码重复://check
customer is adult VIPif (_customer.Age >= 18 && _customer.IsVIP()) { }改成:var isAdultVIP = _customer.Age >= 18 && _customer.IsVIP();if (isAdultVIP) { }实践:快速返回把你的 if 反转,快速返回。if (customer.Age >= 18) {
if (customer.IsVIP())
{
// do something
return true;
}
return false; }return false上面的代码非常常见,因为符合人类的正常思考。我们需要反向思考,改成:if (customer.Age < 18)
return false;if (customer.IsVIP() == false)
return false; //do somethingreturn true;尤其是中间的 do something 的代码又臭又长时,修改后的代码有助于减低看代码时的心理负担,因为你看到 do something 的地方时,只剩最后一个逻辑分支了。而修改前的代码,绞尽脑汁在看 do something 的代码逻辑时,还惦记着,IsVIP 为 False 时是什么逻辑? 不满 18 岁是什么逻辑?这样读代码累。顺便说一句,注意到修改后代码有一个空行了吗?那边有且只有一个空行,自行体会一下。工匠之心,细微至此。实践:避免 Magic Number 或 Magic Stringif (customer.Age < 18)应改成:const AgeForAdult = 18;if (customer.Age < AgeForAdult)实践:单一抽象级别function PromoteToVip (Customer customer){
customer.IsVIP = true;
SendCongratulationEmail(customer);
}上面方法中第一行是一个语句,第二行是一个方法,它们就不是在一个抽象级别上,应该把第一行也抽一个方法。行动!1. 阅读 《代码整洁之道》。2. 保持沉默,让看你代码的同事告诉你,你的代码做了什么。3. 先让每个函数不超过 20 行,然后不超过 5 行。4. 代码的逻辑段落之间,留一个空行。5. 用工具(比如 SonarQube)扫描你的代码,来熟悉常见的代码坏味道。6. 杜绝 else 。7. 如果你的公司有代码规范,它只是比底线好一点点,超越它。4.1.3 度量之 SOLID / DRY / KISS / YAGNI 原则前两个度量是外在的,分别针对我们最直接的两个下游:QA和其它程序员。现在说说内在的度量。等等,怎么跳过了设计模式?我是故意为之。设计模式很重要,但因为那本著名的书,让它变得过于重要了。很多人没有意识到,它们本身是一种“术”,术,就是手段,而不是目标。很多设计模式的初学者,容易把模式往代码上套,“这边弄一个装饰者模式就妥了!”这是易犯的错误。正确的认知是,这些设计模式,无非也是为了让代码符合一些原则,比如这节要说的 SOLID,以及下一节要说的高内聚、低耦合。我把设计模式放在度量圈中,是为了提醒它跟原则的关系。可以以设计模式为起点,但不要以设计模式为终点。最终的目标是原则,心中无模式:你写完的代码只是恰巧用了某个模式而已。S.O.L.I.DWikipedia 的解释如下:1. Single responsibility principle - A class should have only a single responsibility.2. Open–closed principle - Software entities ... should be open for extension, but closed for modification.3. Liskov substitution principle - Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.4. Interface segregation principle - Many client-specific interfaces are better than one general-purpose interface.5. Dependency inversion principle - One should depend upon abstractions, not concretions.翻译一下:1. 单一职责原则:一个类只应该有一个职责。2. 开闭原则:软件实体应该对扩展开放,对修改闭合。3. 里氏替换原则:用子类代替父类,程序不受影响。4. 接口隔离原则:多个特有接口好于一个通用接口。5. 依赖倒置原则:依赖于抽象,而不是依赖于具体。SOLID 是人称 Uncle Bob 的 Robert C. Martin 总结的五大原则,这个跟我父亲同龄的老程序员,敏捷宣言的签署者之一,坊间传闻是这世界上最贵的程序员,请他写代码的费用是每小时 2 万美元。单单一个 SOLID 就值得写一篇长文了,所以这边不会深入,只简单谈谈第一个原则:单一职责原则(Single Responsibility Principle)。什么叫代码的职责?Uncle Bob 的解释是:职责就是可能变化的方向。如果可能变化的方向多于一个,就破坏单一职责原则了。给大家看一个伪代码片段:if ("xxx正则表达式语句".validate(email)) {
user.Email = email;
user.IsActive = True; }代码功能很简单:如果用正则表达式验证 email 是有效的,则激活用户。它破坏单一职责原则了吗?是的,虽然只有寥寥几行。它可能的变化方向有 2 个。1. 激活用户的条件:不仅看 email 是否有效,还要用 email 黑名单过滤。2. 激活用户本身:可能需要发送通知,或者给用户初始奖励。这两个变化的方向,意味着这个代码片段承担了两个职责:一个是激活用户的条件验证,一个是激活本身。一般的重构方法是把第一行抽成另一个 Validator 类。从这个简单的例子可以看出,识别单一职责不是一件容易的事情。初学者可以用一些简单的规则去看看自己的代码,作为开始:1. 一个类只能有一个 public 方法,除非多个 public 方法是 overload 的。一个 public 方法就是该类对外提供的一个职责,多个 public 方法,无疑 99% 是提供了多个职责。2. 一个类不超过 30 行。3. 尝试用自然语言的一句话描述一个类做的事,如果需要多于一句话,职责就多了。DRY - Don’t Repeat YourselfWikipedia 的解释是:Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.系统中的每一部分,都必须有一个单一的、明确的、权威的表示。重复代码是最常见的代码坏味道,而最常见的原因是懒和 copy paste 大法。一个有追求的程序员,可以做的第一步,就是消除代码重复。除了代码的重复,还有注释与代码的重复,这就是我们说 Comment Why not What 的原因,不要让注释重复一遍代码做的事。还有文档的重复,QA 写的 Test Case,是不是和你写的自动化测试用例重复了?这个话题就高级了,后面讲 BDD 时会聊到。所以, DRY 是要求非常高的一个原则,绝不是听上去那么简单的。能够完全遵守 DRY 的程序员,绝对是个大师。而违反 DRY 原则的,我们称为大湿,WET,啥都写两遍( write everything twice) 或者,浪费大家时间(waste everyone's time)。KISS - Keep It Simple, Stupid有一次,有人问二十世纪最伟大的演讲家之一的丘吉尔:“如果让你作10分钟的演讲,需要多少时间准备?” 丘吉尔答:“半个月。” “如果让你作半小时的演讲,需要多长时间?” “一星期。” “如果是能讲多长时间就讲多长时间的演讲呢?” “不需要准备,现在就可以开始。”写更少的代码比写更多的代码难得多。简洁一点都不简单。举一个常见的例子,很多人都喜欢为 Domain Service 类抽一个接口,用来做依赖注入。public Interface ITaxCalculator;public Class TaxCalculator: ITaxCalculator;public Class TaxService {
public ITaxCalculator TaxCalculator{get;set;} }上面的 ITaxCalculator 如果没有多个实现,则是多此一举。有趣的是按 SOLID 的 D 来说,这个接口是必要的,让 TaxService 依赖于这个抽象的接口,而不是具体的实现。我的做法会 KISS 原则优先,大家可以看看这篇文章:停止滥用接口,多一些独立的思考。YAGNI - You Aren't Gonna Need It极限编程的创始人之一、敏捷宣言的签署人之一,Ron Jeffries 说:不要在你预见到需要它的时候实现它,而是在你真正需要它的时候实现它。使用这个原则,也可以得出上面例子不需要接口的结论。我用一个 TDD 的练习例子来说明如何修炼。假设要实现一个分解质因子的函数,就是输入一个正整数,输出它的质因子列表。比如输入 1, 输出 [](意为空集合,1 没有质因子);输入 6,输出 [2, 3]。TDD 中设计第一个 case:public void Primary_factors_of_one_are_empty(){
Assert.Equals([], PrimaryFactor.Calcualte(1)); }现在来实现这个方法,只需如下代码:public list Calculate(int n){
return []; }这个方法当然是不正确的,但是为了让第一个 case 通过,它已经足够了。这就是刻意练习 YAGNI 的方法。这四个字已经出现好几次了。本文的正文如果只写四个字,就是刻意练习。行动!1. 把上面的原则研究透。2. 别看《设计模式》了。3. 一年学一门新的编程语言。4. 学习并透彻理解依赖注入 (DI)和依赖反转(IoC)的区别。4.1.4 度量之 高内聚、低耦合所有的设计模式和原则,最终都是为了追求高内聚、低耦合。该度量可谓万法归宗,道可道非常道,本文以自己的体会,尝试用简单的语言道一道。三问我的第一次对写代码的觉醒是,开始不断问自己几个问题:1. 我这一行代码应该写在哪个类里面?为此我要想这行代码到底在干嘛?承担什么职责?这个职责应该由目前这个类承担,还是新的类?2. 这个类应该写在三层架构里的哪一层?3. 这个应用应该属于哪个 domain ?拆分微服务的话,它属于哪个微服务?这些问题其实是不同的粒度的同一个问题。下面看看不同粒度上的实践或原则。类:Law of Demeter又称“最少知识原则”。下面的代码对 customer知道的太多了,知道它有 Orders 属性,知道它的 Order 有 IsPaid 和 Amount 属性,还知道具体是怎么算的。var totalPaidAmount = customer.Orders.where(o =>o.IsPaid).Sum(o =>o.Amount);改成如下比较好,只知道 customer有个 TotalPaidAmount 方法,之于它是怎么计算的,不需要知道。var totalPaidAmount = customer.TotalPaidAmount();public Class Customer{
public Decimal TotalPaidAmount()
{
return Orders.where(o =>o.IsPaid).Sum(o =>o.Amount);
} }类:Tell, don't ask下面的代码问 customer 的Age 信息,然后拿到结果后自己计算。这就相当于 ask 你的秘书:“你有空吗?”她说:“有。”你再说:“那帮我买杯咖啡。”var age = customer.Age;var birthday = calculate(age);改成如下。直接 Tell 秘书:“帮我买杯咖啡。”如果她没空,她会返回 False 给你。你关心的是咖啡,不用关心她有没有空。var birthday = customer.BirthDay();Layer:封装各种逻辑所谓的三层架构,其实就是对不同逻辑的封装。1. UI 逻辑,或者 Presentation 逻辑。2. Application 逻辑:这个有些人不理解,举个例子,Application 特有的 Workflow 是,注册用户后,发送一个 Welcome Email。两件事本身都是业务逻辑,但是做完前一件要做第二件,这个调度逻辑,就是 Application 逻辑。应该写在 Application Layer (大约是 MVC 中的 Controller 里)。3. 业务逻辑。4. 数据存储逻辑。常见的错误是,为了简单痛快,把业务逻辑写在 service 层,或 MVC 的 controller 层。或者不小心写在了 Data Access 层。那完全失去了分层的意义。Domain: Bounded Context & Micro ServiceBounded Context 意思是界定上下文,是 DDD 里面的一个实践。看个例子。 1. Bounded Context 基本上可以等同于 Micro Service。拆好了 Bounded Context,也就拆好了 Micro Service。(更高级的用法,Bounded Context 可以对应多个 Micro Service。)2. Customer 模型在两个 Bounded Context 都存在,它不是共享的,是 2 个模型,它们的属性不尽相同,都只包含各自 Bounded Context 所需要的属性。目的就是松耦合,修改一个 Customer 模型,只影响它所在的 Bounded Context,不影响别人。类与类之间,Layer 与 Layer 之间,Micro Service 与 Micro Service 之间,道理都是相通的,各管各的,松耦合。行动!1. 写代码时不断问自己:这段代码应该写在哪?2. 阅读 《企业架构应用模式》。3. 阅读 《实现领域驱动设计》。4.2 心法与启示4.2.1 代码质量心法:以终为始“这个功能做完了吗?”作为一个程序员,被问到这个问题时,你的答案是不是在下面?差不多做完了。 完成 90% 了。 做完了,在等测试。 做完了,也测过了,我再补一个单元测试。 做完了,但有个地方性能有点问题,我再调一下。 做完了,但是还没有 Code Review。 做完了,但还有个小 Bug。对不起,这些回答都不合格,首先你要非常清楚,做完的完是什么意思?就是说,首先要定义你的终点,也就是定义你的完成(DoD, Definition of Done)。工匠的 DoD 清单应该包含:1. 代码完成2. 代码提交并 Merge 到 Git 的开发分支3. 单元测试逻辑覆盖率足够到让自己自信4. 没有性能问题5. Code Review 过6. 自测过7. 没有已知 Bug8. 你可以继续加……这个 DoD 的清单下,只有完成和没完成两种状态,没有“ 90% 完成”这回事,也没有“完成了,但是……”这回事。大部分程序员都习惯于把没做完的功能声称做完了,然后开始下一个功能的开发。开始比结束容易得多。就像前一阵很火的流浪大师沈巍引述的一句古语说的:善始者众,善终者寡。而工匠必须是个终结者,当终结者说做完了,那就是做完了,Done!行动!1. 构建自己的 DoD 清单。2. 把 DoD 清单推广给团队。4.2.2 管理者启示这一节说了很多度量的事情,所以总结一下度量三大法则。所谓三大,是我编的,仅供参考。从问题开始:作为技术管理者,你会为你的团队强制代码静态检查以及单元测试覆盖率吗?为什么?之前一个团队,我没有设置单元测试的代码覆盖率标准,原因是我自己的代码质量从来不依靠代码覆盖率。而且我相信度量三大法则之一:你度量什么,你就得到什么。我可以用意大利面条式、及其难以维护的代码来达到很高的覆盖率,这时候对覆盖率做硬性要求的话,你得到的是双份烂代码,恭喜。但是,能说它们是没用的吗?不能。度量三大法则之二:你没法管理你不能度量的东西。我一个人写代码时确实不需要软件帮我度量,一切度量在我心中。然而一个团队人多了以后,不能靠自己的意志力去管理大家的代码,所以,客观的度量还是需要的。重点是要明白,而且要让团队明白,度量是一种手段,它不是目标本身。覆盖率是一种手段,高质量才是目标。这就是度量三大法则之三:当度量成为目标时,那么它就不再是好的度量了。5 终极修炼大法之 TDD至此,我们讲述了过程质量和产出质量,在继续质量圈的讲解之前,插播一个终极修炼大法,一个可以最大限度地助力我们提升代码质量和过程质量的大法,就是 TDD。5.1 开始:Coding KataCoding Kata 中 Kata 的意思是武术里的招式、套路。Dave Thomas 观察到一个有意思的事实:运动员也好,唱歌的也好,在上台表演以外,都会大量的练习。而程序员是个例外,不练习,直接上场(工作)。为此他提出 Coding Kata ,把刻意练习这个实践带到软件开发中来。行动!1. 先不要在工作中用 TDD ,先自己玩。2. 照着Uncle Bob 的 Coding Kata PPT,用你最熟悉的语言,练 10 遍,录下来。3. 找我拉你进聊 TDD 和 Coding Kata 的微信群,发送你的 Kata 录像。4. 阅读 Kent Beck 写的《测试驱动开发》。5. 参加一次外部社区的 Coding Dojo。6. 组织一次团队内部的 Mob Programming。5.2 番外:TDD 八卦TDD 要的是行动,所以没什么好讲的,后面讲到 BDD 时会说一些代码细节。这边先侃侃大山。没有实践过的人可先略过。5.2.1 TDD 与 Kent BeckTDD 是 Kent Beck 重新发现(不是发明)的,他也是敏捷宣言的签署者之一。“发现”的意思就是这种方法早已有之,比如砌墙的师傅,会用一个陀螺定好垂直线再根据它砌墙,确保墙是直的,那根垂直线就是一个测试。Kent Beck 几年前在 Quora 上透露,他已经不怎么写 Unit Test 了,更不用说 TDD 了。怎么解释这个呢?守破离。比如练武之人,扎过几年甚至十几年马步的人,才有资格说他已经不需要扎马步了。5.2.2 TDD 的第一要务TDD 的红-绿-重构循环,强调了小步伐前进,强调了频繁重构,也强调了 KISS 原则,几乎覆盖了上面我们聊到的所有原则和实践。所以,TDD 不是测试方法,是设计方法,一种用测试驱动开发的设计方法。这是第一要务。测试只是 TDD 的副产品,然而,这个副产品比很多事后写的 Unit Test 要强大得多。很多事后写的 Unit Test 都容易有如下的问题。1. 生产代码已经完成,写 Unit Test 的动力大为下降,很多都是应付了事,导致测试代码和生产代码都不会高质量。2. 如果有代码覆盖率的要求,则有时更为糟糕,为了覆盖而覆盖,也不会管代码多乱了。TDD 解决了这两个问题。它注重过程多于结果,而且因为对过程的注重,它的测试覆盖率是逻辑覆盖率,而不是代码覆盖率,这个比能用工具衡量的代码覆盖率好得多。5.2.3 TDD 的核心智慧:Tasking上面以老虎伍兹的挥杆为例,说明了“拆”字诀,事实上,这也是 TDD 的核心。它的每个“红-绿-重构”循环,就是一个简单的 Task,难的是如何把要解决的复杂问题,渐进式地拆解成一个个的 Task: 写完这个 test case ,下一个 test case 是什么?6 如何提升基于产品的质量你是否有时觉得迷茫,前面对过程质量和产出质量的极致追求,在产品和市场面前,还有意义吗?当然有,你有没有这个能力是一回事,什么时候该使用这个能力是另一回事。但是确实要意识到,如果不能确定我们的产品、功能是正确的,那么把时间花在 TDD 和对代码的精雕细琢上,显得不合时宜。说到产品质量,绕不开产品经理,产品经理对外连接业务,对内连接开发团队。所以为了提升产品质量,首先要确保:1. 程序员与产品经理之间没有鸿沟。2. 开发团队与业务之间没有鸿沟。6.1 跨越鸿沟鸿沟是会越来越大的,所以要把它消灭在源头,下面介绍一些实践,听着简单,实则四两拨千斤,越是大的团队大的项目,越是威力十足。在复杂的项目面前,好的实践有一个特征:你做了,也没什么;但不做,会很惨。6.1.1 实践:协作画概念模型 Conceptual Model开始一段感情之前,程序员、QA、产品经理以及 UX 先合作把 Conceptual Model 画出来。这样做的好处是,尽早取得对业务、业务术语、业务对象、对象之间关系、数据流等等的一致性认识。大家有个共同的概念模型基础后,1. UX 开始画 UI 设计2. 产品经理继续整理业务流程3. 技术团队开始设计类图和 ERD 图。这些后面的工作就不会偏离太多,大家交流效率也会大大提升。下图是个我工作中用 Processon 画的一个例子的剪影,不用漂亮,只需把概念表达清楚就行。 6.1.2 实践:协作写实例化需求这边有两个重点,一是“协作”,二是“实例化需求”。现在 SCRUM 成了很多团队的标配了,所以需求的形式是 User Story。传统的做法,是产品经理 (PO)写 User Story,再跟团队讲。更好的实践是:开发团队在 Grooming Meeting 中一起来写 User Story,PO 只需验证和回答问题。并且 User Story 的 AC(Acceptance Criteria 验收条件)使用实例,而不仅仅是描述性的文字。假设 AC 是下面这样,看上去很清晰了?AC:如果运送地址为上海本市,且买的书超过5本,则免运费;否则不免。但如果把关键的实例写下来,就会有更多的讨论。1. 一个普通客户买了 6 本书,送货地址到上海,运费为 0。2. 一个普通客户买了6本书,送货地址到西藏,运费为 5 元。3. 一个普通客户买了 5 本书,运费为 5 元。有了例子,人们很容易想到更多的例子:1. 如果是 VIP 客户呢?2. 如果普通客户买了 5 本书加一个 U 盘呢?3. 如果普通客户买了 5 本书加一个冰箱呢?这些例子可以帮助程序员和 PO 以及 QA 之间尽早地澄清需求和范围,避免事后才发现遗漏和理解的不一致。6.1.3 实践:验收测试驱动开发每个 User Story 如果拆得足够小的话,AC 不会太多。比如上面的例子,讨论出来 9 个 AC,那么把 9 个 AC 按一定逻辑分成 3 组,就可以拆成 3 个 User Story。QA 基于此开始写 Test Case,并及时和程序员与 PO 一起 Review。程序员则把这些 AC 作为验收测试的测试用例,开始代码的实现。自动化的验收测试变得越来越重要,有好几个名词,BDD、ATDD、SBE,而且它们跟 TDD 有关系。所以后面 6.2 小节详细阐述一下。6.1.4 模式:协作得到一致性理解总结一下上面三个实践背后的模式。1. 找到不同角色的工作的共同起点。2. 不同角色的人一起把这个起点做出来。概念模型是产品、开发、QA、UX 后续工作的共同起点,那就一起来画。实例化的 AC 是开发、UX、QA 后续工作的共同起点,那就一起来写。行动!1. 把手头项目的 Conceptual Model 画出来,跟团队 Review 一下。2. 你要开发的下一个 User Story,为 AC 补上实例,跟产品经理和 QA 过一下。3. 下载一个 BDD 框架,比如 RSpec、JBehave、Cucumber、Specflow、Mocha/Chai 。开始用 BDD 风格写 Coding Kata。6.1.5 管理者启示沟通的鸿沟可以说是团队协作效率和质量的大敌,交流并且确保大家的理解一致,是管理者的天职之一。有一个实践被绝大多数的 SCRUM 团队忽略,就是启动会议 (Inception Meeting)。我之前经历的一个项目,到中后期的时候,发现每次 PMO 开会讨论的时候,都很低效,各种争论,各个大佬都不在一个频道上。我分析了一下,发现是因为每个人对项目的期望都不一样。有些人想着要守住时间,为此想加人或者砍掉 Scope;有些人想着要守住质量;有些人想着要守住成本。大家各怀心思,可是又没有明确说出来,导致对具体问题的讨论就变得很没效率。我重新组织了一个迟到的启动会议,在会议上重新取得一致:这个项目的 Top 3 的约束是什么?质量、范围、成本、时间等等,做个排序,哪个是最大的约束,就是说不能牺牲的。有了排序,大家的目标就一致了,面对新问题的时候,寻求解决方案的思路也就一致了。如果你发现自己的项目陷入或大或小的僵局,是时候开一个这样的会议,确保各个大佬取得方向上一致的意见。6.2 BDD实践 TDD 一段时间后,你一定会发现一些困惑的。这些困惑前人都碰到过,所以特地用一节来阐述一下 BDD,希望帮助你澄清一些困惑和可能的误解。先介绍一下它们的历史,然后再分享我的一个实战。6.2.1 BDD 小史先从 TDD 说起。实践 TDD 之后会注意到:做断言(Assertion)时,可断言两件事情。1. 断言 State,就是对象的状态。代码示例如下:Assert.AreEquals(10, user.TotalPoint);2. 断言 Behavior,对象的交互行为,或者说是实现细节,第二行是示例伪代码。Assert.IsCalled (_mockCalculator.Calculate(user), Times.once);(重要:这边的 Behavior ,不是 BDD 中 Behavior 的意思!这边的 Behavior 是指类与类的交互行为,也就是类的具体实现细节。 BDD 首字母的 Behavior 的意思,下面会提到。)对 Behavior 的断言,意思是断言所依赖的 Calculator.Calculate() 方法被调用了 1 次。这个有时候就会有问题了,比如随着重构的进行,我们觉得不应该依赖于 Calculator 类了,怎么办?这个 Test Case 也要修改。对 Behavior 的断言其实是在测试代码的实现细节。这有时候是必要的,比如我们依赖于第三方的库时,有时需要确保调用了合适的第三方方法,确保我们遵循对方约定的 Contract 。然而,如果我们的实现细节有问题,需要不断重构,这种 Test Case 就提供不了什么帮助了,只能跟着我们的重构不断地修改。Mock and Stub上面这种对所依赖的类的行为进行断言的方法,叫验证间接输出,它就是著名的 Mock 方法。另外一种提供间接输入的方法,叫 Stub ,参考如下代码片段:public int Calculate(int i, j){
return _adder.Add(i, j); }它使用 Stub 的测试如下:_stubAdder.Expect(Add(1, 2)).Returns(3); Assert.AreEquals (3, sut.Calculate(1, 2));体会一下,这个断言只断言结果(state),不断言实现(Behavior)。详细可以阅读一下 Martin Fowler (又一位敏捷宣言签署者) 12 年前的文章,没读过的不要错过:Mocks Aren‘t Stubs。不得不感叹,敏捷宣言和它的十二位签署者,真的是推动了软件行业的发展。BDD 出山言归正传,在 Mock 技术越来越滥用,Unit Test 越来越局限于实现细节的时候,另一个大神 Dan North 闪亮登场,提出了 BDD。我第一次听说 BDD 是他写的这篇博文:Introducing BDD,对于英文不够好的,我这边帮总结一下要点:1. 以前的 Unit Test 的方法名字一般都包含 Test, 比如测试一个 Calculator 类的话,测试类的名字就是 CalculatorTest。有一天,作者从同事那学到了用一句话来命名测试方法,甚至这句话可以是业务语言,让业务人员、QA 都看得懂。public class CustomerLookup{
public void FindsCustomerById()
{
}
public void FailsForDuplicatedCustomers()
{
} }2. 进一步的讨论中,作者发现用 The class should do something 的格式,还有额外的好处,可以让程序员思考 something 是不是这个 class 的职责。而 XX 类应该做 YY 事 (比如上面的例子,CustomerLookup 应该能 Finds Customer By Id),这个叫做类的行为,这就是 BDD 的 Behavior 的意思。区分上面 Behavior 断言的 Behavior 。3. 如果因为改动代码,导致某测试失败,很容易从这个句子中知道哪里失败了。 (不再需要 debug 。)4. 经过上面的讨论,作者恍然大悟,每个用一句话描述的 test case ,就是代码或软件提供的一个行为。TDD 的最大的困惑在于 Test,更好的词应该是 Behavior,所以他引入了一个新名词:BDD 。这让他的思路从 Test 转换到了 Behavior。5. 牛人绝不光说不练,2003年,他开始写 JBehave 来替代 JUnit。6. 他的同事提醒它,Behavior 需要考虑业务价值。就是说,所开发的系统,它下一步应该提供什么样的行为,来提供某个业务价值。7. 2004 年底,作者开始把 BDD 的想法应用到需求分析上。因为 User Story 有个众所周知的格式: >As a [X] I want [Y] so that [Z]而 Story 的行为就是它的 Acceptance Criteria (验收标准),作者提出了用 Scenario 作为 AC 的格式:>Given some initial context (the givens), When an event occurs, Then ensure some outcomes.8. 作者进一步,让这些 AC 变得可执行。很简单,让那些 Given When Then 映射到 Java 的代码上,就可以了。总结一下,BDD 是对 TDD 思考的产物,但是超越了单元测试,也可以应用到验收测试。如果你在写单元测试也碰到这个最常见的问题:你重构一段代码,并没有破坏任何东西,但对应的单元测试也要跟着改。那么,把思路切到 BDD 上来,不要滥用 Mock。6.2.2:ATDD SBE 小史ATDD 即 Acceptance Test Driven Development,验收测试驱动开发。我知道的 ATDD 可以从 wiki 之父 Ward Cunningham 2002 发明的 Fit (Framework for Integration Test)说起,它是用来自动化客户测试的工具,把客户按某种格式的写的文档变成可执行的文档。SBE 即 Specification By Example,实例化需求,它最早也是被 Ward Cunningham 提出,Martin Fowler 在 2004 年 的博文中有所论述。2011 年,Gojko Adzic 的同名书籍出版,系统地梳理了关于 SBE 的实践。BDD、ATDD、SBE、STDD(Story Test Driven Development )这些方法一开始各有偏重,但最后殊途同归,核心都是业务人员、开发、测试三方合作,在开发开始之前写验收测试。6.2.3 BDD 实战分享了解了这些概念和历史,我在此分享一个我的 BDD 实战经验。几年前,我接到一个需求,根据系统的各种事务产生会计科目(比如客户下单,则产生应收账款科目),便于以后系统跟财务系统直接的集成。我的财务和会计知识非常缺乏,而提需求的财务人员则没有 IT 背景,刚开始的几个对方,相信双方都有鸡同鸭讲的感觉。所幸,财务用 Excel 提供了一个实例,描述了在最正常的场景下,应该产生的会计科目。我基于这个实例,利用对会计的粗浅认识和对系统事务各场景的分析,慢慢总结了 60 多个场景,并且全部用 Specflow (.net 版本的 Cucumber)写出来。当然,这 60 多个场景都是好几轮跟财务人员讨论而来的,因为都是财务人员能够理解的语言,而且都用的实际例子,每次讨论前,我们都直接把这些 spec 文件打印出来直接讨论。这是其中一个例子。 1. 一个 Scenario 就是一个测试。2. Given When Then 都会隐射到后面的代码,而那些表格里的数据,会作为测试数据传递到代码。它是迄今为止我实施 BDD 最为成功的一次经历。总结一下:1. 它搭起了业务和开发团队的沟通桥梁。2. 用实际的例子,让双方的沟通不存在歧义和误解。3. 那些实际例子组成的场景,都被代码用 Unit Test 或者 Integration Test 实现,真正变成了活文档。避免了文档和代码不一致的常见陷阱。关于最后一点,我想着重说明一下,澄清很多人的一个误解。很多人认为 TDD 是关于 Unit Test 的,BDD 是关于 Integration Test 的。前半句是对的,后半句是错的。BDD 是一种风格,如果看前面 BDD 小史就知道,它着重于描述软件功能提供的行为,而不是着重于测试或者实现。所以,可以用 Unit Test 来实现,也可以用 Integration Test 来实现。关于这次 BDD 实践的后续,有个插曲。几年之后,两个日薪 2 万的咨询师进入团队,负责这个会计科目功能的重新设计和实现。有一天,两人兴冲冲地来找我,说翻到了我的代码,准备跟老大说,不用重写了,直接拿过来用就行。他俩眼里闪着光,语速很快,英文口语也比较重,所以我没百分百听懂,但是他们的手势和表情都在告诉我:你牛!做个工匠,念念不忘,必有回响。

我要回帖

更多关于 如何写程序入门 的文章