本书(中文名《程序员修炼之道》)指明了一条通向“务实”的道路。引用书中原话,“务实”就是:“get the job done, and do it well.” 因此,也可将“pragmatic”的含义引申为“靠谱”:能把事做成,还能办好。

Even though your title might be some variation of “Software Developer” or “Software Engineer,”
in truth it should be “Problem Solver.”
That’s what we do, and that’s the essence of a Pragmatic Programmer.
We solve problems.

——《The Pragmatic Programmer》, Topic 52: Delight Your Users

本书适合有一定工程经验,且希望变得更资深 的程序员阅读。第一版副标题“From Journeyman to Master” 体现了这个定位,书评也验证了这点:开发经验丰富的程序员早已将书中的原则内化到日常工作,觉得没有新意;而毫无经验的新人则对书中描述的场景缺乏感受、无法理解。但总的来看,豆瓣9.1/10,Goodreads 4.3/5,达到了好书的标准,值得一读。

那如何才能成为靠谱的程序员呢?作者分了9个章节、53个话题来讨论,并总结了100条tips。由于篇幅有限,我将其聚合成三个维度:意愿原则经验。用迷宫做比喻:首先,你必须想要走出迷宫(意愿);其次,你需要掌握迷宫的基本规则(原则),以判断自己是否走在正确的路上;最后,这迷宫并不只有你一个人在走,很多前人踩过的坑(经验),应该尽量避免。

1. 意愿

意愿是进阶之路的先决条件。没有强烈的意愿,无论学了什么原则、获知了什么经验,都无法驱动自己将其内化吸收,最终毫无进步。

这个因素常被人忽视,因为很多人默认自己有意愿成为专家,但实际并没有。

1.1 缺乏意愿的表现

一种表现就是抱怨:“工作不喜欢”、“leader不懂管理”、“公司没前景”……但这些可以用一句话回答:“Why can’t you change it?” 抱怨是种被动面对问题的方式,但人有主观能动性1,要么改变自己,要么换家公司。

You can Change Your Organization (change how the work is done at your current employer)
or Change Your Organization (find a new employer).

—— Martin Fowler

另一种表现是逃避:不愿意承担责任、不愿面对真实情况。出了问题第一反应是找借口,而不是提供解决方案2。这种行为往往源于自身实力不够,但又不愿承认并直面,企图维持一个靠谱的表象,而下意识地选择对解决问题无益的辩解3

The greatest of all weaknesses is the fear of apearing weak.

—— J.B. Bossuet

1.2 具备意愿的表现

知易行难,关于主观能动性的讨论,经常会陷入:懂的都懂,不懂的说了也不懂。

书中建议可总结为:时刻保持对自己所做事情的批判性思考4,不要让自己大脑处于“自动驾驶”的状态5;严格要求自己的交付质量,不要搁置已知的问题(避免破窗效应)6;不给自己设置边界,主动保持对全局的把握7;技术的发展日新月异,保持对领域内外知识的学习8;严肃对待提升沟通能力这件事9,毕竟一个“好的想法”只有经过“好的表达”才能被理解10

引一句我很喜欢的话收尾:

Everything that needs to be said has already been said.
But since no one was listening, everything must be said again.

—— André Gide

2. 原则

软件设计对缩略词的狂热堪比00后:KISS、DRY、SOLID(SRP、OCP、LSP、ISP、DIP)、YAGNI、LoD、DBC……但如果只能保留一个原则,首选是ETCEasier to Change)。

软件设计的目的是为了解决问题,而问题本身是不断变化的(市场需求、业务目标……),所以好的设计应该拥抱变化、易于改变(ETC11。其它设计原则都可看作ETC的特例。

那怎么才能写出易于改变的代码?回答这个问题前,我们需要先了解为什么有的代码难以改动。这可以从DRY原则,即Don’t Repeat Yourself12,窥得一二。本书对DRY的解读是我见过最透彻的,令人耳目一新,后来才发现DRY的出处正是本书。

2.1 为什么有的代码难以变动?

常见对DRY的解读局限于“copy-paste”。新人程序员为了不破坏原有功能而复用代码,会把已有的代码“cpoy-paste”一份,在此基础上修改并新增功能。“不破坏原有功能”13这个意识很好,但这种做法的灾难在于:把相同功能的代码分散在多处,之后一旦逻辑变动,需要同时更新多处的代码。稍微有点经验的人都不会这么做,但DRY不止于此。

表面上是「相同的代码」被分散在多处,本质上是「相同的知识」被分散在多处。这里的“知识”是泛指:可以是一种实现、功能……或者通常意义的知识。比如下面两个代码样例,就是「同一个知识」以「不同表现形式」(Representation)重复。代码1中,注释与代码是同一个“加法”知识的不同表现形式;代码2中,字段length的含义与知识“线段两端点决定线段长度”发生重复。

代码1. 注释与函数包含了重复的知识,违背DRY
如果后续实现变动,不仅需要改代码,还需要改注释。
并非让人不写注释,而是不要写代码已经清楚体现的事情。

def add(a, b):
    """Add a and b.
    """
    return a + b

代码2. length 和线段长度的概念存在发生重复,违背DRY
确定了 startend,线段长度天然就确定了。
这里显式定义 length 变量,迫使下游代码需不断维护长度的正确性(比如移动端点时)。

class Line {
    Point start;
    Point end;
    double length;
}

另一种常见现象是一个模块拥有过多的外部知识。链式调用(chain method call)14就是一个经典的例子,见代码3。当模块A拥有的对外部模块B的知识越多,之后对模块B的改动就愈发困难,因为模块B为了兼容模块A的“已有认知”,需要做很多妥协。

代码3. 为了得到 banana,知道了过多 jungle 的内部结构。
这使得 jungle 对象之后无法灵活地改动,不然这段代码就会被破坏。

# You wanted a banana,
# but what you got was a gorilla holding the banana and the entire jungle.
# —— Joe Armstrong
banana = jungle.gorillas.last().holding().banana;

可以说,编程的本质是将知识变成代码,而知识是不断变化的。随着项目的推进,新的需求、新的目标、对业务新的理解,都会带来知识的更新,进而需要调整代码。而糟糕的设计(相同的知识分散在多处、拥有过多的外部知识),会让这一过程变得困难。

2.2 如何让代码易于改动?

清楚了代码难以改动的原因,对策也呼之欲出:1. 不要有重复的知识 DRY;2. 对外部有尽可能少的知识 LoD(即Law of Demeter,或称principle of least knowlege)。通俗说,就是要让每个模块又“懒”又“蠢”。“懒”指的是绝对不做重复的工作:已经实现的知识,要尽可能复用,如果发现没法复用,考虑及时重构15;“蠢”指的是每个模块只需懂自己内部的事情16,对于外部模块,只要了解它们主动对外公开的功能17

这样可以得到一个易于修改(ETC)的设计。因为“懒”,所以一旦某个知识有变动,只需要对一处代码做修改,而所有依赖这个知识的模块都能得到正确的更新;因为“蠢”,没有其它模块了解自己的内部细节,因此只要保证公开的功能不变,就可以改动自身而不影响别人。这满足我们对易于修改的期望:1. 只要改一个地方;2. 改完没有更多负面影响。

很多软件设计的概念,都可以在这套框架中找到自己的位置。比如,解耦(decouple)18就是要将两个互相了解的模块,拆解成两个互不关心的模块,从而变得正交(orthogonality)19;比如“避免全局变量20(包括单例),是为了防止不同模块通过共有的变量产生间接的联系与约束(一些资源访问类确实需要定义为全局,此时最好包成API21);比如微服务架构中常见的DBCDesign by Contract)22,就是让每个模块公开说明自己能做什么,但对各自怎么做到的,毫不关心;比如“接口优于继承”(interface over inheritance)23,是避免子类了解关于父类的知识,从而形成约束24(如果真有共享的需求,可以考虑用Mixin25);甚至比如“尽早崩溃”(crash early26,也是为了不让下游模块感知过多当前模块的内部细节(比如“try-catch”会让下游感知上游有哪些异常),一旦出错就让程序在此终止。

:作者用ETC一个概念,囊括市面上基本所有的原则,是本书最让我震撼的部分。

3. 经验

如果软件设计的原则可以用ETC概括,那经验也能用一句话概括吗?作者没有明说,但从字里行间能总结出:人都是不完美的。这包括两个维度:1. 自己是不完美的;2. 别人是不完美的。而很多时候,我们其实假设了相反的事情,即以为人是完美的,这是导致很多错误的根源。

3.1 自己是不完美的

尽管常有这样的冲动与期望,但人永远无法写出完美的软件27。正如人月神话中所说,这种冲动在做第二个项目的时候尤为强烈:总想“吸取前一个项目的经验”,但往往导致过度设计。所以,作者指出,应该转向追求“足够好”(good-enough)的软件,把软件设计的质量当成目标函数“之一”而不是“唯一”28。也不应该自己去臆测未来的需求29,而应该积极与用户沟通30,不断迭代31,慢慢将软件完善。

大多情况下,别说完美的软件,人可能都无法写出正确的软件。因此测试显得尤为重要32。不应该只把测试当成找bug的工具33,而应该将其当作自己的第一个用户34,让自己能及时收到反馈。因此,在设计时,就应该将“是否便于测试”35作为重要考量因素之一。此外,“不可能”的事情也经常发生,所以用assert36去确保你认为不可能的事情真的不会发生37。一旦真的出了问题,也不要慌张38,先将导致错误的样例做成单元测试39,再开始调试寻找原因。将测试自动化40,以保证“同样的问题只犯一次”41。这些都是用外部的手段,来保证不完美的自己能写出正确的代码。

其实有时候,别说正确的代码,人可能都不知道怎么写代码。比如收到一个复杂的需求,第一时间甚至觉得无法做到。此时不必逃避,找到困难的根源,把限制条件弄清楚42,近似的解法总是存在的,工程问题往往也不需要完美解法。如果对项目前景没把握,可以先做个原型43,以验证想法;如果是觉得某一部分实现困难,则可以考虑用“示踪弹”(tracer bullet)44来尝试相关方案。

3.2 别人是不完美的

“严于律人,宽以待己”是人的天性。很多时候,自己的软件设计没做好,但却要求leader或PM给出完美详尽的任务清单或PD,指望他们作出“最终的决定”不再更改。但这是不可能的45。没人准确知道自己到底要什么46,leader、PM、甚至用户都不例外。而程序员的任务之一就是帮助他们理解自己到底要什么47。有价值的业务需求往往要经过多轮的沟通、迭代才能准确把握48。且只有让自己站在他们的视角思考问题49,才能做到这点。




最后,用「Tip 10050作为本文结尾:

It’s Your Life. Share it. Celebrate it. Build it. AND HAVE FUN !




引用

  1. Tip 3 : You Have Agency (pg. 2) 

  2. Tip 4 : Provide Options, Don’™t Make Lame Excuses (pg. 4) 

  3. Tip 29 : Fix the Problem, Not the Blame (pg. 89) 

  4. Tip 10 : Critically Analyze What You Read and Hear (pg. 17) 

  5. Tip 2 : Think! About Your Work (pg. xxi) 

  6. Tip 5 : Don’™t Live with Broken Windows (pg. 7) 

  7. Tip 7 : Remember the Big Picture (pg. 10) 

  8. Tip 9 : Invest Regularly in Your Knowledge Portfolio (pg. 15) 

  9. Tip 11 : English is Just Another Programming Language (pg. 20) 

  10. Tip 12 : It’™s Both What You Say and the Way You Say It (pg. 22) 

  11. Tip 14 : Good Design Is Easier to Change Than Bad Design (pg. 28) 

  12. Tip 15 : DRY Don’t Repeat Yourself (pg. 31) 

  13. Tip 98 : First, Do No Harm (pg. 286) 

  14. Tip 46 : Don’™t Chain Method Calls (pg. 134) 

  15. Tip 65 : Refactor Early, Refactor Often (pg. 212) 

  16. Tip 41 : Act Locally (pg. 121) 

  17. Tip 45 : Tell, Don’™t Ask (pg. 132) 

  18. Tip 44 : Decoupled Code Is Easier to Change (pg. 131) 

  19. Tip 17 : Eliminate Effects Between Unrelated Things (pg. 40) 

  20. Tip 47 : Avoid Global Data (pg. 136) 

  21. Tip 48 : If It’™s Important Enough To Be Global, Wrap It in an API (pg. 136) 

  22. Tip 37 : Design with Contracts (pg. 107) 

  23. Tip 52 : Prefer Interfaces to Express Polymorphism (pg. 162) 

  24. Tip 51 : Don’t Pay Inheritance Tax (pg. 161) 

  25. Tip 54 : Use Mixins to Share Functionality (pg. 165) 

  26. Tip 38 : Crash Early (pg. 113) 

  27. Tip 36 : You Can’™t Write Perfect Software (pg. 102) 

  28. Tip 8 : Make Quality a Requirements Issue (pg. 12) 

  29. Tip 43 : Avoid Fortune-Telling (pg. 127) 

  30. Tip 88 : Deliver When Users Need It (pg. 273) 

  31. Tip 24 : Iterate the Schedule with the Code (pg. 70) 

  32. Tip 70 : Test Your Software, or Your Users Will (pg. 223) 

  33. Tip 66 : Testing Is Not About Finding Bugs (pg. 214) 

  34. Tip 67 : A Test Is the First User of Your Code (pg. 216) 

  35. Tip 69 : Design to Test (pg. 221) 

  36. Tip 39 : Use Assertions to Prevent the Impossible (pg. 115) 

  37. Tip 34 : Don’™t Assume It, Prove It (pg. 96) 

  38. Tip 30 : Don’™t Panic (pg. 89) 

  39. Tip 31 : Failing Test Before Fixing Code (pg. 91) 

  40. Tip 90 : Test Early, Test Often, Test Automatically (pg. 275) 

  41. Tip 94 : Find Bugs Once (pg. 278) 

  42. Tip 81 : Don’™t Think Outside the Box. ”Find the Box (pg. 254) 

  43. Tip 21 : Prototype to Learn (pg. 57) 

  44. Tip 20 : Use Tracer Bullets to Find the Target (pg. 51) 

  45. Tip 18 : There Are No Final Decisions (pg. 48) 

  46. Tip 75 : No One Knows Exactly What They Want (pg. 244) 

  47. Tip 76 : Programmers Help People Understand What They Want (pg. 245) 

  48. Tip 77 : Requirements Are Learned in a Feedback Loop (pg. 246) 

  49. Tip 78 : Work with a User to Think Like a User (pg. 247) 

  50. Tip 100 : It’™s Your Life. Share it. Celebrate it. Build it. AND HAVE FUN! (pg. 287)