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