AI 现在能够编写大量软件代码,包括日益庞大和复杂的系统。这正在迅速改变软件的开发方式,并且已经影响到了业内许多人的职业生涯。它还引发了诸多问题:AI 编写复杂系统的可靠性如何,其风险如何管理,以及工作被取代的程度有多大。最重要的是,它提出了一个根本性问题:AI 编写软件的能力是否让软件工程变得不那么重要了?
答案在于区分编码与软件工程。软件开发远不止编写代码。至少,它涉及五项活动:理解需求、设计、编码、验证和支持。每项活动的质量都会影响后续活动。编码只是这个更大工程过程中的一部分。
本文聚焦于软件设计,特别是 AI 编程工具的使用如何改变其角色和重要性。软件设计是软件生命周期中的关键活动之一。假设用户需求已被正确收集,设计就成为系统成功的关键因素。AI 让代码生成变得更容易,但这使得软件设计更加重要,而非更不重要。对于大型软件系统,AI 在编码和支持方面的有效性将在很大程度上取决于设计的质量。随着团队越来越依赖 AI 编程工具,他们必须更加关注软件的设计方式。
为了理解为什么会发生这种情况,我们将简要讨论软件和设计的基本原理。我们将采用以利益相关者为中心的方法,并看看 AI 本身如何成为软件设计中的一个利益相关者。通过这些讨论,我们将看到软件设计的根本目标保持不变,但随着 AI 在软件开发中承担更大的角色,其重要性反而增加。
我们常把软件看作一种机器。像所有机器一样,它接收某些输入并提供输出。
但软件是一种特殊的机器。软件的输入和输出可以频繁变化。更重要的是,软件本身在持续运行的同时不断被修改。在一段时间内,一个软件系统可能经历如此多的修改,以至于它可能与其初始状态几乎没有相似之处。
任何复杂系统都可以用一个简单的公式来思考:
y = A(x)
其中 x 是所有输入的集合,y 是所有输出的集合,A 包含将 x 转换为 y 所进行的所有处理。
以飞机为例。它从传感器和通过控制装置接收许多输入。但这些并不是唯一的输入。环境因素如风和气压、高度、重力,甚至乘客的重量和移动,都是飞机的输入。输出是它对发动机参数所做的更改,以及最终速度、角度等的变化。我们可以想象 x 和 y 都是很大的集合,而 A 由成千上万个相互作用的部件组成。
我们的大脑拥有800亿个神经元以及近乎无限的输入和输出,也是复杂系统的一个很好的例子。
软件是人类创造的最复杂的系统之一。像ERP这样的系统,其输入和输出数量可能令人难以置信。但软件系统真正的挑战不仅在于其复杂性,还在于其弹性特质:它在整个生命周期中频繁经历修改,而且往往是在继续为用户服务的同时进行修改。
大脑是一个随时间变化的复杂系统的良好范例。神经元之间新的连接,以及现有连接强度的变化,使大脑能够学习新事物。学习可以意味着纳入新的输入、新的输出,或它们之间的新关系。
但大脑是一个独特的系统。几乎没有任何其他自然或人造系统的例子,能够在持续运行的同时,如此频繁地进行修改以适应其输入和输出的变化。这使软件进入了一个特殊的俱乐部:在其生命周期内被持续修改的复杂系统。
这一特征对软件设计有着重要的启示。设计者不仅是在设计今天存在的系统;设计者还在设计该系统明天如何被理解、修改和扩展。因此,设计不仅要考虑系统的当前需求,还要考虑系统在其整个生命周期中可能经历的变化。
设计是一种规划。更准确地说,设计是实现系统目标的计划。想象你正在规划一次旅行。计划将包括目的地、交通、住宿、景点、每日行程、所需证件以及许多其他事项。综合起来,这个计划可以称为一个设计。计划中的某些部分会比其他部分更具体。例如,机票和住宿可能会提前确定预订,而某一天的具体安排可能只是模糊地决定。
当你真正开始旅行时,设计就转化为执行。有些部分会按计划进行,而另一些则必须调整以适应实时情况。软件设计遵循类似的模式。由于其本质上是虚拟的,它可以比假期旅行描述得更加详细,但它仍然是一个指导执行的计划。
对软件设计更贴切的类比是布置房屋。如果我们想要一个舒适且功能齐全的家,就必须提前规划布置。我们首先要确定所需的土建工程、电线布线、管道和家具。有些组件可以专门定制,而另一些则可以从市场上购买。然后我们必须确保所有这些组件能够相互配合并作为一个整体运作。例如,一台洗衣机需要水、排水和电力连接,设计必须确保这些在正确的位置可用。设计也可以逐步细化。一旦我们决定在第二间卧室安装壁挂式折叠床,我们就可以深入设计床本身。
软件设计与这一过程有许多共同之处。设计者的首要工作是将系统逐步分解为越来越小的组件,直到每个组件都足够简单,易于理解和构建。这些组件之间的互连关系被确定下来,每个部分的技术方案也被决定。有些组件可能是专门为该系统构建的,而另一些可能是现成的并直接复用。
软件设计还包括规划的其他方面。它为系统未来的构建者制定了应遵循的编码指南。它规定了不同组件应如何交互以及系统应如何测试。这些指南有助于确保各个代码片段与整体设计保持一致。
需要记住的一个重要点是,软件设计不是一次性活动。虽然大部分设计在编码前阶段完成,但设计也发生在编码期间(编码中)和支持期间(编码后)。正如旅行的某些方面在执行期间才决定一样,一些设计决策是在软件构建或修改时做出的。这些决策在编码期间做出这一事实并不使它们成为编码的一部分;它们仍然是设计活动的一部分。例如,开发者可以根据设计指南在编码期间决定数据库表、函数和变量的名称。这类决策是设计活动的一部分,只是为了方便而委托给编码阶段。同样,随着新的用户需求出现,设计在支持阶段也会继续演进。
因此,软件设计并非只是在编码开始前创建的一份蓝图。它是一个不断演进的计划,用于实现复杂系统的目标,同时包含指导系统在其生命周期内如何构建、理解和变更的决策与意图。
我们已经讨论了假期旅行和室内装饰中的设计示例。当你去旅行时,你会仔细规划,以便享受旅程而不遇到不必要的麻烦。同样,你设计家中的陈设,以便为你和客人提供理想的氛围、舒适度和实用性。
任何设计最终都与其利益相关者的目标紧密相连。在旅行中,旅行成员是利益相关者,计划的设计旨在帮助他们实现目标。在家庭装饰中,你的家人和客人是利益相关者,设计旨在为他们提供所需的舒适、实用和体验。
软件设计遵循同样的原则。一个软件系统有多个利益相关者,每个都有不同的目标,好的设计必须帮助每个重要的利益相关者实现这些目标。利益相关者的目标决定了软件需要提供什么,而这又决定了设计需求。
列出软件系统的利益相关者将有助于我们理解这些目标。首先想到的利益相关者是:
系统的用户
系统的管理员
代码的所有者
代码的客户
这些是软件中较为明显的利益相关者。但还有两个不太明显的:
代码消费者
未来的开发者
软件的模块化特性产生了对代码消费者的需求。你编写的代码通常被其他开发者用来构建软件的“他们那部分”。这就像木工车间制作的一张壁挂床,被另一位木匠用来布置你的家。代码消费者可以是你自己团队的成员,也可以是你公司其他团队的成员。如果你的业务涉及创建软件库,他们也可以是客户。
代码消费者的目标是轻松、可靠地使用代码。这一目标转化为若干设计需求。软件设计师必须确保:
代码提供适量的控制。消费者应通过其接口提供的参数和选项,对代码的行为拥有适当的控制。
代码是健壮的。代码消费者可能会传入并非严格正确的值。设计良好的代码单元应优雅地处理这些情况,而不是简单地崩溃。
代码具有响应性且信息丰富。当代码未按预期工作时,应通过错误码、相关值、警告和其他反馈提供有用的信息。
代码有良好的文档。例如,如果你在设计一个 API(应用程序编程接口),其用途、工作原理、输入、输出和可能的错误都应被清晰记录,以便消费者无需不必要的挣扎即可使用。
未来的开发者是另一大类重要的利益相关者。他们将在软件系统的整个生命周期中维护、修改和支持该系统。软件系统生命周期中很大一部分时间用于维护和支持,这使得未来的开发者成为原始设计的重要利益相关者。
当未来的开发者对软件进行工作时,他们会修改它。一次修改通常经历以下阶段:
理解需求的变化
识别对代码的影响
更新设计和测试用例
进行修改
测试更新后的软件
这些活动让我们很好地了解软件应具备哪些品质才能更好地服务于未来的开发者。这些品质可以概括为“友好性”属性。其中一些描述如下:
导航友好:应当能够追踪从用户需求到最小代码组件的连接。例如,如果有一个单点登录(SSO)的需求,未来的开发者应当能够将该需求追踪到实现该功能的数据库表和模块。这使得理解系统和识别需求变化的影响变得更加容易。
修改友好:有许多设计原则属于这一类别。如果软件具有设计良好、接口简单的模块,那么设计和执行修改就会变得更加容易。如果常用的例程(如数据库操作)被分组为独立的、易于调用的单元,那么修改就更容易实现和管理。
隔离友好:变更的影响应限制在系统中尽可能小的部分。否则,一个根本性的变更(例如数据库表的变更)可能会波及整个系统。良好的设计在组件之间建立边界,从而将此类影响隔离。
因此,利益相关者、他们的目标与设计需求之间的关系,为理解软件设计的目的提供了一种有用的方式。本文的目的不是列出所有可能的利益相关者目标或设计原则,而是提供足够的示例来展示设计如何服务于软件系统的不同利益相关者。这一视角也将帮助我们理解为什么当 AI 成为软件的消费者和修改者时,需要将其视为软件设计中的利益相关者。
AI 在编码中的普及正在改变我们需在软件设计中考虑的利益相关者的性质。代码消费者和未来的开发者现在都可能是 AI 编码工具,而非人类开发者。我们可以称这些工具为 AI 软件开发工程师,或 AI-SDE,以对应人类开发者中流行的 SDE 称号。
作为代码消费者,AI-SDE 可能需要在复用现有模块和编写新模块之间做出选择。考虑一个简单的例子。假设它需要在数据库中存储一些值。它可以轻松生成执行此操作的代码,但软件设计可能指定了一个处理所有数据库操作的库。AI-SDE 必须发现这个库,选择正确的类和方法,理解接口,正确传递参数并处理返回值。
控制、信息和文档的设计目标现在变得尤为重要。人类开发者通常可以在文档不完整的情况下工作,因为他们可以依赖经验、与其他开发者的交流以及对系统的隐性知识。而 AI-SDE 则必须更加依赖明确可用的信息。因此,数据库库的架构、设计指南、接口和规范需要清晰且保持最新。
如果这些信息缺失或不正确,AI-SDE 可能会重新生成数据库操作的代码,而不是使用现有的库。如果文档或接口规范有误,它可能会生成同样错误的代码,需要人类 SDE 介入。因此,良好的设计有助于 AI 代码消费者判断哪些代码不应编写,因为所需的能力已经存在。
设计者必须更加关注 AI-SDE 作为系统未来开发者或修改者的角色。生成一个新函数相对容易,因为 AI 可以根据明确的需求进行工作并产出实现。修改一个现有的成熟系统则完全不同。在进行更改之前,AI-SDE 必须理解现有系统、评估更改的影响、识别涉及的组件、更新设计和测试用例、执行更改,最后验证系统是否继续正常工作。
这正是前面讨论的导航友好、修改友好和隔离友好特性变得重要的地方。AI-SDE 必须能够从需求导航到系统的相关部分,理解这些部分之间的关系,并确定更改可能传播的范围。设计良好的系统通过明确其架构、依赖关系和边界,使这一过程更加容易。
这也意味着设计不能仅仅是项目开始时创建的一份文档。随着软件的演进,设计必须随之演进。当 AI-SDE 提出重大更改时,相应的设计应该被更新,以继续准确地反映系统。人类 SDE 可以审查和批准这些更改,尤其是在影响重大的地方。测试计划也应更新,以便评估更改的影响。
正如我们所看到的,作为代码消费者和未来开发者的开发者——无论是人类还是 AI——都需要大量的设计支持。当代码遵循清晰的设计原则时,他们的工作效率会高得多。例如,一个试图评估更改影响的未来开发者,将从导航友好的代码库中受益匪浅。
但当系统的设计不充分时会发生什么?人类 SDE 仍然可以有效地工作,尽管可能需要更多的时间和精力。他们可以与其他团队成员交流、查阅文档,或利用自己的经验。SDE 联系已经离开组织的前团队成员询问代码库相关问题的情况并不少见。人类 SDE 还可以利用他们对组织实践的了解来填补缺失的部分。
AI-SDE 则不同。当设计不能充分支持其任务时,它的效率会大打折扣。更重要的是,它弥补缺失信息的能力有限。AI-SDE 可能在不完全理解系统或现有设计意图的情况下尝试完成任务。在缺乏强设计的情况下,它可能因此生成不准确、低效或不一致的代码。
这可能会产生循环效应。如果 AI-SDE 引入了理解不足或设计不佳的代码,下一个在该代码库上工作的 AI-SDE 将更难理解它。每次修改都可能增加进一步的复杂性,使系统逐渐变得更难以导航和修改。这个问题会随着时间的推移而累积。
因此,当 AI 参与开发时,软件设计的良好基础变得更加重要。良好的设计不仅帮助 AI 编写更好的代码,还有助于确保每一次后续的 AI 生成更改都使软件更容易——而非更难——理解和演进。
虽然当开发者是 AI 时,软件设计的基本原则不会发生剧烈变化,但在设计应如何表达和维护方面存在重要差异。对人类开发者行之有效的设计,可能无法为 AI-SDE 提供同等程度的支持。
只有当设计以显式方式可供 AI-SDE 使用时,它才能从中受益。团队中每个人都知道的隐性准则对 AI 几乎没有用处。例如,一个团队可能知道某个特定库应始终用于数据库访问,或者某个特定组件绝不应被直接修改。如果这些规则仅作为团队成员之间的共享知识存在,AI-SDE 可能并不知道它们。
在编码前阶段,当架构、组件、接口和准则正在被记录时,让设计显式化相对容易。在编码和编码后支持阶段,当设计决策频繁地以增量方式做出时,这就变得更难。这使得随着软件演进保持设计更新尤为重要。
创建和修改代码的同一个 AI 也可以帮助创建和维护设计。AI 可以将记录的讨论、手写笔记和粗略的文字转换为结构化的设计文档。它可以分析现有代码,并识别可能未被显式记录的设计模式、约定和准则。
这创造了一个机会,使设计维护的负担大大减轻。团队不必期望开发者手动记录每一个设计决策,而是可以使用 AI 从讨论、代码和其他开发制品中持续提取和更新设计。目标应是确保所记录的设计始终是对实际系统的有用且最新的表示。
将 AI-SDE 视为编译器之上的一层。正如代码包含给编译器的指令一样,设计可以包含给 AI-SDE 的指令。这改变了一些传统设计决策的经济性。
许多软件设计技术的部分开发目的是减少开发者必须编写或维护的代码量。借助 AI,生成代码的成本可能非常低。这意味着一些主要目的是节省开发者精力的技术可能变得不那么有价值。
考虑面向对象编程和类层次结构。开发者创建类层次结构的部分原因可能是避免重复编写相似代码。借助 AI-SDE,有时更简单的做法可能是描述所需行为并让 AI 生成实现,即使这会导致一些重复。例如,与其设计一个复杂的层次结构来最大化代码复用,设计可以指示 AI 根据所需行为生成一个专用类。
因此,对于设计者而言,重要的问题从“我怎样才能最小化必须编写的代码量?”转变为“当 AI 能够生成实现时,什么样的设计能产生最好的软件?”
如果你负责代码库的质量、软件交付的效率,或者更广泛地负责为软件系统的利益相关者服务,那么现在是时候重新审视你的设计了。随着 AI-SDE 成为软件开发的一部分,值得问一个简单的问题:你的软件设计是否已为 AI 做好准备?
一个好的起点是检查设计是否存在、是否是最新的,以及是否足够完整以指导开发。AI-SDE 能否发现架构、理解接口和依赖关系、识别约束和约定,并将需求追溯到代码和测试?如果重要的设计决策仅作为团队内部的隐性知识存在,那么该软件尚未为 AI 做好准备。
如果你的工程团队正在使用 AI-SDE 在没有经过批准且最新的设计的情况下生成或修改代码,那么可能是时候介入并建立一个设计了。设计不应成为 AI 辅助开发的瓶颈;利用 AI 来创建、记录和更新设计本身,可以使这一过程变得更加可控。
对于技术领导者而言,对软件设计进行 AI 就绪度评估,因此可以成为大规模采用 AI 编码的一个有用起点。
随着 AI 编码工具使用的增长,AI-SDE 将越来越多地同时成为软件的消费者和修改者。这改变了对软件设计的要求。设计必须是明确的、最新的,并且足够详细,以便 AI-SDE 能够理解架构、发现并复用现有组件、安全地进行更改,并验证其影响。
软件设计的基本原则在 AI 时代并没有消失。但当代码可以廉价且快速地生成时,良好的设计变得更加重要。它提供了结构、意图和约束,防止编码速度的提升转变为复杂性的增加。
对于一个由 AI 赋能的软件团队来说,软件设计的质量将越来越决定 AI 能在多大程度上有效地为编码做出贡献。对于技术领导者而言,随着 AI 成为软件开发中不可或缺的一部分,构建这一设计基础应当成为一个关键优先事项。
转载声明:本文为转载整理,原文版权归原作者及原站点所有。
原文来源:Towards Data Science
原创地址:https://towardsdatascience.com/software-design-in-the-age-of-ai/
原文作者:Devesh Rajadhyax;如有侵权,请联系管理员删除。