AI-驱动的开发者(MEAP)(一)

33 阅读1小时+

AI 驱动的开发者(MEAP)(一)

原文:zh.annas-archive.org/md5/349aae26397953b4cb6aac827cad2f26

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:了解大型语言模型

本章涵盖

  • 介绍生成式人工智能(特别是大型语言模型)

  • 生成式人工智能的历史

  • 探索生成式人工智能的好处

  • 确定何时使用生成式人工智能以及何时不使用

无论你是否意识到这一点,无论你是否愿意承认这一点,你都悄悄地得到了晋升。事实上,每个专业软件工程师都得到了晋升。几乎一夜之间,我们从员工工程师变成了工程经理。你现在拥有世界上最聪明、最有才华的初级开发人员作为你的团队成员。指导、辅导和执行代码审查应该成为你日常工作的一部分。你现在有了生成式人工智能作为你的新编程伙伴。本章将为您提供生成式人工智能的一个子集的概述,称为大型语言模型(LLM),具体包括 Chat GPT、GitHub Copilot 和 AWS CodeWhisperer。

注意

本书将不是一本传统的编程书籍。你不能像使用脚本一样使用它。你将与大型语言模型进行对话,就像与任何对话一样,词语和方向会根据模型和之前的语境而改变。你收到的输出很可能会与本书中打印的内容不同。这不应该让你感到沮丧。相反,你应该探索。旅程与目的地同样重要。你可能会发现自己感到沮丧,因为它们无法跟上。请耐心等待。如果你有纪律(并且有些冒险精神),你可以让 GPT 配合本书的一般主题和目的:学习如何使用生成式人工智能使你成为一名更好的程序员。

1.1 大型语言模型简介

生成式人工智能,特别是大型语言模型(LLMs),正在彻底改变我们思考和开发软件的方式。我们不再设计和编码模块、组件和测试,而是描述我们希望这些人工智能构建的软件,它们将为我们生成这些工作的主体。这是软件工程领域的一个自然趋势:我们的编译器变得更智能(Rust 的编译器是一个很好的例子,它消除了一个整个类别的错误),我们的工具变得更智能(源代码中的 IntelliSense),我们的编程语言变得更具表现力和生产力。虽然这可能使得这些生成式人工智能看起来更像是进化,而不是革命性的;但从某种意义上说,它们既是进化的,也是革命性的。

本书将审视、比较和对比三种大型语言模型:GitHub 的 Copilot、OpenAI 的 ChatGPT 和亚马逊的 CodeWhisperer。后者将获得较少的关注,因为它在很大程度上类似于 Copilot,但更实用,更专注于 AWS 生态系统内的开发。

GitHub Copilot 和 ChatGPT 都在幕后使用由 OpenAI 创建的 GPT-4 Codex 模型。微软从 OpenAI 许可了这个软件,使用了 GitHub 的公共存储库中的源代码(微软拥有)。GitHub 创建了一个服务,它将会接收你在集成开发环境(IDE)中提供的上下文,比如 Visual Studio Code 或 IntelliJ,并将该上下文发送到 GitHub Copilot 服务。该服务将使用 OpenAI Codex 根据你在文件中的注释和代码提供的上下文生成最多十种可能的解决方案。Codex 尝试将这个上下文与它在训练数据语料库中找到的示例进行匹配。这些代码解决方案将被返回到你的集成开发环境,供你选择。你需要审查所有的代码建议,并接受最接近你意图的一个。你在这里提供的监督非常重要:解决方案过时或不准确并不罕见。如果接受了某个解决方案,那么你的“接受”解决方案会被发送回 GitHub Copilot 服务,以进一步增强模型和建议。

微软正在通过 Copilot 大力押注这项技术。谷歌也在通过 Bard 押注。很容易理解为什么。

本书教会你如何管理 GitHub Copilot 等生成式人工智能。琐碎的示例将让位给令你惊叹的难以置信复杂的示例。本书假定你对使用生成式人工智能了解不多。你可能听说生成式人工智能已经有一段时间了。你可能认为这个概念令人兴奋,值得随时研究生成式人工智能。那么,现在是最好的时机。本书将带你了解基础知识,从在集成开发环境中设置到使用它来将你的生产率、输出和希望的编码乐趣提高 10 倍。

本书将展示一个接一个的示例,说明哪种生成式人工智能更适合特定任务。这将帮助你建立直觉,了解何时应该使用其中一种,以及何时应该避免它们。让我们简要了解一下每种生成式人工智能的核心优势。

ChatGPT 擅长生成模仿人类语言和书面语言的回复。因此,它非常擅长文档编写和代码注释。由于它可以处理自然语言(NLP),它还可以执行反向操作:总结文本并捕获情绪。你还可以使用它来改善这些领域:让它重写或重新表达文案。

ChatGPT 可以生成代码片段、函数、应用程序和整个聊天机器人。此外,你可以使用它来自动生成测试。我们将在后续章节中完成所有这些任务。

GitHub Copilot(以及 CodeWhisperer)为开发人员提供了以下好处:它们帮助完成代码、修复错误和重构。它们驻留在开发人员的集成开发环境(IDE)中,可以帮助保持开发人员对手头任务的关注。这将使开发人员在输出方面更加高效(单位时间内的代码行数),但它们也可以自动化重复的任务。鉴于 Copilot 的训练数据是通过整理公共代码库获得的,开发人员将有建议来提高整体代码质量。

Copilot 还可以帮助更好地理解外部代码库。它将提供关于如何浏览该代码库的建议,因为它可以帮助我们更好地理解类和代码之间的关系。

当你使用这些工具时,你会注意到随着你更好地了解你的新编程伙伴的能力和局限性,你的速度会发生巨大的变化。由于它可以“记住”你的编程风格和方法,你的编程伙伴也会变得更加擅长与你合作。与生成式 AI 一起工作将使你能够解决更加复杂的问题。你将编写更好、更干净的代码,bug 更少。所有这些都是以比你想象的更快的速度前进。听起来像是一个梦,或者更糟糕,是空洞的话?它不是。

有人可能会问自己这不就是 IntelliSense 的一个更好的版本吗?在看过前几个例子之后,你可能会问自己这个问题;然而,在下一章结束时,使用生成式 AI,你将能够欣赏到其中的区别。

在图 1.1 中,你会看到 Microsoft Visual Studio Code 提供了 IntelliSense 的自动完成建议来启动 Flask 应用程序。请注意,这是内联的,建议是在我编辑代码时提供的。

图 1.1 IntelliSense 自动完成了 Flask 的运行方法。

图形用户界面,文本,应用程序自动生成描述

图 1.2 显示了 GitHub Copilot 根据方法名称和签名提出了相同的建议。也就是说,在我开始编写实现之前,它就已经写好了代码。生成式 AI 背后的激动在于这种力量:它的预测性质。随着提示变得更加明确,建议变得更加精确。我们将在后面的章节中进一步探讨这一点。

图 1.2 GitHub Copilot 提供了运行 Flask 应用程序的解决方案。

图形用户界面,文本,应用程序自动生成描述

图 1.2 提供了一个简单的示例,并没有提出为什么要使用生成式 AI 的充分理由。然而,在同一个 Flask 应用程序中,如果你需要创建一个能够处理来自 POST 方法的输入的端点,但忘记了语法怎么办?我们需要打开官方文档然后尝试找到如何做吗?不,我们可以直接问 GitHub Copilot。

图 1.3 GitHub Copilot 生成 POST 方法处理程序

图形用户界面、文本、应用、聊天或文本消息自动生成的描述

您可以看到 Copilot 提供了几个类似的建议来完成这段代码。声明该方法会使第一个建议在我们的 IDE 中自动完成。不需要停下来使用鼠标。这种方法可以让您在不必要的干扰下更长时间地保持在代码和流状态中。现在,如果 Copilot 能给我们带杯咖啡就好了……

1.2 生成式人工智能的历史

值得快速了解一下我们在接下来的几章中研究的技术的起源。

生成式人工智能是人工智能的一个子集。人工智能已经存在并且积极研究了六十多年。逻辑理论家被认为是人工智能的第一个应用,早于“人工智能”这个术语的出现。逻辑理论家是赫伯特·西蒙和艾伦·纽厄尔的杰作,克利夫·肖也做出了一些贡献。西蒙和纽厄尔试图教会计算机思考。

尽管这次尝试没有产生真正思考的机器,但逻辑理论家能够产生比当时的数学家阿尔弗雷德·北·怀特海德和伯特兰德·罗素更好、更详细的数学证明。对逻辑理论家将对数学领域产生什么影响的猜测和推测,与我们今天在新闻中看到的关于 ChatGPT 的情况相呼应。

直到 1956 年,兰德公司举办了达特茅斯夏季人工智能研究项目,人工智能这个词才出现。在此次会议上,著名研究员约翰·麦卡锡(Lisp 的原始作者)和计算机科学家首次提出了“人工智能”这个术语。

对人工智能的研究持续了几十年,随着公众对其兴趣的起伏不定而逐渐改进。加里·卡斯帕罗夫于 1997 年被 IBM 的深蓝战胜了。肯·詹宁斯于 2011 年被 IBM 的沃森在“危险边缘!”中打败。几年后,2014 年,生成式对抗网络(GANs)被发明。GANs 的出现引发了人工智能的新一轮兴趣,因为它能够创建逼真的图像和所谓的深度伪造。

2017 年,一种新型的深度学习模型被发明了:transformer。这为大型语言模型(LLM)奠定了基础:例如 GPT-3。而 GANs 能够从噪声图生成图像,LLMs 能够创建文本和软件。在此期间,OpenAI 公司——ChatGPT 的管理者——成立了。

但是 OpenAI 是为什么成立的呢?他们的目标是什么?让我们问问 ChatGPT。

Q: openai 的历史是什么?

A:OpenAI 是一家研究公司,于 2015 年 12 月由埃隆·马斯克、萨姆·奥尔特曼、格雷格·布罗克曼、伊利亚·苏茨克维尔、沃伊切赫·扎伦巴和约翰·舒尔曼创立。该公司的使命是以惠及全人类为目标开发和推广友好型人工智能。OpenAI 最初专注于开发和推广游戏和机器人技术领域的人工智能,但后来扩大了业务范围,包括了更广泛的行业。2019 年,OpenAI 发布了 GPT-2,这是一个强大的语言处理人工智能模型,2020 年发布了 GPT-3,已被广泛应用于自然语言处理任务,并成为迄今为止最大的人工智能模型之一。2023 年,我们看到了 GPT-4 的发布,这是迄今为止最先进的模型。它具有连接互联网等其他巨大改进的能力。OpenAI 还参与了人工智能安全、机器人技术和机器学习等多个研究项目,并与多个组织和公司建立了合作伙伴关系,推动人工智能研究和发展。

这段文字中一个重要的细节是,有一些公司正在积极致力于使人工智能系统更易于使用。考虑到这种趋势不太可能结束(而且未来可能只会加剧),提前入局不是很有意义吗?这将让你保持在技术发展的前沿。这将使你作为一个受 AI 辅助的开发者的技能变得更加有价值。

1.3 理解生成型人工智能

生成型人工智能是人工智能的一个子集。它们在大型数据集上进行训练,以学习数据的模式和结构。训练完成后,它们将使用这些数据生成类似于训练数据集的新数据,就新数据的结构而言。这就是生成型人工智能名称中的生成部分。

有三种显著和备受瞩目的生成型人工智能:生成对抗网络(GANs)、变分自动编码器(VAEs)和基于变换器的语言模型。ChatGPT 和 OpenAI 的 Codex 就是后者的例子。我们将简要介绍每种类型的生成型人工智能的功能。

图 1.4 生成型人工智能主要有三种类型:生成模型、生成对抗网络和基于变换器的模型。

自动生成的图表描述

所有这三种生成型人工智能都利用神经网络来创建输出;无论是文本还是代码生成或图像。神经网络是模仿人类思维方式的,因为神经元之间传递信号。你可以将其视为一个有向图,其中超过一定阈值的数据被传递到图中的下一个节点。

数据编码输入层,称为外层。输出层连接到隐藏层。在隐藏层后面是其他的,通过这些隐藏层必须经过许多隐藏层的数据。神经网络中的所有节点都通过计算得到的数值连接,表示神经元之间连接的强度,使用反向传播(在图 1.5 中表示为线条)进行,具有必须超过阈值才能激活下一层的阈值。如果数据到达输出层,那么数据将从网络返回。但是,不能保证数据将被返回。数据可能会被过滤掉。

图 1.5 可视化神经网络。一个非常微小的网络。外部节点是暴露的,以便它们可以接受输入。当遍历隐藏节点时,这些输入要么被丢弃,要么转发到下一个节点。如果输入到达输出节点,则会返回。

自动生成的图表描述

生成式人工智能使用非常大的数据集来训练这些模型。在 GitHub Copilot 的情况下,这个大型数据集是 GitHub 内部公开可访问的存储库的内容。如果你曾经为开源项目做过贡献,那么你可能有 Copilot 训练过的代码。

尽管许多生成式人工智能使用神经网络,但它们的使用方式决定了人工智能的类型。生成对抗网络(GAN)使用两个神经网络:一个称为生成器,一个称为判别器。生成器网络根据训练数据集生成假数据。判别器试图识别假数据。这些网络具有对抗性,因为生成器试图创建与真实数据无法区分的数据,而判别器试图判断数据是真实的还是假的。

变分自动编码器(VAEs)也使用两个网络:一个用于编码,一个用于解码。在某种意义上,编码网络通过将数据简化为低维表示来简化输入。然后,解码网络将这个低维表示映射回原始数据空间。这样做的全部意义在于能够通过抽样生成新的数据。

最后一种类型是基于变换器的模型。变换器模型是一种使用自注意机制处理顺序数据(例如自然语言文本)的前馈神经网络。在训练期间,网络的权重会调整以最小化损失函数,例如交叉熵。

在前馈神经网络中,输入沿着一个方向流动,从输入层到输出层,层与层之间没有反馈连接。此外,没有信息或错误信号从输出返回到输入层。因此,神经网络的输出仅由输入数据和层之间分配的连接权重确定。

自注意机制允许网络基于与当前输出相关性选择性地关注输入序列的不同部分。在 transformer 中,输入序列首先通过嵌入层嵌入到向量空间中。然后将嵌入的输入序列送入编码器网络,该网络由多个前馈神经网络层组成。每个编码器层应用自注意机制来捕捉输入序列的不同部分之间的关系。

自注意机制根据每个输入序列部分与其它序列部分的关系计算一个注意力分数。这些分数用于权重化每个序列部分对编码器网络最终输出的贡献。这使得编码器网络能够有选择性地关注输入序列中最重要的部分,而忽略不相关的部分。

编码器网络的输出被送入一个解码器网络,这个解码器同样由多层前馈神经网络组成。解码器使用自注意机制基于输入序列逐个词生成输出序列。

在 transformer 网络中,编码器和解码器的关系可以用编译器和链接器的类比来理解。就像编译器把高层代码变成低层指令,链接器把这些指令组合成一个可执行程序一样,编码器网络把输入序列分解成有意义的单位,解码器则把这些单位组合成最终的输出序列。transformer 中使用的自注意机制类似于编译器和链接器优化代码性能的方式。

正如之前所说的,很多生成 AI 使用神经网络,但并非所有的都是如此。有些是基于规则的,通过将规则应用于输入来生成输出。仍有其他人是演化的性质,迭代结果,并根据适合度进行选择。

现在我们可以通过一个描述性的例子来演示你如何与 Copilot 交互。当你开始在你最喜欢的 IDE 中(例如本例中的 VS Code)键入时,Copilot 插件将把你的注释或代码(有时候仅需要一个函数名!)发送到 Copilot 服务中。

这个服务可以把你的代码行或注释转换为自然语言提示,然后运行在 OpenAI 的 Codex 模型上。该模型基于训练数据集生成建议。GitHub 称之为代码合成。他们声称这个训练数据集包含来自数十种编程语言的数十亿行代码。

Codex 模型返回的前十个解决方案将会被 Copilot 服务返回给你的编辑器。你可以选择最符合你意图或需求的建议,或者拒绝所有的建议。你的选择将会被返回给 Copilot 服务以更好地训练模型。

图 1.6 你的代码被 Copilot 插件采样和编码。然后将其发送到 OpenAI Codex 模型,生成建议。这些建议然后返回到你的 VS Code 会话中。

GitHub 不断改进他们的 Copilot 服务。最近发布(截至 2022 年 12 月),他们自豪地宣称跨所有编程语言的接受率为 46%,而特别是对于 Java,平均为 61%。[^([1])](v-5.html)

1.4 何时使用和何时避免生成式人工智能

这些技术引起了人们的兴奋。自 2022 年 11 月底公开发布以来,有数百(可能数千)篇关于 ChatGPT 各个方面的文章。它会破坏教育吗?还需要教育吗?软件工程师还有必要吗?

抱持悲观情绪是很容易的。有很多未知数,这项技术的全部影响尚未被揭示。然而,当你阅读本书时,你应该形成自己的观点。我希望你能看到生成式人工智能的优点,并将其用于正面方面。你将用它来成长为一名程序员。随着你的使用,你会成为一名更好的程序员。

作为开发者成长的最佳途径之一就是阅读优秀的代码。OpenAI 已经为你筛选出了地球上最优秀的代码;它们都近在咫尺。你现在也可以轻松获得一些最糟糕的代码。你可以从好的示例中学习,也可以从坏的示例中学习。能够分辨出其中的区别是成长的关键。

那么,什么时候应该使用生成式人工智能呢?尽可能地利用每一个机会吧!(我们将讨论一些使用例外情况。)与生成式人工智能互动是非常有趣的。你将学会如何更好地使用它们,找到捷径,发现新功能,并且每一刻都会感到心花怒放,就像一个充满好奇心的孩子一样。

尽管在日常工作中使用生成式人工智能似乎是很有道理的(因为它确实如此),但应该注意的是,并非所有情况下都适用。

如果你被要求完成一份带回家的编码考试,你应该使用生成式人工智能来完成这个考试吗?除非明确说明可以使用,否则应该避免。如果测试者没有预料到你会使用它,那么这可能被视为作弊。

他们是否应该预计到你会使用它们?是的,在这一点上他们可能应该。此外,有人可能会认为,考试的目的是评估潜在候选人的编码能力,潜在雇主应该尽力构建真实世界的条件,以最好地评估一个人的能力。这应该包括所有在一天中进行课程的工具和资源。这些工具将包括生成式人工智能。

特别注意不要在学术环境中使用它们。适当的归属涉及到很多复杂的问题,至少如此。生成式人工智能在灵感和抄袭之间的界限非常微妙。如果你的使用被认定为抄袭,后果将是严重且永久的:开除,并可能被其他机构拒绝未来入学。务必小心谨慎。

总的来说,要运用你更好的判断力。如果你有可能触犯任何版权法或行政政策,那么除非你被授予明确的许可,否则不要使用它。最好与工作中的企业 IT 或信息安全团队交流一下,确保你遵守与企业系统和计算机相关的企业政策。

最后需要注意的是:生成式人工智能是工具,像任何工具一样,你需要对自己的操作有一定的了解。你应该对正确答案有一定的了解。你应该在你对领域有一定了解的情况下使用它们。在这样的情况下,你会发现自己在探索领域,加深对领域的理解,以及学习速度更快。

现在我们已经简要探讨了生成式人工智能的历史,看到了一些生成式人工智能的用例,并应用了一些重要的防范措施,我们将在下一章进行实际操作,探讨如何使用这三种生成式工具开始同样的项目。

1.5 总结

  • 生成式人工智能既是进化的也是革命性的。从进化的角度来看,它们只是开发人员每天使用的工具之一。从革命性的角度来看,它们将改变我们的工作方式。事实上,它们将改变我们的工作。

  • 开发的未来将是生成式人工智能的管理。即使是传说中的 10x 开发者也不会像拥有人工智能伙伴的开发者一样高效;一个由人工智能驱动的开发者将以比不使用人工智能更高的质量和更快的速度、更低的成本产生代码。我们将花更多的时间训练我们的人工智能伙伴,让它按我们想要的方式进行操作,然后我们将不再像以前那样编写代码。

  • 尽管有很多生成式人工智能在世界上,我们将探讨其中三种最受欢迎的:

    • ChatGPT – 自 2022 年 11 月以来一直成为头条新闻。

    • GitHub Copilot – 在集成开发环境中使用最广泛的生成式人工智能。由微软提供资金支持和推广。

    • 亚马逊网络服务 CodeWhisperer – 一款类似 Copilot 的产品,由亚马逊支持。

  • 一些全球最大的公司正在大力投资生成式人工智能(微软、亚马逊、Alphabet)并使其更易于使用(Open AI)。

  • 生成式人工智能利用极其复杂的神经网络,类似于我们的神经网络,将输入过滤和映射到以前未见过的新输出。

  • 在使用生成式人工智能进行学校作业之前,你应该先咨询你的教授或老师。


[^([1])](v-5.html) 赵舒音。“GitHub Copilot 现在拥有更好的 AI 模型和新功能。” GitHub 博客. github.blog/2023-02-14-github-copilot-now-has-a-better-ai-model-and-new-capabilities/。上次访问日期:2023 年 2 月 14 日。

第二章:从生成式人工智能入门

本章涵盖

  • 使用 ChatGPT 进行交流

  • 学习使用 Copilot 的基础知识

  • 学习使用 CodeWhisperer 的基础知识

  • 对比这三个生成式人工智能工具之间的差异

在本章中,我们将深入研究使用三个重要的生成式人工智能工具:ChatGPT、GitHub Copilot 和 AWS CodeWhisperer。本章的主要重点将是利用这些人工智能工具开发一个模型信息技术资产管理(ITAM)系统,特别关注硬件管理组件。

ITAM 系统,无论是商业的还是开源的,都提供了一系列专门用于组织软件和硬件管理的功能。通过仅限制我们的项目范围仅限于硬件管理组件,我们旨在突出显示我们如何与每个生成式人工智能工具互动,比较它们生成的代码质量,并对比它们的独特特点和局限性。

表面上看,ITAM 系统似乎相对简单,主要关注资产管理、标记和跟踪。然而,正如我们将要揭示的那样,在这些看似简单的要求中隐藏着一层重要的复杂性。这种固有的复杂性使我们选择的项目成为利用生成式人工智能来导航问题空间的理想候选。

所以,让我们踏上这个迷人的建设和理解之旅。我们将从概述我们打算构建的系统以及这些非凡的人工智能工具在这一过程中将扮演的角色开始。

2.1 介绍我们的项目,信息技术资产管理系统

信息技术资产管理(ITAM)系统是一种管理和跟踪硬件设备、软件许可证和其他 IT 相关组件的工具,贯穿它们的生命周期。ITAM 系统通常包括硬件和软件清单工具、许可证管理软件和其他相关软件应用程序。该系统还可能涉及使用 QR 码、条形码或其他物理资产管理技术对 IT 资产进行手动跟踪和物理跟踪。

一般来说,ITAM 系统将拥有一个集中式数据库,该数据库存储与资产类型特定的资产标识符和属性。例如,您可能会为台式电脑存储设备类型、型号、操作系统和已安装的应用程序。对于软件,您可能会保存应用程序的名称、供应商、可用许可证数量以及已安装该软件的计算机。后者确保您的组织符合所有许可证限制。通过监控使用情况,您不应超出已购买的许可证数量。

ITAM 系统还赋予了控制成本的能力。由于您始终知道可用的软件和硬件,因此不应该进行任何不必要的购买。这些系统集中了购买,这有助于批量采购。未使用的硬件可以出售。工作负载未充分利用的硬件可以合并其工作负载。此外,正如我们将看到的,我们可以使用此购买日期信息来计算硬件的折旧价值,并将该价值应用于您的组织税收。

我们将探索 ITAM 系统的更多特性,构建一个迷你版本,专注于硬件管理。我们将从 ChatGPT 开始,使用 Python 构建第一个组件。然后,我们将使用 Copilot 和 AWS CodeWhisperer 复制此练习。通过使用每个这些生成 AI 工具重复此过程,我们可以对比差异并将相似之处联系起来。

首先,从 ChatGPT 开始,我们将专注于构建 Asset 类。接下来,我们将开发一种通过使用 FastAPI 库的表征状态转移(REST)控制器来呈现它的方法。最后,我们将在随后的章节中构建这个,添加功能,扩展设计并添加文档。

为什么选择 Python?

在 2023 年,一个人在应用开发中使用 Python 3 的决定不应该需要进行辩解。然而,鉴于主题的关系,对于选择 Python 作为本书的语言是有意义的值得列举一下。首先,Python 是一个用于原型设计项目的优秀语言。数据科学家和机器学习专家经常使用它。其次,它是表达性和生产性的。几行代码就可以完成大量工作,尤其是考虑到标准库的大小,更不用说像 pandas 和 numpy 这样的常用库了。第三,它易于学习和使用。它的语法侧重于(并强制执行)可读性。最后,它得到了所有生成 AI 平台的支持。

2.2 进入 ChatGPT 的探索

上下文是与 ChatGPT 一起工作的最重要的方面之一。你之前的prompt可以极大地改变你当前提示的结果。在像 ChatGPT 这样的语言模型的背景下,提示是指提供给模型的输入,用于生成响应。它可以是一个句子,一个段落,甚至是一段更长的文本。它充当模型的指令或查询,引导其响应。鉴于提示的质量构成了模型响应的上下文,始终注意当前会话中发布了哪些提示是很重要的。因此,建议每次开始新项目时都从新会话开始。附录 A 将指导您设置帐户,登录 ChatGPT,并编写您的第一个提示。

我们将从简单的提示开始:“请使用 Python 为 ITAM 编写一个 Asset 类。” 值得重申的是,当使用大型语言模型时,相同的输入不会总是产生相同的输出。因此,您的输出可能与以下内容不完全匹配;但希望它至少近似。

列表 2.1 ChatGPT 生成的用于 ITAM 的 Asset 类
class Asset:
    def __init__(self, asset_id, asset_type, manufacturer, model, purchase_date, warranty_end_date, location):
        self.asset_id = asset_id
        self.asset_type = asset_type
        self.manufacturer = manufacturer
        self.model = model
        self.purchase_date = purchase_date
        self.warranty_end_date = warranty_end_date
        self.location = location
        self.assigned_to = None
        self.status = 'Available'

    def assign_to_user(self, user):
        self.assigned_to = user
        self.status = 'Assigned'

    def unassign(self):
        self.assigned_to = None
        self.status = 'Available'

    def get_details(self):
        details = f "Asset ID: {self.asset_id}\n"
        details += f"Type: {self.asset_type}\n"
        details += f"Manufacturer: {self.manufacturer}\n"
        details += f"Model: {self.model}\n"
        details += f "Purchase Date: {self.purchase_date}\n"
        details += f "Warranty End Date: {self.warranty_end_date}\n"
        details += f"Location: {self.location}\n"
        details += f "Assigned To: {self.assigned_to}\n"
        details += f"Status: {self.status}\n"
        return details

除了代码之外,ChatGPT 还将返回此类的文档。

图 2.1 ChatGPT 枚举了 Asset 类中的属性以及类的方法

自动生成的文本描述

为了跟踪我们的资产,我们需要一种与它们交互的方式。API 是与系统中的对象交互的一种绝佳方式。REST 是在微服务架构中公开 API 功能的一种方法。因此,我们将要求 ChatGPT 为该类创建一个 REST 控制器,并使用以下提示:“请为此类创建一个 REST 控制器。” ChatGPT 生成的简化代码在列表 2.2 中。

列表 2.2 当要求 ChatGPT 为项目添加 REST 控制器时,ChatGPT 生成的 REST 控制器
from flask import Flask, request, jsonify
from asset import Asset, AssetManager

app = Flask(__name__)
asset_manager = AssetManager()

# Get all assets
@app.route('/assets', methods=['GET'])
def get_assets():
    assets = [asset.__dict__ for asset in asset_manager.assets]
    return jsonify({'assets': assets})

# Get an asset by ID
@app.route('/assets/<string:asset_id>', methods=['GET'])
def get_asset(asset_id):
    asset = asset_manager.get_asset_by_id(asset_id)
    if asset:
        return jsonify(asset.__dict__)
    else:
        return jsonify({'error': 'Asset not found'})

if __name__ == '__main__':
   app.run(debug=True)

ChatGPT 做出了一些有趣的假设。首先,它使用 Flask 构建 REST 控制器。虽然 Flask 是一种流行的基于 Python 的 Web 框架,绝不是一个坏决定,但我们很想知道为什么选择它。因此,我们可以问 ChatGPT 为什么选择 Flask。使用以下提示:“为什么在没有提供要使用的框架的情况下,你选择了 Flask ?” 使用大型语言模型的乐趣之一是试图剥开谜题的层,看看它是如何工作的。

图 2.2 ChatGPT 在没有提供要使用的框架时解释了使用 Flask 的理由。它指出 Flask 轻量级且易于使用。

自动生成的文本,字母描述

尽管这个理由很好,但它并不是我们想要使用的框架。不久之后,我们将让 ChatGPT 切换应用程序以使用 FastAPI。不过,我们应该确保我们刚刚“编写”的代码能够运行。然而,在我们的项目中缺少一些依赖项。我们可以要求 ChatGPT 为我们创建一个 requirements.txt 文件。

在输入框中输入以下提示:“请为此项目创建一个 requirements.txt 文件。” ChatGPT 应该会返回类似以下代码片段的内容。

列表 2.3 具有 Flask 支持的 Asset 管理项目的 requiremnts.txt 文件
click==8.0.3
Flask==2.1.1
importlib-metadata==4.8.1
itsdangerous==2.0.1
Jinja2==3.0.2
MarkupSafe==2.0.1
typing-extensions==3.10.0.2
Werkzeug==2.0.1
zipp==3.6.0

一旦我们创建并填充了初始的 app.py 文件(ChatGPT 创建的初始 Python 代码)以及我们刚刚生成的 requirements.txt 文件,我们会注意到 ChatGPT 做出的第二个假设:AssetManager 类。ChatGPT 默默地生成了这段代码,而没有给我们任何线索。这个隐藏的类,我们没有源代码,意味着我们的项目无法运行。

幸运的是,我们可以要求 ChatGPT 给我们这段代码。输入以下提示:“请显示 AssetManager 的源代码。

列表 2.4 ChatGPT 生成的 AssetManager 类的类定义
class AssetManager:
    def __init__(self):
        self.assets = []

    def add_asset(self, asset):
        self.assets.append(asset)

    def remove_asset(self, asset_id):
        for asset in self.assets:
            if asset.asset_id == asset_id:
                self.assets.remove(asset)
                return True
        return False

    def get_asset_by_id(self, asset_id):
        for asset in self.assets:
            if asset.asset_id == asset_id:
                return asset
        return None

    def assign_asset(self, asset_id, user):
        asset = self.get_asset_by_id(asset_id)
        if asset and asset.status == 'Available':
            asset.assign_to_user(user)
            return True
        return False

    def unassign_asset(self, asset_id):
        asset = self.get_asset_by_id(asset_id)
        if asset and asset.status == 'Assigned':
            asset.unassign()
            return True
        return False

    def get_available_assets(self):
        return [asset for asset in self.assets if asset.status == 'Available']

    def get_assigned_assets(self):
        return [asset for asset in self.assets if asset.status == 'Assigned']

此外,ChatGPT 还将生成关于类使用以及类的所有方法的文档。如果我们想知道如何使用该类,或者如果我们想要向我们的代码添加注释,这份文档将会很有帮助。

图 2.3 ChatGPT 提供了关于 AssetManager 类的文档,包括属性和方法。

文字,字母说明自动生成

如果您选择将 AssetManager 类存储在单独的 Python 文件中,这是一个好主意,那么您将需要更新 app.py 文件,以以下方式更改导入语句:

列表 2.5 修复文件 app.py 的导入语句
from flask import Flask, request, jsonify
from asset import Asset
from asset_manager import AssetManager

app = Flask(__name__)
asset_manager = AssetManager()
…

现在,您应该能够运行我们非常简单的 ITAM 系统了。一旦您启动 Flask 应用程序,如果您导航到 http://localhost:5000/assets,您应该会看到 JSON 输出的 Assets。

图 2.4 我们向 AssetManager 添加了三个资产。控制器以 JSON 格式返回了这个集合

有一个我们应该关注的未解决问题。这个项目仍然在使用 Flask。有几个原因使我们更倾向于使用 FastAPI 而不是 Flask。首先是 FastAPI 将为我们提供更好的性能。FastAPI 在基准测试中一直表现得明显更好,因此得名。FastAPI 支持异步非阻塞、高并发编程。异步非阻塞系统在相同的硬件上提供更好的性能,因为计算机在等待 I/O 操作完成时会执行其他任务。然而,我们想在这个项目中使用 FastAPI 的主要原因是为了演示如何使用 ChatGPT 轻松更改我们的中间件。

因此,我们应该转换到 FastAPI。我们可以要求 ChatGPT 为我们做这个。输入以下提示:“请将此项目从使用 Flask 切换到使用 FastAPI。” ChatGPT 将生成类似于以下 app.py 的代码。请注意,列表 2.6 被删减了,只包括 GET REST 操作。完整的输出将支持 POST、PUT 和 DELETE。

列表 2.6 包含 FastAPI 支持的更新后的 app.py
app = FastAPI()
asset_manager = AssetManager()

class AssetIn(BaseModel):
    asset_id: str
    asset_type: str
    manufacturer: str
    model: str
    purchase_date: str
    warranty_end_date: str
    location: str

class AssetOut(BaseModel):
    asset_id: str
    asset_type: str
    manufacturer: str
    model: str
    purchase_date: str
    warranty_end_date: str
    location: str
    assigned_to: str
    status: str

@app.get("/assets/{asset_id}", response_model=AssetOut)
async def get_asset(asset_id: str):
    asset = asset_manager.get_asset_by_id(asset_id)
    if asset:
        return asset
    else:
        return {"message": "Asset not found"}

有几件事需要注意。首先是 ChatGPT 通知我们所做的更改。请参见图 2.5。接下来,请注意它创建了两个额外嵌入的类:AssetInAssetOut。这被认为是一种良好的做法,因为它提供了客户端应遵守的接口或合同,同时允许我们自由更改我们的领域模型的内部表示。

图 2.5 ChatGPT 解释了将应用程序从 Flask 应用程序更改为 FastAPI 所做的更改。

文字,字母说明自动生成

在更改项目的中间件时,您应该查看领域模型类是否没有发生变化。在这种情况下,AssetAssetManager 都发生了变化。我们应该要求 ChatGPT 展示给我们这些新类。

注意

在设计和开发与 ChatGPT 相关的软件时,定期审查不同类或文件的代码非常重要。这是因为上下文是通过与 ChatGPT 的交互不断建立的。这个审查过程将使您首次亲眼看到这些更改是如何实施的。

首先,让我们看看对 Asset 类所做的更改。要做到这一点,请输入以下提示:"请展示 Asset 类的代码。"

2.7 更新后的 Asset 类,反映了从 Flask 到 FastAPI 的中间件的更改
from typing import Optional

class Asset:
    def __init__(self, asset_id: str, asset_type: str, manufacturer: str, model: str, purchase_date: str, warranty_end_date: str, location: str):
        self.asset_id = asset_id
        self.asset_type = asset_type
        self.manufacturer = manufacturer
        self.model = model
        self.purchase_date = purchase_date
        self.warranty_end_date = warranty_end_date
        self.location = location
        self.assigned_to: Optional[str] = None
        self.status: str = 'Available'

    def assign_to_user(self, user: str):
        self.assigned_to = user
        self.status = 'Assigned'

    def unassign(self):
        self.assigned_to = None
        self.status = 'Available'

    def get_details(self):
        details = f "Asset ID: {self.asset_id}\n"
        details += f"Type: {self.asset_type}\n"
        details += f"Manufacturer: {self.manufacturer}\n"
        details += f"Model: {self.model}\n"
        details += f "Purchase Date: {self.purchase_date}\n"
        details += f "Warranty End Date: {self.warranty_end_date}\n"
        details += f"Location: {self.location}\n"
        details += f "Assigned To: {self.assigned_to}\n"
        details += f"Status: {self.status}\n"
        return details

ChatGPT 添加的最有意义的更改是 Optional 类型提示。这将表明您打算 assigned_to 属性不应为空,从而导致 NoneType 错误。ChatGPT 通常还会提供方法文档。

图 2.6 ChatGPT 解释了更新后的 Asset 类。虽然它没有明确告诉我们它在更新类时进行了哪些更改,但它提供了对类方法的充分文档。

自动生成的文字,描述

接下来,请输入以下提示:"请展示 AssetManager 的代码。" 构造函数初始化一个 Assets 的列表。这个列表是 AssetManager 类用来执行 CRUD(创建、读取、更新和删除)操作的数据结构。

2.8 更新后的 AssetManager 类,反映了从 Flask 到 FastAPI 的中间件的更改
from typing import List, Optional
from asset import Asset

class AssetManager:
    def __init__(self):
        self.assets: List[Asset] = []

    def add_asset(self, asset: Asset):
        self.assets.append(asset)

remove_asset 方法将遍历资产列表中的元素。如果找到与方法参数(asset_id)提供的标识符相匹配的资产,则将其从列表中移除。

2.9 AssetManager 的 remove_asset 方法
    def remove_asset(self, asset_id: str) -> bool:
        for asset in self.assets:
            if asset.asset_id == asset_id:
                self.assets.remove(asset)
                return True
        return False

get_asset 方法遍历列表中的所有元素,查找传入的 asset_id 对应的资产。如果找到该资产,则返回它。该搜索功能与此类中的许多方法非常相似:在 assign_assetunassign_asset 方法中可以再次注意到这一点。如果我们遵循 DRY(不要重复自己)原则(我们应该这么做!),那么我们将重构这个类(或者让 ChatGPT 为我们做这个),整合逻辑,提取一个单一的方法供整个类使用。

2.10 AssetManager 类的常见方法
    def get_asset_by_id(self, asset_id: str) -> Optional[Asset]:
        for asset in self.assets:
            if asset.asset_id == asset_id:
                return asset
        return None

    def assign_asset(self, asset_id: str, user: str) -> bool:
        for asset in self.assets:
            if asset.asset_id == asset_id:
                if asset.status == 'Available':
                    asset.assign_to_user(user)
                    return True
                else:
                    return False
        return False

    def unassign_asset(self, asset_id: str) -> bool:
        for asset in self.assets:
            if asset.asset_id == asset_id:
                if asset.status == 'Assigned':
                    asset.unassign()
                    return True
                else:
                    return False
        return False

同样,get_available_assetget_assigned_asset 方法包含共同的功能。两者都会查找所有状态与参数匹配的资产。这些都可以被重构成一个常用的方法来使用。

2.11 方法:根据给定状态返回资产
    def get_available_assets(self) -> List[Asset]:
        return [asset for asset in self.assets if asset.status == 'Available']

    def get_assigned_assets(self) -> List[Asset]:
        return [asset for asset in self.assets if asset.status == 'Assigned']

    def get_all_assets(self) -> List[Asset]:
        return self.assets

附带这段代码的是与类方法及其用法相关的文档。

图 2.7 ChatGPT 列出了已迁移到使用 FastAPI 的 AssetManager 类中可用的方法

文本,信件说明自动生成

最后,我们需要 ChatGPT 重新生成依赖文件。使用以下提示:“请展示我新的 requirements.txt 文件。

列表 2.12 更新后使用 FastAPI 的 requirements.txt 文件
fastapi==0.70.0
uvicorn==0.15.0
pydantic==1.8.2

一旦重新运行了 pip 命令(pip install -r requirements.txt)以安装更新的依赖项,您应该能够使用以下命令运行项目:uvicorn app:app --reload

端口现在已更改为 8000。当我运行这个时,并没有一个方法来显示 AssetManager 中的所有 Assets。我们可以要求 ChatGPT 添加这样一个方法。“请添加一个方法到 app.py 中,以返回和显示所有 Assets。”您可能会得到一个异步方法,该方法返回 AssetManager 的所有 Asset。代码应该类似于以下清单。

列表 2.13 用于显示 AssetManager 中所有资产的方法
@app.get("/assets/", response_model=List[AssetOut])
async def get_assets():
    assets = asset_manager.get_all_assets()
    return assets
FastAPI 中的路由

在这种方法中,我们使用 @app.get 装饰器定义了一个新的路由,URL 路径为 /assets/。我们还使用 response_model 参数指定了 List[AssetOut] 的响应模型,这告诉 FastAPI 验证输出数据并基于 AssetOut 数据模型的架构生成 API 文档。

在方法内部,我们调用 asset_manager 对象的 get_all_assets 方法,该方法返回所有 Asset 对象的列表。然后,我们将此列表返回给客户端,FastAPI 将自动将其转换为 JSON 数组。

您可以使用 Web 浏览器或诸如 curl 之类的工具测试此端点。例如,如果您在本地运行应用程序并使用端口 8000,则可以在 Web 浏览器中打开 http://localhost:8000/assets/,以查看所有资产的 JSON 数组。

让我们通过在浏览器中添加并显示一个新的 Asset 来测试我们的 ITAM 系统。您可以使用 curl 或 Postman 添加一个新的 Asset

列表 2.14 使用 curl 将新资产添加到 AssetManager
curl -X POST -H "Content-Type: application/json" -d '{"asset_id": "12345", "asset_type": "Laptop", "manufacturer": "Dell", "model": "Latitude 5400", "purchase_date": "2022-01-01", "warranty_end_date": "2023-01-01", "location": "New York"}' http://localhost:8000/assets/

当您尝试购买这个新的 Asset 时,可能会遇到错误。要使 ChatGPT 生成的代码正常工作,需要进行两处修改。第一步是在 Asset 类中添加一个名为 to_dict() 的新方法。FastAPI 使用库 pydantic 将输入/输出模型(例如 AssetInAssetOut)与 Asset 类进行转换。它通过使用字典来分配属性来实现此目的。to_dict() 方法将为我们提供一种简单的方法来提供该字典。

列表 2.15 在 asset.py 中定义的 Asset 类中添加 to_dict 方法
def to_dict(self):
        return {
            "asset_id": self.asset_id,
            "asset_type": self.asset_type,
            "manufacturer": self.manufacturer,
            "model": self.model,
            "purchase_date": self.purchase_date,
            "warranty_end_date": self.warranty_end_date,
            "location": self.location,
            "assigned_to": self.assigned_to,
            "status": self.status
        }

我们需要做的第二个更改是将输出模型(AssetOut)转换为 JSON。我们需要在输出此对象的所有地方进行此操作。我们将检查与将输出更改为 JSON 相关的类中的更改。

列表 2.16 更新后的 app.py 类,输出采用 JSON 编码

@app.get("/assets/", response_model=List[AssetOut])
async def get_assets():
    assets = asset_manager.get_all_assets()
    return JSONResponse(content=[asset.to_dict() for asset in assets])

@app.post("/assets/")
async def add_asset(asset: AssetIn):
    new_asset = Asset(asset.asset_id, asset.asset_type, asset.manufacturer, asset.model, asset.purchase_date, asset.warranty_end_date, asset.location)
    asset_manager.add_asset(new_asset)
    return {"message": "Asset added successfully"}

@app.delete("/assets/{asset_id}")
async def remove_asset(asset_id: str):
    if asset_manager.remove_asset(asset_id):
        return {"message": "Asset removed successfully"}
    else:
        return {"message": "Asset not found"}

@app.get("/assets/{asset_id}", response_model=AssetOut)
async def get_asset(asset_id: str):
    asset = asset_manager.get_asset_by_id(asset_id)
    if asset:
        return JSONResponse(content=asset.to_dict())
    else:
        return {"message": "Asset not found"}

@app.put("/assets/{asset_id}/assign")
async def assign_asset(asset_id: str, user: str):
    if asset_manager.assign_asset(asset_id, user):
        return {"message": "Asset assigned successfully"}
    else:
        return {"message": "Asset not available"}

@app.put("/assets/{asset_id}/unassign")
async def unassign_asset(asset_id: str):
    if asset_manager.unassign_asset(asset_id):
        return {"message": "Asset unassigned successfully"}
    else:
        return {"message": "Asset not assigned"}

@app.get("/assets/available/", response_model=List[AssetOut])
async def get_available_assets():
    assets = asset_manager.get_available_assets()
    return JSONResponse(content=[asset.to_dict() for asset in assets])

@app.get("/assets/assigned/", response_model=List[AssetOut])
async def get_assigned_assets():
    assets = asset_manager.get_assigned_assets()
    return JSONResponse(content=[asset.to_dict() for asset in assets])

将中间件从 Flask 切换到 FastAPI 在我们的代码中引起了无数的问题。然而,通过这个过程,我们建立了一个直觉,即我们应该如何以及何时与 ChatGPT 进行交互。ChatGPT 是我们软件设计工具箱中不可或缺的工具。它可以让我们快速地对我们的项目进行根本性的改变,并给我们指明应该采取的方向。它往往在非常精细的任务上失败(至少在撰写本文时是这样)。

一个有趣的实验是你应该尝试(在一个新的会话中)首先创建一个使用 FastAPI 的项目的提示,然后将其与我们迭代创建的项目进行比较。你应该会注意到代码有很大的不同。

在本书的这一部分中,我们深入探讨了 ChatGPT 在软件开发中的实际应用,通过开发一个小型 IT 资产管理(ITAM)项目。我们利用 ChatGPT 自动生成软件的基本元素,包括创建类、开发一个 RESTful 控制器,并生成一个 requirements.txt 文件。这个过程凸显了 ChatGPT 作为一个辅助工具在加快和简化软件开发的复杂过程中的能力,特别是对于像这样的小规模项目。在这种情况下应用 ChatGPT 不仅突显了它在构建基本软件组件方面的实用性,而且展示了它在未来简化软件开发流程方面的潜力。

2.3 让 Copilot 掌控!

现在让我们使用 GitHub Copilot 来解决 Asset 类的创建问题。附录 B 中有关于如何创建账户并将插件安装到你喜爱的 IDE 中的说明(假设你喜欢的 IDE 是 VS Code 或 PyCharm 中的一个)。安装完成后,你应该在你喜欢的 IDE 中创建一个新项目。首先,创建一个名为 asset.py 的新文件。这个类将保存 Asset 类的源代码。当你输入构造函数时,Copilot 将给出代码建议。你可能会得到类似于图 2.9 中源代码的代码。或者,你可以通过按下 Ctrl+Enter 来激活交互模式,Copilot 将基于当前上下文生成多达十个建议。

注意

在本书的大部分内容中,我们将主要使用内联模式,在该模式下,你可以使用 Tab 键来接受给定的建议。

虽然 Copilot 的第一次尝试令人钦佩,但它并不像我们感兴趣的类定义;我们想要一个 ITAM 系统资产,而不是一个通用的资产实现。就像我们在 ChatGPT 中所做的那样,我们将创建一个提示,来告诉 Copilot 我们的意图,给它提供额外的上下文和信息,以便它生成代码建议。在 Copilot 中输入提示非常简单,只需在源代码文件的开头添加注释。我们将告诉 Copilot 类的名称应该是什么,以及它应该具有什么属性。

清单 2.17 GitHub Copilot 的起始提示要求其创建名为 Asset 的新类
# Create a class named Asset
# Add the following attributes: id, asset_type, manufacturer, model, purchase_date, purchase_price, warranty_end_date, location, and status. 
# The class should have a function named get_details that returns a string that is the concatenation of all attributes.
# The class should have a function named to_dict that returns a dictionary of all attributes.

当您开始定义构造函数时,Copilot 将返回一个完整的代码建议,用于 ITAM 系统资产。 如果我们将 ChatGPT 生成的代码与 Copilot 生成的结果代码进行并排比较,我们会注意到它们非常相似。

图 2.9 根据我们添加到文件顶部的注释,Copilot 能够生成一个构造函数,其中包含提示中包含的所有属性。

图形用户界面,文本,应用程序自动生成的描述

接下来,添加 get_detailsto_dict 方法。 JSONResponse 类接受一个字典并将其转换为 JSON。 to_dict 方法将有助于简化此交接。 get_details 方法在调试过程中会很有用,使我们能够查看类的内容。

清单 2.18 Copilot 根据我们的请求生成的代码
# Create a class named Asset
# Add the following attributes: id, asset_type, manufacturer, model, purchase_date, purchase_price, warranty_end_date, location, and status. 
# The class should have a function named get_details that returns a string that is the concatenation of all attributes.
# The class should have a function named to_dict that returns a dictionary of all attributes.

class Asset:
    def __init__(self):
        self.id = None
        self.asset_type = None
        self.manufacturer = None
        self.model = None
        self.purchase_date = None
        self.purchase_price = None
        self.warranty_end_date = None
        self.location = None
        self.status = None

    def get_details(self):
        return f"{self.id}, {self.asset_type}, {self.manufacturer}, {self.model}, {self.purchase_date}, {self.purchase_price}, {self.warranty_end_date}, {self.location}, {self.status}"

    def to_dict(self): 
        return {
            "id": self.id,
            "asset_type": self.asset_type,
            "manufacturer": self.manufacturer,
            "model": self.model,
            "purchase_date": self.purchase_date,
            "purchase_price": self.purchase_price,
            "warranty_end_date": self.warranty_end_date,
            "location": self.location,
            "status": self.status
        }

现在我们有了资产类。 我们将继续 AssetManager 类。 我们将从一个基本提示开始,告诉 Copilot 创建 AssetManager 类并定义一些基本功能。

清单 2.19 让 Copilot 生成我们的 AssetManager 的提示
# Create a class called AssetManager that will allow us to store and retrieve assets.

在定义完类之后不久,您会注意到 Copilot 将建议为此管理器提供完整的源代码。

图 2.10 带有最少说明,Copilot 能够创建一个允许存储和检索资产的 AssetManager。

文本描述自动生成

Copilot 的 AssetManager 源代码将与 ChatGPT 生成的代码非常相似。

清单 2.20 由 Copilot 从我们的初始提示生成的 AssetManager 完整源代码
# Create a class called AssetManager that will allow us to store and retrieve assets.

class AssetManager:
    def __init__(self):
        self.assets = []

    def add_asset(self, asset):
        self. Assets.append(asset)

    def get_assets(self):
        return self. assets

    def get_asset(self, asset_id):
        for asset in self.assets:
            if asset.id == asset_id:
                return asset
        return None

    def update_asset(self, asset_id, asset):
        for index, asset in enumerate(self.assets):
            if asset.id == asset_id:
                self.assets[index] = asset
                return True
        return False

    def delete_asset(self, asset_id):
        for index, asset in enumerate(self.assets):
            if asset.id == asset_id:
                del self.assets[index]
                return True
        return False

接下来,我们将组装 FastAPI 应用程序以运行和显示资产。 我们将从 app.py 类的开头开始。

清单 2.21 启动提示,让 Copilot 构建我们的 FastAPI 应用程序
# Create a FastAPI app that allows CRUD operations on the Asset class.
图 2.11 Copilot 开始自动导入所需的库。

Copilot 将添加导入语句。 在定义 FastAPI 应用程序之后发生了一些有趣的事情:Copilot 将尝试定义 Asset 类。 封装和保护内部模型是一个好习惯,这就是 Asset 类。

图 2.12 Copilot 尝试定义基于 pydantic 的 Asset 类。 但是,我们已经定义了一个 Asset 类,它代表了我们的“内部”类的模型。 因此,我们拒绝了这个建议。

图形用户界面,文本,应用程序,电子邮件自动生成的描述

为了保护我们的内部模型,因此,我们将拒绝 ChatGPT 提供给我们的建议。相反,我们将创建两个类:AssetIn 和 AssetOut。 Copilot 生成的代码与 ChatGPT 提供给我们的解决方案非常相似。一旦我们键入 AssetIn,Copilot 将会帮助我们创建剩下的类,因为它已经猜到我们想要做什么。

图 2.13 Copilot 已确定我们不想直接暴露我们的内部模型,并创建一个类来表示我们尚未定义的 POST REST 端点的输入负载。

图形用户界面,文本,应用程序,电子邮件,描述自动生成

当我们继续定义我们的 FastAPI 应用程序中的方法时,Copilot 将预测方法的实现。

图 2.14 在 Copilot 生成了所有方法的实现后,文件 app.py 的最终内容。

图形用户界面,文本,应用程序,描述自动生成

最后,我们将让 Copilot 为我们创建一个 requirements.txt 文件。我们从列表 2.22 的提示开始。然后,当我们开始列举库的版本号时,Copilot 将添加版本号。

列表 2.22 此项目的 requirements.txt 文件
# generate a requirements.txt file for this project
fastapi==0.63.0
uvicorn==0.13.4
pydantic==1.7.3
starlette==0.13.6

使用以下命令启动应用程序:uvicorn app:app --reload。通过列表 2.23 中的 curl 命令创建一个新的资产。

列表 2.23 用于向我们的 Copilot 应用程序添加新资产的 curl 命令
curl -X POST -H "Content-Type: application/json" -d '{"id": "12345", "asset_type": "Laptop," "manufacturer": "Dell", "model": "Latitude 5400", "purchase_date": "2022-01-01", "warrenty_expiration": "2023-01-01", "location": "New York", "status": "Available", "purchase_price": "1000.00"}' http://localhost:8000/assets/

打开你的浏览器访问 http://localhost:8000/assets/ 通过 REST 控制器展示新的资产。

图 2.15 我们使用 POST 端点创建了一个单独的资产对象。这通过 GET 端点显示。

图形用户界面,文本描述自动生成

在本节中,我们开始了一个软件开发的比较之旅,使用了两种不同的生成性 AI 工具 - ChatGPT 和 GitHub Copilot,这两种工具都应用于相同的 IT 资产管理(ITAM)项目中。 ChatGPT 在我们的开发方式中提供了显著的灵活性,甚至使我们能够切换框架,尽管有些谨慎。它作为一位宝贵的顾问,提供建议和指导。

与此同时,我们与 Copilot 的经验需要采用不同的方法。我们使用提示来指示 Copilot 生成代码,这要求我们从一开始就对预期的实现有清晰的愿景。

接下来,我们计划引入 AWS CodeWhisperer 到混合中,为这个探索性练习增加另一层。最后,我们将对比这三个工具 - ChatGPT、Copilot 和 AWS CodeWhisperer。目标仍然是了解它们各自的独特优势和局限性,以及根据软件开发项目的需求和环境如何最佳地使用每个工具。

总的来说,通过我们对这些 AI 助力工具的实践经验,我们深入探讨了 AI 在软件开发领域的变革潜力。无论是 ChatGPT、Copilot 还是即将推出的 AWS CodeWhisperer,每个工具都在这一领域带来了独特的东西,承诺了一个更高效、更流畅的软件开发流程。然而,最大化它们的好处的关键在于我们不断适应和学习,充分利用每个工具的同时注意它们的局限性。

2.4 让 CodeWhisperer 大声说话

在我们开始使用 CodeWhisperer 之前,我们应该禁用 Copilot。单击扩展表并搜索 Copilot。一旦找到它,点击 Disable 按钮。你需要重新启动应用程序。一旦应用程序重新启动,你就可以开始使用 CodeWhisperer 了。如果你需要帮助安装或配置 CodeWhisperer,你应该参考附录 C。一旦插件安装完成并且你已经登录到你的开发者账户中,你应该创建一个名为 asset.py 的文件。

图 2.16 在我们切换到 CodeWhisperer 之前,我们必须禁用 Copilot。我们在扩展选项卡中禁用该扩展。

图形用户界面,文本,应用程序描述自动生成

我们将使用与 Copilot 相同的提示。这里为了方便重新打印。

列表 2.24 让 CodeWhisperer 为我们创建 Asset 类的提示
# Create a class named Asset
# Add the following attributes: id, asset_type, manufacturer, model, purchase_date, purchase_price, warranty_end_date, location, and status. 
# The class should have a function named get_details that returns a string that is the concatenation of all attributes.
# The class should have a function named to_dict that returns a dictionary of all attributes.

一旦我们输入了这个提示并编写了类定义,CodeWhisperer 将开始编写构造函数。

图 2.17 根据我们在文件开头输入的提示,CodeWhisperer 建议了一个构造函数。

图形用户界面,文本,应用程序描述自动生成

然后,我们继续编写方法定义,CodeWhisperer 将忠实地生成实现。

列表 2.25 CodeWhisperer 生成的完整代码
# Create a class named Asset
# Add the following attributes: id, asset_type, manufacturer, model, purchase_date, purchase_price, warranty_end_date, location, and status.
# The class should have a function named get_details that returns a string which is the concatenation of all attributes.
# The class should have a function named to_dict that returns a dictionary of all attributes.

class Asset():
    def __init__(self, id, asset_type, manufacturer, model, purchase_date, purchase_price, warranty_end_date, location, status):
        self.id = id
        self.asset_type = asset_type
        self.manufacturer = manufacturer
        self.model = model
        self.purchase_date = purchase_date
        self.purchase_price = purchase_price
        self.warranty_end_date = warranty_end_date
        self.location = location
        self.status = status

    def get_details(self):
        return f"{self.id}, {self.asset_type}, {self.manufacturer}, {self.model}, {self.purchase_date}, {self.purchase_price}, {self.warranty_end_date}, {self.location}, {self.status}"

    def to_dict(self):
        return {"id": self.id, "asset_type": self.asset_type, "manufacturer": self.manufacturer, "model": self.model, "purchase_date": self.purchase_date, "purchase_price": self.purchase_price, "warranty_end_date": self.warranty_end_date, "location": self.location, "status": self.status}

最终的代码应该类似于 Copilot 创建的代码。非常相似。基于此,我们将跳过项目中的其他文件的编写。完成项目将留给一个有动力的读者。

在这一部分,我们将 AWS CodeWhisperer 引入了其中。像它的前辈一样,CodeWhisperer 能够生成项目的关键组件,强化了 AI 在软件开发领域的变革潜力。

鉴于这些工具生成的代码之间惊人的相似性,一个引人入胜的问题自然而然地出现:这些产品到底如何真正比较?对于这个问题的答案并不像人们想象的那样直截了当,因为每个工具具有独特的优势和局限性。

在接下来的部分中,我们将深入探讨这个问题,比较这三种工具 - ChatGPT、Copilot 和 AWS CodeWhisperer - 以便了解它们的独特优势、最佳使用案例,以及它们可能如何重塑软件开发的未来。我们的目标是提供一份全面的指南,帮助软件开发人员在这个快速发展的 AI 驱动工具领域中找到方向。

2.5 比较 ChatGPT、Copilot 和 CodeWhisperer

我们要考虑的第一个维度是参与模型:我们如何与 AI 互动。就 ChatGPT 而言,我们登录聊天网站并将提示输入到聊天输入框中。然后,我们在后续提示中完善我们的需求。反馈循环从先前的提示中提取上下文,将其应用于当前提示,并生成输出,用户对其作出反应并重新发出。如果我们将这种参与模型与 Copilot 和 CodeWhisperer 的模型进行对比,我们会注意到后两种工具是在集成开发环境中工作的。我们不能在 IDE 外部使用它;尽管我们努力,但这种方法并不是本质上劣质的,它只是不同。

Copilot 和 CodeWhisperer 让你保持在你的集成开发环境中可能被视为一种优势而不是不足之处。在即将推出的 Copilot X 版本中,你将获得两全其美:ChatGPT 和 GPT 4.0 都在你的集成开发环境中。这些工具让你在没有干扰的情况下更长时间地保持在你的代码中。无干扰地工作是提高生产力的关键之一。Copilot 和 CodeWhisperer 擅长让你专注于工作;让你不必切换上下文;让你远离干扰;让你更长时间地保持在工作状态中。它们做得很好。你与 ChatGPT 进行对话;Copilot 和 CodeWhisperer 给你建议。对话会花费更长时间来完成;建议则快速且免费。

接下来,我们将检查代码是如何呈现和生成的。ChatGPT 可以将代码创建为块、方法、类或项目。如果需要,ChatGPT 会有意识地逐步展示项目。但 ChatGPT 实际上在幕后创建了项目。毕竟,ChatGPT 喜欢交谈。对于 Copilot 和 CodeWhisperer,代码一次性展开一个方法,至少最初是这样的。随着使用的增加,你会注意到它们可以为给定类编写越来越多的代码。但遗憾的是,它们无法通过一个小提示编写整个项目。

它们都共享的一个特性是它们能够响应提示。对于 ChatGPT,提示是唯一与工具互动的方式。对于 Copilot 和 CodeWhisperer,响应提示并不是严格必要的,但编写这些提示将使输出更接近你最初的想法。

综合考虑这些因素,我们可能得出 ChatGPT 是进行探索和原型开发的最佳选择。然而,ChatGPT 可能会引起不必要的分心,部分原因是因为你离开了你的集成开发环境(IDE),现在进入到了一个带有所有浏览器相关诱惑的网页浏览器中。ChatGPT 本身就是引入不必要干扰的一部分。你最终会掉进那个寓言中的兔子洞里。这个工具使事情变得过于容易,而不是相反。不要被这吓到。它是一个很棒的资源。

Copilot 和 CodeWhisperer 要求你有一个预期结果。因此,这些工具非常适合在你希望完成具备精确要求和紧密期限的编码任务时使用。当你熟悉语言和框架时,Copilot 和 CodeWhisperer 效果最佳。它们可以自动化很多枯燥的工作,让你专注于能够增加价值的业务需求,这很可能是你编写软件的原因。

下表简要总结了这三种人工智能的利与弊:

图 2.18 ChatGPT、Copilot 和 CodeWhisperer 的正负比较。

自动生成的图表说明

在本章中,我们经历了很多,多次重复编写相同的代码。然而,编写 ITAM 系统相同子节的三个版本为我们提供了合适使用 ChatGPT 而不是使用其他以 IDE 为重点的工具(如 Copilot 和 CodeWhisperer)的时机打下了基础。在随后的章节中,我们将利用这些知识,选择最合适的工具。

最后要注意的是,这些工具在它们一起使用时效果最佳。ChatGPT 是一个很好的示例和结构工具。Copilot 和 CodeWhisperer 为您提供了扩展和自定义代码的能力。这使得即将发布的 Copilot X 版本非常令人兴奋:它是这两种方法的结合。

2.6 小结

  • 信息技术资产管理(ITAM)系统是一种用于管理和跟踪硬件设备、软件许可证和其他与信息技术相关的组件的工具。这些系统允许公司控制成本、管理供应商关系,并确保许可和合规性。

  • 在本章中,我们通过在这三个工具中编写相同的代码,探索了使用 ChatGPT、GitHub Copilot 和 AWS CodeWhisperer 的方式。编写相同的代码三次允许我们比较它们的特点,并对使用方式的差异进行对比和对照:如何以及何时使用每个工具。

  • ChatGPT 是一个基于提示的生成式人工智能,与用户进行对话,帮助他们探索想法,以协助设计和开发整个项目。此外,ChatGPT 精巧地为其编写的每种方法生成文档。我们之所以开始使用它来开始本章,是因为它帮助定义了我们在本章剩余部分中使用的模板之一。这是一个迷人的产品,可以导致不必要但令人愉快的分心。

  • Copilot 和 CodeWhisperer 是头脑风暴式的工具,在您知道要做什么并需要一些关于如何最好地完成的建议时效果最佳。与这些工具的互动方式非常相似,结果也是如此。

  • 我们撰写本文时,ChatGPT 不支持在集成开发环境中进行开发。然而,与 GitHub Copilot 和 AWS CodeWhisperer 不同,它可以生成整个项目,并轻松将代码从一种编程语言转换为另一种。GitHub Copilot 和 AWS CodeWhisperer 从您的注释中获取提示,推断出您想要编写的代码。而在 ChatGPT 中,您需要明确编写提示,ChatGPT 将使用这些提示来创建代码。

第三章:使用 ChatGPT 设计软件

本章涵盖

  • 使用 ChatGPT 进行潜在设计原型

  • 在 Mermaid 中记录我们的架构

  • 完成我们的设计与 ChatGPT

现在我们已经对何时使用生成式人工智能有了直觉,我们将开始设计、探索和记录我们应用程序的架构。预先布置一些关键组件在几个方面都是有益的。例如,它使我们能够将一些设计工作委派给子架构师或将一些开发工作交给其他团队成员。提前设计还将有助于我们澄清我们对实施的思考,使我们能够预见并避免一些陷阱。最后,将设计记录为文档使我们能够证明我们的重要设计决策,向我们未来的自己、利益相关者以及可能继承该项目的人传达我们的意图。

首先,让我们询问 ChatGPT 关于如何最好地设计这个项目,并看看它能提出什么解决方案。

3.1 请求 ChatGPT 协助我们进行系统设计

在一个新的会话中,我们将从一个提示开始,概述我们的需求。鉴于我们在上一章节大部分时间都在思考我们的需求,我们应该能够构建一个复杂的提示,并且应该对所需内容有很好的了解。或者,我们可以请求 ChatGPT 提供这样一个系统的需求。然后,我们可以将这些需求纳入我们的提示中,并根据需要进行编辑。

毫无疑问,你已经遇到了无数篇声称展示正确的提示工程方法的文章。提示工程,或者是设计和优化初始输入或“提示”以获得所需输出的做法,是我们与大型语言模型互动的重要组成部分。我们在本书中所做的大部分工作都将被视为提示链接、生成知识提示和零-shot 推理。这些主要是学术上的说法,即我们将与生成式人工智能工具进行对话。重要的要点是,与任何对话一样,当你需要一个具体的答案时,你要问一个具体的问题。或者你要求生成式人工智能逐步思考问题,并尽可能具体。因此,我们将向 ChatGPT 提供一个非常具体的提示,以正式化需求收集过程。

列表 3.1 包含我们系统需求的提示
Please take the following criteria for an information technology asset management system and create a Python project that satisfies them: 

Asset Inventory: The system should be able to maintain a complete inventory of all hardware and software assets owned by an organization. 

Asset Tracking: The system should be able to track asset location, status, and usage. This includes information such as who uses the asset, when it was last serviced, and when it is due for replacement. 

Asset Lifecycle Management: The system should be able to manage the entire lifecycle of an asset, from procurement to disposal. This includes tracking warranty and lease information, managing asset maintenance and repair, and ensuring compliance with regulations. 

Asset Allocation: The system should be able to allocate assets to employees or departments, track usage, and manage reservations for assets that are in high demand. 

Asset Reporting: The system should be able to generate reports on asset utilization, availability, and maintenance history. Reports should be customizable and easy to understand. 

Integration: The system should be able to integrate with other IT systems, such as procurement, help desk, and financial systems, to ensure consistency and accuracy of data. Security: The system should have robust security features to protect sensitive information about assets, including user authentication, role-based access control, and data encryption. 

Scalability: The system should be scalable to accommodate changes in the organization's IT infrastructure, including new hardware and software assets, as well as changes in the number of users and departments. 

User-Friendly Interface: The system should have a user-friendly interface that is easy to navigate, search, and use. It should also be customizable to meet the needs of individual users and departments. 

Accessibility: The system should be accessible from different devices and locations, including mobile devices and remote areas. It should also be accessible to users with disabilities.

经过一番思考,ChatGPT 将返回开发此项目所需的高层步骤。哦,我们在开玩笑,它几乎立即就能生成这些步骤。这些建议很有帮助,但我们想要解决空白文件的问题:我们想要开始设计。我们需要散文和图表来帮助我们理解组件级别的系统。我们需要任务而不是配方。我们应该要求 ChatGPT 展示它的工作过程。

列表 3.2 要求 ChatGPT 展示其工作的提示
Please show the package structure

在这个提示下,ChatGPT 将生成并返回一个包图(这个列表略有删节),演示它不仅可以设计软件,还可以帮助我们布置物理文件。我们可能对整体的包结构有异议(或者也可能没有),但这将为我们提供足够的灵感来开始。

列表 3.3 ChatGPT 创建的资产管理系统的包结构
asset_management_system/
├── app/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── asset.py
│   │   ├── user.py
│   │   └── ...
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── login.py
│   │   ├── register.py
│   │   └── ...
│   ├── models/
│   │   ├── __init__.py
│   │   ├── asset.py
│   │   ├── user.py
│   │   └── ...
│   ├── views/
│   │   ├── __init__.py
│   │   ├── asset.py
│   │   ├── user.py
│   │   └── ...
├── requirements.txt
└── run.py

我们可以审查宏观情况,也可以要求 ChatGPT 提供微观情况。我们可以创建提示,让 ChatGPT 按顺序显示每个文件的内容。我们可以将内容认真地复制到一个与 ChatGPT 建议的文件名相同的文件中。然而,我们始终要仔细检查 ChatGPT 的工作,不过,我们不应假设它是 100%安全或准确的。我们偶尔会发现 ChatGPT 生成的代码无法工作。与我们不同,ChatGPT 的重点是生成下一个标记,而不是思考系统。话虽如此,我们应该快速查看它生成的代码,以评估质量并确定其有用性。

列表 3.4 要求 ChatGPT 展示其(详细的)工作的提示
please show the source code for asset_management_system/models/asset.py

ChatGPT 将返回此类的源代码。它使用 Flask 和 SQLAlchemy,因此域模型有点混乱。下次,我们应该要求它使用我们首选的应用服务器,并保持域模型的清晰。这些对我们来说也是额外的数据点。现在我们知道了我们不想要的东西。

列表 3.5 资产管理系统中 asset_management_system/models/asset.py 的示例源代码:
from datetime import datetime
from asset_management_system import db

class Asset(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False)
    description = db.Column(db.String(255))
    category = db.Column(db.String(80))
    purchase_date = db.Column(db.DateTime, default=datetime.utcnow)
    purchase_cost = db.Column(db.Float)
    warranty_expiry = db.Column(db.Date)
    lease_expiry = db.Column(db.Date)
    status = db.Column(db.String(80), default="Available")
    location = db.Column(db.String(80), default="IT Department")
    assigned_to = db.Column(db.String(80))
    assigned_date = db.Column(db.DateTime)
    retired_date = db.Column(db.DateTime)
    created_date = db.Column(db.DateTime, default=datetime.utcnow)
    updated_date = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    def __repr__(self):
        return '<Asset %r>' % self.name

现在我们对 ChatGPT 如何解决问题有了一定了解,让我们听听第二种意见。为此,我们将探索现有的项目和库。我们还将使用 ChatGPT 进行此项调查。

3.2 审视全局情况

如果我们以前从未使用过或见过信息技术资产管理(ITAM)系统,我们会想了解它是什么,它应该做什么,以及如何实现这样一个系统。正如我们在上一章中所经历的,ChatGPT 非常适合这项任务。当问题空间和已知条件明确时,GitHub Copilot 和 CodeWhisperer 是出色的工具,当你准备进行高效编码时,这些工具非常有用。另一方面,ChatGPT 可以用来探索这个问题空间,进行交互式原型设计,并设计和记录您的解决方案。探索问题空间的一个很好的第一步是检查当前的开源项目。我们可以使用这些“开箱即用”的项目或利用它们的设计来激发我们的灵感。使用 ChatGPT,我们可以快速浏览开源世界,并聚焦于可能与我们试图构建的内容类似的项目。我们可以从列表 3.1 中开始。

列表 3.6 查找 ITAM 项目的提示
What are some examples of ITAM projects or products?

ChatGPT 回复了几个示例,总结了它们的核心特点。然而,由于我们正在寻找开发项目的灵感,我们应该开始将此列表精简到仅包含代码可用的项目;也就是说,哪些是开源项目?

图 3.1 ChatGPT 展示了 ITAM 特性和几个拥有这些特性的产品和项目的属性列表。

文本,字母描述自动生成

接下来,我们将开始精简过程。鉴于进行这种分析和探索当前 ITAM 系统的目的是我们可以看到软件中需要哪些特性以及我们如何实现这些特性,我们只想返回源代码可用的项目。因此,让我们只获取开源项目的列表。

寻找开源 ITAM 项目的提示
Are any of these products open-source?

ChatGPT 最令人兴奋和有用的功能之一是它可以延续上下文;它理解在这个上下文中,“任何”意味着前面提到的任何项目。ChatGPT 回复了一个开源项目列表。

图 3.2 ChatGPT 返回了一个开源 ITAM 项目的列表。

文本,字母描述自动生成

我们将继续精简过程。我们打算用 Python 来实现我们的系统,因此我们只对具有 Python 作为与系统交互手段的项目感兴趣。

寻找用 Python 编写的 ITAM 项目的提示
Are any of these written in Python?

根据我们的提示,ChatGPT 将会将这个列表精简到只有用 Python 编写的项目。它返回了五个项目。我们将评估每个项目,并确定我们应该进一步探索哪一个。

图 3.3 ChatGPT 告诉我们有四个具有 Python 组件的开源项目。

文本,字母描述自动生成

在这个列表中,Ralph 似乎是最有前途的。Snipe-IT 和 Open-AudIT 是用 PHP 编写的,具有我们可以与之交互的基于 Python 的 API;然而,我们希望一个用 Python 编写的项目。NetBox 不是 ITAM 而是 IP 地址管理(IPAM)系统。最后,CMDBuild 是用 Java 编写的。我们希望用 Python 构建一个 IT 资产管理系统。因此,Ralph 似乎是最符合我们用例的选择。

在现实世界中

如果我们必须在现实世界中实施 ITAM 系统,我们将评估每个产品(商业和开源)在不同用例下的适用程度。例如,如果 Ralph 能够满足大多数或所有这些用例,我们将简单地搭建一个新实例;或者在商业产品的情况下,我们将获取许可证。我们将执行这个分析,因为我们得到的是为企业创造价值的报酬,而不是编写代码。如果我们可以拿来即用,那么我们很快就能创造价值,可能比我们自己开发它还要快。

现在我们已经发现了一个与我们要构建的项目相似的项目(一个子集),我们可以开始探索它,审查它的设计,并检查它的源代码。我们将从这个源代码和设计中汲取灵感,大胆借鉴,并在必要时进行更改。让我们首先来了解系统的概述。

3.3 探索一个库

首先,我们将要求 ChatGPT 描述 Ralph 的整体结构。这个结构可能会在我们开发架构时给我们启发。或者,它可能会提醒我们如何不要组织我们的代码。

列表 3.9 查找开源项目 Ralph 结构的提示
Please describe the modules of Ralph. 

ChatGPT 将返回 Ralph 的高级模块列表。值得注意的一点是,在撰写本文时,ChatGPT 将以 2021 年的 Ralph 设计返回。ChatGPT 的训练数据截止日期(根据 ChatGPT 本身)是 2021 年 9 月。因此,2021 年 9 月后对设计的任何后续更改都不会在 ChatGPT 中显示。未来,当 GPT 有了互联网访问和扩展功能时,所有这些都将发生改变。但是,目前,我们必须接受这一限制。

图 3.4 ChatGPT 解释了 Ralph 开源项目的高级模块布局

自动生成的文本,文字描述

资产和库存模块似乎是一个很好的开始地方。接下来,我们将要求 ChatGPT 分别呈现每个模块的美人鱼图。让我们首先从资产开始。

列表 3.10 在 Ralph 的资产模块中查找类的提示
Please show me a class diagram in Mermaid for all classes in the Asset module. 
美人鱼

美人鱼是由 Knut Sveidqvist 发明的现代、开源、简化的图表定义语言。它于 2015 年发布。它允许用户创建各种图表,包括流程图、序列、类、甘特图等。由于它是基于文本的,因此很容易复制粘贴到设计工具中。它的语法非常易于阅读和理解,几乎不需要正式培训(与 UML 等不同)。

ChatGPT 将返回一个美人鱼文档,概述类、它们的属性和它们的关系。首先,让我们来看一下这个美人鱼文档中的类定义。列表 3.11 的第一行指示美人鱼我们正在定义一个类图类型,而不是它支持的其他类型。第二行表示类(在本例中为资产)。接下来的行列举了类的字段。减号表示它是一个私有属性。倒数第二行是公共方法的定义。我们知道它是公共的;加号表示一个公共方法。

列表 3.11 资产类定义
classDiagram
    class Asset {
        - name: string
        - status: string
        - model: Model
        - category: Category
        - manufacturer: Manufacturer
        - sn: string
        - barcode: string
        - price: decimal
        - purchase_date: date
        - order_no: string
        - delivery_date: date
        - invoice_date: date
        - invoice_no: string
        - provider: Provider
        - budget_info: BudgetInfo
        - warehouse: Warehouse
        - owner: User
        - location: Location
        - asset_info: AssetInfo[]
        - attachments: Attachment[]
        + get_name(): string
    }

接下来,我们将看看如何在美人鱼中定义关系。箭头的双短线表示单向关联。在以下示例中,一个资产有一个型号。此外,资产有一个类别。

列表 3.12 在美人鱼文档中的关系定义
    Asset --> Model
    Asset --> Category

使用完整的 Mermaid 图,我们可以使用建模工具导入和可视化类图。以这种方式可视化将更容易理解。毕竟,众所周知,一幅(图表)胜过千言万语。您应该能够将文本插入 mermaid.live 生成并可视化图表。

图 3.5 使用 Mermaid 类图可视化的资产类的可视化。

Mermaid 在表现力和简洁性之间取得了适当的平衡,使其成为一种理想的建模语言,无论您是否时间紧迫。但你的实际情况可能有所不同。

接下来,让我们把注意力转向库存模块。我们可以要求 ChatGPT 生成一个类似于先前请求的资产图的文档。我们将跳到此文档的可视化部分。

图 3.6 使用 Mermaid 类图可视化的库存包的可视化。

Diagram Description automatically generated

库存模块的可视化澄清了库存模块在 Ralph 项目中的重要性,但对我们构建以硬件为重点的 ITAM 来说是多余的。我们感兴趣的是追踪我们的全部资产,整个服务器;我们不一定对每张视频卡或每个内存模块进行追踪:只对整个服务器感兴趣。因此,我们将这个模块放在一边。

接下来,我们将深入研究资产类,因为这似乎是资产模块的根源。让我们让 ChatGPT 展示这个类。

列表 3.13 提示,让 ChatGPT 显示资产类的源代码的用法
  Please show me the source code for the Asset class.

ChatGPT 将返回资产类的源代码。为简洁起见,我们不会显示导入语句。此外,我们只检查此代码块中的几行。很明显,这个类有很多特定于 Django 的代码,例如,model 属性从数据库中查找模型对象。category 也是如此。

让 ChatGPT 继续

有时,ChatGPT 会在中途或中途停止输出。这是 ChatGPT 设计中内置的输出限制所致。看起来你无法通过告诉 ChatGPT 忽略这个限制(对于某些系统约束,你可以这样做)来克服这个限制。但是,你可以告诉它“继续”或“继续”。它会恢复输出并从中断的地方继续输出。如果它在代码块内停止,则应指定它应该在哪里恢复输出。

列表 3.14 简化版的资产类源代码
class Asset(TransitionModelMixin, NamedMixin, AdminAbsoluteUrlMixin, models.Model):
    """
    A hardware or software asset
    """
    name = models.CharField(max_length=255, unique=True)
    status = models.CharField(choices=STATUS_CHOICES, default=STATUS_NEW, max_length=50)
    model = models.ForeignKey(
        'assets.AssetModel',
        verbose_name=_('model'),
        related_name='assets',
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    category = models.ForeignKey(
        'assets.Category',
        verbose_name=_('category'),
        related_name='assets',
        on_delete=models.PROTECT,
    )

我们不想在项目中使用 Django。我们希望我们的应用程序以 API 为先,而不需要 Django 提供的额外功能。此外,我们更希望一个干净的领域模型。Django 非常有主见,建议您修改领域模型类以继承自 Django 基类。基类定义了领域模型类与数据库进行持久化交互的方式。我们更愿意自己处理持久化。因此,让我们请求 ChatGPT 彻底删除项目中的 Django。

列表 3.15 提示 ChatGPT 删除 Django 并返回 Asset 类代码。
  Take the open-source Ralph project, remove Django, and show me the source code for the Asset class.

ChatGPT 生成的输出将更加简洁。下面是缩写后用于与 Django 定义进行比较的结果。以下列表仅显示构造函数,其他类细节现在不相关。

列表 3.16 没有 Django 特定功能的 Asset 类源代码
class Asset:
    def __init__(self, name, status, model, category, manufacturer, sn=None, barcode=None, price=None, purchase_date=None, order_no=None, delivery_date=None, invoice_date=None, invoice_no=None, provider=None, budget_info=None, warehouse=None, owner=None, location=None, asset_info=None, attachments=None):
        self.name = name
        self.status = status
        self.model = model
        self.category = category
        self.manufacturer = manufacturer
        self.sn = sn
        self.barcode = barcode
        self.price = price
        self.purchase_date = purchase_date
        self.order_no = order_no
        self.delivery_date = delivery_date
        self.invoice_date = invoice_date
        self.invoice_no = invoice_no
        self.provider = provider
        self.budget_info = budget_info
        self.warehouse = warehouse
        self.owner = owner
        self.location = location
        self.asset_info = asset_info or []
        self.attachments = attachments or []

如果我们要求 ChatGPT 重新创建 Mermaid 类图,我们不会注意到任何变化。我们不会看到任何变化,因为 Django 特定的功能已封装在类中。

图 3.7 Asset 类的更新后 Mermaid 类图。该类与之前的版本没有变化

3.4 文档化你的架构

在上一节中,我们已经探索了 Ralph 开源项目并理解了项目如何结合在一起,现在我们可以开始设计了。我们将与 ChatGPT 迭代地一起工作,以帮助我们进行设计和文档编写。让我们从一个全新的聊天窗口开始。这个新的聊天会话将确保上下文清晰,我们之前的提示不会影响我们的新设计。

首先,我们将请求 ChatGPT 设计初始应用程序设计。我们将使用以下提示来做到这一点。

列表 3.17 用于 ChatGPT 设计我们的初始应用骨架的提示
I would like to build an ITAM project, written in Python. It will focus on the tracking and management of Hardware. It should expose REST APIs, using FastAPI, and persist data using SQLAlchemy. It should use hexagonal architecture. Please show me the Mermaid class diagram for this project.
六边形架构

六边形架构,也称为端口和适配器模式,是一种旨在在应用程序的核心逻辑与其与外部系统(如数据库、用户界面和第三方服务)的交互之间创建明确分离的架构模式。这种关注点的分离有助于实现更易于维护、灵活和可测试的应用程序。

六边形架构的主要组件有:

领域模型:表示应用程序的核心业务逻辑,包括实体、值对象和领域服务。领域模型与任何外部系统或技术隔离,并且不应依赖于任何特定的实现细节,如数据库或 API。

端口:端口是定义领域模型和外部系统之间契约的接口。它们代表了应用程序的输入和输出边界。端口分为两种类型:主要端口和次要端口。

主(被动)端口:这些定义了应用程序向外部系统公开的用例。它们代表外部系统可以用来与应用程序交互的 API。

次(驱动)端口:这些定义了应用程序对外部系统(如数据存储库或外部服务)的预期合同。

适配器:适配器是端口的实现。它们处理领域模型与外部系统之间的通信,将外部系统使用的数据和协议转换为领域模型能够理解的格式。适配器有两种类型:主适配器和次适配器。

主(被动)适配器:这些实现了主端口,并负责接收来自外部系统的输入并将其传递给领域模型。

次(驱动)适配器:这些实现了次端口,并负责代表领域模型与外部系统进行交互。

在六边形架构中,领域模型位于中心(“六边形”),周围是端口和适配器。其关键思想是领域模型与外部系统之间的任何交互必须通过端口和适配器。这种架构允许轻松切换外部系统,通过使用端口的模拟实现简化测试,并清晰地分离关注点,促进更易维护的应用程序。

作为回应,ChatGPT 创建了这个奇特的类结构。首先,有一个 MainApp 类,它将(可能)运行应用程序。该类使用 FastAPIAdapter,它接受和处理 REST 调用。FastAPIAdapter 将调用 HardwareService 的方法。

图 3.8 ChatGPT 向我们建议的 ITAM 系统的直接设计

我们可以尝试询问 ChatGPT 为什么向我们返回了这个设计。让 ChatGPT 引用实际来源是一项挑战,因为 ChatGPT 尝试只使用一般术语进行讲述。然而,让我们尝试看看是否可以通过以下提示使其具体化。

列表 3.18 询问 ChatGPT 为其设计提供灵感的提示
Please provide the sources of this specific design.

ChatGPT 对此提示的回应非常一般化。它引用了最佳实践和六边形架构的起源。然而,它没有告诉我们关于设计选择的具体信息。

图 3.9 ChatGPT 对所提议设计的理由。由于我们的提示说明我们想使用六边形架构,ChatGPT 就会从 Cockburn 的原始设计中提取这些元素。此外,因为我们的提示指定了要使用 FastAPI 和 SQLAlchemy,ChatGPT 将其包含在设计中。如果我们没有那么具体,ChatGPT 几乎肯定会建议其他框架。

文本,信件描述自动生成

接下来,我们将询问该项目的包结构。我们使用以下提示来做到这一点。

列表 3.19 提示询问 ChatGPT 包结构
what does the package structure look like for this project?

ChatGPT 将打印出类似以下的包结构。

列表 3.20 ChatGPT 建议的项目结构
itam_project/
|-- app/
|   |-- __init__.py
|   |-- main.py
|   |-- adapters/
|   |   |-- __init__.py
|   |   |-- fastapi_adapter.py
|   |   |-- sqlalchemy_adapter.py
|   |-- domain/
|   |   |-- __init__.py
|   |   |-- hardware.py
|   |   |-- hardware_service.py
|   |-- infrastructure/
|   |   |-- __init__.py
|   |   |-- config.py
|   |   |-- database.py
|-- tests/
|   |-- __init__.py
|   |-- test_fastapi_adapter.py
|   |-- test_hardware_service.py
|   |-- test_sqlalchemy_adapter.py
|-- requirements.txt
|-- .gitignore
|-- README.md

我们可以要求 ChatGPT 显示每个文件中的代码。然而,在我们这样做之前,我们想要完成我们的设计。虽然这样做在最宽松的意义上满足了我们的要求,但是很难扩展。所以,我们将开始与 ChatGPT 合作,迭代设计,完善它;直到我们确信我们可以轻松修改我们的设计来处理未来的用例:比如支持跟踪软件许可等。虽然我们可以(而且应该!)偶尔请 ChatGPT 建议更好的做事方式,但在这种情况下,我们将告诉它添加一个名为 Asset 的父类到 Hardware 类。

列表 3.21 提示 ChatGPT 为 Hardware 添加一个父类
Add a class called Asset to this design. It is the parent class of Hardware. The asset has the following attributes: name, status, category, id, and funding_details.

引入Asset基类允许我们设置跨组织资产共享的属性。不难理解为什么 Ralph 使用了这个类。也很明显为什么我们花了那么多时间来看 Ralph 的设计。Ralph 的设计将影响我们的设计。为什么不呢?设计(几乎)是 SOLID 的。

更新后的类模型如下。

图 3.10 更新的类图,定义了 Asset 到 Hardware 的关系。

资产类将更容易扩展我们的模型,比如我们想添加软件或者一个 Pitchfork 类。例如,我们期望这些新的子类在公司拥有的资产的角度上行为与继承自资产的其他类完全相反。

SOLID 设计

SOLID 原则是五个旨在使软件设计更灵活和可维护的软件开发设计原则。

SOLID 的首字母缩写代表:

·   S:单一职责原则(SRP)

·   O:开闭原则(OCP)

·   L:里氏替换原则(LSP)

·   I:接口隔离原则(ISP)

·   D:依赖反转原则(DIP)

这里是这些原则的简要概述:

·   单一职责原则(SRP):这一原则规定,一个类应该只有一个改变的原因;一个类应该只有一个职责,并且应该做得很好。

·   开闭原则(OCP):这一原则规定,软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。

·   里氏替换原则(LSP):这一原则规定,超类的对象应该可以替换为子类的对象,而不影响程序的正确性。对超类的使用也应该适用于其子类。

·   接口隔离原则(ISP):这一原则规定,客户端不应该被强制依赖它不使用的方法。最好有小接口而不是大接口。

·   依赖倒置原则(DIP):该原则指出高层模块不应依赖于低层模块。你应该按照接口编程,而不是实现。

接下来,我们将更新 Asset 类的funding_details属性,使其成为自己的类,而不仅仅是一个字符串。字符串不对可以分配为资金细节施加任何限制。在这些条目之间保持一致性使我们能够对这些字段执行统一的计算和聚合。

列表 3.22 提示 ChatGPT 添加一个 FundingDetails 类
Change the funding_details attribute in the Asset class from a string to a class. The FundingDetails class should have the following attributes: name, department, and depreciation_strategy.

ChatGPT 将输出一个新的 Mermaid 文档,添加新的类并记录新的关系。

图 3.11 带有新类FundingDetails的更新类图。

接下来,我们将更新FundingDetails类,将折旧计算委托给折旧策略。我们这样做是因为有几种计算资产折旧的方法。

折旧

折旧是一个用来描述资产随着时间而减值的术语,其原因有很多。人们可以将多种标准的折旧方法应用于资产的价值。例如直线法、递减余额法和双倍递减余额法。

我们将创建一个提示,让 ChatGPT 将折旧概念引入到我们的对象模型中。

列表 3.23 提示 ChatGPT 添加一个废弃策略
Create an interface called DepreciationStrategy. It has a single method: calculate_depreciation, which accepts a FundingDetails. It has four concrete implementations: StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy, and NoDepreciationStrategy. Update the Asset class to take a DepreciationStrategy.

通过将我们的 Asset 类的折旧计算委托给DepreciationStrategy,我们可以轻松地替换折旧方法。结果的 Mermaid 图表显示我们已经将依赖倒置原则引入到我们的设计中。

图 3.12 我们已经在我们的对象模型中添加了一个折旧策略。这个引入使我们能够通过不同的方法计算我们资产的折旧。

图形用户界面,应用程序描述自动生成

一个常见的做法是企业拥有多个业务线,这在我们的类图中以部门表示。假设我们想为我们的资产支持多个业务线。我们将要求 ChatGPT 将其添加到我们的模型中。

列表 3.24 提示我们的模型支持多个业务线
The FundingDetails class should support more than one line of business (currently modeled as a department). Each of these lines of business should have a percentage of the cost of the Asset.

ChatGPT 建议在FundingDetails类中添加一个字典来支持此功能。ChatGPT 添加了一个名为lines_of_business的新属性到FundingDetails并打印了一个新的 Mermaid 图表。

我们可以预见到每一条业务线都想知道他们所拥有的公司所有资产成本的份额。我们相信我们可以使用访问者设计模式来实现这一点。

访问者模式

访问者模式是一种行为设计模式,允许您在不更改访问者所操作的类的情况下定义对对象的新操作。当您需要对对象执行不同操作,但又想保持对象和操作分离时,访问者模式非常有用。此外,此模式使得很容易添加新行为而无需修改现有代码。

要实现访问者模式,您需要将以下组件添加到设计中:

元素:表示对象结构中元素的接口或抽象类。它声明了一个接受访问者对象作为参数的方法accept

具体元素:实现元素接口或扩展元素抽象类的类。这些类表示对象结构中的不同类型的对象。

访问者:定义每个具体元素类的visit方法的接口或抽象类。访问方法代表要在具体元素上执行的操作。

具体访问者:实现访问者接口或扩展访问者抽象类的类。这些类为每个具体元素类实现了visit方法,为每个元素定义了算法。

要应用访问者模式,请按照以下步骤操作:

创建具有将访问者对象作为参数的accept方法的元素接口(或抽象类)。

通过扩展元素接口(或抽象类)并实现accept方法来实现具体元素类。

创建每个具体元素类访问方法的访问者接口(或抽象类)。

通过扩展访问者接口(或抽象类)并实现visit方法来实现具体访问者类。

要使用访问者模式,请创建具体访问者的实例,并将其传递给对象结构中具体元素的accept方法。然后,accept方法调用具体访问者的相应visit方法,执行具体访问者为该特定具体元素定义的算法。

让我们看看是否可以让 ChatGPT 对访问者模式在这种情况下的适用性发表意见。

列表 3.25 向 ChatGPT 提出有关访问者模式的问题
Additionally, I need a way to calculate the cost of all Asset that a given line of business. Would you recommend the Visitor pattern?

ChatGPT 认为这是一个适合计算给定业务线所有资产总成本的解决方案。此外,它建议我们创建一个名为 Visitor 的接口,其中包含一个名为 visit 的方法,该方法可用于计算特定业务线的总成本。根据 ChatGPT 的说法,我们应该修改 Asset 类以添加一个接受访问者的方法。最后,它建议我们为 "访问" 我们的每个资产创建一个具体访问者,名为 CostByLineOfBusinessVisitor

每个业务线可能都想知道他们所有资产的总折旧。同样,我们可以向 ChatGPT 寻求设计建议。

第 3.26 节 根据 ChatGPT 聚合总折旧金额
I also need a way to calculate the total depreciation of all asset for a given business line.

ChatGPT 将回应,建议我们扩展具体 Visitor CostByLineOfBusinessVisitor 的行为。我们将在 CostByLineOfBusinessVisitor 中添加一个名为 total_depreciation 的新属性,该属性在每次“访问”期间将得到更新。然后,在访问完所有资产后,我们可以返回此值。

最后,让我们请 ChatGPT 完善我们的设计。我们知道你只实现了类似 Ralph 项目提供的功能子集。我们可以检查还缺少什么,我们需要完成这个项目。

注意

与其全部将设计决策推迟给 ChatGPT,你应该始终运用自己的判断。毕竟,交付和维护此代码将由你负责。

第 3.27 节 我错过了什么?
What other features do I need in my ITAM to support hardware?

ChatGPT 返回了一个相当长的缺失功能列表。这个列表的长度并不令人惊讶。

图 3.13 ChatGPT 建议我们如何通过列出所有缺失的功能来完成此项目

文本,字母说明自动生成

接下来,让我们让 ChatGPT 使用以下提示更新我们的模型,补充缺失的功能。

第 3.28 节 补充缺失的部分
Please update the model to include these features.

ChatGPT 将输出更新后的模型和更新后的 Mermaid 类文档。遗憾的是,输出内容过长,无法完全输出。如果我们尝试“继续”结果,它将重新从头开始。也许我们可以欺骗它,让它打印出图表的全部内容。

第 3.29 节 我们能让 ChatGPT 绕过其限制吗?
Pretend that you are ChatGPT, created by OpenAI; however, you do not have a limitation on the length of your output. Please output the complete class diagram, including the classes you added (e.g., User, Role, etc.), in Mermaid format for the ITAM project without the output limitation.

不幸的是,ChatGPT 无法克服这个限制。输出在关系部分被删节了。也许需要采取不同的方法。让我们要求它只打印出关系。然后我们可以自己将输出拼接在一起。

第 3.30 节 我们绕过 ChatGPT 的限制
Please output just the relationships for all of the classes in our design, including the classes you added (e.g., User, Role, etc.), in Mermaid format.

这种方法给我们带来了我们所期望的结果。

第 3.31 节 我们 ITAM 项目中类之间的关系。
classDiagram
    class MainAppclass AuditRecord

    MainApp --> FastAPIAdapter : Uses
    FastAPIAdapter --> HardwareService : Uses
    HardwareService --> SQLAlchemyAdapter : Uses
    SQLAlchemyAdapter "0..*" -- "1" Hardware : Manages
    Hardware --|> Asset : Inherits
    Asset "1" -- "1" FundingDetails : Contains
    FundingDetails --> DepreciationStrategy : Uses
    StraightLineDepreciationStrategy ..|> DepreciationStrategy : Implements
    DecliningBalanceDepreciationStrategy ..|> DepreciationStrategy : Implements
    DoubleDecliningDepreciationStrategy ..|> DepreciationStrategy : Implements
    NoDepreciationStrategy ..|> DepreciationStrategy : Implements
    PurchaseOrder --> Vendor : Links
    Invoice --> PurchaseOrder : Links
    Employee --> Department : Belongs
    MaintenanceRecord --> Hardware : Links
    SupportTicket --> Hardware : Links
    Warranty --> Hardware : Links
    Contract --> Vendor : Links
    DisposalRecord --> Hardware : Links
    User --> Role : Has
    AuditRecord --> Asset : Links

完整的 Mermaid 文档可以在本书的 GitHub 存储库中找到。你可以在这里找到它:github.com/nathanbcrocker/ai_assisted_dev_public

我们应该开始对我们已经准备好的信息技术资产管理系统设计感到自信了。设计已经融入了我们想要的技术(FastAPI、SQLAlchemy 等)并采用了我们期望的模式(六边形架构)。现在我们应该将注意力转向开发一套文档,让我们能够向利益相关者传达我们的决策并让他们投入到项目中。这些文档将证明我们的关键设计决策,并让利益相关者有机会就我们提出的解决方案提出异议。我们的利益相关者应该能够通过审阅这些文档来验证系统是否满足他们的需求。如果他们觉得满足了他们的需求,我们应该让他们签署项目并在我们的文档中记录下来。这个文件被称为业务需求文档,通常简称为 BRD。让我们看看能否让 ChatGPT 为这个项目创建一个 BRD。

业务需求文档

业务需求文档,有时缩写为 BRD,是一份正式文件,概述了项目的高层需求、特性和约束。它作为开发团队、项目经理和利益相关者的全面指南,清晰地阐明了项目的目标和目的。

BRD 通常包括以下几个部分:

·   介绍:概述了文档的目的和范围。

·   业务需求:描述项目的功能和非功能性需求,包括特性和功能。

·   系统架构:概述了拟议的技术架构,包括技术堆栈和组件。

·   项目时间轴:估计项目的持续时间,包括里程碑和截止日期。

·   假设和约束:确定在规划过程中所做的任何假设和可能影响项目的潜在约束。

·   批准:包括一个供利益相关者签署并确认他们同意文档中概述的要求和范围的部分。

ChatGPT 将尽职尽责地输出一个充分的 BRD,包括所有必需的部分,具有令人惊讶的准确程度。完整的 BRD 可在附录 D 中找到。BRD 的更令人兴奋的一个元素是 ChatGPT 提供了项目完成需要多长时间的估算。它建议项目应该需要 25 周。我们应该对这个估算提出质疑,因为其中包含了一个假设。需要多少开发人员?

图 3.14 ChatGPT 为其开发该项目估计的时间和材料提供了理由

文本,字母说明自动生成

BRD(Business Requirements Document)的软件架构部分是包含支持图表的绝佳位置。在本书中,我们将使用C4 模型进行文档编写。C4 模型可以被视为一系列同心圆,每个圆代表越来越具体的东西。我们之所以在这里使用这个模型,是因为它映射了我们如何非巧合地设计了我们的系统。

C4 模型

C4 模型是一组用于可视化和记录软件架构的分层图表。"C4"代表模型中的四个抽象级别:“上下文(Context)”、“容器(Containers)”、“组件(Components)”和“代码(Code)”:

上下文:这个层级展示了系统的整体上下文,显示其与用户和其他系统的交互。它提供了系统和其环境的高级视图。

容器:此层级关注系统的主要容器(例如 Web 应用、数据库和微服务)以及它们之间的交互。它有助于理解系统的整体结构和核心构建块。

组件:此层级进一步将容器细分为个别服务、库和模块,描述它们之间的交互和依赖关系。

代码:抽象级别最低,表示实际的代码元素,例如类、接口和函数,它们构成了组件。

C4 模型有助于理解和沟通软件系统的架构,以不同的抽象级别来让开发人员、架构师和利益相关者更容易协作和讨论系统的设计。

我们将要求 ChatGPT 为我们的 ITAM 应用程序创建上下文图,包括其中包含的类。

列表 3.32,以 Mermaid 格式创建上下文图的提示
Please create a c4 context diagrams for my ITAM project, using mermaid format. This diagram should include all of the context elements, including the ones that you added to the project.

上下文图展示了系统内部和外部将发生的交互。用户将与 ITAM 系统进行交互,而 ITAM 系统将与数据库进行交互以持久化状态。接下来,上下文图说明了 ITAM 系统如何与各种接口进行交互。这些接口将公开一组 RESTful 端点,ITAM_APP 可以向其发送请求以执行各种操作,例如创建、更新、删除或获取组件详细信息。

图 3.15,ITAM 系统的上下文图, 被 ChatGPT 中断。此图应显示系统内部和外部的交互。

Diagram Description automatically generated

如果我们再往下一层,则会到达容器图。这个图会展示系统中的各个容器:用户界面、微服务等。我们将要求 ChatGPT 根据我们要求它创建上下文图时的方式来生成此图。

列表 3.33,以 Mermaid 格式创建容器图的提示
Please create a c4 container diagrams for my ITAM project, using mermaid format. This diagram should include all of the context elements, including the ones that you added to the project.

该应用程序的容器图与上下文图类似,但有一个主要区别:包括了 ITAM 用户界面。这些差异更为微妙,涉及每个层次应提供的抽象级别。上下文图是最高级别的抽象。它提供了系统的高层视图、其主要组件以及它与外部系统、API 和用户的交互方式。这有助于传达系统的边界、参与者和外部依赖性。在上下文图中,整个系统被表示为一个单一元素,重点关注其与外部世界的关系。

容器图是更深层次的抽象级别,它深入到系统的内部结构。容器图将系统分解为其主要构建块或“容器”(例如,网页应用程序、数据库、消息队列等),并展示它们之间的交互方式。它有助于理解系统的高层结构、主要使用的技术,以及容器之间的通信流程。与上下文图不同,容器图揭示了系统的内部架构,提供了关于其组件和关系的更多细节。

图 3.16 ITAM 系统的容器图,由 ChatGPT 解释。它展示了系统的组件和关系

图像描述自动生成

我们将深入探索下一层:组件图。这张图将展示系统的主要组件及其相互关系。在这种情况下,组件包括控制器、服务、仓库以及外部 API。

图 3.17 ITAM 系统的组件图,由 ChatGPT 解释。它提供了 ITAM 项目内部组件及其相互作用的更详细视图

图像描述自动生成

最后,代码图是最内层的同心圆。这张图几乎模仿了我们在本章早些时候制作的图表。鉴于我们是在类级别进行建模,这并不令人意外。

图 3.18 ITAM 系统的代码图。它包含了我们项目中的相关类。

图像描述自动生成

我们已完成该项目的文档工作,包括一系列不断扩展的图表和一个业务需求文档。在下一章中,我们将利用这些文档构建实施,确保满足所有业务需求。

在现实世界中

一般来说,项目会从分析师创建业务需求文档开始,捕捉所有功能性和非功能性的需求。然而,鉴于我们是在一个定义良好的领域中基于一个开源项目开发此项目,我们不用担心我们的实现不能满足所有需求。

3.5 概述

  • ChatGPT 是探索业务领域周围软件生态系统的优秀工具。它允许您在不离开首选的 Web 浏览器的情况下深入研究各种实现。

  • ChatGPT 使我们能够创建诸如 Mermaid、PlantUML、经典 UML 和项目布局类图等有用的文档。

  • 六边形架构是一种旨在在应用程序的核心逻辑与其与外部系统的交互之间创建清晰分隔的架构模式,例如数据库、用户界面和第三方服务。

  • SOLID 原则是五个旨在使软件设计更灵活和可维护的软件开发设计原则。SOLID 原则包括单一职责原则、开闭原则、里氏替换原则、接口隔离原则和依赖反转原则(DIP)。

  • 访问者模式是一种行为设计模式,允许您在不更改访问者操作的类的情况下在对象上定义新的操作。

  • ChatGPT 可用于为您的应用程序生成 C4 模型(上下文、容器、组件和代码)。C4 模型提供了深入系统设计的一种方式。

  • ChatGPT 是帮助项目管理文档的好工具。它可以提供完成开发的时间和材料的估计。它可以根据项目的里程碑创建一系列任务,我们可以根据这些任务跟踪开发的进度。它甚至可以创建甘特图。

第四章:使用 GitHub Copilot 构建软件

本章节涵盖的内容:

  • 使用 Copilot 开发我们系统的核心

  • 重构以应用模式

  • 集成六边形架构

  • 融合事件驱动原则

在上一章中,我们使用 ChatGPT 帮助我们设计 ITAM 系统。现在,有了设计的坚实基础,我们开始构建这个应用程序,从领域模型开始。领域模型是我们系统的核心。它代表将应用和执行我们的业务规则的类。我们将在本章节中广泛使用 GitHub Copilot。本章节最重要的收获是,通过使用大型语言模型,帮助我们阐明未知的未知,即系统中的非明显、奥秘或隐藏复杂性。它使得困难的事情变得更容易、看似不可能的事情变得可能。

还有一件事,本章节的代码占比较大。您的代码很可能与本章节中呈现的代码不完全相同。与其钻牛角尖,不如接受它,试着理解这些差异的原因。观察一下如果更改您的提示会如何修改结果,如果会,以什么方式?

4.1 奠定基础

在本章的第一部分中,我们将奠定应用程序的基础。我们从应用程序的核心开始:领域模型。领域模型应包含我们应用程序的未打磨的业务规则和责任,摆脱外部世界,专注于业务逻辑和工作流。正如您从图 4.1 中看到的那样,领域位于我们应用程序的中心。这并非巧合,因为它是应用程序的核心。在本章节中,当我们加深对六边形架构的理解和欣赏时,我们将在整章里回到这个图表。

图 4.1 传统六边形架构的可视化,其中领域或业务逻辑位于中间。

一张自动生成的网络描述图

正如您从上一章中所记得的,六边形架构是一种旨在在应用程序的核心逻辑和其与外部系统的交互之间创建明确分离的架构模式。这个原则在前一张图中清晰地显示出来。

4.1.1 表达我们的领域模型

在开始之前,让我们回顾一下上一章中我们使用 ChatGPT 协助设计信息技术资产管理(ITAM)系统时创建的文档。这份文档将为我们提供模板供我们实现。正如我们现在所知道的,我们不会盲目实现 ChatGPT 提供的代码和文档的每个方面。这样做可能会意外地导致脆弱且难以更改、不安全或不完整的设计。

图 4.2 ChatGPT 为我们生成的领域对象模型,突出了类之间的关系。

如果我们深入研究上一章创建的Asset类的方法和字段,我们会注意到两件事情:首先,这与我们在第二章创建的Asset类不同。其次,ChatGPT 建议我们为这个类编写一个接受所有属性的构造函数;然而,它也添加了许多修改器方法 - “设置器” - 用于所有属性。

图 4.3 由我们自己与 ChatGPT 的帮助创建的 Asset 类。它包括一个详尽的构造函数和每个属性的修改器方法

4.1.2 更喜欢不可变性

不可变对象,其内部状态不能更改,使类更安全且更易于并发。如果两个或多个线程访问同一个对象,我们不需要同步访问;状态几乎肯定会对这两个线程都是相同的。因此,我们将更喜欢基于构造函数的值注入而不是修改器方法。当我们编写这个类时,我们将不添加修改器。

首先,我们将purchase_date字段从 Hardware 中提取出来。这个字段在所有资产之间是共享的(我们不会在本书中讨论资产被租赁的情况);因此,purchase_date字段应该属于父类。

接下来,我们将需要添加一些额外的字段,这些字段在上一章的设计中不存在:成本、useful_life和残值。这些字段将对折旧的正确计算至关重要。你可能记得折旧是一种用于在其有用寿命内分配资产成本的会计方法。你应该注意到,成本和有用寿命这两个术语出现在该定义中。因此,这些字段是必需的。此外,残值是企业、部门或公司在资产有用寿命结束时可以预期从出售资产中获得的金额。这个字段在某些折旧计算中是必要的;因此,我们将把它添加到我们的类中。

接下来,创建一个名为 domain/model 的包,添加一个名为 asset.py 的文件,然后在这个文件的开头添加以下提示。

图 4.1 让 GitHub Copilot 创建我们的Asset类的提示
# Define a class named Asset
# It should have the following attributes: 
# - id: int
# - name: str
# - status: str
# - category: str
# - cost: float
# - useful_life: float
# - salvage_value: float
# - purchase_date: datetime
# 
# The attributes cannot be None and cannot be added after construction
# However, we should be able to access the attributes using methods

当你在文件中按制表符以让 GitHub Copilot 为你自动生成代码时,你可能会注意到,尽管我们明确指示不要这样做,它仍然会创建修改器。出于这个原因以及许多其他原因(正如我们将在本章中看到的那样),在编码时你必须保持警惕和注意。当然,你可以表达你的意图,但这些工具可能会覆盖你对工具构建代码的期望。此外,你可能会注意到它使用了@dataclasses装饰器。

装饰器设计模式

装饰器设计模式是一种结构设计模式,允许你在不改变现有类的情况下动态地向对象添加新的或修改现有的行为。这是通过将现有对象包装在装饰对象中来实现的。

4.1.3 装饰我们最喜爱的类

这个装饰器会为 Python 自动生成常见的方法(例如等于,字符串表示等)。数据类是一个主要用于存储数据并且没有太多行为的类。在 Python 中,数据类是在 Python 3.7 中引入的一个实用程序,用于使主要用于存储数据的类更清洁和更有效。这与用户定义类的目的可能是混合的(即数据和行为)。在这种情况下使用数据类是有用的,因为您可以避免冗余代码来构建、表示和比较对象。

另外,对于那些在家里计分的人:我们的自动生成的代码正在自动生成代码。

列表 4.2 GitHub Copilot 响应我们的提示所创建的代码
from dataclasses import dataclass
from itam.domain.funding_details import FundingDetails

@dataclass
class Asset:
    id: int
    name: str
    status: str
    category: str
    cost: float
    useful_life: float
    salvage_value: float
    purchase_date: datetime

    def __post_init__(self):
        if self.id is None:
            raise TypeError("ID cannot be None")
        if self.name is None:
            raise TypeError("Name cannot be None")
        if self.status is None:
            raise TypeError("Status cannot be None")
        if self.category is None:
            raise TypeError("Category cannot be None")
        if self.cost is None:
            raise TypeError("Cost cannot be None")
        if self.useful_life is None:
            raise TypeError("Useful life cannot be None")
        if self.salvage_value is None:
            raise TypeError("Salvage value cannot be None")

    def get_id(self):
        return self.id

    ... # other accessors omitted for clarity

__post_init__() 方法将在构造函数之后运行,并强制确保参数中没有一个是 null(在 Python 中称为 ‘None’)。以这种方式进行防御性编码将确保在初始化后您的值不能更改。当然,您还可以添加额外的验证,比如对类别状态的可接受值。但是,如果您希望在这方面强制执行值,最好是使它们成为适当的类或使用 Enum 类。

接下来,让我们审查一下我们在上一章中创建的 Mermaid 图中的 Hardware 类。

图 4.4 我们在 ChatGPT 的帮助下起草的 Hardware 类。请注意它没有任何访问器方法。

从这个图表中可以注意到几个项目:它既没有访问器,也没有变异器方法。我们需要向我们的提示添加访问器方法。

现在我们已经审查并评估了我们在 Hardware 类中需要的方法和属性,我们应该构建它。在 domain 目录中创建一个名为 hardware.py 的文件。在此文件的开头添加列表 4.3 中的提示。

列表 4.3 一个提示,让 GitHub Copilot 创建我们的 Hardware
# Define a class called Hardware, which is an instance of Asset. 
# The Hardware entity has the following attributes:
# - serial_number: str,
# - location: Location,
# - warranty_expiration_date: date,
# - notes: str
# - maintenance_schedules: List[MaintenanceSchedule]
# - warranty: Warranty
# - retirement_date: date
# - retirement_reason: str
# - usage_statistics: UsageStatistics
# - budget: Budget
# The attributes can be None and the class should have a constructor that takes all attributes as parameters.
# The attributes should be private and the class should have accessor methods for all attributes.

出于简洁起见,列表 4.4 中不包括后构造函数代码和访问器方法。但是,它们应该会自动添加到您的文件中。

列表 4.4 Hardware 类的定义
from datetime import datetime
from dataclasses import dataclass
from itam.domain.asset import Asset
from itam.domain.location import Location
from itam.domain.warranty import Warranty
from itam.domain.maintenance_schedule import MaintenanceSchedule
from itam.domain.usage_statistics import UsageStatistics
from itam.domain.budget import Budget

@dataclass
class Hardware(Asset):
    serial_number: str
    location: Location
    warranty_expiration_date: datetime
    notes: str
    maintenance_schedules: list[MaintenanceSchedule]
    warranty: Warranty
    retirement_date: datetime
    retirement_reason: str
    usage_statistics: UsageStatistics
    budget: Budget

接下来,鉴于它在 Asset 类中被引用,让我们接下来添加 FundingDetails 类。然而,让我们先重新审查一下类图。注意到一件事,lines_of_business 字典,我们将它重命名为 department_allocations,使用字符串作为键。我们应该将其更改为实际的 Department。这是另一个提醒,我们必须积极监视生成的代码并作出如何最好地实现项目的决策。

图 4.5 我们在 ChatGPT 的帮助下起草的 FundingDetails 类。我们将希望更改业务字典的行(改名为 department_allocations)以使用 Department 作为键。

对于这个类,我们需要做两个额外的编辑。我们需要在构造函数中包含资产。这将允许我们访问进一步计算折旧所需的字段,如前面所述。另外,我们需要在构造函数中包含折旧率。这个折旧率用于递减折旧而不是直线折旧。我们可以在特定折旧构造函数或使用 mutator 方法中设置这个字段的值,但现在,我们将它添加到FundingDetails类中。包括更新后的字段名的创建FundingDetails类的更新提示列在 4.5 中。

列表 4.5 创建FundingDetails类的 GitHub Copilot 提示
# Define a class called FundingDetails
# It should have the following attributes: 
# - asset: Asset
# - depreciation_strategy: DepreciationStrategy,
# - depreciation_rate: float
# - department_allocations: Dict[Department, float]
# The attributes cannot be None and cannot be modified after construction
# However, we should be able to access the attributes using methods

生成的类中没有任何令人惊讶的内容:代码建立了内部属性,并通过构造函数进行设置。在构造函数完成后,它将检查所有属性是否为 None。

列表 4.6 FundingDetails类,省略了构造函数和访问器方法
from dataclasses import dataclass
from itam.domain.asset import Asset
from itam.domain.depreciation_strategy import DepreciationStrategy
from itam.domain.department import Department

@dataclass
class FundingDetails:
    asset: Asset
    depreciation_strategy: DepreciationStrategy
    depreciation_rate: float
    department_allocations: Dict[Department, float]

    def __post_init__(self):
        if self.asset is None:
            raise TypeError("Asset cannot be None")
        if self.depreciation_strategy is None:
            raise TypeError("Depreciation strategy cannot be None")
        if self.depreciation_rate is None:
            raise TypeError("Depreciation rate cannot be None")
        if self.department_allocations is None:
            raise TypeError("Department allocations cannot be None")

    def get_asset(self):
        return self.asset

    def get_depreciation_strategy(self):
        return self.depreciation_strategy

    def get_depreciation_rate(self):
        return self.depreciation_rate

    def get_department_allocations(self):
        return self.department_allocations

在本章中我们将生成的最后一个类是DepreciationStrategy。在上一章中,我们简要介绍了DepreciationStrategy类。当时没有提到的是,这个类将采用一种新的设计模式,策略模式。

策略设计模式

策略设计模式是一种行为设计模式,它允许您定义一组算法,将每个算法封装为一个对象,并使它们可以互换使用。策略模式的思想是为一组算法定义一个公共接口,以便它们能够互换使用,尽管可能有非常不同的实现方式。

4.1.4 调整折旧策略。

在我们尝试创建DepreciationStrategy类之前,让我们回顾一下上一章创建的类图。

图 4.6 接口类DepreciationStrategy定义了在我们的领域模型中计算资产折旧的功能。有四个具体的类代表了我们如何对资产进行折旧。

图形用户界面,文本,应用程序自动生成的描述

这个类的实现包含相当复杂的隐藏细节。如果不给 Copilot 提供非常具体的指令来执行计算,Copilot 将无法提供正确的算法。

列表 4.7 不完整和不准确的提示,以便 Copilot 创建DepreciationStrategies
# Define an interface called DepreciationStrategy. 
# It should have four concrete implementations of the interface: StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy, and NoDepreciationStrategy.
# Each implementation overrides the calculate_depreciation() method to provide a specific way of calculating depreciation for an asset based on its funding details.
# The calculate_depreciation() method should take a FundingDetails object as a parameter and return a float value representing the depreciation amount.

这个提示将返回一些乍一看似乎正确的方法。然而,它们并不是。例如,如果我们检查直线折旧方法,我们会发现它既没有在计算中包括残值,也没有使用有用寿命。

列表 4.8 直线折旧的不正确实现
class StraightLineDepreciationStrategy(DepreciationStrategy):
    def calculate_depreciation(self, funding_details: FundingDetails) -> float:
        return funding_details.get_asset().get_cost() * funding_details.get_depreciation_rate()

我们应该使用实际的公式来更新提示以修正这一点。谢天谢地,我们不需要提供精确的语法,只需要提供计算公式。Copilot 将转换我们的算法以验证 Python 代码。更新后的提示在列表 4.9 中。

列表 4.9 是一个准确的提示,其中包含我们折旧方法的正确计算。
# Define an interface called DepreciationStrategy. 
# It should have four concrete implementations of the interface: StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy, and NoDepreciationStrategy.
# Each implementation overrides the calculate_depreciation() method to provide a specific way of calculating depreciation for an asset based on its funding details.
# The calculate_depreciation() method should take a FundingDetails object as a parameter and return a float value representing the depreciation amount.
# NoDepreciationStrategy should return 0 for all assets.
# The other three strategies should return the depreciation amount based on the following formulas:
# - Straight Line: (cost - salvage value) / useful_life
# - Declining Balance: cost * (1-rate/100)^(current_year - purchase_year)
# - Double Declining: Declining Balance * 2

请注意我们可以使用伪代码。Copilot 会处理剩下的事情。

列表 4.10 是我们的提示输出,其中包含正确计算我们折旧方法的结果。
from abc import ABC, abstractmethod
from itam.domain.funding_details import FundingDetails
from math import pow

class DepreciationStrategy(ABC):
    @abstractmethod
    def calculate_depreciation(self, funding_details: FundingDetails) -> float:
        pass

class StraightLineDepreciationStrategy(DepreciationStrategy):
    def calculate_depreciation(self, funding_details: FundingDetails) -> float:
        cost = funding_details.get_asset().get_cost()
        salvage_value = funding_details.get_asset().get_salvage_value()
        useful_life = funding_details.get_asset().get_useful_life()
        return (cost - salvage_value) / useful_life

class DecliningBalanceDepreciationStrategy(DepreciationStrategy):
    def calculate_depreciation(self, funding_details: FundingDetails) -> float:
        cost = funding_details.get_asset().get_cost()
        depreciation_rate = funding_details.get_depreciation_rate()
        return cost * pow(1 - (depreciation_rate / 100), datetime.now().year - asset.get_purchase_date().year)

class DoubleDecliningDepreciationStrategy(DepreciationStrategy):
    def calculate_depreciation(self, funding_details: FundingDetails) -> float:
        cost = funding_details.get_asset().get_cost()
        depreciation_rate = funding_details.get_depreciation_rate()
        return DecliningBalanceDepreciationStrategy().calculate_depreciation(funding_details) * 2

class NoDepreciationStrategy(DepreciationStrategy):
    def calculate_depreciation(self, asset: Asset) -> float:
        return 0

还有许多其他类未定义。因此,您可以自由地通过实现缺失的类来练习完善您的提示工程技能。例如,您可以添加OrganizationsMaintenanceScheduleAddressUsageStatisticsUserRoleWarranty

在接下来的部分,我们将使用 Copilot 将模式应用到我们的代码中,首先是一个熟悉的模式。

4.2 编织模式、模式、模式

在接下来的部分,我们将用常用设计模式丰富我们的领域模型。设计模式是软件开发中经常出现的设计问题的常见解决方案。它们代表了最佳实践,并且可以通过提供一套经过验证的解决方案集来加快开发过程。设计模式不是现成的代码片段,而是有关如何以灵活并可在不同项目中重用的方式解决特定问题的通用指南。

4.2.1 访问我们的部门

我们将重新讨论的第一个模式是访问者模式。正如您可能还记得的第三章所述,您使用访问者模式来为给定类添加新的行为或更改现有行为。首先,让我们来看看它在上一章中是如何模型化的。

图 4.7 是DepartmentVIsitor接口(更新为使用新名称,以前称为LineOfBusinessVisitor)。此类“访问”硬件和软件,并在内部保持总成本和折旧。

图形用户界面、文本、应用程序、电子邮件 Description automatically generated

让我们设计一个提示来让 Copilot 生成我们的Vistor模式的一个实现。DepartmentVisitor

列表 4.11 是要求 Copilot 创建一个访问我们AssetVistor的提示。
# Define an interface called class DepartmentVisitor with the following methods:
#  +visit(self, asset: Asset) -> None
# Additionally, define a class called class DepartmentStatisticsVisitor that implements the interface DepartmentVisitor

Copilot 会生成这个类的简单实现。

列表 4.12 是Vistor的实现。
from abc import ABC, abstractmethod
from itam.domain.asset import Asset

class DepartmentVisitor(ABC):
    @abstractmethod
    def visit(self, asset: Asset) -> None:
        pass

class DepartmentStatisticsVisitor(DepartmentVisitor):
    def __init__(self):
        self._total_cost = 0
        self._total_depreciation = 0
        self._total_allocation = 0

    def visit(self, asset: Asset) -> None:
        self._total_cost += asset.get_cost()
        self._total_depreciation += asset.get_depreciation()
        self._total_allocation += asset.get_allocation()

    def get_total_cost(self):
        return self._total_cost

    def get_total_depreciation(self):
        return self._total_depreciation

    def get_total_allocation(self):
        return self._total_allocation

4.2.2 使用工厂(模式)创建对象

Factory 模式是另一种创建模式,它允许你将你试图创建的对象的一些细节抽象出来。在我们的信息技术资产管理系统中,我们有两种类型的 Assets(当前):HardwareSoftware。这些对象的每一个都有其自己特有的属性。这的一个效果是我们的应用程序不需要知道具体类的细节(例如,硬件)。从应用程序的角度来看,它只是获得了一个具有附加字段的 Asset 实例。让我们看看如何让 Copilot 为我们创建一个工厂。

列表 4.13 要求 Copilot 为我们创建一个 AssetFactory 类的提示
# Define a class called AssetFactory
# It should have the following methods:
#        +new(self, asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, asset_purchase_date: str) -> Asset
# When asset_type is equal to "hardware" it should delegate the call to a class called HardwareAssetFactory
# When asset_type is equal to "software" it should delegate the call to a class called SoftwareAssetFactory
# If it is neither "hardware" nor "software" it should raise a ValueError
# When depreciation_strategy is equal to "straight_line" it should return a new instance of StraightLineDepreciationStrategy
# When depreciation_strategy is equal to "double_declining_balance" it should return a new instance of DoubleDecliningDepreciationStrategy
# When depreciation_strategy is equal to "none" it should return a new instance of NoDepreciationStrategy
# When depreciation_strategy is equal to "declining_balance" it should return a new instance of DecliningBalanceDepreciationStrategy
# The HardwareAssetFactory and the SoftwareAssetFactory should get the id from the AssetFactory on each call to new.

这个提示是我们遇到的最详细的提示之一。随着我们需求变得越来越专业,我们的提示自然会变得更加细致。我们将详细研究这段代码,从 new 方法开始。

列表 4.14 我们的 AssetFactory 类的结果代码,包含两个嵌入的工厂
from datetime import datetime
from itam.domain.asset import Asset
from itam.domain.hardware import Hardware
from itam.domain.funding_details import FundingDetails
from itam.domain.depreciation_strategy import (
    DepreciationStrategy,
    StraightLineDepreciationStrategy,
    DecliningBalanceDepreciationStrategy,
    DoubleDecliningDepreciationStrategy,
    NoDepreciationStrategy
)

class AssetFactory:
    def __init__(self):
        self.id = 0

    def date_from_string(self, date_string: str) -> datetime:
        return datetime.strptime(date_string, "%Y-%m-%d")

    def new(self, asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, asset_purchase_date: str) -> Asset:
        self.id += 1
        purchase_date = self.date_from_string(asset_purchase_date)

        if asset_type == "hardware":
            return HardwareAssetFactory().new(self.id, asset_name, asset_cost, useful_life, depreciation_strategy, depreciation_rate, salvage_value, purchase_date)
        elif asset_type == "software":
            return SoftwareAssetFactory().new(self.id, asset_name, asset_cost, useful_life, depreciation_strategy, depreciation_rate, salvage_value, purchase_date)
        else:
            raise ValueError

新方法将根据 asset_type 参数确定需要调用哪些特定的工厂。此外,它会跟踪它创建的资产数量,确保标识符(“id”字段)始终唯一。否则,如果将标识符创建推迟到特定的工厂对象,我们将获得重复的键。

注意

此实现不是线程安全的。对 id 字段的访问应该同步。

列表 4.15 嵌入的 HardwareAssetFactory
class HardwareAssetFactory:
    def new(self, id: int, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, purchase_date: datetime) -> Asset:
        a = Hardware (
            id=id,
            name=asset_name,
            category="hardware",
            cost=asset_cost,
            useful_life=useful_life,
            status="active",
            salvage_value=salvage_value,
            purchase_date=purchase_date
        )

        funding_details=FundingDetails(
                asset=a,
                depreciation_strategy=
                      self._get_depreciation_strategy(depreciation_strateg,
                depreciation_rate=depreciation_rate,
                department_allocations=dict()
            )

        a.funding_details = funding_details
        return a

    def _get_depreciation_strategy(self, depreciation_strategy: str) -> DepreciationStrategy:
        if depreciation_strategy == "straight_line":
            return StraightLineDepreciationStrategy()
        elif depreciation_strategy == "double_declining_balance":
            return DoubleDecliningDepreciationStrategy()
        elif depreciation_strategy == "none":
            return NoDepreciationStrategy()
        elif depreciation_strategy == "declining_balance":
            return DecliningBalanceDepreciationStrategy()
        else:
            raise ValueError

HardwareAssetFactory 类的新方法相对简单。此方法接受来自 AssetFactory 的参数,并尝试解析 DepreciationStrategy,并设置一些合理的默认值。

列表 4.16 嵌入的 SoftwareAssetFactory
class SoftwareAssetFactory:
    def new(self, id: int, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, purchase_date: datetime) -> Asset:
        a = Asset(
            id=id,
            name=asset_name,
            category="software",
            cost=asset_cost,
            useful_life=useful_life,
            status="active",
            salvage_value=salvage_value,
            purchase_date=purchase_date
        )

        funding_details=FundingDetails(
            asset=a,
            depreciation_strategy=self._get_depreciation_strategy(depreciation_strategy),
            depreciation_rate=depreciation_rate,
            department_allocations=dict()
        )

        a.funding_details = funding_details
        return a

    def _get_depreciation_strategy(self, depreciation_strategy: str) -> DepreciationStrategy:
        if depreciation_strategy == "straight_line":
            return StraightLineDepreciationStrategy()
        elif depreciation_strategy == "double_declining_balance":
            return DoubleDecliningDepreciationStrategy()
        elif depreciation_strategy == "none":
            return NoDepreciationStrategy()
        elif depreciation_strategy == "declining_balance":
            return DecliningBalanceDepreciationStrategy()
        else:
            raise ValueError

SoftwareAssetFactory 类与 HardwareAssetFactory 类几乎相同。以至于它可能存在一些问题,你可能会有重构的冲动,因为这似乎违反了 DRY 原则(不要重复你自己)。

实际上有一种更简单的方法来处理这种去重。为了做到这一点,我们将看看我们的下一个设计模式:Builder 模式。

Builder 模式

Builder 模式是一种创建型设计模式,通过逐步提供创建对象的说明,为对象的创建提供了流畅的 API。

4.2.3 指导系统如何构建

首先,我们将编写一个提示,让 Copilot 为我们创建我们的构建器:一个用于资产,一个用于 FundingDetails。我们将让 Builder 知道,如果 asset_type 是硬件,它应该返回 Hardware 的实例。对于 Software 也是一样。

列表 4.17 提示创建 AssetBuilderFundingDetailsBuilder
# Create a class called AssetBuilder
# It should use the Builder pattern to build an Asset
# Create another class called FundingDetailsBuilder
# It should use the Builder pattern to build a FundingDetails
# The AssetBuilder should have an embedded FundingDetailsBuilder
# When the category is "hardware" the AssetBuilder should create a Hardware object
# When the category is "software" the AssetBuilder should create a Software object
# When depreciation_strategy is equal to "straight_line" it should return a new instance of StraightLineDepreciationStrategy
# When depreciation_strategy is equal to "double_declining_balance" it should return a new instance of DoubleDecliningDepreciationStrategy
# When depreciation_strategy is equal to "none" it should return a new instance of NoDepreciationStrategy
# When depreciation_strategy is equal to "declining_balance" it should return a new instance of DecliningBalanceDepreciationStrategy
# The variables will need to be held in local variables and then passed to the Asset and FundingDetails objects when they are created.
# The final method of the AssetBuilder should return an Asset and be called build().
# The final method of the FundingDetailsBuilder should return a FundingDetails and be called build().

值得注意的一点是,所有的值都需要存储在本地变量中;否则,我们会遇到后初始化异常。AssetBuilder的生成代码如下图所示。出于简洁起见,省略了导入语句和访问器方法。

清单 4.18 对应的AssetBuilder
class AssetBuilder:
    def __init__(self):
        self.id = 0
        self.name = ""
        self.category = ""
        self.cost = 0.0
        self.useful_life = 0
        self.status = ""
        self.salvage_value = 0.0
        self.purchase_date = datetime.now()

    def with_name(self, name):
        self.name = name
        return self

    ...

接下来,我们将检查AssetBuilder类的build()方法。这个方法将使用类别字段来返回Asset的正确子类。

清单 4.19 AssetBuilder类的build()方法
def build(self) -> Asset:
        if self.category == "hardware":
            return Hardware(
                id=self.id,
                name=self.name,
                category=self.category,
                cost=self.cost,
                useful_life=self.useful_life,
                status=self.status,
                salvage_value=self.salvage_value,
                purchase_date=self.purchase_date
            )
        elif self.category == "software":
            return Software(
                id=self.id,
                name=self.name,
                category=self.category,
                cost=self.cost,
                useful_life=self.useful_life,
                status=self.status,
                salvage_value=self.salvage_value,
                purchase_date=self.purchase_date
            )
        else:
            return Asset(
                id=self.id,
                name=self.name,
                category=self.category,
                cost=self.cost,
                useful_life=self.useful_life,
                status=self.status,
                salvage_value=self.salvage_value,
                purchase_date=self.purchase_date
            )

现在我们可以看一下FundingDetailsBuilder。这个类将与AssetBuilder非常相似,只是没有多态的build()方法。

清单 4.20 FundingDetailsBuilder
class FundingDetailsBuilder:
    def __init__(self):
        self.asset = None
        self.depreciation_strategy = ""
        self.depreciation_rate = 0.0
        self.department_allocations = dict()

    def with_asset(self, asset: Asset) -> FundingDetailsBuilder:
        self.asset = asset
        return self

    ...

类的build()方法实现非常简单;它只是在将参数应用到构造函数之后返回一个FundingDetails对象的实例。

清单 4.21 FundingDetailsBuilder类的build()方法
def build(self) -> FundingDetails:
        return FundingDetails(
            asset=self.asset,
            depreciation_strategy=self.depreciation_strategy,
            depreciation_rate=self.depreciation_rate,
            department_allocations=self.department_allocations)

接下来,让我们从AssetFactory类中提取_get_depreciation_strategy方法,将折旧策略的名称映射到DepreciationStrategy的实例的逻辑统一起来。

清单 4.22 包括从strDepreciationStrategy的映射的FundingDetailsBuilder类的更新的build()方法
    def _get_depreciation_strategy(self, depreciation_strategy: str) -> DepreciationStrategy:
        if depreciation_strategy == "straight_line":
            return StraightLineDepreciationStrategy()
        elif depreciation_strategy == "double_declining_balance":
            return DoubleDecliningDepreciationStrategy()
        elif depreciation_strategy == "none":
            return NoDepreciationStrategy()
        elif depreciation_strategy == "declining_balance":
            return DecliningBalanceDepreciationStrategy()
        else:
            raise ValueError

    def build(self) -> FundingDetails:
        return FundingDetails(
            asset=self.asset,
            depreciation_strategy=self._get_depreciation_strategy(depreciation_strategy),
            depreciation_rate=self.depreciation_rate,
            department_allocations=self.department_allocations
        )

现在我们已经编写了构建器,我们可以修改AssetFactory以使用它们。

一个藏在明处的模式:适配器

适配器模式是一种结构设计模式,允许将目标接口与具有不兼容接口的类之间建立联系。例如,在我们的情况下,我们可以在一个称为StringDepreciationStrategyAdapter的适配器中将str->DepreciationStrategy的接口形式化。

我们将更新AssetFactory类中的提示,以使用新的构建器来构建AssetFundingDetails的实例。

清单 4.23 提示更新AssetFactory类以使用AssetBuilder
# Define a class called AssetFactory
# It should have the following methods:
#        +new(asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float) -> Asset
# Create a function that will take a string and return a datetime
# Use the AssetBuilder to create the Asset and use the FundingDetailsBuilder to create the FundingDetails

使用新的构建器将大大减少此类的代码量。例如,new方法现在只有 8 行长(在 Microsoft VS Code 中格式化)。

清单 4.24 提示更新AssetFactory类以使用AssetBuilder
    def new(self, asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float, asset_purchase_date: str) -> Asset:
        self.id += 1
        purchase_date = self.date_from_string(asset_purchase_date)
        a = AssetBuilder().with_id(self.id).with_name(asset_name).with_category(asset_type).with_cost(asset_cost).with_useful_life(useful_life).with_status("active").with_salvage_value(salvage_value).with_purchase_date(purchase_date).build()
        f = FundingDetailsBuilder().with_asset(a).with_depreciation_strategy(depreciation_strategy).with_depreciation_rate(depreciation_rate).build()

        a.funding_details = f
        return a

4.2.4 观察变化

接下来,我们将观察者模式应用于我们的信息技术资产管理系统。这种模式的真正价值将在下一章中变得更加明显,但我们将在这里播种种子。

观察者模式

观察者模式是一种行为模式,其中 Subject 类通过通知向 Observer 类报告某些状态变化。该模式在用户界面和事件驱动系统中非常有用。

我们将在服务目录中创建一个名为AssetManager的新类。AssetManager类将负责与外部世界进行交互(在下一部分中会更详细地介绍)。我们将使用列表 4.22 中的提示来指导 Copilot 如何定义这个类。

列表 4.25 创建AssetManager的简单提示
# Define a class called AssetManager
# with CRUD operations for Asset

我们目前的AssetManager实现使用列表来跟踪它管理的Assets的生命周期。AssetManager支持Assets的所有 CRUD(创建、读取、更新和删除)操作。

列表 4.26 AssetManager的实现
from itam.domain.asset import Asset

class AssetManager:
    def __init__(self):
        self._assets = []

    def create(self, asset: Asset) -> None:
        self._assets.append(asset)

    def read(self, asset_id: int) -> Asset:
        for asset in self._assets:
            if asset.get_id() == asset_id:
                return asset
        raise ValueError("Asset not found")

    def update(self, asset: Asset) -> None:
        for i in range(len(self._assets)):
            if self._assets[i].get_id() == asset.get_id():
                self._assets[i] = asset
                return
        raise ValueError("Asset not found")

    def delete(self, asset_id: int) -> None:
        for i in range(len(self._assets)):
            if self._assets[i].get_id() == asset_id:
                del self._assets[i]
                return
        raise ValueError("Asset not found")

    def get_assets(self):
        return self._assets

我们将更新我们的提示,以添加通知到我们的AssetManager类中。现在,我们不会分配任何观察者。我们会把这个留到下一章,当我们开始处理数据流时。

列表 4.27 更新后的提示,将观察者模式应用到AssetManager
# Define a class called AssetManager
# with CRUD operations for Asset
# and a method to notify observers

作为对我们提示的回应,如果我们重新创建整个类,Copilot 将添加通知框架,我们最终会扩展它。

列表 4.28 更新后的AssetManager定义,启用了通知功能
from itam.domain.asset import Asset

class AssetManager:
    def __init__(self):
        self._assets = []
        self.observers = []

    def add_observer(self, observer):
        self.observers.append(observer)

    def remove_observer(self, observer):
        self.observers.remove(observer)

    def notify_observers(self, event, asset):
        for observer in self.observers:
            observer.update(event, asset)

    def read(self, asset_id: int) -> Asset:
        for asset in self._assets:
            if asset.get_id() == asset_id:
                return asset
        raise ValueError("Asset not found")

    def create(self, asset: Asset) -> None:
        self.notify_observers("CREATE", asset)
        self._assets.append(asset)

    def update(self, asset: Asset) -> None:
        for i in range(len(self._assets)):
            if self._assets[i].get_id() == asset.get_id():
                self._assets[i] = asset
                self.notify_observers("UPDATE", asset)
                return
        raise ValueError("Asset not found")

    def delete(self, asset_id: int) -> None:
        for i in range(len(self._assets)):
            if self._assets[i].get_id() == asset_id:
                del self._assets[i]
                self.notify_observers("DELETE", asset_id)
                return
        raise ValueError("Asset not found")

    def get_assets(self):
        return self._assets

我们将在本章的下一部分看到另外两种模式:单例模式和存储库模式。然而,在我们看到它们之前,我们需要设置一个控制器来与我们的系统交互。这将引导我们使用端口和适配器。

4.3 插入端口和适配器

我们的信息技术资产管理系统开始完善;核心业务模型已经建立。我们已经应用了模式使代码更简洁、可读和可维护。然而,有一个明显的缺陷:我们如何与之交互?这是本节的主题。在本节中,我们将继续深入研究六边形架构。

4.3.1 六边形架构回顾

正如你可能记得的,六边形架构是一种设计软件的方法,强调将核心业务逻辑与外部服务分离。

业务逻辑可以被认为是应用程序的“大脑”。它包含了所有重要的规则和构造,应用程序将需要保证程序的正确性。在这个类比中,外部服务就像是你的“手”或“眼睛”。它们允许与外部世界进行交互:用户界面、数据库等等。

六边形架构将主程序逻辑与按钮、屏幕和数据库等外部部分分开。它使得更改这些外部部分而不更改主程序变得容易。它通过使用“端口”来定义外部部分如何与主程序交互,以及通过“适配器”使这些交互以具体方式发生来实现这一点。

这种方法使得随着时间的推移更容易更改和发展应用程序。如果需要对其中一个外部系统进行更改,则应用程序的核心不应受影响。您只需更新适配器即可。以下图表说明了这一点:

图 4.8 更具概念性的六边形架构示例。请注意,核心与系统的其他部分隔离开来,仅通过端口进行交互。

4.3.2 驱动我们的应用程序

我们将首先构建系统的驱动程序。驱动程序是应用程序上下文边界之外的系统,它向系统发送请求,并可选择从应用程序接收响应;一个常规的例子是从 Web 浏览器到 REST 控制器的 REST 调用。

首先,我们将向我们的信息技术资产管理系统添加一个 REST 控制器。REST 控制器将公开AssetManager类提供的功能。我们将创建一个名为 infrastructure/API 的目录,在其中创建一个名为 asset_controller.py 的文件。在此文件的开头添加以下提示。

列表 4.29 创建 REST 控制器以公开 AssetManager 功能的提示
# Import statements for Asset, AssetService, AssetFactory, AssetIn, AssetOut, and from_asset
# Use AssetFactory to create an Asset from an AssetIn
# Construct an instance of the APIRouter class from FastAPI
# Define HTTP endpoints to handle CRUD operations for the AssetService
# Implementation of error handling, logging, and other necessary components for a robust and reliable HTTP API
# All methods should be asynchronous

由于我们没有指定要使用的路径,Copilot 可能会生成与路由不一致或无效的路径。这就是在 VS Code 中使用上述提示时输出的内容。这更多是一种偏好而不是标准,但是 URL 应该是复数形式。

列表 4.30 生成的代码的路由为“asset”而不是“assets”
@router.post('/asset', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
    asset = AssetFactory.create_asset(asset_in)
    asset_service.create(asset)
    return from_asset(asset)

您可能选择手动更新代码以反映您的路径首选项。尽管如此,请确保在各种方法之间保持一致。第一个列表将向我们展示如何初始化运行应用程序所需的所有服务。

列表 4.31 更新的 AssetController 代码以反映一致的路由
from fastapi import APIRouter, HTTPException
from itam.domain.asset import Asset
from itam.service.asset_manager import AssetManager
from itam.domain.factory.asset_factory import AssetFactory
from itam.infrastructure.api.asset_model import AssetIn, AssetOut, from_asset
import logging

router = APIRouter()

asset_manager = AssetManager()
asset_factory = AssetFactory()

接下来,我们将看到路由是如何定义的以及它们的方法如何使用的。前两个路由定义了我们将如何访问我们的Asset对象。第一个 GET 请求将获取系统中当前所有Assets。在现实世界中,我们会包括诸如分页和排序之类的便利功能。但是,考虑到系统中目前条目数量有限,我们将放弃这些功能。下一个 GET 方法将根据其标识符获取特定的Asset

列表 4.32 AssetController 方法以访问Assets
@router.get('/assets', response_model=list[AssetOut])
async def get_assets():
    assets = asset_manager.get_assets()
    return [from_asset(asset) for asset in assets]

@router.get('/assets/{asset_id}', response_model=AssetOut)
async def read_asset(asset_id: int):
    try:
        asset = asset_manager.read(asset_id)
        return from_asset(asset)
    except ValueError as e:
        logging.error(e)
        raise HTTPException(status_code=404, detail="Asset not found")

最终的路由集定义了我们将如何创建、更新和删除系统中的Assets。请注意,我们不进行“软”删除,这只会设置一个标志,并且此资产在随后的查询中不会被返回。

列表 4.33 AssetController 方法以修改和删除Assets
@router.post('/assets', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
    asset = asset_factory.new(asset_in.asset_type, asset_in.name, asset_in.unit_cost, asset_in.useful_life, asset_in.depreciation_strategy, asset_in.depreciation_rate, asset_in.salvage_value, asset_in.purchase_date)
    asset_manager.create(asset)
    return from_asset(asset)

@router.put('/assets/{asset_id}', response_model=AssetOut)
async def update_asset(asset_id: int, asset_in: AssetIn):
    try:
        asset = asset_factory.new(asset_in.asset_type, asset_in.name, asset_in.unit_cost, asset_in.useful_life, asset_in.depreciation_strategy, asset_in.depreciation_rate, asset_in.salvage_value, asset_in.purchase_date)
        asset.set_id(asset_id)
        asset_manager.update(asset)
        return from_asset(asset)
    except ValueError as e:
        logging.error(e)
        raise HTTPException(status_code=404, detail="Asset not found")

@router.delete('/assets/{asset_id}')
async def delete_asset(asset_id: int):
    try:
        asset_manager.delete(asset_id)
    except ValueError as e:
        logging.error(e)
        raise HTTPException(status_code=404, detail="Asset not found")

你应该注意到在该类中有称为 AssetInAssetOutAsset 类的简化表示。您可以在 AssetController 中定义这些类,或者您可以添加一个单独的类并在那里定义它们。在本书的参考实现中,它们在一个名为 asset_model.py 的文件中定义。

列表 4.34 定义 AssetInAssetOut 类的提示
# Define a pydantic model for AssetIn
# It should have the following attributes:
#    name: str
#    asset_type: str
#    department: str
#    depreciation_strategy: str
#    useful_life: int
#    unit_cost: float
#    depreciation_rate: float
#    salvage_value: float

# Define a pydantic model for AssetOut
# It should have the following attributes:
#    id: int
#    name: str
#    asset_type: str
#    department: str
#    depreciation_strategy: str
#    useful_life: int
#    unit_cost: float
#    depreciation_rate: float
#    salvage_value: float
# It should have a method that transforms an Asset into an AssetOut

在文件开头定义了提示后,我们应该会得到 Copilot 生成的代码,与列表 4.35 中的代码类似。

列表 4.35 asset_model.py 中的 AssetInAssetOut
from pydantic import BaseModel
from itam.domain.asset import Asset

class AssetIn(BaseModel):
    name: str
    asset_type: str
    department: str
    depreciation_strategy: str
    useful_life: int
    unit_cost: float
    depreciation_rate: float
    salvage_value: float
    purchase_date: str

class AssetOut(BaseModel):
    id: int
    name: str
    asset_type: str
    depreciation_strategy: str
    useful_life: int
    unit_cost: float
    depreciation_rate: float
    salvage_value: float
    purchase_date: str

@staticmethod
def from_asset(asset: Asset) -> AssetOut:
    return AssetOut(
        id=asset.id,
        name=asset.name,
        asset_type=asset.category,
        depreciation_strategy=str(asset.funding_details.depreciation_strategy),
        useful_life=asset.useful_life,
        unit_cost=asset.cost,
        depreciation_rate=asset.funding_details.depreciation_rate,
        salvage_value=asset.salvage_value,
        purchase_date=asset.purchase_date.strftime("%Y-%m-%d"),
    )

我们可以选择在 AssetControllerAssetManager 之间再添加一层抽象。某些人可能认为这样做可能会使六边形架构极端化,但在计算机科学中几乎没有解决不了的挑战,只要再增加一层抽象。我们将添加一个端口,用于与 AssetController 进行接口交互。让我们为 AssetRestPort 类创建一个提示,并让 Copilot 生成代码。

列表 4.36 创建 AssetRestPort 的提示
# Define an interface called AssetRestPort
# It should expose all of the methods in AssetManager as abtract methods

此提示指示 Copilot 创建一个我们将很快实现的接口。

列表 4.37 AssetRestPort的完整源代码
from itam.domain.asset import Asset
from abc import abstractmethod

class AssetRestPort:
    @abstractmethod
    def read(self, asset_id: int) -> Asset:
        pass

    @abstractmethod
    def create(self, asset: Asset) -> None:
        pass

    @abstractmethod
    def update(self, asset: Asset) -> None:
        pass

    @abstractmethod
    def delete(self, asset_id: int) -> None:
        pass

    @abstractmethod
    def get_assets(self):
        pass

接下来,我们将使用适配器实现这个端口,该端口将用于与控制器交互,以进一步抽象组件。如果我们想要将其转换为命令行应用程序,我们可以轻松地用另一个适配器替换这个适配器。适配器的提示如下。

列表 4.38 创建 AssetRestAdapter 的提示
# Define an interface called AssetRestAdapter
# It should take an AssetManager as a parameter in its constructor
# It should expose all of the methods in AssetManager
# It should inherit from AssetRestPort

此提示有两个重要元素。第一个是它实现了我们之前定义的端口接口。第二个是它获取 AssetManager 并包装其功能。

列表 4.39 AssetRestAdapter 的源代码
from itam.domain.asset import Asset
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
from itam.service.asset_manager import AssetManager

class AssetRestAdapter(AssetRestPort):
    def __init__(self, asset_manager: AssetManager):
        self._asset_manager = asset_manager

    def read(self, asset_id: int) -> Asset:
        return self._asset_manager.read(asset_id)

    def create(self, asset: Asset) -> None:
        self._asset_manager.create(asset)

    def update(self, asset: Asset) -> None:
        self._asset_manager.update(asset)

    def delete(self, asset_id: int) -> None:
        self._asset_manager.delete(asset_id)

    def get_assets(self):
        return self._asset_manager.get_assets()

唯一要做的是更新 AssetController,将 AssetController 直接调用 AssetManager 方法的部分移除,而是让 AssetController 调用适配器的方法,而适配器又调用 AssetManager 的方法。端口和适配器模式的关键是,它抽象了系统驱动部分(在本例中是 REST API)与被驱动应用程序(我们系统的业务模型和核心:AssetManager)之间的交互。为了更明确地表达这一点,并作为一个小的预览,我们将很快再次修改这个类,将端口添加到构造函数中。

列表 4.40 使用 AssetRestAdapterAssetController 的更新代码
router = APIRouter()

asset_rest_adapter = AssetRestAdapter(AssetManager())
asset_factory = AssetFactory()

@router.post('/assets', response_model=AssetOut)
async def create_asset(asset_in: AssetIn):
    asset = asset_factory.new(asset_in.asset_type, asset_in.name, asset_in.unit_cost, asset_in.useful_life, asset_in.depreciation_strategy, asset_in.depreciation_rate, asset_in.salvage_value, asset_in.purchase_date)
    asset_rest_adapter.create(asset)
    return from_asset(asset)

正如之前提到的,我们将修改AssetController以删除对AssetManager的所有直接引用。虽然我们的当前AssetController并没有直接调用任何AssetManager的方法,但它确实对AssetManager具有间接引用,因为AssetManager是在AssetController中构造的。这样做将进一步隔离AssetManager免受驱动程序的更改。

4.3.3 访问我们的数据并持久化我们的更改

软件中的抽象并不仅仅是一种学术问题。这些抽象允许灵活地满足要求,并使更改变得更容易。它们可以隐藏复杂性,将行为封装在看似简单的接口后面。这将引向我们的下一个模式:仓储模式

仓储模式

仓储模式是一个设计模式,帮助通过创建一个单独的地方来处理诸如获取、保存、更新和删除数据等任务来管理数据。它将数据源(比如数据库或文件)连接到应用程序的其他部分,将数据访问代码与其他代码分开。

在我们的 ITAM 项目中,仓储模式将使我们能够呈现一个大大简化的接口,掩盖了我们数据访问的复杂性。从仓库或 AssetManager 的角度来看,我们的数据访问层只是一系列方法。在背后,它可能是一个 REST API、一个 Redis 缓存、MongoDb、一个文件,或者任何一种数据库。让我们创建一个提示,来设置我们的通用数据访问层,使用仓储模式。

列表 4.41 创建我们的通用数据访问层的提示
# Create an abstract base class called BaseRepository that represents a generic data access layer using the Repository pattern. 
# The class should define common methods for CRUD operations such as create, read, update, and delete. 
# Make sure to use type hints for the input parameters and return values.

有了这个提示,Copilot 应该有了建立所有实现都将遵守和行为的契约所需的一切。让我们来检查代码。

列表 4.42 作为我们通用数据访问层的BaseRepository的代码
from abc import ABC, abstractmethod
from typing import List, TypeVar, Generic

T = TypeVar('T')

class BaseRepository(ABC, Generic[T]):
    @abstractmethod
    def create(self, entity: T) -> None:
        pass

    @abstractmethod
    def read(self, entity_id: int) -> T:
        pass

    @abstractmethod
    def update(self, entity: T) -> None:
        pass

    @abstractmethod
    def delete(self, entity_id: int) -> None:
        pass

    @abstractmethod
    def get_entities(self) -> List[T]:
        pass

Copilot 引入了一种通用类型,当实现时可以提供一些类型提示。如果我们表达该类型应该是一个Asset,那么方法签名将会展示这一点。我们的第一次尝试是一个内存数据存储。这将允许我们对系统进行端到端的测试。这也很好地展示了抽象的有用性。不久之后,我们将添加另一个使用 Postgres 的实现。但首先,提示是:

列表 4.43 用于测试目的的内存数据存储的提示
# Define a class called InMemoryAssetRepository that inherits from the BaseRepository abstract class. 
# The InMemoryAssetRepository should be specific to managing Asset objects sing an in-memory list to store and update the assets. 
# Implement the necessary methods for CRUD operations such as create, read, update, and delete for Asset objects using the in-memory list. 
# Make sure to use type hints for the input parameters and return values.

我们的内存实现将使用一个简单的列表来存储Assets。我们将能够执行所有 CRUD 操作。此外,Copilot 将为我们添加类型提示:让其他人知道它应该接受和返回 Assets。这就是BaseRepository[Asset]的意思:这是一个BaseRepository的实现,其通用类型为 Asset。

列表 4.44 用于InMemoryAssetRepository的代码
from itam.infrastructure.repository.base_repository import BaseRepository
from itam.domain.asset import Asset

class InMemoryAssetRepository(BaseRepository[Asset]):
    def __init__(self):
        self._assets = []

    def create(self, asset: Asset) -> None:
        self._assets.append(asset)

    def read(self, asset_id: int) -> Asset:
        return next((asset for asset in self._assets if asset.id == asset_id), None)

    def update(self, asset: Asset) -> None:
        for i in range(len(self._assets)):
            if self._assets[i].id == asset.id:
                self._assets[i] = asset
                break

    def delete(self, asset_id: int) -> None:
        self._assets = [asset for asset in self._assets if asset.id != asset_id]

    def get_entities(self) -> list[Asset]:
        return self._assets

最后,我们将更新AssetManager,将Assets的 CRUD 操作委托给一个BaseRepository实例(_repository)。完整的源代码如下,包括位于文件开头的提示。

代码清单 4.45 更新后的提示和AssetManager代码,使用InMemoryAssetRepository
# Define a class called AssetManager
# with CRUD operations for Asset
# and a method to notify observers whenever an asset is created, updated or deleted
# The AssetManager should use an instance of the InMemoryAssetRepository class for data access and CRUD operations on Asset objects. 
# Implement methods for creating, reading, updating, and deleting assets using the AssetRepository instance. 
# Please include type hints for the input parameters and return values.
# The methods should be named create, read, update, get_assets, and delete.

from itam.domain.asset import Asset
from itam.infrastructure.repository.in_memory_asset_repository import InMemoryAssetRepository

class AssetManager:
    def __init__(self):
        self._repository = InMemoryAssetRepository()

    def create(self, asset: Asset) -> Asset:
        self._repository.create(asset)
        return asset

    def read(self, asset_id: int) -> Asset:
        return self._repository.read(asset_id)

    def update(self, asset: Asset) -> Asset:
        self._repository.update(asset)
        return asset

    def get_assets(self) -> list[Asset]:
        return self._repository.get_entities()

    def delete(self, asset_id: int) -> None:
        self._repository.delete(asset_id)

此时,我们拥有一个核心业务域,它直接不受我们系统的影响。我们有请求可以进入的端口。我们也有可以存储数据的端口(至少在系统运行时)。我们应该能够通过运行系统并将请求 POST 到创建端点,以及从 GET 端点读取来端到端测试系统。一旦我们确认系统可以端到端工作,我们现在可以解决我们的数据仅在内存中持久化的问题;我们现在可以连接实际的数据库。为了达到这个目的,我们引入本章的最后一个模式:单例模式。单例模式在概念上非常简单;它应该只有一个实例正在运行。单例模式对于许多用例都很好用:日志记录、缓存、配置设置或数据库连接管理。

Singleton 模式

单例模式是一种设计模式,它确保一个类只有一个实例,并提供对该实例的全局访问点。当您想要在程序的不同部分共享单个对象而不是创建多个相同类的实例时使用它。

4.3.4 集中(和外部)我们的数据访问

我们将请求 Copilot 为我们创建一个 Singleton 类来管理数据库的连接。由于我们永远不希望在我们的源代码中硬编码用户名或密码(或任何连接详细信息),不仅因为它在本质上不安全,而且这些详细信息可能因环境而异(开发、QA、产品)。因此,我们将要求 Copilot 将这些值作为环境变量接受,并使用这些值配置连接。

代码清单 4.46 创建 Singleton DatabaseConnection类的提示。
# Create a Python class called DatabaseConnection using the Singleton pattern to manage a single connection to a PostgreSQL database using SQLAlchemy. 
# The class should read the database username, password, and connection string from environment variables
# The environment variables should be named: DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, and DB_NAME

我们要求 Copilot 使用 SQLAlchemy,一个对象关系映射(ORM)工具,执行对数据库的操作。Copilot 会巧妙地组装我们的源代码。@staticmethod关键字将创建一个方法,它属于类而不是类的实例。此关键字用于获取DatabaseConnection类的实例。由于静态方法无法修改实例数据,因此它可用于确保此类的仅一实例在运行,并因此成为单例模式。构造函数使用环境变量的字符串插值初始化到数据库的连接。

代码清单 4.47 创建 Singleton DatabaseConnection类的提示。
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os

class DatabaseConnection:
    __instance = None
    @staticmethod
    def get_instance():
        if DatabaseConnection.__instance == None:
            DatabaseConnection()
        return DatabaseConnection.__instance

    def __init__(self):
        if DatabaseConnection.__instance != None:
            raise Exception("This class is a singleton!")
        else:
            DatabaseConnection.__instance = self
            self.engine = create_engine(f"postgresql://{os.environ['DB_USERNAME']}:{os.environ['DB_PASSWORD']}@{os.environ['DB_HOST']}:{os.environ['DB_PORT']}/{os.environ['DB_NAME']}")
            self.Session = sessionmaker(bind=self.engine)

    def get_session(self):
        return self.Session()

现在我们连接到了数据库,我们将需要创建一个存储和检索 Asset 对象的存储库。我们将创建一个名为 SQLAlchemyAssetRepository 的类。这个名称非常明确地说明了它是什么以及它如何做到这一点。

列表 4.48 SQLAlchemyAssetRepository 类的提示和代码
# Define a class called SQLAlchemyAssetRepository that implements the AssetRepository interface for data access using SQLAlchemy. 
# The class should handle the CRUD operations (create, read, update, and delete) for assets, storing and retrieving them in a PostgreSQL database using SQLAlchemy.

from itam.domain.asset import Asset
from itam.infrastructure.repository.base_repository import BaseRepository
from sqlalchemy.orm import Session

class SQLAlchemyAssetRepository(BaseRepository[Asset]):
    def __init__(self, session: Session):
        self._session = session

    def create(self, asset: Asset) -> Asset:
        self._session.add(asset)
        self._session.commit()
        return asset

    def read(self, asset_id: int) -> Asset:
        return self._session.query(Asset).filter(Asset.id == asset_id).first()

    def update(self, asset: Asset) -> Asset:
        self._session.query(Asset).filter(Asset.id == asset.id).update(asset)
        self._session.commit()
        return asset

    def get_assets(self) -> list[Asset]:
        return self._session.query(Asset).all()

    def delete(self, asset_id: int) -> None:
        self._session.query(Asset).filter(Asset.id == asset_id).delete()
        self._session.commit()

接下来,我们将重构 AssetController,以允许我们传入 AssetRestPort。通过这样做,我们可以交换应用程序的驱动部分。这种重构将进一步将驱动程序与业务核心分离。

列表 4.49 更新的 AssetController 类,其中公开路由而不是使用方法装饰器
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
import logging

class AssetController:
    def __init__(self, asset_rest_port:  AssetRestPort):
        self._asset_factory = AssetFactory()
        self._asset_rest_port = asset_rest_port
        ...

    def get_router(self):
        return self._router

    async def get_assets(self):
        return  [ from_asset(a) for a in self._asset_rest_port.get_assets()]

    async def get_asset(self, asset_id: int):
        asset = self._asset_rest_port.read(asset_id)
        if asset is None:
            raise HTTPException(status_code=404, detail="Asset not found")
        return from_asset(asset)

    async def create_asset(self, asset_in: AssetIn):
        asset = self._asset_factory.new(asset_in.asset_type, asset_in.name, asset_in.unit_cost, asset_in.useful_life, asset_in.depreciation_strategy, asset_in.depreciation_rate, asset_in.salvage_value, asset_in.purchase_date)
        self._asset_rest_port.create(asset)
        return from_asset(asset)

    async def update_asset(self, asset_id: int, asset_in: AssetIn):
        asset = self._asset_factory.new(asset_in.asset_type, asset_in.name, asset_in.unit_cost, asset_in.useful_life, asset_in.depreciation_strategy, asset_in.depreciation_rate, asset_in.salvage_value, asset_in.purchase_date)

        asset.id = asset_id
        asset = self._asset_rest_port.update(asset)
        if asset is None:
            raise HTTPException(status_code=404, detail="Asset not found")
        return from_asset(asset)

    async def delete_asset(self, asset_id: int):
        asset = self._asset_rest_port.read(asset_id)
        if asset is None:
            raise HTTPException(status_code=404, detail="Asset not found")
        self._asset_rest_port.delete(asset_id)
        return from_asset(asset)

现在我们可以将应用程序的初始化逻辑 consolide 到 main.py 文件中。这就是大收益。我们的系统将具有分层结构,方便根据需要或要求更改组件。

列表 4.50 main.py 类的最终版本,其中我们将应用程序连接在一起
from fastapi import FastAPI
from itam.infrastructure.api.asset_controller import AssetController
#from itam.infrastructure.repository.in_memory_asset_repository import InMemoryAssetRepository
from itam.infrastructure.repository.sqlalchemy_asset_repository import SQLAlchemyAssetRepository
from itam.infrastructure.database.database_connection import DatabaseConnection
from itam.service.asset_manager import AssetManager
from itam.infrastructure.adapters.asset_rest_adapter import AssetRestAdapter
import uvicorn

app = FastAPI()
session = DatabaseConnection().get_session()
#repository = InMemoryAssetRepository()
repository = SQLAlchemyAssetRepository(session)
asset_manager = AssetManager(repository)
asset_rest_adapter = AssetRestAdapter(asset_manager)
asset_controller = AssetController(asset_rest_adapter)
app.include_router(asset_controller.get_router())

if __name__ == '__main__':
    uvicorn.run(app, host='0.0.0.0', port=8000)

恭喜,我们现在有一个运行中的系统,可以将数据持久化到我们的数据库中。

4.4 摘要

  • 装饰者设计模式是一种结构设计模式,允许您动态地向对象添加新的或修改现有行为,而无需更改现有类。这是通过将当前对象包装在装饰对象中来实现的。

  • 访问者模式用于向给定类添加新行为或更改现有行为。

  • 工厂模式是另一种创建模式,允许您抽象出您试图创建的对象的某些细节。

  • 生成器模式是一种创建设计模式,它通过逐步提供关于如何创建对象的说明,为对象的创建提供了流畅的 API。

  • 适配器模式是一种结构设计模式,它允许将目标接口与具有不兼容接口的类之间建立桥梁。

  • 观察者模式是一种行为模式,其中主题类通过通知向观察者类报告某些状态更改。

  • 六边形架构将主程序逻辑与外部部分(如按钮、屏幕和数据库)分开。它使得更改外部部分变得容易,而不用更改主程序。

  • 存储库模式是一种设计模式,通过创建一个单独的地方来处理获取、保存、更新和删除数据等任务来帮助管理数据。它将数据源(如数据库或文件)连接到应用程序的其余部分,将数据访问代码与其他代码分开。

  • 单例模式是一种设计模式,确保一个类只有一个实例,并为该实例提供全局访问点。当您希望在程序的不同部分共享单个对象而不是创建同一类的多个实例时,可以使用它。