软件工程:理由与限度
这份非规范性论证解释软件工程哲学。它既不确立采用,也不向该文本增加义务。其起点是 Organon Core 0.1.2;新增的价值立场面向由人与 agent 在已说明的生命周期内维护的软件。在这一范围内,用户偏好朝有可信根据的变更方向持续演进,同时接受在其成本威胁具体目标时作出让步。可信程度关乎预期某一变更方向的可辨识理由,而非所有可想象的扩展。
以下论证区分这一价值选择、Core 的约束与工程文献。来源摘要只陈述各来源的有限贡献。将这些贡献与本哲学联系起来的解释,是作者的论证,不是归于这些来源的结论。构造的反例均作相应标明。
Core 的贡献与留下的开放问题
自超越质疑架构、工具链或设计词汇的终局性。在软件中,这为保留学习更好分解方式的可能性提供了理由。若要优先投资于这种可能,而非完成一个有明确边界的产品,还需要额外前提:未来的构建能力必须在产品生命周期内具有重要性。Core 本身允许有根据的稳定。一个构造的反例是一次性转换器,其经验证的输出已经完成了目的;用扩展框架替换其直接实现,并不必然拓展任何维护者珍视的东西。
自洽有助于区分不相容的承诺与普通权衡。体系不能一面承诺某种数据新鲜度保证,一面又在完全相同的保证下接受过时结果,并仍称自身自洽。适用这一约束,需要明确新鲜度、确认以及相关执行条件的含义。它不选择这些含义,也不确立这些含义是否充分。作为构造的反例,两个组件可能一致地实施同一种错误的单位换算。自洽揭示立场内部的冲突,不提供缺失的领域真相。
反身将架构规则和 agent 指令与应用代码一同纳入批评范围。意在减少协调的规则,本身可能成为协调开销的来源。额外的工程前提是:这类开销与维护者的目标有关。一个构造的反例是某项审查规则,在没有可辨识的剩余问题时,仍要求每次审查都再接受一次审查。反身使这一规则可以接受检验;它既不证明审查有效,也不蕴含这种无止境程序或任何特定的独立审查安排。
根据:说明与检验区分有说服力的解释与充分支持。关于吞吐量的主张需要相关负载的证据;关于接口隔离变更的主张需要针对依赖和变更情境的论证;对未来灵活性的偏好需要理由,并承认其后果。工程应用提供这些负载和情境。一个构造的反例是文档完备的抽象,其设想的未来客户却从未出现。它的理由可以说明,但其预测可能并不可靠。
根据:范围与度量防止用局部成功决定更广泛的架构比较。在未改变的输入上得到相同输出,仍未考察并发、更新、故障处理和维护工作量。哪些差异在这里重要,由额外前提决定。这一限度也适用于负面判断:孤立事件可以成为调查故障的理由,却不足以确立整个总体存在缺陷。一个构造的反例是仅因某份有用的故障报告尚未复现,就拒绝它。数值评分与可重复性都不能单独解决相关性问题。
能力主张需要明确检验对象。agent 可以在受约束的仓库中可靠地完成某种变换,而不理解架构的每一项理由。Core 允许用外部证据支持前一种主张,并不普遍要求内省式解释。要求交接内容可被理解,是一项额外的应用选择,这里以持续维护为其理由。一个构造的反例是不透明但可靠的生成器,它通过稳定接口和可检查输出支持可维护的产品。仅凭其不透明,不能确立它缺乏能力;不过,更强的理解能力主张仍未得到支持。
对替代实现的开放排除仅凭模式的声誉给予优先的做法。这项贡献具有实质内容:SOLID、函数式编程与元编程,都不能自行确立自身的适用性。工程补充了使比较成为可能的目标和约束。针对不加辨别地追逐新颖性的一个构造反例是:用一种陌生的替代方案取代熟悉且有支持的平台,相关行为相同,交接却更困难。熟悉程度可以提供实际理由;称一种实现符合惯例,并不能反驳它。
这些承诺相互约束,但不规定软件价值的层级。将它们结合起来,并不能推出在相关必要要求得到满足后,演进应优先于当下抽象的简单性。本哲学增加这一可让步的偏好,是因为其目标维护者预期会持续负责不断变化的软件。反过来,与 Core 兼容本身也不足以成为采用该偏好的理由。它的价值取决于生命周期、预期变更的可信程度,以及保留这些可能性所需的成本。
工程论证及其边界
Parnas 比较了分配处理步骤的分解方式与隐藏设计决策的分解方式。他的论证支持考察哪些知识跨越模块边界。它没有将良好结构等同于某一种运行时安排:他明确讨论了机械照搬的实现所带来的调用开销,以及在保持分解方式的同时组装高效代码的替代方案。这是原文已有的条件,不是后来为保护模块化而添加的例外。Parnas, “On the Criteria To Be Used in Decomposing Systems into Modules,” pp. 1056–1058。
关于 SRP,Martin 后来的解释将职责定位于提出变更要求的业务职能,并将内聚与共同的变更理由联系起来。这支持调查代码为何一同变化,而非计算方法数量。构造的反例:仅因验证、计算和解释是不同动词,就将它们分开,可能把同一项业务规则修订分散到独立维护的组件中。这是本论证对机械解释的反例,不是 Martin 的经验发现。Martin, “The Single Responsibility Principle,” 2014。
关于 OCP,Martin 明确将封闭性限定于选定的变更。他原文中的绘图示例能够容纳新形状,却不能让绘图循环免受新排序要求的影响。因此,有用的问题是扩展机制保护了哪种变更,又将哪种变更暴露在外。承诺普遍的可扩展性超出了该来源的主张;面对不同的变更方向,修改既有边界可以是有理由的响应。Martin, “The Open-Closed Principle,” p. 6。
关于 LSP,Liskov 和 Wing 将子类型判断建立在规格以及对可观察性质的保持之上,包括不变量和历史性质。他们的窗口示例说明,为何允许额外的移动可能违反既有客户端所依赖的性质。签名匹配或不改动继承方法都不足以成立。将类似推理用于协议或泛型参数,是对这一思想的拟议迁用;他们的论文不确立每一种此类应用。Liskov and Wing, “A Behavioral Notion of Subtyping,” §§5.2–5.3。
关于 ISP,Martin 考察了被迫依赖其不使用接口的客户端。分离这些依赖,不必拆开有状态对象:定时门示例在共享数据之上实现不同的客户端接口。因此,原始案例质疑了“接口分离总是要求对象分离”这一推论。拟议的划分是否减少了产生实际后果的耦合,仍需针对真实客户端判断。Martin, “The Interface Segregation Principle,” pp. 4–6。
关于 DIP,关注点是高层策略、抽象和实现细节之间的关系。Martin 的例子包括 C 标准 I/O,因此这一机制不限于继承框架。构造的反例:某个接口照搬供应商的数据模式和异常,即便采用依赖注入,也可能保留对这些细节的依赖。因此,架构主张需要接口声明之外的支持。Martin, “The Dependency Inversion Principle,” pp. 5–6。
Martin 更广泛的论述为这些原则提供了重要限定:静态模板可以实现 OCP;ISP 不要求每个客户端类都有独立接口;依赖稳定的具体实现可以是合理的。他提醒字符编码变更可能推翻最后一项判断,保留了该判断的条件。这些限定支持依情境应用原则,而非强制罗列一套抽象类。Martin, “Design Principles and Design Patterns,” pp. 7, 14–16。
重构有行为边界。Fowler 将其定义为在保持可观察行为的同时,使软件更易理解、修改成本更低的内部结构调整。这支持区分结构准备与功能变更;仅凭这一定义,无法确定项目中的观察范围。构造的反例:某次重写保持返回值,却改变了外部所依赖的失败行为。称其为重构,不能决定该行为是否在保持主张的范围内。Fowler, “Definition of Refactoring”。
函数式组合提供另一条分解路径。Hughes 展示了高阶函数和惰性求值如何分离可复用的计算模式,以及如何将生成与选择分离。他的论证没有将不使用赋值视为充分的质量尺度。原文的一项限定涉及副作用:其发生时机可能依赖相隔很远的消费位置。相关收益是有用的组合边界,不是遵循某套词汇或全面禁止状态。Hughes, “Why Functional Programming Matters,” §§1–4。
泛型编程同样需要共同语义,不能只有语法匹配。Dehnert 和 Stepanov 从操作定律与复杂度预期出发论证。他们对迭代器的讨论解释了,为何在不同复杂度模型上提供形似随机访问的操作,会误导算法选择。这支持考察可复用算法中隐藏的前提;它不支持要求每个具有身份或拥有资源的对象都建模为规则值(regular value)。Dehnert and Stepanov, “Fundamentals of Generic Programming,” pp. 3–5, 10–11。
模板元编程与部分求值说明,如何利用较早可用的信息,对较后的计算进行特化。Veldhuizen 将模板实例化与离线部分求值联系起来,同时区分参数化复用、编译期计算和生成。他的递归实例化示例也暴露了终止性方面的限度。这一联系可以解释某种收益;它不能确立特化没有成本,也不能确立每个参数都值得静态绑定。Veldhuizen, “C++ Templates as Partial Evaluation”。
表达式模板提供了一个具体案例:Veldhuizen 在特定基准条件下展示了避免临时向量和合并求值。这些结果不能确立表达式模板具有普遍优势。Veldhuizen, “Expression Templates”。Iglberger 及其同事随后展示了矩阵、稀疏和复合运算中的局限,从而提出采用不同求值策略和优化计算内核的理由。他们的实验是针对无条件性能主张的反例,不是有关每个当前库版本的证据。Iglberger et al., “Expression Templates Revisited,” §§5–8。
本论证进一步作出工程推论:将工作从执行期移到编译期,会改变成本与限制出现的位置。有用的比较可以包括构建延迟、生成代码的体积、诊断、部署组合、运行时分派,以及修改该机制所需的专长。构造的反例:发布时已知的有限配置可能适合特化,而客户加载的行为可能适合运行时选择。任何一种安排都不能仅因把决策前移或后移就胜出。
组合与继承
前述 LSP 论证关乎行为子类型,本身不决定为复用而继承实现或组合协作者之间的选择。以下比较是本配套文档的工程论证。当子类型契约与受支持的变化点稳定时,继承可以提供有用的共享实现。组合可以让维护者替换协作者,而不声称包含它的对象是它的子类型。任何一种机制都不能单独确立独立变更:如果委托包装层暴露协作者的异常与生命周期要求,就可能保留同样具有实际后果的依赖。
一个构造的例子是重试策略不断变化的服务。策略协作者可以允许替换,而无需编辑无关的存储继承层级。但如果重试、事务和确认共同决定可观察行为,拆分类并不会使其变更独立。对于实际维护者,具有明确且稳定钩子的继承框架,仍可能比一组相互转发的对象更易理解和验证。比较契约、允许的覆盖、所有权及有可信根据的变更如何传播。当这些条件改变时重新审视选择;不能只凭口号优先采用组合或继承。
有理由的设计判断
前述论证提出一条依情境展开的考察路径,不是一套强制评分表。设计提案可以从一项具体变更开始,说明在当前结构下需要理解、修改、验证和交接什么。将拟议边界与保留当前安排作比较,随后可以揭示它是在使真实决策局部化,还是仅仅搬移复杂度。一项有可信根据的变更可能足以支持一条边界;大量假设性变更也可能一条都不能支持。
重要的推论关乎相关维护者承担的整体后果。修改文件更少,可能掩盖更难理解的核心抽象。增加模块,可能使不同职责能够被独立理解。特化实现可以简化使用者的工作,同时将复杂机制集中在具有足够专长的地方。这些可能性否定了对范式、层数或抽象密度的固定偏好。它们不使替代方案无法区分:已说明的任务和约束仍可明确支持其中一种。
验证可以结合保持行为的比较、相关变更的实际演练、依赖审查,以及维护者对设计工作方式的解释。这些是建议的取证方式,其负担取决于主张。通过范围有限的测试,只支持相应有限结果;对未来变更的清晰论证,仍以其前提为条件。不确定性可以成为推迟、小范围预留改动接口或直接实现的理由,而非采用通用框架。
撤除、迁移与持续责任
演进可以包括移除抽象。在本提案下,抽象不能凭创建成本或架构声望获得继续存在的资格。一个构造的例子是策略框架,其中原以为相互独立的变化已经收敛为一条稳定规则。内联这一规则,可以改善可理解性,使维护者不再需要协调过时的扩展点。相关比较既包括当下变得更简单的方面,也包括日后会变得更难修改的方面。
迁移提出了一个不同于“目标状态是否值得追求”的问题。更简单的表示,如果在迁往它的过程中丢失所需数据、破坏依赖它的使用方,或超出运行预算,就可能不是当下的好选择。逆转可能需要恢复数据和协调状态,而非仅仅回退代码。作为应用建议,分阶段替换可以在旧行为仍然可用时揭示这些后果;同时保留两个版本也有成本,不能预设它会永远更优。
交接对可维护性主张提出了具体挑战。未来的人与 agent 接手的是接口、前提、故障模式和工具,而非作者私有的理解。因此,本论证珍视可重新还原的理由与可用的修改路径。它不推断每一位贡献者都必须解释每一种内部机制。当受约束的生成器、专人负责和稳定组件等安排在选定生命周期内仍有可信根据时,维护者可以合理依赖它们。
在特定情境中,有充分理由拒绝演进优先原则或使其退让。短命产物可能没有有价值的未来扩展。重新打开已验证的边界可能成本很高。运行时约束、部署体积、互操作性或维护专长稀缺,可能使灵活设计威胁产品目的。预测也可能让想象中的未来用户优先于承担当下成本的人。这些是需要权衡的实质反对理由,不是未能理解架构价值的表现。
主要权衡是:为有可信根据的未来能力接受多少当下复杂度。这一偏好赋予这种能力一定分量,却没有证明它在每一种情形中都有价值。未解决的问题包括项目如何确立其生命周期、哪些人的维护负担应被计入,以及什么证据能让预测足够可信,从而指导昂贵的选择。本论证为考察这些问题提供理由;它既不给出通用阈值,也不证明已获采用。
条款对照
这份对照使提案的理由与修订条件可以检查。它解释五个领域单元,不向它们增加义务。
| 单元 | 问题、前提与支持 | 替代方案、反例与修订理由 |
|---|---|---|
| 4.1 目的、主体与生命周期 | 作者能够编辑,可能掩盖接手者无法维护。持续责任使工具、知识与交接同所声称的能力相关。 | 个人原型或一次性产物可能只需完成有限目的。实际维护责任或退役预期改变,可以成为重新审视范围的理由;仅凭标签不能确立这种变化。 |
| 4.2 有可信根据的演进优先 | 当某一变更方向有可辨识根据时,优化当下抽象的简单性,可能带来本可避免的后续成本。用户珍视这种未来能力,并愿为之承担一定当下复杂度。 | 当下简单性、推迟与小范围预留改动接口仍是替代方案。预测未实现,或灵活性威胁具体目标,都可能使某次投资失去理由。即便预测准确,这项价值偏好仍可受批评。 |
| 4.3 演进的含义 | 在同一方案内扩展,可以与高昂的退出成本并存。当撤除、删除与迁移具有相关性时,持续维护也包括这些变更。 | 刻意固定的范围可能足以服务有明确边界的产品。扩展便宜不证明替换便宜;相关义务或变更方向不同,可以成为重新审视判断的理由。不存在让所有变更都便宜的要求。 |
| 4.4 结构判断 | 接口形式与模式名称遗漏了行为、依赖及验证工作。整体工程后果将设计选择与所声称的能力联系起来。 | 直接代码、抽象、静态特化与运行时选择,均可能在不同约束下得到支持。签名兼容却破坏所需顺序的替换,是对仅凭形式提供保证的反例。契约改变需要相应的新判断。 |
| 4.5 修订与稳定 | 失败预测与沉没投入可能在理由减弱后仍维持机制。Core 的非终局性与根据支持重新审视理由,同时允许有根据的保留。 | 替代方案缺乏有支持的贡献,或妨碍目标达成时,保留设计可以有根据。没有相关收益的昂贵重写反驳了强制变更。根据的实质变化可以支持另一决定,而无需改写先前决定所依据的理由。 |