最初是轻描淡写的偶像破坏,戳着 SOLID 的熊市,现在已经发展成更具体和有形的东西。如果我认为这些天 SOLID 原则没有用,那么我会用什么来代替它们呢?任何一套原则都适用于所有软件吗?我们所说的原则是什么意思?
我相信软件的某些特性或特征使它成为一种乐趣。您的代码越具有这些品质,使用起来就越愉快;但一切都是权衡,因此您应该始终考虑您的上下文。
可能有许多这些特性,重叠和相互关联,并且有很多方法可以描述它们。我选择了五个支持我在代码中关心的大部分内容。收益递减;五个就足以成为一个方便的首字母缩略词,而且足以记住。
我将在以后的文章中对每个特性进行扩展,以便不再获得这个特性,所以请原谅我没有更全面。
五个 CUPID 特性是:
- 可组合(Composable):与他人相处融洽
- Unix哲学(Unix philosophy):做好一件事
- 可预测(Predictable):做你所期望的
- 惯用语(Idiomatic):感觉自然
- 基于领域(Domain-based):解决方案领域在语言和结构上对问题领域进行建模
序言:很久以前……¶
您是否曾经破解过一个不熟悉的代码库并且只知道如何绕过?结构、命名、流程很明显,有点熟悉。笑容出现在你的脸上。“我有这个!”您认为。
在三十年的职业生涯中,我有幸经历过几次这样的经历,每一次都让我充满喜悦。第一次是在 1990 年代初——我记得很清楚——当时我打开了一个巨大的 C 代码库,它为数字印刷进行复杂的图像处理。别人的代码™中有一个错误,我要追踪并修复它。我记得作为一个新手程序员的感觉:既害怕又害怕背叛自己,因为我知道自己是个业余爱好者。
我的编辑器——带有 ctags 的 vi——允许我从调用站点导航到函数定义,几分钟之内,我就深入到一个调用嵌套中,在一个包含数百个源文件和头文件的代码库中,我确信我知道什么我在看。我很快找到了罪魁祸首,这是一个简单的逻辑错误,进行了更改,构建了代码并对其进行了测试。这一切都没有自动化测试,只是使用 Makefiles。TDD 在我的未来将近十年,而 C 在任何情况下都没有那种工具。
我对几个示例图像进行了转换,结果看起来还不错。我尽我所能确信我已经 a) 发现并修复了错误,并且 b) 没有同时引入任何令人讨厌的惊喜。
快乐软件¶
一些代码是一种愉快的工作。你知道如何找到你需要做的事情。你知道如何做出你需要的改变。该代码易于浏览,易于理解,易于推理。您确信您的更改将产生您想要的效果,而不会产生任何过度的副作用。代码引导你,邀请你环顾四周。在你之前来的程序员在乎后来的人,也许是因为他们意识到后来的程序员可能就是他们!
Martin Fowler 在他的开创性著作《重构》中说:
“任何傻瓜都可以编写计算机可以理解的代码。优秀的程序员编写人类可以理解的代码。” ——重构,Martin Fowler 和 Kent Beck,1996
我在 2000 年代初读到了这篇文章,他的话让我的编程世界彻底颠覆了。如果好的编程是为了让其他人可以理解代码呢?如果其中一个人类是未来的我怎么办?这听起来像是一件令人向往的事情。
但是,虽然“可以理解”可能是一个崇高的愿望,但它并不是那么高的标准!大约在 Martin 撰写有关重构的文章的同时,计算先驱 Richard P. Gabriel 描述了代码可居住的想法:
“宜居性是源代码的特性,它使 [人们] 能够理解其结构和意图,并舒适而自信地对其进行更改。
“宜居性使一个地方像家一样宜居。” — 可居住性和零碎增长1,软件模式第 7-16 页,Richard P. Gabriel
这感觉更像是要争取的东西。改变其他人的代码感到自在和自信会有多好?如果我们可以让代码变得宜居,那么快乐呢?代码库有可能让你充满喜悦吗?
如果您将工作日花在编程上,那么导航和操作代码库将定义您的用户体验。你可以体验到惊喜、沮丧、恐惧、期待、无助、希望、喜悦,这一切都是因为早期程序员在代码库中做出的选择。
如果我们假设代码库可能是快乐的,那么每个代码库是否都有自己特殊的雪花,它对你的心灵的影响是独一无二的?或者我们能否阐明是什么让它变得快乐,并提供一条途径来增加我们接触的代码的快乐?
特性高于原则¶
当我开始对 SOLID 的五项原则做出回应时,我设想用我认为更有用或更相关的东西来替换每一项。我很快意识到原则的想法本身是有问题的。原则就像规则:你要么顺从,要么不顺从。这就产生了规则追随者和规则执行者的“有界集合”,而不是具有共同价值观的人的“中心集合”。2
相反,我开始考虑特性:代码的品质或特征,而不是要遵循的规则。特性定义要移动的目标或中心。您的代码只是离中心更近或更远,并且始终有明确的行进方向。您可以使用特性作为镜头或过滤器来评估您的代码,并且您可以决定接下来要解决哪些问题。由于 CUPID特性都是相互关联的,因此您为改进一个特性所做的任何更改都可能对其他一些特性产生积极影响。
特性的特性¶
那么我们如何选择特性呢?是什么让特性或多或少有用?我决定了三个我希望 CUPID 特性具有的“特性的特性”。它们应该是实用的、人性化的和分层的。
为了实用,特性需要:
- 易于表达:因此您可以用几句话描述它们中的每一个,并提供具体的例子和反例。
- 易于评估:因此您可以将它们用作审查和讨论代码的镜头,并且您可以轻松确定代码展示了每个特性的程度。
- 易于采用:因此您可以从小规模开始并沿着任何 CUPID 维度逐步发展代码。没有“全押”,也没有“失败”,就像从来没有“完成”一样。代码总是可以改进的。
作为人类,特性需要从人的角度来阅读,而不是代码。CUPID 是关于使用代码的感觉,而不是对代码本身的抽象描述。例如,虽然“做好一件事”的 Unix 哲学听起来像单一职责原则,但前者是关于你如何使用代码,而后者是关于代码本身的内部结构。3
要分层,特性应该为初学者提供指导——这是易于表达的结果——并为那些发现自己想要更深入地探索软件本质的更有经验的人提供细微差别。每个 CUPID 特性都是“显而易见的”,只是名称和简要描述,但每个特性都包含许多层、维度和方法。我们也许可以描述每个特性的“中心”,但是有很多路径可以到达那里!
可组合¶
易于使用的软件会被使用、使用并再次使用。有一些特征使代码或多或少可组合,但这些对于做出任何保证既不是必要的也不是充分的。在每种情况下,我们都可以找到双方的反例,因此您应该将这些视为有用的启发式方法。更多不一定更好;这都是取舍。
小表面积¶
具有狭窄、固执己见的 API 的代码让您学习的东西更少,出错的可能性也更少,与您正在使用的其他代码发生冲突或不一致的可能性也更小。这有一个递减的回报;如果您的 API 太窄,您会发现自己将它们组合在一起使用,并且了解常见用例的“正确组合”会成为可能成为进入障碍的隐性知识。获得正确的 API 粒度比看起来更难。碎片化和臃肿之间有一个“恰到好处”的凝聚力。
意图揭示¶
意图揭示代码很容易发现和评估。我可以很容易地找到您的组件,并尽快决定它是否是我需要的东西。我喜欢的一个模型——来自像古老的 XStream 这样的开源项目——有一个 2 分钟的教程、一个 10 分钟的教程和一个深入研究。这让我可以增量投资,并在我发现这不适合我时立即退出。
我不止一次开始编写一个类,给它一个意图揭示的名称,只是为了让 IDE 弹出一个具有相同名称的建议导入。通常结果是其他人也有同样的想法,我偶然发现了他们的代码,因为我们选择了相似的名字。这不仅仅是巧合;我们精通同一个领域,这使得我们更有可能选择相似的名字。当您拥有基于域的代码时,这种情况更有可能发生。
最小依赖¶
具有最小依赖性的代码让您不必担心,并降低版本或库不兼容的可能性。我用 Java 编写了我的第一个开源项目 XJB,并使用了几乎无处不在的 log4j 日志框架。一位同事指出,这创建了一个依赖关系,不仅依赖于作为库的 log4j,而且依赖于特定的版本。我什至没有想到。为什么有人要担心像日志库这样无害的东西?因此,我们删除了依赖项,甚至提取了一个完整的其他项目,该项目使用 Java 动态代理做有趣的事情,它本身具有最小的依赖项。
Unix 哲学 ¶
Unix和我差不多大;我们都始于 1969 年,而 Unix 已成为地球上最流行的操作系统。在 1990 年代,每个严肃的计算机硬件制造商都有自己的 Unix,直到关键的开源变体 Linux 和 FreeBSD 变得无处不在。如今,它以 Linux 的形式运行几乎所有的业务服务器,包括云服务器和本地服务器;它在嵌入式系统和网络设备中运行;它支持 macOS 和 Android 操作系统;它甚至作为 Microsoft Windows 的可选子系统提供!
一个简单、一致的模型¶
那么,一个从电信研究实验室开始的小众操作系统是如何被一个大学生作为爱好项目复制的,最终成为世界上最大的操作系统呢?毫无疑问,在一个操作系统供应商因其技术而闻名的时代,它的成功有商业和法律原因,但其持久的技术吸引力在于其简单而一致的设计理念。
Unix 哲学说要编写能够很好地协同工作的 [组件],在上面的可组合性特性中进行了描述,并且只做一件事并且做好。4 例如,ls 命令列出了有关文件和目录的详细信息,但它不知道关于文件或目录的任何事情!有一个名为 stat 的系统命令提供信息;ls 只是将信息显示为文本的工具。
同样,cat 命令打印(连接)一个或多个文件的内容,grep 选择与给定模式匹配的文本,sed 替换文本模式,等等。Unix 命令行具有强大的“管道”概念,它将一个命令的输出作为输入附加到下一个命令,创建一个选择、转换、过滤、排序等管道。您可以编写复杂的文本和数据处理程序,这些程序基于组合一些精心设计的命令,每个命令都做一件事,而且做得很好。
单一目的与单一职责 ¶
乍一看,这看起来像是单一职责原则 (SRP),对于 SRP 的某些解释,存在一些重叠。但“做好一件事”是一种由外而内的观点;它具有特定、明确和全面的目的。SRP 是一个由内而外的观点:它是关于代码的组织。
SRP,用创造这个词的 Robert C. Martin 的话来说是,[代码]“应该有一个,而且只有一个,改变的理由。” Wikipedia 文章中的示例是一个生成报告的模块,您应该在其中将报告的内容和格式视为单独的关注点,它们应该存在于单独的类中,甚至是单独的模块中。正如我在其他地方所说,根据我的经验,这会产生人为的接缝,最常见的情况是数据的内容和格式一起改变;例如,一个新字段或对某些数据源的更改会影响其内容和您希望显示它的方式。
另一个常见场景是“UI 组件”,其中 SRP 要求您将组件的呈现和业务逻辑分开。作为开发人员,让这些人生活在不同的地方会导致将相同字段链接在一起的行政工作。更大的风险是,这可能是一种过早的优化,会阻止随着代码库的增长而出现的更自然的关注点分离,以及随着“做好一件事”并且更适合问题空间的域模型的组件的出现。随着任何代码库的增长,将其分离为合理的子组件的时候到了,但是可组合性和基于域的结构的特性将更好地指示何时以及如何进行这些结构更改。
可预测的 ¶
代码应该做它看起来做的事情,一致且可靠,没有令人不快的意外。这不仅应该是可能的,而且应该很容易确认。从这个意义上说,可预测性是可测试性的概括。
可预测的代码应该按预期运行,并且应该是确定性和可观察的。
表现如预期¶
Kent Beck 的四项简单设计规则中的第一条是代码“通过了所有测试”。即使没有测试,这也应该是正确的!可预测代码的预期行为应该从其结构和命名中显而易见。如果没有自动化测试来实现这一点,那么编写一些应该很容易。Michael Feathers 将这些特性称为测试。用他的话说:
“当一个系统投入生产时,在某种程度上,它变成了它自己的规范。”——迈克尔·费瑟斯
这不是必需的,我发现有些人认为测试驱动开发是一种信仰,而不是一种工具。我曾经开发过一个复杂的算法交易应用程序,它的“测试覆盖率”约为 7%。这些测试分布不均!大部分代码根本没有自动化测试,有些代码有大量复杂的测试,检查细微的错误和边缘情况。我有信心对大部分代码库进行更改,因为每个组件都做一件事,而且它的行为是直接且可预测的,因此更改通常是显而易见的。
确定性¶
软件每次都应该做同样的事情。即使设计为非确定性的代码(例如随机数生成器或动态计算)也将具有您可以定义的操作或功能界限。您应该能够预测内存、网络、存储或处理边界、时间边界以及对其他依赖项的期望。
决定论是一个广泛的话题。出于可预测性的目的,确定性代码应该是健壮的、可靠的和有弹性的。
- Robustness :稳健性是我们涵盖的情况的广度或完整性。限制和边缘情况应该是显而易见的。
- Reliability :在我们涵盖的情况下,可靠性按预期运行。我们每次都应该得到相同的结果。
- Resilience :复原力是我们处理未涵盖的情况的能力;输入或操作环境中的意外扰动。
可观察的¶
代码在控制理论的意义上应该是可观察的:我们可以从它的输出推断它的内部状态。这只有在我们设计时才有可能。一旦几个组件交互,尤其是异步交互,就会出现紧急行为和非线性后果。
从一开始就检测代码意味着我们可以获得有价值的数据来了解其运行时特性。我描述了一个四阶段模型——有两个奖励阶段!——像这样:
- Instrumentation :Instrumentation 是您的软件说明它在做什么。
- Telemetry :遥测使这些信息可用,无论是通过拉(请求)还是推送(发送消息);“远距离测量”。
- Monitoring :监控正在接收仪器并使其可见。
- Alerting :警报是对监控的数据或数据中的模式做出反应。
奖金:
- Predicting :预测是使用这些数据在事件发生之前对其进行预测。
- Adapting :适应是动态地改变系统,以抢占或从预测的扰动中恢复。
大多数软件甚至都没有通过第 1 步。有一些工具可以拦截或改变正在运行的系统以增加洞察力,但这些工具永远不如为应用程序设计的故意仪表。
惯用语¶
每个人都有自己的编码风格。无论是空格还是制表符、缩进大小、变量命名约定、大括号或圆括号的位置、源文件中的代码布局,还是无数其他可能性。在此之上,我们可以对库、工具链、生存路径、甚至版本控制注释样式或提交粒度的选择进行分层。(你确实使用版本控制,不是吗?)
这会给使用不熟悉的代码增加显着的额外认知负担。除了理解问题域和解决方案空间之外,您还必须解释其他人的意思,以及他们的决定是经过深思熟虑的和上下文相关的,还是任意的和习惯性的。
最大的编程特质是同理心。同情你的用户;对支持人员的同理心;对未来开发者的同理心;任何人都可能成为未来的你。编写“人类可以理解的代码”意味着为其他人编写代码。这就是惯用代码的含义。
在这种情况下,您的目标受众是:
- 熟悉该语言、它的库、它的工具链和它的生态系统
- 了解软件开发的经验丰富的程序员
- 努力完成工作!
语言习语¶
代码应该符合语言的习惯用法。有些语言对代码的外观有强烈的看法,这使得评估代码的惯用程度变得容易。其他人不那么固执己见,这让你有责任“选择一种风格”然后坚持下去。Go 和 Python 是固执己见的语言的两个例子。
Python 程序员使用术语“pythonic”来描述惯用代码。如果你从 Python REPL 导入它,或者从 shell 运行 python -m this,就会出现一个美妙的复活节彩蛋。它打印了一个名为“Python 之禅”的编程格言列表,其中包括这一行,抓住了惯用代码的精神:“应该有一种——最好只有一种——明显的方式来做到这一点。”
Go 语言附带了一个名为 gofmt 的代码格式化程序,它使所有源代码看起来都一样。这一下子消除了关于缩进、大括号放置或其他语法怪癖的任何分歧。这意味着您在库文档或教程中看到的任何代码示例看起来都是一致的。他们甚至有一个名为 Effective Go 的文档,展示了语言定义之外的惯用 Go。
光谱的另一端是 Scala、Ruby5、JavaScript 和古老的 Perl 等语言。这些语言是故意的多范式;Perl 创造了首字母缩略词 TIMTOWTDI——“有不止一种方法可以做到”——发音为“Tim Toady”。您可以在其中的大多数中编写函数式、过程式或面向对象的代码,这会从您所知道的任何一种语言中创建一个浅薄的学习曲线。
对于处理一系列值这样简单的事情,这些语言中的大多数都允许您:
- 使用迭代器
- 使用索引 for 循环
- 使用条件 while 循环
- 使用带有收集器的函数管道(“map-reduce”)
- 写一个尾递归函数
这意味着在任何不平凡的代码大小中,您都可能会找到其中每一个的示例,通常是相互结合的。同样,这一切都会增加认知负担,影响您思考手头问题的能力,增加不确定性并减少快乐。
代码习语出现在所有粒度级别:命名函数、类型、参数、模块;代码布局;模块结构;工具的选择;依赖项的选择;你如何管理依赖关系;等等。
无论您的技术堆栈处于自以为是的范围内,如果您花时间学习该语言的习语、它的生态系统、它的社区和它的首选风格,您编写的代码将会更加富有同情心和快乐。
您对一项技术的学习曲线可能比您在其中编写的任何代码都更短,因此抵制编写现在对您来说很好读的代码的冲动很重要,因为那个人不会存在很长时间!确信您正在编写惯用代码的唯一方法是花时间学习惯用语。
地方习语 ¶
当一种语言在惯用风格或几种替代方案方面没有达成共识时,由您和您的团队来决定“好的”是什么样的,并引入约束和指导方针以鼓励一致性。这些约束可以很简单,例如 IDE 中的共享代码格式化规则、检查和批评代码的“构建 cop”工具,以及标准工具链上的协议。
架构决策记录 6 或 ADR 是记录您对风格和习语的选择的好方法。与任何其他架构讨论相比,这些都是“重要的技术决策”。
基于域 ¶
我们编写软件来满足需要。这可能是具体的和情境化的,也可能是笼统的和影响深远的。不管它的目的是什么,代码都应该用问题域的语言来表达它正在做什么,以最小化你写的东西和它所做的事情之间的认知距离。这不仅仅是“使用正确的词”。
基于领域的语言 ¶
编程语言及其库充满了计算机科学结构,如哈希映射、链接列表、树集、数据库连接等。它们具有包括整数、字符、布尔值的基本类型。您可以将某人的姓氏声明为字符串 [30],这很可能是它的存储方式,但定义姓氏类型将更能揭示意图。它甚至可能具有与姓氏相关的操作、特性或约束。银行软件中的许多细微错误是由于将金额表示为浮点值;有经验的金融软件程序员会定义一个 Money 类型,其中包含 Currency 和 Amount,它本身就是一个复合类型。
正确命名类型和操作不仅仅是为了捕捉或防止错误,而是为了让代码中的解决方案空间更容易表达和导航。这是我对“每个程序员都应该知道的 97 件事”的贡献,即“领域语言中的代码”。
领域驱动代码成功的一个标准是,不经意的观察者无法判断人们是在讨论代码还是在讨论领域。我曾经在一个电子交易系统中遇到过这种情况,一位金融分析师正在与两名程序员讨论复杂的交易定价逻辑。我以为他们在讨论定价规则,但他们指着一屏代码,分析师正在通过定价算法与程序员交谈,这是代码如何逐行读取的!问题域和解决方案代码之间唯一的认知距离是一些语法标点符号!
基于域的结构¶
使用基于域的语言很重要,但如何构建代码也同样重要。许多框架都提供了一个“骨架项目”,其目录布局和存根文件旨在帮助您快速入门。这会在您的代码上强加一个与您正在解决的问题无关的先验结构。
相反,代码的布局——目录名称、子文件夹和同级文件夹的关系、相关文件的分组和命名——应该尽可能地反映问题域。
应用程序框架 Ruby on Rails 在 2000 年代初期通过将其构建到其工具中而普及了这种方法,Rails 的广泛采用意味着许多后来的框架都复制了这个想法。CUPID 与语言和框架无关,但 Rails 提供了一个有用的案例研究来理解基于域的结构和基于框架的结构之间的区别。
下面是生成的 Rails 应用程序的部分目录布局,重点关注开发人员将花费大部分时间的目录(应用程序)。在撰写本文时,完整的框架运行到大约 50 个目录,其中包含 60 个文件7。
代码语言:javascript复制app
├── assets
│ ├── config
│ ├── images
│ └── stylesheets
├── channels
│ └── application_cable
├── controllers
│ └── concerns
├── helpers
├── javascript
│ └── controllers
├── jobs
├── mailers
├── models
│ └── concerns
└── views
└── layouts
想象一下,这将是一个医院管理应用程序,其中有一个病人记录部分。这种布局表明我们至少需要:
- 一个模型,它映射到某处的数据库
- 一个视图,在屏幕上呈现患者记录
- 一个控制器,在视图和模型之间进行调解
然后是帮助器、资产和其他几个框架概念的范围,例如模型关注点或控制器关注点、邮件程序、作业、通道,以及可能与 Ruby 控制器一起使用的 JavaScript 控制器。这些人工制品中的每一个都存在于一个单独的目录中,即使它们在语义上是紧密集成的。
对患者记录管理的任何重大更改都可能涉及分散在代码库中的代码。单一职责的 SOLID 原则说视图代码应该与控制器代码分开,并且像 Rails 这样的框架将其解释为意味着将它们放在完全不同的位置。这增加了认知负荷,降低了凝聚力,并增加了进行产品更改的努力。正如我之前所讨论的,这种意识形态约束会使工作更加困难,代码库的乐趣也会降低。
我们仍然需要模型、视图和控制器等人工制品,无论我们以何种方式布置代码,但按类型对它们进行分组不应形成主要结构。相反,代码库的顶层应该显示医院管理的主要用例;也许是患者历史、约会、人员配备和合规性。
对代码结构采用基于域的方法可以很容易地理解代码的用途,并且可以轻松导航到任何比“使该按钮变成浅蓝色”更复杂的地方。
基于域的边界¶
当我们按照我们想要的方式构建代码并按照我们想要的方式命名时,模块边界就变成了域边界,部署就变得简单了。将组件部署为单个工件所需的一切都在一起,因此我们可以将域边界与部署边界对齐,并部署有凝聚力的业务组件和服务。无论您将您的产品或服务打包为单个单体、许多小型微服务,还是介于两者之间的任何地方,这种一致性都降低了您的生存路径的复杂性,并降低了您忘记某些东西或包含来自不同环境的人工制品或不同的子系统。
这并不限制我们使用单一的、扁平的、顶级的代码结构。域可以包含子域;组件可以包含子组件;部署可以在对您的变更和风险状况有意义的任何粒度级别进行。将代码边界与域边界对齐可以使所有这些选项更易于推理和管理。
结论性想法¶
我相信拥有更多这些特性的代码——可组合性、Unix 哲学、可预测性,或者是惯用的或基于域的——比不具备这些特性的代码更令人愉悦。虽然我独立评估每个特征,但我发现它们是相辅相成的。
既可组合又全面的代码——做好一件事——就像一个可靠的朋友。即使您以前从未见过惯用代码,也感觉很熟悉。可预测的代码为您提供了空闲周期来专注于其他地方的惊喜。基于领域的代码最小化了从需求到解决方案的认知距离。将代码移向这些特性中的任何一个的“中心”会比你发现的更好。
因为 CUPID 是一个反义词,所以每个字母我都有几个候选者。我之所以选择这五个,是因为它们以某种方式感到“基础”;我们可以从中得出所有其他候选特性。未来的文章将探讨一些没有入选的候选名单特性,并看看它们是如何成为编写 CUPID 软件的自然结果。
我很想听听人们与 CUPID 的冒险经历。我已经听说有团队使用这些特性来评估他们的代码,并制定清理遗留代码库的策略,我迫不及待地想听到经验报告和案例研究。与此同时,我想更深入地了解 CUPID,依次探索每个特性,看看还有什么隐藏在显而易见的地方。
- 我建议任何参与软件开发的人,而不仅仅是程序员,阅读这篇短文。这是一篇深刻而优美的文字。↩︎
- 在 1970 年代,人类学家和基督教传教士(传教士的观察者)保罗·G·希伯特(Paul G. Hiebert)使用有界和中心集的数学概念来对比“有界”社区,这些社区通过谁在谁在外面的规则来定义自己,与“中心”社区一起,他们通过一组核心价值观来定义自己,人们更接近或远离这些核心价值观,但从不“外部”。↩︎
- 单一职责的定义是代码应该有“一个且只有一个改变的理由”,例如,你应该将 UI 代码与业务逻辑分开。这种约束不仅很容易被驳斥——因为出于安全性、合规性、上游或下游依赖性、操作特性等原因,即使是一行代码也可能需要更改,而且我认为它是一个任意约束往往是过早的隔离,带来负面后果。↩︎
- 除此之外,Unix 操作系统的设计还有一个优雅的简洁性:一切都是文件;一切要么是文字,要么不是文字;我们通过一系列转换处理文本来构建整个程序。↩︎
- Ruby 在这里可能是一个异常值,因为肯定存在“Ruby 美学”,并且很多人都写过“惯用的 Ruby”,但这仍然是个人分享他们喜欢的编程风格,而不是社区固有的任何东西。↩︎
- 架构决策记录由 Michael Nygard 于 2011 年首次提出,此后一直在发展。↩︎
- 关于框架应该为“原始”项目的开发人员施加多少脚手架和生成的样板,还有一个完整的其他讨论,这超出了本文的范围。↩︎