本系列共 5 篇,通译自:97-things-every-x-should-know
License:由 CC BY-SA 3.0 获得许可;
欢迎点赞、收藏、评论~ O(∩_∩)O
- 本瓜并未逐字逐句翻译,而是取其精要、理解抽象,结合自身进行撰文表达,与各位看官分享。认知好的编程概念,走向优秀~
- 《程序员优秀之路:认知 97 个好的编程概念(1)》
- 《程序员优秀之路:认知 97 个好的编程概念(2)》
干净的构建
当你构建项目时,是否会出现一串警告⚠?
你确实是知道应该修复它,但是项目还能正常跑,现在确实没时间,下次吧 QAQ
但是!随着代码库的增长,这些警告就会开始堆积,当它足够多的时候,你没办法在数百个警告中找到真正想去阅读、修复的那个单个警告了。
为了使警告变得有用起来,我们必须对构建产生的警告使用零容忍策略。即使警告不重要、不影响运行、不影响生产线,也要去修复它!
拥有干净的构建可以让其他人更轻松的接管我的工作,否则,其他人也无法从众多警告中区分出哪些是重要的,哪些是可忽略的。
本瓜认为:处理构建中的警告和重构代码是一样的,不要企图等到最后来一次大清理、大重构,我们应该提高处理的频次,细化处理的颗粒度,这样才能让代码走的更远~
命令行操作
现今,我们基本都是借助各类开发集成工具(IDE)进行开发,它们带来了足够的便利,减轻了程序猿对各个环节的细致思考的负担,用就完事了!
然而,事物具有两面性,易用性有着它的缺点:我们不清楚 IDE 实际做了什么,只用点点点各种按钮,魔法就相继发生了~
如果使用命令行,我们将更加清楚的知道可执行文件的所有步骤(比如编译、汇编、链接等)。
你可以使用开源命令行工具,比如 GCC,也可以使用专有 IDE 提供的工具。毕竟,设计良好的 IDE 只是一组命令行的图形前端。
如果你习惯使用命令行,你会发现它的功能比 IDE 的功能更强大。既然说 IDE 是一组特定命令行的图形前端,那么走出 IDE ,就意味着你可以完成各种自定义的命令,比如:grep 和 sed 提供的搜索和替换功能比 IDE 更强大。
当然,这里并不是让你放弃使用 IDE,只是给出一个建议:偶尔走出 IDE ,用命令行操作,你可能会发现新大陆~
精通两种及以上语言
每个程序员都从学习一种编程语言开始,该语言对程序员思考方式具有主导作用。无论使用这门语言有多少年的经验,如果只知道这门语言,思维将一直被这门语言限制着。
而学习第二语言将面临挑战,特别是如果该语言的范式与第一语言不同时。C、Pascal、Fortran,具有相同的范式。从 Fortran 切换到 C 将不会太困难,但如果从 Fortran 迁移到 C++,这将是一个巨大挑战,从 C++ 迁移到 Haskell 也同样~
我们可以列举出许多编程范式:面向过程、面向对象、函数式、逻辑、数据流等。
接受挑战是对自身有益的!知识交叉能让你更专业!一种语言无法解决的问题可能在另一门语言中被轻易解决!
编程语言的交叉融合具有巨大的影响。任何精通函数式编程的人都可以轻松地应用声明式方法,使用声明式方法让程序更短、更易于理解。
每个程序员都应该熟练掌握至少两种不同范式的编程技巧,最好是上述五种范式(都给我往死里学)。
可以把学习一门新语言当作一项兼职,它最终一定会使你受益!
花更多时间学习 IDE
1980 年代,语法高亮是一种奢侈品,格式化工具也是一种外部工具,调试器也是一个单独的程序;
1990 年代,公司开始认为为程序猿提供更好的工具可以获得更高受益,于是集成开发环境(IDE)开始发展,图形界面和鼠标也开始流行,开发人员可以轻松的在各种菜单中点点点;
21 世纪,IDE 变得如此普遍,很多还是免费供大家使用的,这让修改代码、搭建项目变得非常容易。
现代 IDE 还有一个惊人的特性就是自定义设置代码规则,这让团队开发更加规范。
不算太好的一点是,现代 IDE 不需要我们投入很多精力来学习它,这导致我们只能认知到它功能以内的东西。
学习 IDE 的快捷键也是很重要的,建议大家花更多时间来研究:如何使用 IDE 让工作更有效~
认知自己的极限
"Man's got to know his limitations." — Dirty Harry
人必须知道他的局限性!
资源是有限的,你只有这么多时间、这么多金钱、这么多努力、这么多聪明......尊重这些限制,是一切的基础。
对于软件工程师也同样,需要认知:系统的架构和性能特征也有它的限制。
复杂性的软件分析是在抽象层面,但软件在真实机器上运行。
现代计算机系统被分为物理机和虚拟机的两种层次结构,包括语言运行时、操作系统、CPU、缓存、随机存取存储器、磁盘驱动器和网络。
下表显示了典型联网服务器的随机访问时间和存储容量的限制。
access time | capacity | |
register | < 1 ns | 64b |
cache line | 64B | |
L1 cache | 1 ns | 64 KB |
L2 cache | 4 ns | 8 MB |
RAM | 20 ns | 32 GB |
disk | 10 ms | 10 TB |
LAN | 20 ms | > 1 PB |
internet | 100 ms | > 1 ZB |
算法和数据结构在使用缓存的效率方面也各不相同:
Elements | Search time (ns) | ||
线性搜索 | 排序数组的二分搜索 | 搜索 van Emde Boas 树 | |
8 | 50 | 90 | 40 |
64 | 180 | 150 | 70 |
512 | 1200 | 230 | 100 |
4096 | 17000 | 320 | 160 |
清楚你的下一次提交
作者拍了三位程序员的肩膀,问他们在做什么:
第一位说:“我正在重构这些方法”;
第二位说:“我正在向这个网络操作添加一些参数”;
第三位说:““我正在研究这个用户的行为”;
前两位似乎更全神贯注于工作细节,第三位则有着更大的图景。前两个非常清楚将涉及更改哪些文件,并将在一个小时左右完成。第三个程序员则准备在几天内完成这个任务,可能添加一些类、可能会怎样怎样修改服务......
第三位程序员最大的问题在于没有分解问题,任务的颗粒度太大。
如果任务情况发生了变化,前两位可以放弃所作更改,然后重新开始。但第三位由于代码一次性修改太多,将不愿全部丢弃,导致遗留糟糕的代码。
清楚你的下一次提交是什么! 如果你不能完成它,请及时修改。
大型互联数据属于数据库
一旦掌握了 SQL,编写以数据库为中心的应用程序将是一种乐趣(本瓜还没体会过~)。
将正确规范化的数据存储在数据库中后,可以轻松地使用可读的 SQL 查询数据,无需编写任何复杂的代码。
同样,单个 SQL 命令可以执行复杂的数据更改。对于一次性修改,比如改变持久数据的组织方式,您甚至不需要编写代码:只需启动数据库的直接 SQL 接口即可。
在这个相同的界面,还允许你进行测试查询,避开常规编程语言的编译到编辑,再到编译的循环。
高级数据库系统甚至可以在背后利用多核处理器。 而且,随着技术的进步,您的应用程序的性能也会提高。
学习沟通
程序员需要进行很多交流。
通常来说,我们更多时候与计算机进行交流,除此之外,我们也会跟同事交流。
今天的大型项目更多的是社会性的努力,而不仅仅是编程技术的应用。
除了与机器、自我和同行交流之外,一个项目还有许多利益相关者,其中大多数具有不同的技术背景或没有技术背景,包括测试、质检、部署、营销、销售,不同群体的用户等。
如果你不会说他们的语言,就想进入他们的领域,这几乎是不可能的。
如果与会计师交谈,你需要了解中心会计、固定资本、已用资本等方面的基本知识。如果与营销或律师交谈,你应该熟悉他们的一些行话和语言(以及他们的思想)。
所有这些领域特定的语言都需要由项目中的某个人 —— 最好是程序员,来掌握。程序员负责通过计算机将想法变为现实。
当然,生活不仅仅是软件项目。正如查理曼大帝所说,了解另一种语言就是拥有另一种灵魂。
Whereof one cannot speak, thereof one must be silent. - Ludwig Wittgenstein
学会估算
作为程序员,你需要经常对你的任务进行评估,然后提供给经理、同事或用户,以便他们能够对实现目标所需的时间、成本、技术或其它资源有一个准确的把握。
为了能够很好地进行估计,学习一些估计技术显然很重要。
如下项目经理和程序员间的对话并不少见:
项目经理:你能估计下开发功能xyz所需的时间吗?
程序员:一个月。
项目经理:太久了!我们只有一个星期!
程序员:至少需要三个星期。
项目经理:我最多可以给你两个。
程序员:成交!
这个对话中有三个关键的定义:估计、目标、承诺
- 估计是值,数,数量,或对某物的程度的近似计算或判断,它是基于硬数据和以往经验的事实衡量标准,例:开发任务估计持续 234 天;
- 目标是一个对理想实现的判断声明,例:系统必须至少支持400个并发用户;
- 承诺是在某一日期或在一定的质量水平下,提供特定功能的承诺,例: 搜索功能将在产品的下一个版本中可用;
项目经理真正需要的是程序员针对目标做出承诺,而不是估计。
Hello, World
Hello, World 是我们编程的初心。
作者分享了这样一个例子:
他遇到了一个棘手的问题,然后请教了一个解决问题的专家同事,这位同事并没有采取什么高明的办法,而是通过把问题代码从大项目中分离出来,再做测试验证,最终解决了问题;
没错,也许我们已经太久处在一个大项目中了,当遇到一个复杂问题时,如果不能抽象简化为一个原始问题,那么处理起来真的会很麻烦。
回归本地,回归命令行,回归小项目,回归最原始的代码:
#include <stdio.h> int main() { printf("Hello, World\n"); return 0; }
让项目说话
你的项目可能有一个版本控制系统,它连接着一个持续集成的服务器,能够自动化测试进行验证正确性,能这样做太棒了!
持续集成可以帮助你获取很多构建信息,如果你想让你得测试覆盖率不低于 20%,你可以把这个任务委托给这个项目本身,如果它没有完成这个指标,就会进行报警,输出报告。
你需要让你的项目有发言权。
你可以通过电子邮件或者即时消息来完成,通知开发人员应用程序的情况。也可以通过使用反馈设备(XFD)应用到项目中去。
XFD 原理是根据自动分析的结果来驱动物理设备,例如灯、便携式喷泉、玩具机器人,甚至 USB 火箭发射器。每当你得指标限制被打破,设备就会更改其状态,灯就会亮起来,你也可以听见构建中断的声音,甚至可以设定闻到代码的味道~
如果你在项目经理的办公室放一台这种设备,展示着整个项目的健康状态,他一定对你感激不尽!
让你的项目说话,奇妙的感受不言而喻。
链接器不神奇
不少程序员认识到从源代码到可执行文件的过程是:
- 编辑源代码;
- 将源代码编译成目标文件;
- 神奇的事情发生了;
- 运行可执行文件;
作者在做技术支持的几十年来,一直被问到一下问题:
- 链接器表明 def 被定义了不止一次;
- 链接器表明 abc 是一个未解析的符号;
- 为什么我的可执行文件这么大?
紧接着是“我现在该怎么办?”,还带着“似乎”和“不知道什么原因”这种论调。
实际上,这里面没有什么魔法,链接器是一个非常愚蠢、简单、直接的程序。
它所做的只是将目标文件的代码和数据部分连接在一起,将符号的引用与其定义连接起来,将未解析的符号从库中提取出来,并写出一个可执行文件。而已。
没有咒语!没有魔法!
(本瓜对于此条的理解是:作者苦于解答链接器的相关问题,实际上它本身并不复杂,有些问题看起来很复杂,但是实际上是个弟弟,当然这前提是我们理解其原理机制。)
临时解决方案的持久性
我们为什么要有临时的解决方案?
通常有一些直接的问题需要解决,它可能源自开发团队内部,填补一些工具链的空白,也可能来自外部,用于解决一些功能的缺失。
我们有时会采取一些临时的解决方案,它很有用,但是会对代码标准带来冲击(系列第一点就有提到~)。
临时解决方案永远存在,它可能不太符合公认的生产质量,但是它的优势就是快速解决问题。
我们该怎么办?
- 避免采用临时解决方案;
- 改变项目经理的临时决策;
- 保持原样;
如果你的项目非常混乱导致项目频繁停滞,那就需要很认真的思考临时解决方案和项目标准之间的关系了。
愿你能平静地接受你无法改变的事情,有勇气改变你能改变的事情。
接口设计
软件开发中最常见的任务之一是制定接口规范。
接口包括最高抽象级别(用户接口)、最低抽象级别(函数接口)以及介于两者之间的级别(类接口、库接口等)。
无论你是与用户指定他们将如何与系统交互,还是与开发人员合作指定 API,还是声明类的私有函数,界面设计都是工作的重要组成部分。
好的接口是:
- 正确使用很容易:在良好的 GUI 中,我们总是能单击正确的图标、按钮或菜单项,因为这是显而易见且容易的事情。在 API 中,同样如此,以正确的值传递正确的参数,这是最自然的;
- 使用错误很难:好的 GUI 可以预见人们可能犯的错误,并使他们难以犯错。例如,禁用或删除在当前上下文中没有意义的命令,或者 API 通过允许以任何顺序传递参数来消除参数排序问题;
请记住接口的存在是为了方便用户,而不是创建者。
谨慎设计的隐形
软件设计原则之一 —— 机制透明、信息隐藏。
Google 的主页非常简洁,但是你要检索一个信息时,它背后发生的事情是非常复杂的。
肯能 10% 展现给了用户,另外的 90% 被隐藏了。这样做有一个好处,你的项目有更多空间去操作,因为用户都看不到;也有一个坏处,用户可能认为你没有任何进步,或者说缺乏明显的更新。
隐形可能是危险的! 而显性的表示可以使人们相信进步是真实的而不是幻觉,是有意的而不是无意的,可重复的而不是偶然的。
- 编写单元测试提供了单元测试的难易程度的证据。它有助于展示你得代码的发展变化;低耦合、高内聚等特性;
- 运行单元测试可提供有关代码行为的证据。它有助于表明应用程序运行时的质量;
- 使用公告板和卡片可以使进度变得可见和具体。任务可以分为未开始、进行中或完成,无需参考隐藏的项目管理工具,也无需跟踪程序员虚构的状态报告;
- 提高开发进度(或缺乏进度)的可见性。可以完整的表明现实;
消息传递解决并发
程序员可能在学习计算机的最开始就会教导如何处理并发,并发是一个较难的问题,要关注线程、信号量、监视器,以及并发访问变量、安全问题等。
但问题的根源是什么?—— 共享内存。
人们讨论几乎所有的并发问题都与共享内存的使用相关:竞争冒险、死锁、活锁等。
要么放弃并发,要么避开共享内存!
放弃并发肯定不可能,那我们应该避开共享内存吗?
实际上,我们可以使用进程和消息传递,而不是使用线程和共享内存作为我们的编程模型。 这里的进程只是指具有执行代码的受保护的独立单元,不一定是操作系统进程。
Erlang(以及之前的 occam)等语言已经表明,进程是一种非常成功的并发和并行系统编程机制。此类系统没有共享内存、多线程系统所具有的同步压力。此外,还有一个正式模型 —— 通信顺序过程 (CSP) —— 可以作为此类系统工程的一部分加以应用。
我们还可以更进一步,引入数据流系统作为一种计算方式。在数据流系统中,没有明确编程的控制流。取而代之的是,建立一个由数据路径连接的运算符的有向图,然后将数据输入系统。由系统内数据的准备情况控制,没有同步问题。
不使用共享内存编程,而是使用消息传递,可能是实现计算机硬件中普遍存在的并行性的系统的最成功方法。
代码写给未来的自己看
我们都是聪明人,但是仍然认为目前所写的项目代码或者所解决的问题,对于后面接受这个代码或问题的人来说应该也是一个不小的困难。甚至是隔了段时间,自己都看不懂自己写的东西了......
作者举了个例子:
他有一个叫做乔的学生,一次数据结构课上,他问乔:“我猜不透你写的代码的作用是什么,你不是有一个弟弟吗?”
乔说:“是的,他也正在学习编程!”
老师问:“我想知道他是否能读懂这段代码。”
乔说:“不,这太难了!”
老师说:“这是真正的工作上的代码,几年后你弟弟会被雇来进行维护更新。你为他做了什么?”
乔说:“我懂了,我要写的更好一点,让菲尔也能看懂!”
......不得不说,这段翻译起来有些尴尬,但是确实是这个道理。
善用多态
多态性是面向对象的基本思想之一。这个词取自希腊语,意思是许多 ( poly ) 形式 ( morph )。在编程中,多态是指特定类的对象或方法的多种形式。
用代码来演示,例如以下实现简单购物车:
public class ShoppingCart { private ArrayList<Item> cart = new ArrayList<Item>(); public void add(Item item) { cart.add(item); } public Item takeNext() { return cart.remove(0); } public boolean isEmpty() { return cart.isEmpty(); } }
假设我们的网上商店提供可以下载的商品和需要发货的商品。再来构建另一个操作对象:
public class Shipping { public boolean ship(Item item, SurfaceAddress address) { ... } public boolean ship(Item item, EMailAddress address { ... } }
当客户完成结帐后,我们需要运送货物:
while (!cart.isEmpty()) { shipping.ship(cart.takeNext(), ???); }
另一种解决方案是创建两个都扩展 Item 的类。我们称这些为 DownloadableItem 和 SurfaceItem。把 Item 提升为支持单一方法 ship 的接口。要运送购物车的内容,通过致电item.ship(shipper)。
public class DownloadableItem implements Item { public boolean ship(Shipping shipper) { shipper.ship(this, customer.getEmailAddress()); } } public class SurfaceItem implements Item { public boolean ship(Shipping shipper) { shipper.ship(this, customer.getSurfaceAddress()); } }
在这个例子中,我们将处理的责任委托给了每个 Item。由于每件商品都知道它的最佳运输方式,因此这种安排使我们无需if-then-else。
该代码还演示了两种经常一起使用的模式:Command 和 Double Dispatch。这些模式的有效使用依赖于多态性的有效使用。借助它们,我们代码中if-then-else块的数量将会减少。
虽然在某些情况下使用if-then-else比多态更实用,但更多情况下,多态的编码风格将产生更小、更易读和更稳定的代码库。
和测试做朋友
无论把测试叫做质量保证还是质量控制,许多程序员更愿意称他们为“麻烦”。程序员似乎与测试有着敌对关系,因为测试似乎太挑剔了或者他们想要一切都完美......
作者认为:你可能认为测试人员通过写了长篇的测试报告让你觉得自己写的代码很糟糕,但是这会让用户对你有一个更好的反馈~
实际上,测试是帮助我们解决问题的,给力的测试是代码上线质量的最强保障。
被测试怀疑,好过被用户怀疑。
可能一时难以接受:那些总是把代码中的每个小错误暴露出来的测试人员实际上是你的朋友。(本瓜尝试接受~)
环境同步
作者见过几个项目,其中的构建会重写部分代码,为每个目标环境生成自定义二进制文件。
但是这总是使得事情变得比原本更复杂,并给团队带来一定得版本风险,因为大家可能安装版本不一致。
作者建议:构建一个二进制文件,您可以在发布管道中的所有阶段识别和提升它,这与项目代码分离。
保持环境信息版本化! 没有什么比破坏环境配置并且无法弄清楚到底发生了什么更糟糕的了。
环境信息应该与代码分开进行版本控制,因为它们会以不同的速率和不同的原因发生变化。
一些团队为此使用分布式版本控制系统(例如 bazaar 和 git),因为它们可以更轻松地将生产环境中所做的更改(不可避免地发生)推送回存储库。
OK,以上便是系列第 3 篇分享(共5篇),关注专栏,系列持续追踪~
我是掘进安东尼,输出暴露输入,技术洞见生活,下次再会~