《The Pragmatic Programmer》——读书笔记
本书(中文名《程序员修炼之道》)指明了一条通向“务实”的道路。引用书中原话,“务实”就是:“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……但如果只能保留一个原则,首选是ETC(Easier to Change)。
软件设计的目的是为了解决问题,而问题本身是不断变化的(市场需求、业务目标……),所以好的设计应该拥抱变化、易于改变(ETC)11。其它设计原则都可看作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。
确定了start
和end
,线段长度天然就确定了。
这里显式定义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);比如微服务架构中常见的DBC(Design by Contract)22,就是让每个模块公开说明自己能做什么,但对各自怎么做到的,毫不关心;比如“接口优于继承”(interface over inheritance)23,是避免子类了解关于父类的知识,从而形成约束24(如果真有共享的需求,可以考虑用Mixin25);甚至比如“尽早崩溃”(crash early)26,也是为了不让下游模块感知过多当前模块的内部细节(比如“try-catch”会让下游感知上游有哪些异常),一旦出错就让程序在此终止。
注 :作者用ETC一个概念,囊括市面上基本所有的原则,是本书最让我震撼的部分。
3. 经验
如果软件设计的原则可以用ETC概括,那经验也能用一句话概括吗?作者没有明说,但从字里行间能总结出:人都是不完美的。这包括两个维度:1. 自己是不完美的;2. 别人是不完美的。而很多时候,我们其实假设了相反的事情,即以为人是完美的,这是导致很多错误的根源。
3.1 自己是不完美的
尽管常有这样的冲动与期望,但人永远无法写出完美的软件27。正如人月神话中所说,这种冲动在做第二个项目的时候尤为强烈:总想“吸取前一个项目的经验”,但往往导致过度设计。所以,作者指出,应该转向追求“足够好”(good-enough)的软件,把软件设计的质量当成目标函数“之一”而不是“唯一”28。也不应该自己去臆测未来的需求29,而应该积极与用户沟通30,不断迭代31,慢慢将软件完善。
大多情况下,别说完美的软件,人可能都无法写出正确的软件。因此测试显得尤为重要32。不应该只把测试当成找bug的工具33,而应该将其当作自己的第一个用户34,让自己能及时收到反馈。因此,在设计时,就应该将“是否便于测试”35作为重要考量因素之一。此外,“不可能”的事情也经常发生,所以用assert
36去确保你认为不可能的事情真的不会发生37。一旦真的出了问题,也不要慌张38,先将导致错误的样例做成单元测试39,再开始调试寻找原因。将测试自动化40,以保证“同样的问题只犯一次”41。这些都是用外部的手段,来保证不完美的自己能写出正确的代码。
其实有时候,别说正确的代码,人可能都不知道怎么写代码。比如收到一个复杂的需求,第一时间甚至觉得无法做到。此时不必逃避,找到困难的根源,把限制条件弄清楚42,近似的解法总是存在的,工程问题往往也不需要完美解法。如果对项目前景没把握,可以先做个原型43,以验证想法;如果是觉得某一部分实现困难,则可以考虑用“示踪弹”(tracer bullet)44来尝试相关方案。
3.2 别人是不完美的
“严于律人,宽以待己”是人的天性。很多时候,自己的软件设计没做好,但却要求leader或PM给出完美详尽的任务清单或PD,指望他们作出“最终的决定”不再更改。但这是不可能的45。没人准确知道自己到底要什么46,leader、PM、甚至用户都不例外。而程序员的任务之一就是帮助他们理解自己到底要什么47。有价值的业务需求往往要经过多轮的沟通、迭代才能准确把握48。且只有让自己站在他们的视角思考问题49,才能做到这点。
最后,用「Tip 100」50作为本文结尾:
It’s Your Life. Share it. Celebrate it. Build it. AND HAVE FUN !
引用
-
Tip 3 : You Have Agency (pg. 2) ↩
-
Tip 4 : Provide Options, Don’t Make Lame Excuses (pg. 4) ↩
-
Tip 29 : Fix the Problem, Not the Blame (pg. 89) ↩
-
Tip 10 : Critically Analyze What You Read and Hear (pg. 17) ↩
-
Tip 2 : Think! About Your Work (pg. xxi) ↩
-
Tip 5 : Don’t Live with Broken Windows (pg. 7) ↩
-
Tip 7 : Remember the Big Picture (pg. 10) ↩
-
Tip 9 : Invest Regularly in Your Knowledge Portfolio (pg. 15) ↩
-
Tip 11 : English is Just Another Programming Language (pg. 20) ↩
-
Tip 12 : It’s Both What You Say and the Way You Say It (pg. 22) ↩
-
Tip 14 : Good Design Is Easier to Change Than Bad Design (pg. 28) ↩
-
Tip 15 : DRY Don’t Repeat Yourself (pg. 31) ↩
-
Tip 98 : First, Do No Harm (pg. 286) ↩
-
Tip 46 : Don’t Chain Method Calls (pg. 134) ↩
-
Tip 65 : Refactor Early, Refactor Often (pg. 212) ↩
-
Tip 41 : Act Locally (pg. 121) ↩
-
Tip 45 : Tell, Don’t Ask (pg. 132) ↩
-
Tip 44 : Decoupled Code Is Easier to Change (pg. 131) ↩
-
Tip 17 : Eliminate Effects Between Unrelated Things (pg. 40) ↩
-
Tip 47 : Avoid Global Data (pg. 136) ↩
-
Tip 48 : If It’s Important Enough To Be Global, Wrap It in an API (pg. 136) ↩
-
Tip 37 : Design with Contracts (pg. 107) ↩
-
Tip 52 : Prefer Interfaces to Express Polymorphism (pg. 162) ↩
-
Tip 51 : Don’t Pay Inheritance Tax (pg. 161) ↩
-
Tip 54 : Use Mixins to Share Functionality (pg. 165) ↩
-
Tip 38 : Crash Early (pg. 113) ↩
-
Tip 36 : You Can’t Write Perfect Software (pg. 102) ↩
-
Tip 8 : Make Quality a Requirements Issue (pg. 12) ↩
-
Tip 43 : Avoid Fortune-Telling (pg. 127) ↩
-
Tip 88 : Deliver When Users Need It (pg. 273) ↩
-
Tip 24 : Iterate the Schedule with the Code (pg. 70) ↩
-
Tip 70 : Test Your Software, or Your Users Will (pg. 223) ↩
-
Tip 66 : Testing Is Not About Finding Bugs (pg. 214) ↩
-
Tip 67 : A Test Is the First User of Your Code (pg. 216) ↩
-
Tip 69 : Design to Test (pg. 221) ↩
-
Tip 39 : Use Assertions to Prevent the Impossible (pg. 115) ↩
-
Tip 34 : Don’t Assume It, Prove It (pg. 96) ↩
-
Tip 30 : Don’t Panic (pg. 89) ↩
-
Tip 31 : Failing Test Before Fixing Code (pg. 91) ↩
-
Tip 90 : Test Early, Test Often, Test Automatically (pg. 275) ↩
-
Tip 94 : Find Bugs Once (pg. 278) ↩
-
Tip 81 : Don’t Think Outside the Box. Find the Box (pg. 254) ↩
-
Tip 21 : Prototype to Learn (pg. 57) ↩
-
Tip 20 : Use Tracer Bullets to Find the Target (pg. 51) ↩
-
Tip 18 : There Are No Final Decisions (pg. 48) ↩
-
Tip 75 : No One Knows Exactly What They Want (pg. 244) ↩
-
Tip 76 : Programmers Help People Understand What They Want (pg. 245) ↩
-
Tip 77 : Requirements Are Learned in a Feedback Loop (pg. 246) ↩
-
Tip 78 : Work with a User to Think Like a User (pg. 247) ↩
-
Tip 100 : It’s Your Life. Share it. Celebrate it. Build it. AND HAVE FUN! (pg. 287) ↩