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

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])

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

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

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

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

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

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

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

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

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

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

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

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

1.5 总结

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

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

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

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

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

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

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

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

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


^([1]) 赵舒音。“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 MainApp
    …
    class 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。

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

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

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

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

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

第五章:通过 GitHub Copilot 和 Copilot Chat 管理数据

本章内容涵盖

  • 将我们的数据持久化到关系型数据库

  • 使用 Apache Kafka 进行数据流式传输

  • 融合事件驱动原则

  • 使用 Spark 分析我们的数据以监控位置

上一章为我们的信息技术资产管理系统奠定了基础。然而,没有数据,这个应用程序将无法满足我们的要求。数据是每个应用程序的命脉。这正是本章的主题:我们可以使用生成式 AI 来创建数据、流式传输数据、转换数据、对数据做出反应并从数据中学习的各种方式。

敏锐的人可能已经注意到在上一章中,我们的数据访问模式是无法正常工作的,因为它是不完整的。本章的开头部分将解决这个问题。之后,我们将设置我们的数据库,修复访问这些数据的类,并加载一些示例数据,以便在本章的其余部分使用。

5.1 积累我们的数据集

我们的第一个任务将是构建一个大量的数据集,以帮助我们在本章剩余的实验中。首先,我们将使用 GitHub Copilot 生成一千行资产信息。然而,我们很快会发现,这可能不是最适合这项任务的工具。使用这些工具的一个关键驱动因素是发现的概念:测试它们的边界,推动它们,有时候,反击它们。但旅程往往是快乐的源泉。一旦我们找到了这个边缘,我们将被介绍一个新的、以前从未见过的工具:GitHub Copilot Chat。最后,一旦我们创建了我们的资产列表,我们将再次使用 GitHub Copilot Chat 为这些资产添加位置信息。

在构建我们的初始数据集之前,我们需要让数据库运行起来。Docker 让这个任务变得微不足道,让我们能够快速启动一个空的 Postgres(或其他 RDBMS/NoSQL 服务器),几乎不费吹灰之力。你忘记了执行此操作的命令吗?别担心,我们可以问 Copilot。打开一个名为 data/initial_data_laod.sql 的新文件,并在你的新生成的 SQL 文件顶部输入以下提示。

片段 5.1 提示 GitHub Copilot 为我们提供一个 Docker 命令
-- Question: What is the command to run a docker container, running PostgreSQL for a database called itam_db. I would like to specify the password for this database.

Copilot 将逐渐揭示 Docker 命令:--Answer: docker run --name itam_db -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres. 一旦你在终端或命令行运行这个命令,我们就可以构建出我们的数据集。你应该能够连接到本地运行的数据库。你应该注意到里面有一个名为 itam_db 的数据库在运行。然而,这个数据库没有模式、表或数据。让我们首先设置一个新的模式。

在我们的 initial_data_file.sql 中,我们将添加一个提示,让 Copilot 起草模式创建命令。执行此命令将允许您从数据库客户端应用程序(例如 DataGrip、SQuirreL、pdAdmin,甚至使用 Docker exec 命令 docker exec -i itam_db psql -U postgres -c "create schema itam")中创建名为 itam 的新模式。

提示让 Copilot 创建新模式
-- create a schema called itam
create schema itam;

接下来,我们应该为应用程序中使用的用户添加一个用户。此用户将能够对我们的数据执行CRUD(创建、读取、更新、删除)操作,但将无法影响数据库表或存储过程的结构。

注意

以双破折号开头的行是 SQL 中的注释。从 Copilot 的角度来看,注释这些行是可选的,因为它将生成没有注释的解决方案;这样做可以更轻松地将代码直接复制粘贴到我们选择的数据库工具中。

在此期间,我们还将添加一个管理帐户,以执行我们的读写用户无法执行的操作,例如创建或删除表。

提示创建新用户
-- create two users called itam_user and itam_admin. itam_user should be able to read from and write to the tables in the itam schema. itam_admin should be able to read from and write to the tables in the itam schema and create and drop tables in the itam schema.

create user itam_user with password 'itam_user';
create user itam_admin with password 'itam_admin';
grant usage on schema itam to itam_user;
grant usage on schema itam to itam_admin;
grant select, insert, update, delete on all tables in schema itam to itam_user;
grant select, insert, update, delete on all tables in schema itam to itam_admin;
grant usage, drop on schema itam to itam_admin;

接下来,我们将把此模式的所有权授予 itam_admin 帐户。转移此所有权将确保只有此帐户可以更改表结构:数据定义。

将模式所有权转移给管理员帐户的提示
-- grant ownership of the itam schema to itam_admin
alter schema itam owner to itam_admin;

配置完成、帐户创建和系统的崇拜已经完成,我们可以开始专注于数据。我们将从添加参考数据开始,即支持资产的数据:折旧策略。这些数据的性质更加静态;它们的变化频率较低,甚至根本不变。接下来,我们将定义并存储这些策略。

提示创建折旧策略表
-- create a table called depreciation_strategy in the itam schema. the table should have the following columns: id (int), name (varchar), and description (varchar). the table should have a primary key on id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- there are two values for depreciation_strategy: straight line and double declining balance
create table itam.depreciation_strategy (
    "id" int primary key,
    "name" varchar,
    "description" varchar
);

我们将使用序列作为此表的主键。虽然对于一个不会很大并且我们可以手动输入已知值的表而言,这并不是严格必要的,但是添加此序列将允许我们与 Copilot 更多地合作并让它提出一些建议。此外,询问 Copilot 并在文本文件中获得 Copilot 的回答是有趣的。

提示为折旧策略表的主键创建序列
-- create a sequence called depreciation_strategy_seq, which should start at 1 and increment by 1 and should be used as the primary key for the depreciation_strategy table.
create sequence itam.depreciation_strategy_seq start 1 increment 1;

自然地,有了我们手中的序列,我们需要知道如何将序列与depreciation_stategy表的主键列关联起来。幸运的是,Copilot 有答案。

询问 Copilot 如何将序列与主键关联
-- question: how do I make the sequence the primary key for the depreciation_strategy table?

-- answer: use the following command
alter table itam.depreciation_strategy alter column "id" set default nextval('itam.depreciation_strategy_seq'::regclass);

最后,我们将通过将以下静态条目插入表格来完成此表格。目前我们只使用两种折旧策略:直线法和双倍余额递减法。

将静态条目添加到折旧策略表
insert into depreciation_strategy (id, name, description) values (1, 'straight line', 'straight line');

insert into depreciation_strategy (id, name, description) values (2, 'double declining balance', 'double declining balance');

接下来,我们将转向资金细节表。这些信息告诉我们如何为我们的设备进行融资,再销售价值,并对资产在其有用生命周期结束后应采取的措施进行说明。我们在折旧策略中所做的步骤顺序将与此相同,唯一的区别是我们不会添加静态条目,因为这些数据直接与个体资产相关。我们将定义表,创建序列,并将该序列应用于表,作为主键的功能。

列表 5.6 资金详情表的完整代码列表
-- create a table called funding_details in the itam schema. the table should have the following columns: id (int), name (varchar),depreciation_strategy_id (int) and depreciation_rate (float). the table should have a primary key on id.
-- depreciation_stategy_id is a foreign key to the depreciation_strategy table.
-- id needs to be in quotes because it is a reserved word in postgresql
create table itam.funding_details (
    "id" int primary key,
    "name" varchar,
    "depreciation_strategy_id" int,
    "depreciation_rate" float
);

-- create a sequence called funding_details_seq, which should start at 1 and increment by 1 and should be used as the primary key for the funding_details table.
create sequence itam.funding_details_seq start 1 increment 1;

alter table itam.funding_details alter column "id" set default nextval('itam.funding_details_seq'::regclass);

我们将定义和生成的最后信息是资产本身。这个列表也是冗余的,但出于完整性考虑已包括在内。最后,我们创建表,创建序列,并将其用作主键。

列表 5.7 资产表的完整代码列表
-- create a table called assets in the itam schema. the table should have the following columns: 
-- id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The table should have a primary key on id and a foreign key on funding_details_id.
-- id needs to be in quotes because it is a reserved word in postgresql
-- the table should have a sequence called assets_id_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create table itam.assets (
    "id" int primary key,
    "name" varchar,
    "status" varchar,
    "category" varchar,
    "cost" float,
    "useful_life" int,
    "salvage_value" float,
    "purchase_date" date,
    "funding_details_id" int
);

-- create a sequence called assets_seq, which should start at 1 and increment by 1 and should be used as the primary key for the assets table.
create sequence itam.assets_seq start 1 increment 1;

alter table itam.assets alter column "id" set default nextval('itam.assets_seq'::regclass);

在定义和创建表之后,我们现在将专注于创建数据。在我们的文本文件中,我们使用参数指示 Copilot 我们正在寻找的数据集。Copilot 可能会尝试帮助您概述围绕新数据集的属性。

列表 5.8 为资产表创建数据集
-- Generate a dataset of assets for an ITAM system. The dataset should include the following columns: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). The dataset should have 1000 rows, sorted by id. Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 50 characters long.
-- - status should be a random selection from the following valid asset statuses: in use, in storage, disposed of, in repair, in transit, other.
-- - category should be a random selection from the following valid categories: hardware, software, other.
-- - cost should be a random float between 0 and 100000.
-- - useful_life should be a random int between 1 and 10.
-- - salvage_value should be a random float greater than 0 but less than the cost of the asset.
-- - purchase_date should be a random date between 1/1/2019 and 12/31/2022.
-- - funding_details_id should be a random integer either 1 or 2.
-- The dataset should be saved as a CSV file named assets.csv in the data directory. The file should have a header row and the columns should have the following data types: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (float), salvage_value (float), funding_details_id (int)

Copilot 提供的解决方案是新颖的。它使用 Postgres 内置功能构建了一个大系列,这意味着这不是一个可移植的解决方案。然而,考虑到这是我们将要使用的数据库,这是一个足够合适的解决方案。生成的数据集经过了精心制作。如果我们使用 Python 并要求 Copilot 帮助编写一个用于加载到 Postgres 的文件的脚本,我们可能会得到更好的结果。然而,鉴于这个数据集只是用来玩应用程序,目前我们不需要过于担心数据质量。尽管在现实世界中,数据质量是至关重要的。

列表 5.9 Copilot 的响应:基于一系列构建的插入语句
insert into itam.assets (id, name, status, category, cost, useful_life, salvage_value, purchase_date, funding_details_id)
select
    id,
    name,
    status,
    category,
    cost,
    useful_life,
    salvage_value,
    purchase_date,
    funding_details_id
from (
    select
        row_number() over (order by random()) as id,
        md5(random()::text) as name,
        case
            when random() < 0.2 then 'in use'
            when random() < 0.4 then 'in storage'
            when random() < 0.6 then 'disposed of'
            when random() < 0.8 then 'in repair'
            when random() < 0.9 then 'in transit'
            else 'other'
        end as status,
        case
            when random() < 0.5 then 'hardware'
            when random() < 0.9 then 'software'
            else 'other'
        end as category,
        random() * 100000 as cost,
        (random() * 100)::int as useful_life,
        random() * (random() * 100000) as salvage_value,
        -- generate a random date between 1/1/2019 and 12/31/2022
        -- this does not work please fix
        -- '2019-01-01'::date + random() * ('2022-12-31'::date - '2019-01-01'::date) as purchase_date,
        '2019-01-01'::date + (random() * (DATE '2022-12-31' - DATE '2019-01-01')::integer)::integer as purchase_date
        case
            when random() < 0.5 then 1
            else 2
        end as funding_details_id
    from generate_series(1, 1000)
) as assets;

如果我们仅仅转向 ChatGPT,我们可以得到如何创建这样的数据集的第二意见。ChatGPT 建议使用 Python 库fakerfaker包用于生成虚假数据,例如常见的英文姓名。numpy用于生成成本、有用生命和残值的随机浮点值。pandas用于在DataFrame(表)中管理数据。此外,我们可以使用df.to_csv('assets.csv', index=False)方法将DataFrame保存到 CSV 文件中。

列表 5.10 ChatGPT 建议使用 Faker 来生成虚假数据集
import pandas as pd
import numpy as np
import random
from faker import Faker

# Create Faker object
fake = Faker()

# Define our valid statuses, categories, funding_details and depreciation_strategies
valid_statuses = ['in use', 'in storage', 'disposed', 'in repair', 'in transit', 'other']
valid_categories = ['hardware']
valid_funding_details = ['purchased']
valid_depreciation_strategies = ['straight line']

# Generate the data
data = {
    'id': list(range(1, 1001)),
    'name': [fake.first_name() for _ in range(1000)],
    'status': [random.choice(valid_statuses) for _ in range(1000)],
    'category': [random.choice(valid_categories) for _ in range(1000)],
    'cost': np.random.uniform(0, 100000, 1000),
    'useful_life': np.random.uniform(1, 10, 1000),
    'salvage_value': np.random.uniform(0, 10000, 1000),
    'funding_details': [random.choice(valid_funding_details) for _ in range(1000)],
    'depreciation_strategy': [random.choice(valid_depreciation_strategies) for _ in range(1000)],
}

# Create DataFrame
df = pd.DataFrame(data)

# Ensure the DataFrame is sorted by id
df = df.sort_values('id')

# Show the DataFrame
print(df)

对于这些资产的每一项,我们也需要资金细节;它们是如何融资的(在这种情况下购买),以及折旧细节。毫不奇怪,我们从 Copilot 得到了类似的解决方案:使用与我们为资产使用的类似提示生成一系列条目。我们需要确保对于每个资产标识符(1-1000),我们都有相应的资金细节条目。否则,在运行我们的代码时会出现空指针的风险。

清单 5.11 创建funding_details表的数据集
-- Generate a dataset of funding details for an ITAM system. The dataset should include the following columns: id (int), name (varchar), depreciation_strategy_id (int), depreciation_rate (float). The dataset should have 1000 rows, sorted by id. 
-- Each row should have the following characteristics:
-- - id should be a unique integer and sequential starting at 1.
-- - name should be a random string of characters between 1 and 10 characters long.
-- - depreciation_strategy_id should be a random integer either 1 or 2.
-- - depreciation_rate should be a random float between 0 and .4.

insert into itam.funding_details (id, name, depreciation_strategy_id, depreciation_rate)
select
    id,
    name,
    depreciation_strategy_id,
    depreciation_rate
from (
    select
        row_number() over (order by random()) as id,
        md5(random()::text) as name,
        case
            when random() < 0.5 then 1
            else 2
        end as depreciation_strategy_id,
        random() * 0.4 as depreciation_rate
    from generate_series(1, 1000)
) as funding_details;

有了在数据库中生成和存储的数据集,我们应该能够通过 REST API 连接我们应用程序的其余部分来存储和显示资产。然而,由于我们在构建阶段之前已经剥离了所有的 SQLAlchemy 元数据(请参阅上一章),我们需要一种不同的方法来将这些元数据与我们的适配器进行连接。

有了这个,我们已经达到了 Copilot 的能力边缘。我们对接下来会发生什么感到困惑;我们如何解决我们最近的困境。尽管诱人,我们不能放弃回家。因此,现在是时候介绍 Copilot 产品套件的最新成员了:Copilot Chat。Copilot Chat 是嵌入式的 GPT-4 模型,可以集成到你的 IDE 中(目前仅支持 Visual Studio Code)。我们将打开聊天对话框,询问如何保持我们的业务模型清晰,但仍然使用 SQLAlchemy 的 ORM(对象关系模型)功能。

图 5.1 GitHub Copilot Chat 对我们最近困境的解决方案

一张低置信度自动生成的计算机截图的描述

Copilot Chat 建议我们创建一个单独的数据访问层。这种方法与我们迄今为止使用的端口和适配器方法非常契合。此外,Copilot Chat 建议将这些类建模得与领域类类似,但包括 ORM 功能正常工作所需的元数据。生成的代码见清单 5.12。

清单 5.12 领域类之外的 ORM 支持。
Base  = declarative_base()

class FundingDetailsModel(Base):
    __tablename__ = 'funding_details'
    id = Column(Integer, primary_key=True)
    depreciation_rate = Column(Float)
    depreciation_strategy_id = Column(Integer)

    def get_depreciation_strategy(self) -> DepreciationStrategy:
        if self.depreciation_strategy_id is 1:
            return StraightLineDepreciationStrategy()
        else:
            return DoubleDecliningDepreciationStrategy()

class AssetModel(Base):
    __tablename__ = 'assets'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    status = Column(String)
    category = Column(String)
    cost = Column(Float)
    useful_life = Column(Float)
    salvage_value = Column(Float)
    purchase_date = Column(Date)
    funding_details_id = Column(Integer, ForeignKey('funding_details.id'))
    funding_details = relationship('FundingDetailsModel')

现在,外部模型类已经创建,我们必须将这些 ORM 实例映射到我们的领域模型,然后返回给系统核心。虽然对于这样一个简单的应用程序来说,这似乎是过度设计的代码,但这给了我们很大的灵活性,使我们的领域模型可以进行复杂的操作。例如,我们的模型可以执行 CRUD 之外的复杂操作。如果我们将我们的领域模型身份限制在数据访问层中使用的模型上,我们将受到这些操作的限制。

接下来,我们将使用 Copilot 和 Copilot Chat 探索将事件驱动思想纳入我们的应用程序中。事件驱动的概念将允许我们实时跟踪我们的 IT 资产:它们的位置、状态和市场价值,例如。

5.2 使用 Kafka 实时监控我们的资产

我们将实时监控我们的资产,以激励我们探索将生成式人工智能与事件驱动架构结合使用的探索。我们应该认为,信息安全资产管理系统外部的某些系统在我们的资产从一个位置移动到另一个位置时会触发事件。

要深入了解 ITAM 事件,我们需要配置一些额外的服务。在这种情况下,我们将使用 Apache Kafka。Apache Kafka 是一个分布式流平台,用于构建实时数据管道和流应用程序。它被设计用于处理来自多个来源的数据流,并将它们传送到多个消费者,有效地充当我们实时数据的中间人。

首先,我们将询问 Copilot Chat 如何在本地使用 Docker 运行 Kafka。Apache Kafka 有一个不应该存在的声誉,即安装和配置很困难。在 Docker 中运行将允许我们回避这个争议。使用 Copilot Chat,我们可以生成一个 docker compose 文件。然而,通常情况下,版本非常旧,甚至不支持一些硬件。图示 5.13 是从 Confluent(提供 Kafka 商业支持的公司)官方 GitHub 存储库中提取的更新的列表。请注意,docker-compose 文件的内容包括 Kafka 和 Zookeeper。Zookeeper 是 Kafka 用来管理和协调集群内代理的分布式协调服务,至少目前是这样。未来版本的目标是消除对 Zookeeper 的依赖。

图示 5.13 Docker-Compose 文件,用于启动带有 Zookeeper 的 Kafka
version: '2.1'

services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.3.2
    container_name: zookeeper
    ports:
      - "2181:2181"
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_SERVER_ID: 1
      ZOOKEEPER_SERVERS: zoo1:2888:3888

  kafka:
    image: confluentinc/cp-kafka:7.3.2
    hostname: kafka
    container_name: kafka
    ports:
      - "9092:9092"
      - "29092:29092"
      - "9999:9999"
    environment:
      KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:19092,EXTERNAL://${DOCKER_HOST_IP:-127.0.0.1}:9092,DOCKER://host.docker.internal:29092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,DOCKER:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
      KAFKA_BROKER_ID: 1
      KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_JMX_PORT: 9999
      KAFKA_JMX_HOSTNAME: ${DOCKER_HOST_IP:-127.0.0.1}
      KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
      KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
    depends_on:
      - zookeeper

现在我们的新 Kafka 实例正在运行,我们现在需要一个消费者,它将从主题asset_location(我们很快将创建的主题)中拉取更新的位置,并在数据库中更新库存。同样,我们可以请求 Copilot Chat 为我们提供如何做到这一点的建议。

图示 5.14 提示 Copilot Chat 如何使用 Python 订阅主题
In our ITAM project, we would like a Python consumer for the assets_location Kafka topic. Once it receives a new updated location, it should update the domain model to reflect the new location. How should we do this?

Copilot Chat 生成的代码创建一个消费者,监听主题,并使用对AssetManager类的引用来更新受影响资产的位置。

图示 5.15 Copilot Chat 代码,用于监控资产位置
from kafka import KafkaConsumer
from asset_manager import AssetManager

consumer = KafkaConsumer(
    'asset_location',
    bootstrap_servers=['localhost:9092'],
    auto_offset_reset='earliest',
    enable_auto_commit=True,
    group_id='my-group',
    value_deserializer=lambda x: tuple(map(float, x.decode('utf-8').split(',')))
)

asset_manager = AssetManager()

for message in consumer:
    asset_id, latitude, longitude = message.value
    asset_manager.update_asset_location(asset_id, (latitude, longitude))

但是,适配器应该知道AssetManager吗?虽然AssetManager不直接位于领域模型内部,但它是一个至关重要的中介。适配器和AssetManager之间的这种耦合程度可能会违反六边形架构模型中的六边形。让我们让 Copilot Chat 来发表意见。我们可以问它:“考虑到这个项目使用了六边形架构,让适配器知道AssetManager是一个好主意吗?”

图示 5.2 GitHub Copilot Chat 承认它实际上并不完美

一张计算机的截图 用中度信心自动生成的描述

看起来 Copilot Chat 认为这确实会被视为适配器的适当承包责任的违约。我们可以向我们的AssetManager类添加一个新的端口,它可以使用 Kakfa 端口。然而,让我们看看 Copilot Chat 是否有其他建议:“我们不希望 Kafka 消费者直接与AssetManager进行交互。有什么方法可以实现这一点?” Copilot Chat 建议我们应用Mediator模式,这类似于一个端口和适配器的组合。

中介者模式

Mediator 设计模式是一种行为模式,通过将对象之间的交互封装在一个中介对象中,促进对象之间的松耦合。中介对象充当一个中心枢纽,促进对象之间的通信,而无需对象知道彼此。

让我们首先看一下 Copilot Chat 生成的 Mediator 类。该类将位于AssetManager和 Kafka 消费者之间。

图 5.16 Mediator 类,它将调和 AssetManager 和 Kafka 消费者之间的交互
class AssetLocationMediator:
    def __init__(self):
        self.handlers = {}

    def register_handler(self, event_type, handler):
        if event_type not in self.handlers:
            self.handlers[event_type] = []
        self.handlers[event_type].append(handler)

    def publish(self, event):
        event_type = type(event)
        if event_type in self.handlers:
            for handler in self.handlers[event_type]:
                handler(event)

这个实现与消息队列或消息发布者十分相似,这正是关键所在:解耦。

注意

有慧眼的读者可能会注意到我们在资产和硬件类型的资产之间的区别上有一点疏忽。在原始领域模型中,只有硬件有位置。通常,人们不会认为软件有位置。当然,您可以说软件安装在它的位置上,但人们是否会认为这个论点说服力有待商榷。不过,随着项目的继续进行,领域模型变平的原因是为了简化,因为在持久性层中的多态结构是一个令人分心的复杂主题。

现在我们有了一个将 Kafka 消费者与 AssetManager 解耦的策略,我们应该更新 Kafka 消费者以利用它。我们需要在构造函数中将中介者传递给类。这样,AssetManager 和消费者将可以访问相同的实例,并且消息可以自由地来来往往;或者,在这种情况下,流程将是单向的。您应该注意,我们打算在这个主题上读取和写入 JSON,因此我们需要让我们的值反序列化器理解这一点。

图 5.17 将中介者整合到 Kafka 消费者类中
from kafka import KafkaConsumer
from itam.domain.events.asset_location_updated import AssetLocationUpdated
import json

class AssetLocationKafkaConsumer:
    def __init__(self, mediator):
        self.mediator = mediator

        self.consumer = KafkaConsumer(
            'asset_location',
            bootstrap_servers=['localhost:9092'],
            enable_auto_commit=True,
            group_id='itam-group',
            value_deserializer=lambda m: json.loads(m.decode('utf-8'))
        )

    def poll(self):
        print("Polling for asset location updates")
        for message in self.consumer:
            asset_id = message.value['asset_id']
            latitude = message.value['latitude']
            longitude = message.value['longitude']
            timestamp = message.value['timestamp']
            event = AssetLocationUpdated(asset_id, latitude, longitude, timestamp)
            self.mediator.publish(event)

接下来,我们将检查AssetManager类需要的更改,以包含跟踪这些位置的功能。您应该注意,要使该项目完全运行起来,您需要修改AssetManagerSQLAlchemyAssetRepositoryAsset类,并在数据库中创建一个名为itam.asset_locations的新表。完整且更新的源代码可在本书的 GitHub 存储库中找到。现在,我们将专注于为我们的系统流动的事件所需的更改,并在需要时使用存储库作为参考。

图 5.3 AssetManager需要另外一个构造函数参数和一个方法来处理其位置对象的更新

包含文本、屏幕截图、字体、收据描述的图片 自动生成

对于AssetManager类,有两个必需的更改:首先,我们需要将AssetLocationMediator添加到构造函数中,并注册它来处理AssetLocationUpdated事件。其次,我们需要添加一个处理此事件的方法。在这种情况下,我们称此方法为update_asset_location。删节后的代码如下。

图 5.18 更新后的AssetManager类的构造函数和事件处理程序
from itam.infrastructure.mediators.asset_location_mediator import 

class AssetManager:
    def __init__(self, base_repository: BaseRepository[Asset], mediator: AssetLocationMediator):
        self._repository = base_repository
        self.mediator = mediator
        self.mediator.register_handler(AssetLocationUpdated, self.update_asset_location)

    def update_asset_location(self, event: AssetLocationUpdated) -> None:
        asset = self.read(event.asset_id)
        asset.add_location(event.latitude, event.longitude, event.timestamp)
        #self.update(asset)
        print(f"Asset {asset.id} location updated to {event.latitude}, {event.longitude} at {event.timestamp}")

Asset类的add_location方法仅仅是将新的位置追加到位置列表的末尾。更复杂的领域模型可能会包括一个current_location属性,将其余部分归类为历史位置列表;然而,鉴于我们试图使我们的事件在系统中流动,使事情保持简单是明智的。

我们待办事项清单上只有一项最终项目:创建主题。如何创建主题呢?这是一个很好的问题。幸运的是,我们运行的 Docker 容器中有我们需要的所有工具。所以,让我们登录到我们的 Kafka Docker 实例中。我们使用以下命令(假设您的 Docker 实例命名为 kafka):docker exec -it kafka /bin/bash.

应该首先检查的是是否已经创建了任何主题。我们可以使用以下命令来执行:kafka-topics --list --bootstrap-server localhost:9092. 此命令将列出在此 Kafka 集群上运行的所有现有主题。正如您所见,没有任何主题。

需要一个主题,让我们来创建它。您可以使用以下命令:kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic asset_location. 如果您重新运行kafka-topics --list命令,您将看到新主题。我们在创建主题命令中包含的分区和复制因子说明告诉 Kafka 我们希望有一个分区和一个复制因子。如果我们是为生产环境或测试以外的任何目的设置这个,我们可能希望它们大于那个数,以确保数据的可用性。

表 5.1 Kafka 控制台命令摘要
动作 命令
创建
kafka-topics --create --bootstrap-server localhost:9092 --replication-factor 1 --partitions 1 --topic asset_location

|

读取
kafka-console-consumer --broker-list localhost:9092 --topic asset_location –from-beginning

|

写入
kafka-console-producer --broker-list localhost:9092 --topic asset_location

|

删除
kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092

|

列出
kafka-topics --list --bootstrap-server localhost:9092

|

现在是有趣的部分,观察应用程序的运行情况。Kafka 自带一个控制台生产者,允许我们从标准输入向 Kafka 发布消息。要做到这一点,我们应该使用以下命令启动控制台生产者:kafka-console-producer --broker-list localhost:9092 --topic asset_location。您将进入一个交互式会话,可以在每行发布一条消息。让我们发布一些消息,模拟我们的资产在芝加哥附近移动的情况。

列表 5.19 Kafka 控制台生产者的条目
{"asset_id": 1, "latitude": 41.8781, "longitude": -87.6298, "timestamp": "2022-01-01T00:00:00Z"}
{"asset_id": 1, "latitude": 41.9000, "longitude": -87.6244, "timestamp": "2022-01-01T00:10:00Z"}
{"asset_id": 1, "latitude": 41.8676, "longitude": -87.6270, "timestamp": "2022-01-01T00:20:00Z"}
{"asset_id": 1, "latitude": 41.8788, "longitude": -87.6359, "timestamp": "2022-01-01T00:30:00Z"}
{"asset_id": 1, "latitude": 41.8740, "longitude": -87.6298, "timestamp": "2022-01-01T00:40:00Z"}
{"asset_id": 1, "latitude": 41.8800, "longitude": -87.6300, "timestamp": "2022-01-01T00:50:00Z"}
{"asset_id": 1, "latitude": 41.8775, "longitude": -87.6244, "timestamp": "2022-01-01T01:00:00Z"}
{"asset_id": 1, "latitude": 41.8745, "longitude": -87.6328, "timestamp": "2022-01-01T01:10:00Z"}
{"asset_id": 1, "latitude": 41.8825, "longitude": -87.6298, "timestamp": "2022-01-01T01:20:00Z"}
{"asset_id": 1, "latitude": 41.8800, "longitude": -87.6250, "timestamp": "2022-01-01T01:30:00Z"}

当您输入这些消息时,您应该看到应用程序的输出,指示位置已经更新。

为了完整起见,还有一个命令需要您注意:在输入这些消息时,您可能会犯错。一个无效的消息可能会导致您的消费者出现问题。一个可能的解决方案是删除主题。删除主题可能听起来很夸张,但这将解决问题。因此,这就是那个命令:kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092.

在本节中,我们已经添加了实时跟踪我们的 资产 在 Apache Kafka 中的位置变化的能力。在本章的最后一节中,我们将使用 Copilot Chat 扩展能力,通过实时监控我们的 资产,尝试确定它们是否位于它们应该位的位置。同样,我们将探讨使用 Spark 和 Kafka 一起完成此分析。完成后,我们将感谢我们的信息安全团队,他们担心我们的核心业务和知识产权过多存在于这些 资产 中。

5.3 使用 Apache Spark 进行分析、学习和跟踪

资产的实时跟踪是一个业务关键功能。您的 IT 资产包含着敏感的商业数据、客户名单、销售数据、利润和损失(PnL)预测以及销售策略,以及许多其他项目。一次丢失的资产可能是公司的生存危机。因此,对于许多信息安全专业人员来说,仔细的管理和监控是首要任务。在本节中,我们旨在使他们的工作变得更加轻松。现代数据平台使实时跟踪资产并在出现可疑情况时发送通知变得微不足道。让我们开始吧。

Apache Spark 是一个强大的、开源的数据处理引擎,以速度、易用性和复杂的分析而闻名。它旨在提供一个比 MapReduce 更好的处理大数据集的替代方案,并且可以处理批处理和实时分析。Spark 提供了 Scala、Java、Python 和 R 的 API,并且具有用于 SQL 查询的内置模块。其核心数据结构,弹性分布式数据集(RDD),支持容错操作,并允许数据在计算机集群上并行处理。

Spark 还包括几个库来扩展其功能,包括用于机器学习的 MLlib,用于处理实时数据流的 Spark Streaming,以及用于处理结构化数据的 Spark SQL 和 DataFrames。这些工具使其非常适合从机器学习到实时数据流和批处理的任务。其内存处理能力使 Spark 比其前身快得多,使其成为大数据处理的热门选择。

首先,我们将要求 Copilot Chat 推荐一种使用 Apache Spark 跟踪我们资产的策略。

清单 5.20 请求 Copilot Chat 如何最好地实时跟踪我们的资产
Let's imagine that our company is in Chicago. We want to create a class called AssetLocationSparkAdapter that will continuously stream AssetLocation json messages from a Kafka topic called asset_location. An AssetLocation has the following attributes asset_id: int, latitude: float, longitude: float, and timestamp: datetime. The AssetLocations are stored in a Postgres database in a table called itam.asset_locations. AssetLocationSparkAdapter should calculate if the new AssetLocation is more than 25 miles from Chicago. If it is, then it should write a message to the console. This should be using the latest version of Spark. How would we do this?

Copilot Chat 生成了应该放入 infrastructure 包中名为 asset_location_spark_adapter.py 的文件的类。幸运的是,它还为每一行包括了注释,因此你应该会发现生成的代码很容易跟踪。导入语句包括 Spark 库以及 geopy。

清单 5.21 运行 Spark 所需的导入项
from pyspark.sql import SparkSession
from pyspark.sql.functions import from_json, col, expr
from pyspark.sql.types import StructType, StructField, IntegerType, DoubleType, TimestampType
from geopy.distance import distance

课程以一个过于臃肿的构造函数开始,该构造函数定义了 Spark 在将 JSON 转换为 DataFrame 时将使用的模式。

注意

如定义的 AssetLocationSparkAdapter 是一个阻塞进程。因此,在 Spark 进程被终止之前,你的 FastAPI 应用程序将不会“完全”启动。你希望这是一个独立的进程,或者你需要引入一个异步框架来使这两个进程同时运行。

接下来,它将启动一个本地 Spark 实例/会话,允许 Spark 连接到 Kafka 主题并持续地流式处理记录。

清单 5.22 负责处理 Kafka 主题并生成通知的 AssessLocationSparkAdapter
class AssetLocationSparkAdapter:
    def __init__(self):
        # Define the schema for the incoming JSON data
        self.schema = StructType([
            StructField("asset_id", IntegerType()),
            StructField("latitude", DoubleType()),
            StructField("longitude", DoubleType()),
            StructField("timestamp", TimestampType())
        ])

        # Create a SparkSession
        self.spark = SparkSession.builder \
            .appName("AssetLocationSparkAdapter") \
            .getOrCreate()

        # Create a streaming DataFrame from the asset_location topic
        self.df = self.spark \
            .readStream \
            .format("kafka") \
            .option("kafka.bootstrap.servers", "localhost:9092") \
            .option("subscribe", "asset_location") \
            .option("startingOffsets", "earliest") \
            .load() \
            .selectExpr("CAST(value AS STRING)")

        # Parse the incoming JSON data
        self.parsed_stream = self.df \
            .select(from_json(col("value"), self.schema).alias("data")) \
            .select("data.*")

AssetLocationSparkAdapter 类的最后一部分将计算资产当前位置到芝加哥的距离。如果差距大于 25 英里,则将结果集发送到控制台。此外,它还提供了一个方法来启动和停止适配器。

清单 5.23 AssessLocationSparkAdapter 计算当前资产位置与芝加哥的距离
        # Calculate the distance between the current location and Chicago for each asset
        self.distance = self.parsed_stream \
            .withColumn("distance", expr("calculate_distance(latitude, longitude, 41.8781, -87.6298)")) \
            .select(col("asset_id"), col("timestamp"), col("distance")) \
            .filter(col("distance") > 25)

        # Write the results to the console
        self.query = self.distance \
            .writeStream \
            .outputMode("append") \
            .format("console") \
            .start()

    def run(self):
        # Start the streaming query
        self.query.awaitTermination()

    def stop(self):
        # Stop the streaming query and SparkSession
        self.query.stop()
        self.spark.stop()

calculate_distance 方法接受资产位置的经度和纬度,并使用 geopy.distance 函数确定距离芝加哥的距离。

清单 5.24 Spark 用于计算芝加哥和你的资产之间距离的函数
def calculate_distance(lat1, lon1, lat2, lon2):
    return distance((lat1, lon1), (lat2, lon2)).miles

在这种情况下,Copilot Chat 生成的代码存在一些问题,阻止其在本地运行。在本地运行并遇到这些问题后,搜索 Stack Overflow,你会发现代码的两个主要问题的解决方案:缺少用于本地运行的环境变量,以及未能注册您的 UDF(用户定义的函数)。幸运的是,你不需要进行测试和研究,因为清单 5.23 中提供了解决方案。

清单 5.25 在本地运行应用程序所需的编辑
os.environ['PYSPARK_SUBMIT_ARGS'] = '--packages org.apache.spark:spark-streaming-kafka-0-10_2.12:3.2.0,org.apache.spark:spark-sql-kafka-0-10_2.12:3.2.0 pyspark-shell'

class AssetLocationSparkAdapter:
    def __init__(self):
        # Create a SparkSession
        self.spark = SparkSession.builder \
            .appName("AssetLocationSparkAdapter") \
            .getOrCreate()
         self.spark.udf.register("calculate_distance", calculate_distance)

最后,要运行你的 Spark 应用程序,你需要在 main.py 中的 main 函数中更新以下代码。

列表 5.26 对 main 函数的更新
if __name__ == "__main__":
    adapter = AssetLocationSparkAdapter()
    adapter.run()

当你将资产位置输入到 Kafka 控制台生产者中时,如果距离芝加哥市中心超过二十五英里,你会注意到条目被写入到控制台中。更新类以将这些结果输出到 Twilio 的短信 API 或类似 SendGrid 的电子邮件服务是微不足道的。

列表 5.27 你的资产位置的流式输出
+--------+-------------------+------------------+
|asset_id|          timestamp|          distance|
+--------+-------------------+------------------+
|       1|2021-12-31 20:30:00| 712.8314662207446|
+--------+-------------------+------------------+

恭喜,你正在实时追踪你的资产,并在公司资源离开的时候发送实时警报。

5.4 总结

  • GitHub Copilot Chat 是一款创新性工具,结合了 ChatGPT 的全面语言理解和 Copilot 的便捷功能。这是编程辅助领域的一个值得注意的发展,特别是在实时提供详细且上下文相关的建议方面,促进了更高效的编码体验。

  • 中介者设计模式是一种独特的行为模式,它促进了对象之间高度解耦,从而增强了代码的模块化。通过在一个中介者对象中包含对象之间的交互,对象可以间接地进行通信,从而减少了依赖性,促进了代码的可重用性和易修改性。

  • Apache Kafka 是一个健壮的、分布式的流平台,专为创建实时数据管道和流应用而设计。它可以有效处理来自多种来源的数据流,并将其传输给各种消费者,使其成为处理大量实时或准实时数据的理想解决方案。需要记住的是,Kafka 优化了追加式、不可变数据,而不适用于需要记录更新或删除,或复杂查询的用例。

  • Apache Spark 是一款性能卓越的、分布式的数据处理引擎,以其速度、易用性和高级分析功能而闻名。它非常适用于需要实时数据处理或对大量数据集进行操作的场景。然而,对于诸如基本分析或简单聚合等较简单的任务,传统的关系型数据库可能是更合适的选择。

  • 尽管生成式人工智能迅速发展,但并非无懈可击。仔细审查所有生成的输出以确保其符合你的特定要求和质量标准至关重要。虽然生成式人工智能不能替代深入的领域知识或编码专业知识,但它通过提供有价值的见解和减少在例行任务上花费的时间来显著提高生产力。

第六章:测试、评估和解释大型语言模型

本章内容包括

  • 轻松草拟单元测试

  • 生成集成测试

  • 确定代码质量和覆盖率

  • 评估软件复杂性

  • 翻译代码和文本

本章将探讨软件工程的一个关键方面:测试。测试软件的行为有多个重要目的。首先,它有助于识别可能会影响软件功能、可用性或性能的错误和问题。此外,它确保软件符合所需的质量标准。通过进行全面的测试,我们可以验证软件是否满足指定的要求,正如预期的那样工作,并产生预期的结果。通过全面的测试,开发人员可以评估软件在各种平台和环境中的可靠性、准确性、效率、安全性和兼容性。在开发过程的早期检测和解决软件缺陷可以节省大量的时间和成本。

当我们完成测试的制定后,我们将评估代码的质量。你将了解到几个有助于评估软件质量和复杂性的度量标准。此外,如果我们需要对代码的目的有更清晰的了解,或者是首次审核代码,我们将寻求解释以确保全面理解。

6.1 测试,测试...一、二、三种类型

测试在软件工程中扮演着重要的角色,因此我们将详细探讨各种类型的测试。这包括单元测试、集成测试和行为测试。首先,我们将利用 Copilot Chat 来帮助我们创建一个单元测试

单元测试

单元测试是一种专注于测试单个组件或代码单元的测试类型,以确保它们在独立环境中的正确运行。通常由开发人员执行这种测试,以帮助识别特定软件单元中的错误和问题。

6.1.1 单元测试

在本节中,我们将创建单元测试来测试我们的软件组件。Python 的有几个用于单元测试的测试框架。每个框架都有其独特的特点,适用于不同的场景。在我们的 AI 工具提供的建议基础上,我们将简要介绍每个框架,然后选择一个特定的框架。

第一个框架是unittest:这是 Python 用于创建单元测试的标准库。它与 Python 捆绑在一起,无需单独安装。unittest提供了丰富的断言集,并非常适合编写简单到复杂的测试用例,但是代码量可能会相当庞大。unittest适合编写基本的单元测试,特别是如果您不想在项目中引入其他依赖项时。在任何需要独立于系统其余部分确认代码功能的情况下,它都非常有用。

接下来,让我们来看一下pytestpytest是一个流行的第三方库,用于单元测试,尽管它足够灵活,可以处理更多不仅仅是单元测试。它比unittest需要更少的样板代码,并且具有强大的功能,例如设置和拆卸的 fixture,参数化测试,以及运行unittest和 nose 测试套件的能力。pytest非常适合简单和复杂的单元测试用例。它也适用于功能和集成测试。如果你重视简单和易用性,并且你的项目不限于仅使用 Python 标准库,那么pytest是一个绝佳的选择。

接下来,我们有nose2,它是被废弃的"nose"测试框架的继承者。它扩展了unittest,使测试变得更容易。它以其测试发现功能而闻名,该功能可以自动找到您项目中的测试,因此您无需手动列出它们。nose2适用于大型项目,其中测试发现可以节省时间。与pytest一样,它可以运行unittest测试套件,因此如果您正在从unittest迁移但希望保留现有的测试,它也是一个不错的选择。

最后,还有doctest。这是 Python 标准库的另一个模块。它不像其他模块那样是一个功能齐全的测试框架,但它允许你直接在文档字符串中编写测试。doctest最适合简单的情况,你想演示如何使用一个函数或模块,并确认示例代码是否按预期工作。这是确保你的文档与代码保持最新的好方法。

Copilot Chat 建议我们使用unittest,所以我们会这样做。你可以自己尝试询问它,并看看它是否会得出不同的结论,这取决于你如何提问。

首先,我们将通过创建从unittest.TestCase类继承的类来定义测试用例。在编写我们的第一个单元测试时,我们将发现一个相当隐匿的错误,这个错误迄今为止一直使我们束手无策。我们马上就要解决这个问题了。最后,我们将测试DepartmentStatisticsVisitor类的正确性。

我们从提示开始,请 Copilot Chat 创建一个DepartmentStatisticsVisitor的单元测试。回复将提醒我们上下文的重要性。Copilot Chat,考虑到我们开始了一个新的聊天会话,将尝试创建这个单元测试,但它缺乏我们任何类的定义。

列表 6.1:为DepartmentStatisticsVisitor定义单元测试的第一次尝试
import unittest
from department_visitor import DepartmentStatisticsVisitor

class TestDepartmentStatisticsVisitor(unittest.TestCase):
    def test_visit_department(self):
        visitor = DepartmentStatisticsVisitor()
        department = {'name': 'Sales', 'employees': [{'name': 'John', 'salary': 50000}, {'name': 'Jane', 'salary': 60000}]}
        visitor.visit_department(department)
        self.assertEqual(visitor.total_salaries, 110000)
        self.assertEqual(visitor.num_employees, 2)
        self.assertEqual(visitor.avg_salary, 55000)

我们应该继续将定义输入到 Copilot Chat 中,以便它可以完善测试用例。我们可以告诉它更多关于访问者类的信息,如下面的代码片段所示。

The DepartmentStatisticsVisitor class visits to determine the total asset depreciation based off of the asset allocation for each department

有了这些更新的信息,Copilot Chat 即将更好地估计我们的单元测试应该如何运行。

列表 6.1:为DepartmentStatisticsVisitor定义单元测试的第一次尝试
import unittest
from department_visitor import DepartmentStatisticsVisitor, Asset

class TestDepartmentStatisticsVisitor(unittest.TestCase):
    def test_visit(self):
        visitor = DepartmentStatisticsVisitor()
        asset1 = Asset(1000, 500, 0.5)
        asset2 = Asset(2000, 1000, 0.3)
        visitor.visit(asset1)
        visitor.visit(asset2)
        self.assertEqual(visitor.get_total_cost(), 3000)
        self.assertEqual(visitor.get_total_depreciation(), 1500)
        self.assertEqual(visitor.get_total_allocation(), 0.8)

单元测试接近所需的内容;但是,Asset 的定义是错误的。我们应该从我们的 asset.py 类中输入正确的定义。Copilot 聊天将继续完善单元测试。

第 6.2 节 在 Copilot 聊天了解如何定义 Asset 后,对 DepartmentStatisticsVisitor n 定义单元测试的下一次尝试。
import unittest
from datetime import date
from typing import List
from dataclasses import dataclass

@dataclass
class Location:
    name: str
    address: str

@dataclass
class Asset:
    id: int
    name: str
    status: str
    category: str
    cost: float
    useful_life: int
    salvage_value: float
    purchase_date: date
    locations: List[Location]

class TestAsset(unittest.TestCase):
    def test_asset(self):
        location1 = Location('Office', '123 Main St')
        location2 = Location('Warehouse', '456 Elm St')
        asset = Asset(1, 'Computer', 'In use', 'Hardware', 1000.0, 3, 100.0, date.today(), [location1, location2])
        self.assertEqual(asset.id, 1)
        self.assertEqual(asset.name, 'Computer')
        self.assertEqual(asset.status, 'In use')
        self.assertEqual(asset.category, 'Hardware')
        self.assertEqual(asset.cost, 1000.0)
        self.assertEqual(asset.useful_life, 3)
        self.assertEqual(asset.salvage_value, 100.0)
        self.assertEqual(asset.purchase_date, date.today())
        self.assertEqual(asset.locations, [location1, location2])

这更接近了,我们还应该提醒 Copilot 聊天,Asset 类还有一个名为 FundingDetails 的类。一旦我们提供了 FundingDetails 的定义,并告知 Copilot 聊天它应该是 Asset 的一部分,我们就更接近了。

第 6.3 节 第三次尝试编写一个相当接近的单元测试。
import unittest
from department_visitor import DepartmentStatisticsVisitor, Asset, FundingDetails
from datetime import date
from typing import List, Dict
from dataclasses import dataclass

@dataclass
class Location:
    name: str
    address: str

class TestDepartmentStatisticsVisitor(unittest.TestCase):
    def test_visit(self):
        visitor = DepartmentStatisticsVisitor()
        location1 = Location('Office', '123 Main St')
        location2 = Location('Warehouse', '456 Elm St')
        asset1 = Asset(1, 'Computer', 'In use', 'Hardware', 1000.0, 3, 100.0, date.today(), [location1, location2])
        asset2 = Asset(2, 'Printer', 'In use', 'Hardware', 500.0, 2, 50.0, date.today(), [location1])
        funding_details1 = FundingDetails(asset1, 'Straight Line', 0.33, { 'IT': 0.5, 'HR': 0.5 })
        funding_details2 = FundingDetails(asset2, 'Double Declining Balance', 0.25, { 'IT': 0.3, 'HR': 0.7 })
        visitor.visit(asset1, funding_details1)
        visitor.visit(asset2, funding_details2)
        self.assertEqual(visitor.get_total_cost(), 1500.0)
        self.assertEqual(visitor.get_total_depreciation(), 550.0)
        self.assertEqual(visitor.get_total_allocation(), 0.8)

唯一需要手动更新的是导入语句。

注意

让这个练习成为对使用大型语言模型的警示,强迫它们在高度专注的任务上表现得很具有挑战性,甚至可能不值得付出这种努力。在现实世界中,没有人会责怪您放弃提示工程,而回到只是编写出这个测试的代码。然而,通过一些坚持,您可能能够建立一个模板库,用于构建一套类似形状的类的单元测试。另一个额外的注意是 Copilot 聊天可以生成编辑器窗口中文件的测试,如果您指示它“为我的代码生成一个单元测试”,但是,它将模拟所有不直接属于正在测试的类的对象/属性。根据您尝试测试的内容,此功能的效用可能值得怀疑。

当我们尝试运行此测试时,我们发现 visitor、asset、funding details 和 depreciation strategy 之间存在循环依赖。循环依赖是指两个或多个模块或组件直接或间接地彼此依赖的情况。在我们的情况下,当 Python 尝试实例化 Asset 时,它会加载 FundingDetails 的定义。

我们通过摆脱对 FundingDetails 类的直接实例化或引用来修复这个问题。

第 6.4 节 更新后的 Asset 类,不再直接引用 FundingDetails 类。
@dataclass
class Asset():
    id: int
    name: str
    status: str
    category: str
    cost: float
    useful_life: int
    salvage_value: float
    purchase_date: date
    locations: List[Location]
    funding_details: None or 'itam.domain.funding_details.FundingDetails'

我们需要对 FundingDetails 类执行相同的操作。它不应该直接引用 DepreciationStrategy 类。

第 6.5 节 更新后的 FundingDetails 类,不再直接引用 DepreciationStrategy 类。
@dataclass
class FundingDetails:
    depreciation_rate: float
    department_allocations: Dict[Department, float]
    depreciation_strategy: DepreciationStrategy or 'itam.domain.depreciation_strategy.DepreciationStrategy'
    asset: None or 'itam.domain.asset.Asset'

正如我们所见,我们能够使用 Copilot 聊天创建一个单元测试。然而,如果我们没有使用 Copilot,可能会更容易地创建它。这个工具非常擅长提供何时以及如何测试您的代码的指导,但是实施(至少目前)还有待改进。

在现实世界中,我们将继续添加单元测试来建立一个实质性的测试体系。你可能会问,什么样的测试是实质性的?我们马上就会探讨这个问题。但是,我们应该首先把注意力转向下一种类型的测试:集成测试

集成测试

集成测试涉及测试软件的不同组件或模块之间的交互,以确保它们能够无缝地配合工作。它验证集成系统是否按预期功能,并检测模块之间的任何不一致或通信问题。

6.1.2 集成测试

在本节中,我们将开发一个集成测试,以便测试端到端的系统。幸运的是,fastapi 自带了自己的测试客户端,这将帮助我们创建这个测试。

我们首先将 AssetController 的定义复制到 Copilot Chat 窗口中。然后我们可以询问 Copilot Chat 如何为这个控制器创建集成测试。鉴于我们在定义中包含了路由,Copilot Chat 应该能够为我们提供准确的集成测试。我们需要指定我们将使用 InMemoryAssetRepository 类,或者在生成测试后修复它。

列表 6.6 AssetController 生成的 Copilot Chat 集成测试
from fastapi.testclient import TestClient
from itam.infrastructure.adapters.memory_asset_repository import MemoryAssetRepository
from itam.infrastructure.ports.asset_rest_port import AssetRestPort
from itam.interfaces.asset_controller import AssetController

def test_get_assets():
    # Create an instance of the InMemoryAssetRepository
    asset_repository = InMemoryAssetRepository()

    # Add some assets to the repository
    asset_repository.create(Asset(name="Asset 1", unit_cost=1000))
    asset_repository.create(Asset(name="Asset 2", unit_cost=2000))

    # Create an instance of the AssetRestPort using the MemoryAssetRepository
    asset_rest_port = AssetRestPort(asset_repository)

    # Create an instance of the AssetController using the AssetRestPort
    asset_controller = AssetController(asset_rest_port)

    # Create a TestClient instance using the AssetController's router
    client = TestClient(asset_controller.get_router())

    # Send a GET request to the /assets endpoint
    response = client.get("/assets")

    # Check that the response status code is 200 OK
    assert response.status_code == 200

    # Check that the response body contains the expected assets
    assert response.json() == [
        {"id": 1, "name": "Asset 1", "unit_cost": 1000},
        {"id": 2, "name": "Asset 2", "unit_cost": 2000},

现在我们将注意力转向我们要检查的最后一种测试类型:行为测试

行为测试

行为测试是一种侧重于系统整体行为的测试类型,从最终用户的角度来看待。行为测试通常用于测试系统的功能,并确保它符合为其定义的要求和规范。

6.1.3 行为测试

从某种意义上说,所有测试都是行为测试,因为测试验证系统的行为。然而,行为测试在某些方面是独特的。让我们总结一下到目前为止我们遇到的不同类型的测试,并将其与行为测试进行对比。

单元测试是一种侧重于测试系统的单个单元或组件的测试类型,通常使用自动化测试。单元测试旨在测试单个函数或方法的功能,并确保它们在各种条件下表现正确。

另一方面,集成测试是一种侧重于测试系统的不同组件或单元之间的交互的测试类型。集成测试通常用于测试不同组件或单元之间的接口,并确保它们正确地配合工作。集成测试可以手动执行或使用自动化测试,并且通常涉及测试系统的不同组件或单元之间的交互,而不是整个系统。

行为测试侧重于根据用户故事或场景定义软件的行为。这些场景以一种特定的格式写入,称为“给定-当-那么”(GWT),用于驱动开发过程。GWT 格式描述了特定场景的前提条件(给定)、操作(当)和预期结果(那么)。

随着我们在测试中的进展,我们可能会发现一些行为或组件在我们的测试中很难设置。此外,我们可能会发现难以隔离特定对象或模块的行为,并测试不同对象之间的交互。为了解决这个限制,我们可以使用一个模拟对象

模拟对象

模拟对象是一种以受控方式模拟真实对象行为的测试替身。它们也可以用来模拟难以通过真实对象复制的错误条件或边缘情况。模拟对象可以手动创建,使用诸如unittest.mockpytest-mock等模拟库。这些库提供了用于创建和配置模拟对象的函数和类。模拟对象可以配置为在调用其方法时返回特定值或引发特定异常。它们还可以用于记录对其方法的调用,以便您可以验证是否正确地使用了正确的参数调用了正确的方法。

我们将在提示中引入模拟对象,以创建AssetManager的行为测试。

列表 6.7 设置AssetManager行为测试的提示
We have a class called AssetManager. AssetManager's constructor takes two class BaseRepository and AssetLocationMediator. The BaseRepository is a repository in which Assets are stored. We want to use InMemoryAssetRepository, which is a subclass of BaseRepository that stores the Assets in memory. Use a mock object for AssetLocationMediator. The AssetManager stores Assets using the method create and gets all of the Assets using the method get_assets. An asset has 10 required positional arguments: 'id', 'name', 'status', 'category', 'cost', 'useful_life', 'salvage_value', 'purchase_date', 'locations', and 'funding_details'. Use named parameters to make it clear which param is going where. Create a BDD script that will add two Assets to the AssetManager, confirming that all Assets cost $4000.00\. This should be implemented in Python. Take it step by step, telling us the tools and libraries you will use.

Copilot Chat 将告诉我们需要创建一个名为asset_manager.feature的新文件(称为特性文件)。在这个文件中,我们将定义AssetManager的属性(以及任何支持类)。例如,在特性文件中定义了一个场景,将两个资产添加到AssetManager,确认所有资产的总成本为$4000.00,并模拟AssetLocationMediator类。

列表 6.8 根据 Copilot Chat 定义的asset_manager.feature文件
Feature: Asset Manager
  As an IT Asset Manager
  I want to be able to manage assets
  So that I can keep track of all IT assets in my organization

  Scenario: Add two assets to the Asset Manager
    Given the Asset Manager is running
    And the InMemoryAssetRepository is initialized
    And the AssetLocationMediator is mocked
    When I create an asset with a cost of $2000.00
    And I create another asset with a cost of $2000.00
    Then the total cost of all assets should be $4000.00

我们应该在项目的根目录中创建一个名为features的目录。我们应该将这个文件放在那个目录中。此外,我们应该添加一个名为steps的子目录。我们将很快得到steps

接下来,我们将创建一个名为 steps.py 的新 Python 文件,并将其放置在 steps 目录中。这个 Python 文件定义了用于场景的步骤实现。AssetManagerInMemoryAssetRepositoryAssetLocationMediatorAsset 类被导入,并定义了六个函数来对应场景中的步骤。这些函数使用 context 对象在步骤之间存储和检索数据。使用 unittest.mock 库的 Mock 类来为 AssetLocationMediator 类创建一个模拟对象。Asset 类使用命名参数来实例化,以澄清哪个参数属于哪个属性。如果我们向 Copilot Chat 请求 steps.py 文件的内容,我们将看到类似以下列表的输出。

列出 6.9 我们在 steps.py 中定义的行为测试的实现细节
from behave import given, when, then
from itam.service.asset_manager import AssetManager
from itam.infrastructure.repository.in_memory_asset_repository import InMemoryAssetRepository
from itam.domain.asset import Asset
from itam.infrastructure.mediators.asset_location_mediator import AssetLocationMediator
from unittest.mock import Mock

@given('the Asset Manager is running')
def step_impl(context):
    context.asset_repository = InMemoryAssetRepository()
    context.asset_location_mediator = Mock(spec=AssetLocationMediator)
    context.asset_manager = AssetManager(context.asset_repository, context.asset_location_mediator)

@given('the InMemoryAssetRepository is initialized')
def step_impl(context):
    pass

@given('the AssetLocationMediator is mocked')
def step_impl(context):
    pass

@when('I create an asset with a cost of ${cost}')
def step_impl(context, cost):
    asset = Asset(id=1, name='Test Asset 1', status='Available', category='Test Category', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Test Location'], funding_details={'Test Funding': 1000})
    context.asset_manager.create(asset)

@when('I create another asset with a cost of ${cost}')
def step_impl(context, cost):
    asset = Asset(id=2, name='Test Asset 2', status='Available', category='Test Category', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Test Location'], funding_details={'Test Funding': 1000})
    context.asset_manager.create(asset)

@then('the total cost of all assets should be ${total_cost}')
def step_impl(context, total_cost):
    assets = context.asset_manager.get_assets()
    assert sum(asset.cost for asset in assets) == float(total_cost)

如果您尚未安装它,请使用 pip 安装 behave 库:pip install behave. 此外,您应将其添加到 requirements.txt 文件中,以确保在稍后构建可部署版本的此应用程序时会被引入。我们将通过从项目的根目录发出以下命令来运行行为测试。

列出 6.10 运行行为测试并生成输出
% behave features
Feature: Asset Manager # features/asset_manager.feature:1
  As an IT Asset Manager
  I want to be able to manage assets
  So that I can keep track of all IT assets in my organization
  Scenario: Add two assets to the Asset Manager          # features/asset_manager.feature:6
    Given the Asset Manager is running                   # features/steps/steps.py:8 0.000s
    And the InMemoryAssetRepository is initialized       # features/steps/steps.py:14 0.000s
    And the AssetLocationMediator is mocked              # features/steps/steps.py:18 0.000s
    When I create an asset with a cost of $2000.00       # features/steps/steps.py:22 0.000s
    And I create another asset with a cost of $2000.00   # features/steps/steps.py:27 0.000s
    Then the total cost of all assets should be $4000.00 # features/steps/steps.py:32 0.000s

1 feature passed, 0 failed, 0 skipped
1 scenario passed, 0 failed, 0 skipped
6 steps passed, 0 failed, 0 skipped, 0 undefined
Took 0m0.001s

在本节中,我们通过使用三种类型的测试:单元测试、集成测试和行为测试,为良好的软件开发奠定了基础。现在,有人可能会争辩说它在项目的开发生命周期中出现得很晚。这个争论也不无道理。在现实世界中,我们会在开发代码时开发我们的测试。有些人可能会认为我们应该在编写代码之前构建测试。您可能持有这种信念,也可能不持有,但无论如何,您都应该尽早测试,并经常测试。

在本书的下一部分中,我们将深入研究一些可用于确定我们软件总体质量的指标,并请求 Copilot 帮助我们评估到目前为止我们代码的质量。

6.2 评估质量

理解软件应用程序的性能、可靠性、可维护性和总体质量是软件工程的重要方面。本章将深入探讨软件质量指标领域的迷人和复杂内容 – 这些量化标准和基准指导我们理解软件系统质量的。

软件质量指标是必不可少的工具,它允许利益相关者 – 开发人员、测试人员、经理和用户 – 评估软件产品的状态,识别其优点和改进空间。它们为产品开发、测试、调试、维护和改进倡议等各种流程提供了经验基础。通过量化软件的特定特性,这些指标提供了一种具体的方法来理解软件质量这一抽象概念。

在本节中,我们将探讨软件质量度量的几个重要类别,包括产品度量、过程度量和项目度量。我们将分析它们的重要性、计算方法以及如何有效利用它们来评估和提高软件质量。这个探讨将包括静态度量,即应用于静态软件系统的度量,以及动态度量,它们评估系统在执行过程中的行为。

软件质量度量不仅有助于软件系统的技术完整性,还有助于确保客户满意度、盈利能力和长期的商业成功。因此,了解这些度量是对软件开发领域的任何从业人员都是非常宝贵的,从工程师和项目经理到高管和软件用户。

在本节中,我们将研究类或代码复杂性和可维护性的一些常见指标。复杂的软件很难理解,这使得开发人员,尤其是新手开发人员,很难把握软件不同部分是如何相互交互的。这可能会减慢员工的适应速度和开发时间。

复杂的代码往往会导致更高的维护工作量。当代码复杂时,修改或修复 bug 可能需要更长的时间,因为很难预测改动系统中某一部分的影响。这可能会导致软件整个生命周期的更高成本。

复杂的软件往往更容易出错。因为它更难理解,开发人员在进行改动时更有可能引入 bug。此外,复杂的代码可能存在许多相互依赖的关系,一处的改动可能在其他地方产生意想不到的影响。

软件越复杂,就需要更多的测试用例来进行彻底测试。由于涉及逻辑的复杂性,编写这些测试用例可能也更加困难。

编写简单和易维护的代码应该是我们的首要任务之一。观察与我们的代码相伴的度量变化应该有助于我们在这方面的努力。在这个目标的推动下,我们可以(也应该)首先使用的度量是圈复杂度

圈复杂度

圈复杂度是量化软件模块中独立路径的数量的度量。它衡量了代码中的决策复杂性,包括循环、条件和分支。较高的圈复杂度值表示增加的复杂性,并暗示着可能存在更多的 bug 和理解、维护代码的挑战。

在文件 department_visitor.py 中,输入片段 6.3 的提示任何位置。Copilot 将立即输出答案。


# Question: What is the cyclomatic complexity of the class DepartmentStatisticsVisitor?
# Answer: 1

Copilot 会告诉您这个类的复杂性为 1。您可能不清楚这个值的含义。如果是后者,您可以要求 Copilot 加以解释。


# Question: Is 1 an excellent cyclomatic complexity?
# Answer: Yes

# Question: Why is one a good value for cyclomatic complexity?
# Answer: Because it is low

Copilot 告诉我们,如果圈复杂度低,则好。 这在直觉上是有道理的。 代码复杂度低意味着更容易理解,因此更容易理解和推理。 也更有可能更容易维护。 我们将要探讨的下一个指标是Halstead 复杂度度量

Halstead 复杂度度量

Halstead 复杂度度量评估软件程序的复杂性,基于代码中使用的唯一运算符和操作数的数量。 这些度量包括程序长度(N1)、程序词汇量(n1)、体积(V)、难度(D)、工作量(E)等。 这些度量提供了有关代码的大小和认知复杂性的见解。

与上次类似,我们将从一个提示开始,要求 Copilot 确定我们的访问者类的 Halstead 复杂度度量。


# Question: What is the Halstead Complexity Measure of the class DepartmentStatisticsVisitor?
# Answer: 2

# Question: What is the Halstead Difficulty Measure of the class DepartmentStatisticsVisitor?
# Answer: 1

# Question: Is 2 a good Halstead Complexity Measure?
# Answer: Yes

# Question: Is 1 a good Halstead Difficulty Measure?
# Answer: Yes

# Question: What is a bad Halstead Difficulty Measure?
# Answer: 10

# Question: What is a bad Halstead Complexity Measure?
# Answer: 10

# Question: What does a high Halstead Difficulty Measures mean?
# Answer: It means that the code is hard to understand

您可能想要继续进行一段时间的问答会话,以查看 Copilot 可以从中获取的信息。 一旦您准备好继续,还有一个指标要探讨:可维护性指数

可维护性指数

可维护性指数是一个综合指标,结合了多个因素,包括圈复杂度、代码行数和 Halstead 复杂度度量,以提供软件可维护性的整体度量。 更高的可维护性指数表示更容易维护和潜在较低的复杂性。

您应该在访问者文件中开始一个类似的讨论,以了解可维护性指数。


# Question: What is the maintainability index of the class DepartmentStatisticsVisitor?
# Answer: 100

# Question: Do we want a high Maintainability Index or low Maintainability Index?
# Answer: high

# Question: Why do we want a high Maintainability Index?
# Answer: Because it is easier to maintain

如果我们得到一个较低的可维护性指数,我们可以重构以减少这个数字。 指标在于它给了我们一个钉子来挂我们的帽子;也就是说,我们可以采取这个措施来改善它。 指标使我们超越了个体的纯美感或主观性。 指标是真实的、可操作的数据。 但 Copilot 还有(至少)一项更多的技巧。 Copilot 不仅能够编写和评估我们的代码,还可以解决代码的缺陷。 让我们来捕虫吧。

6.3 搜索错误

在这一部分,我们将使用一个基本的(尽管相当牵强)示例来演示我们如何使用 Copilot 来查找和修复我们代码中的问题。 这段代码应该循环遍历整数列表并计算总和。 但是,存在一个“眨眼就会错过”的错误。 总和被赋予了 i 的值,而不是将 i 的值添加到累加总和中。

列表 6.11 简单循环遍历整数列表并计算总和
l = [1, 2, 3, 4, 5]

if __name__ == '__main__':
    sum = 0
    for i in l:
        sum = i

    print("sum is", sum)

要调试此问题,我们将引入一个新工具:Copilot 实验室。在 Copilot 聊天之前,Copilot 实验室是我们的 IDE 中某些功能可用的唯一方式,具体来说是 VS Code。例如,我们需要使用 Copilot 实验室来查找和修复错误。Copilot 实验室今天仍然具有的主要优势是,它可以访问编辑器窗格中突出显示的内容。此功能使 Copilot 实验室能够直接在您的 IDE 中的可编辑代码上操作。安装扩展到您的 IDE 后,您应该在 IDE 的左侧看到一个 Copilot 实验室工具包。如果您需要提醒如何将扩展安装到您的 IDE 中,请参考附录 A 到 C,其中包含有关安装扩展的说明。

图 6.1 Copilot 实验室工具包菜单,其中包括查找和修复错误的选项。该工具包还提供增强您的代码以及对其进行文档化的功能。

计算机屏幕截图,自动以低置信度生成的描述

我们将暂时更改 main.py 文件的内容为列表 6.9 中列出的代码。完成此更改后,请突出显示代码,并在 Copilot 实验室工具包中按下“修复 Bug”按钮。您应该会看到类似于图 6.2 中的输出。Copilot 实验室能够确定此代码中的问题,并提供解决此问题的建议。

图 6.2 Copilot 实验室,使用 GPT 模型,已经识别出了错误以及如何解决此错误

计算机屏幕截图,自动以低置信度生成的描述

或者,我们可以将这段代码复制到 ChatGPT 中,并要求它找到错误。然而,可以争论的是,这可能不太方便,因为在请求 ChatGPT 修复之前,您必须知道代码中存在错误。

6.4 覆盖代码

代码覆盖率是衡量您的代码被测试覆盖程度的一种指标。通常以百分比表示,代表您的代码被测试执行的比例。

代码覆盖率可以用作评估测试效果的指标。如果您的代码覆盖率较低,可能表示您的代码的某些部分未经过测试,这可能导致未捕获的错误和其他问题。另外,如果代码覆盖率高,则您可以放心您的代码经过了充分测试。这并不保证您的代码是无错的,但应该表明对于应该在测试中捕获的错误,您具有很高的信心。

为了确定我们的 Python 项目中的代码覆盖率,我们将使用 coverage 库中提供的代码覆盖率工具 coverage。coverage 库通过对我们的代码进行工具化来收集运行时的覆盖率数据。它可以收集任何 Python 代码的覆盖率数据,包括测试、脚本和模块。通过使用像 coverage 这样的代码覆盖率工具,我们可以更好地了解我们的代码有多少被我们的测试所覆盖,并识别可能需要更多测试的代码区域。

首先,让我们使用 pip 安装 coverage:pip install coverage. 接下来,让我们使用 coverage 运行我们的测试:coverage run -m pytest. 这将运行您的测试并收集覆盖率数据。

接下来,我们将生成一个覆盖率报告。覆盖率报告将显示项目中每个文件的代码覆盖率。我们使用以下命令创建基于文本的覆盖率报告:coverage report 或使用以下命令生成报告的 HTML 版本:coverage html。报告的 HTML 版本将位于 htmlcov 目录中。图 6.3 显示了覆盖率报告。

图 6.3 代码覆盖率报告显示了我们信息技术资产管理系统项目中每个文件的覆盖情况。

一张电脑截图,自动以中等置信度生成的描述

代码覆盖率达到 70% 是一个不错的起点。在现实世界中,我们将继续与我们的团队和生成式人工智能小伙伴合作,将这个指标提高到高 90%。

我们将转向一个新的主题:使用生成式人工智能为我们描述代码列表。

6.5 将代码转换成描述 - 从代码到描述

通常,人们会交给你现有的源代码。确定这段代码的确切入口点、代码的目的以及棕地项目的整体结构可能是具有挑战性的。幸运的是,这正是生成式人工智能真正擅长的领域之一:将代码翻译成文本描述。

首先,我们将把(有 bug 的)代码从上一节复制到 Copilot 聊天对话框中,并加上提示语:“这段代码是做什么的?

图 6.4 上一节的有 bug 代码,并附有提示,要求 Copilot 聊天向我们解释这段代码。

一张电脑截图,自动以中等置信度生成的描述

这个解释引人注目的地方在于,Copilot 聊天检测到代码中存在一个 bug,并提出了如何修复这个 bug 的建议。

图 6.5 Copilot 聊天解释了代码的目的,识别了 bug,并提出了修复建议。

一张手机截图,自动以低置信度生成的描述

接下来,我们将提出一个更具挑战性的问题。我们将要求它解释我们在本章前面设置的行为测试。将代码复制到 Copilot 聊天窗口中,并询问这个类做什么。您可能会收到类似以下清单的回复。

列表 6.12 Copilot Chat 解释了我们之前编写的行为测试的行为。
The code you provided is not a class but a series of step definitions for a Behavior-Driven Development (BDD) test using the Behave framework in Python. These steps correspond to a specific scenario written in Gherkin language (Given, When, Then format).

These step definitions describe how to interact with an AssetManager class and an InMemoryAssetRepository class. They're meant to test the creation of assets and the calculation of the total cost of all assets.

Let's go through each step:

@given('the Asset Manager is running'): This sets up the initial state for the test. It initializes the AssetManager with an InMemoryAssetRepository (which presumably stores assets in memory during the execution of the test) and a mocked AssetLocationMediator.

@given('the InMemoryAssetRepository is initialized') and @given('the AssetLocationMediator is mocked'): These steps appear to be placeholders, as they don't execute any actions.

@when('I create an asset with a cost of ${cost}') and @when('I create another asset with a cost of ${cost}'): These steps create Asset instances with the given cost and add them to the AssetManager.

@then('the total cost of all assets should be ${total_cost}'): This step retrieves all the assets from the AssetManager, calculates the total cost, and checks that it matches the expected total cost.

This sequence of steps allows the user to test the process of creating assets and validating the total cost of all assets in the AssetManager.

    # Question: What does this method do?
    # Answer: It gets an asset by its id
    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)

6.6 从一种语言翻译成另一种语言

看起来很遥远,您可能会发现自己正在查看一个语言只是稍有了解的编程语言编写的代码库。或者,您可能想在当前语言中使用来自另一种语言的特定函数。例如,以 JavaScript 重写 AssetFundingDetails 类会是什么样子呢?好吧,让我们问问 Copilot Chat。

列表 6.13 Copilot Chat 将我们的 AssetFundingDetails 类翻译成了 JavaScript。
class Asset {
  constructor(id, name, status, category, cost, useful_life, salvage_value, purchase_date, locations, funding_details) {
    this.id = id;
    this.name = name;
    this.status = status;
    this.category = category;
    this.cost = cost;
    this.useful_life = useful_life;
    this.salvage_value = salvage_value;
    this.purchase_date = purchase_date;
    this.locations = locations;
    this.funding_details = funding_details;
  }
}

class FundingDetails {
  constructor(asset, funding_source, funding_amount) {
    this.asset = asset;
    this.funding_source = funding_source;
    this.funding_amount = funding_amount;
  }
}

我们可能会认为翻译一个或两个类太容易了。好吧,我们将尝试更复杂的事情。如果用 JavaScript 写 AssetManager 行为测试,它会是什么样子?

列表 6.14 Copilot Chat 将我们的 AssetManager 行为测试翻译成了 JavaScript。
const AssetManager = require('./asset_manager');
const Asset = require('./asset');

describe('AssetManager', () => {
  let assetManager;

  beforeEach(() => {
    assetManager = new AssetManager();
  });

  describe('addAsset', () => {
    it('should add an asset to the asset list', () => {
      const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      assetManager.addAsset(asset);
      expect(assetManager.assets.length).toBe(1);
      expect(assetManager.assets[0]).toBe(asset);
    });
  });

  describe('removeAsset', () => {
    it('should remove an asset from the asset list', () => {
      const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      assetManager.addAsset(asset);
      assetManager.removeAsset(asset);
      expect(assetManager.assets.length).toBe(0);
    });
  });

  describe('getAssetById', () => {
    it('should return the asset with the given ID', () => {
      const asset1 = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      const asset2 = new Asset(2, 'Printer', 'Active', 'Hardware', 500.0, 3, 50.0, new Date(), [], null);
      assetManager.addAsset(asset1);
      assetManager.addAsset(asset2);
      const result = assetManager.getAssetById(2);
      expect(result).toBe(asset2);
    });

    it('should return null if no asset with the given ID is found', () => {
      const result = assetManager.getAssetById(1);
      expect(result).toBeNull();
    });
  });
});

另外,您可能希望让 GPT 以惯用的 JavaScript 重写代码。您只需要说:“用惯用的 JavaScript 编写 AssetManager。”Copilot Chat 将告诉您它使用 ES6 中引入的 class 语法来定义 AssetManager 类。

列表 6.15 AssetManager 类被以惯用的 JavaScript 重写。
class AssetManager {
  constructor() {
    this.assets = [];
  }

  addAsset(asset) {
    this.assets.push(asset);
  }

  removeAsset(asset) {
    const index = this.assets.indexOf(asset);
    if (index !== -1) {
      this.assets.splice(index, 1);
    }
  }

  getAssetById(id) {
    return this.assets.find(asset => asset.id === id) || null;
  }
}

这是 AssetManager 行为测试在 JavaScript 中使用 Jest 测试框架的示例(Copilot Chat 在没有我们的建议的情况下包含了这个):

列表 6.16 Copilot Chat 用惯用的 JavaScript 重写了我们的 AssetManager 测试。
const AssetManager = require('./asset_manager');
const Asset = require('./asset');
describe('AssetManager', () => {
  let assetManager;
  beforeEach(() => {
    assetManager = new AssetManager();
  });
  describe('addAsset', () => {
    it('should add an asset to the asset list', () => {
      const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      assetManager.addAsset(asset);
      expect(assetManager.assets.length).toBe(1);
      expect(assetManager.assets[0]).toBe(asset);
    });
  });
  describe('removeAsset', () => {
    it('should remove an asset from the asset list', () => {
      const asset = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      assetManager.addAsset(asset);
      assetManager.removeAsset(asset);
      expect(assetManager.assets.length).toBe(0);
    });
  });
  describe('getAssetById', () => {
    it('should return the asset with the given ID', () => {
      const asset1 = new Asset(1, 'Computer', 'Active', 'Hardware', 1000.0, 5, 100.0, new Date(), [], null);
      const asset2 = new Asset(2, 'Printer', 'Active', 'Hardware', 500.0, 3, 50.0, new Date(), [], null);
      assetManager.addAsset(asset1);
      assetManager.addAsset(asset2);
      const result = assetManager.getAssetById(2);
      expect(result).toBe(asset2);
    });
    it('should return null if no asset with the given ID is found', () => {
      const result = assetManager.getAssetById(1);
      expect(result).toBeNull();
    });
  });
});

在此示例中,我们使用 Jest 测试框架为 AssetManager 类定义了一组行为测试。我们使用 describe 库确定测试套件和单个测试。

在每个测试中,我们创建 AssetManager 类的一个实例并对其执行某些操作,例如添加或删除资产。然后,我们使用 expect 函数对 AssetManager 类的行为进行断言,例如检查资产是否已添加到资产列表或资产是否已从资产列表中删除。

请注意,在 JavaScript 中,定义类和构造函数的语法与 Python 略有不同,我们使用 required 函数从各自的模块中导入 AssetManager 和 Asset 类。

最后,为了大结局:我们将开始一个新的 ChatGPT 4.0 会话,并将第三章中的 Mermaid 图表粘贴到其中。请以提示“这是我应用程序的 Mermaid 图表,请用 Golang 实现。”为主导。它将返回用 Golang 渲染的代码。

列表 6.17 Copilot Chat 用惯用的 Golang 重写了我们的 Asset 类。
type Asset struct {
    id            int
    name          string
    status        string
    category      string
    fundingDetails *FundingDetails
}

func NewAsset(name, status, category string, fundingDetails *FundingDetails) *Asset {
    return &Asset{
        name:          name,
        status:        status,
        category:      category,
        fundingDetails: fundingDetails,
    }
}

接下来,告诉它切换到 Java。

列表 6.18 Copilot Chat 用惯用的 Java 重写了我们的 Asset 类。
class Asset {
    private int id;
    private String name;
    private String status;
    private String category;
    private FundingDetails fundingDetails;

    public Asset(String name, String status, String category, FundingDetails fundingDetails) {
        this.name = name;
        this.status = status;
        this.category = category;
        this.fundingDetails = fundingDetails;
    }

6.7 摘要

  • 单元测试:重点测试代码的单个组件或单元,以识别特定单元内的错误和问题。单元测试将是您代码库中数量最多的部分。

  • 集成测试:测试软件的不同组件或模块之间的交互,以确保无缝集成并检测通信问题。

  • 行为测试:从最终用户的角度测试系统的功能,确保其符合要求和规格。

  • 模拟对象:以受控的方式模拟自然对象的行为,对于测试和模拟错误条件非常有用。Mock 对象特别擅长模仿测试运行所需但不在测试范围内的系统的某些部分。例如,如果您的类有一个构造函数参数为数据库,但您不想直接测试数据库,因为数据可能会更改,导致您的测试无法得出结论、不可重复或不确定。

  • 圈复杂度:衡量软件模块独立路径的数量,表示复杂性和潜在漏洞。

  • Halstead 复杂度度量:根据独特的运算符和操作数评估软件复杂度,提供关于代码大小和认知复杂度的见解。

  • 可维护性指数:组合了圈复杂度、代码行数和 Halstead 度量等因素,评估软件的可维护性。

  • 代码覆盖率:用于评估测试效果的衡量标准,表示代码被测试的程度以及出现未捕获错误的潜力。通常情况下,覆盖率越高越好。

  • 语言熟悉度:需要在一个陌生的编程语言中导航代码或希望在当前语言中使用另一种语言的功能。

第七章:编码基础设施和管理部署

本章内容包括

  • 使用 Copilot 的帮助创建 Dockerfile

  • 使用大型语言模型起草您的基础设施代码

  • 使用容器注册表管理 Docker 镜像

  • 发挥 Kubernetes 的强大能力

  • 无缝地使用 GitHub Actions 发布您的代码

没有比让应用程序闲置更令人沮丧的事情了。因此,将经过充分测试的应用程序快速推向生产是每个称职开发人员的明确目标。由于我们上一章节花费了时间测试我们的产品,现在它已经准备好发布了。

本章将重点介绍从开发到产品发布的关键时刻。在这个关键阶段,理解部署策略和最佳实践对确保成功的产品发布至关重要。

随着我们的应用程序成功地得到了保障和测试,现在是时候将注意力转向推出产品了。为此,我们将利用大型语言模型(LLMs)的强大功能,探索针对云基础设施量身定制的各种部署选项。

通过利用 LLMs 的力量并拥抱它们的部署选项和方法,我们可以自信地在推出产品的复杂领域中航行,为我们的客户提供强大且可扩展的解决方案,同时利用云计算的好处。

首先,我们将为 Docker 开发部署文件。我们将探讨如何创建 Docker 镜像和定义部署文件。此外,我们还将讨论容器化我们的应用程序的最佳实践,以及实现无缝部署的方法。

接下来,我们将利用 Terraform 来定义我们的基础设施代码,并自动部署 AWS 上的 Elastic Compute Cloud(EC2)实例。我们将演示如何编写 Terraform 脚本来在 EC2 实例上提供和部署我们的应用程序,以确保一致且可重现的基础设施设置。

接下来,我们将利用 LLMs 将我们的应用程序部署到 Kubernetes(AWS EKS/ECS)。我们将让 GitHub Copilot 创建适当的 Kubernetes 部署文件,以简化我们的部署流程并高效地管理我们应用程序的生命周期。鉴于我们应用程序的相对简单性,我们不需要像 Helm 这样的 Kubernetes 包管理器。然而,随着服务复杂性和依赖关系的增加,您可能希望将其作为一个选项进行探索。幸运的是,Copilot 也可以为您编写 Helm 图表!

最后,我们将简要展示如何使用 GitHub Actions 从本地迁移到自动化部署。我们可以通过将 LLMs 与这一广泛使用的持续集成和交付(CI/CD)工具集成,自动化我们的构建和部署过程,确保更快和更有效的部署。

注意

虽然本章将使用 AWS 作为我们的云服务提供商,但本章涵盖的原则和实践可以适用于其他云平台甚至没有虚拟化的本地基础设施(裸机),这使我们能够根据业务需求调整和扩展我们的产品部署策略。你会发现,通过使用 LLM 和使用基础设施即代码,你可以(部分地)减轻云平台常见的供应商锁定问题。

你应该注意,如果你选择将这个(或任何应用程序)部署到 AWS,那么你的活动将会产生费用。AWS 和大多数云服务提供商为你提供了免费试用期来了解他们的平台(例如 Google Cloud Platform 和 Azure),但一旦这些信用额用完,你可能会被意外的大额账单所冲击。如果你决定在本章跟随进行,你应该为你能轻松承受的金额设置阈值警报。Andreas Wittig 和 Michael Wittig 的 Amazon Web Services in Action, Third Edition, 2023 年 3 月, Manning Publishing, 第 1.9 节是设置此类计费通知警报的绝佳资源。

7.1 构建一个 Docker 镜像并在本地“部署”它

正如你可能还记得第六章所讲的,Docker 是一个容器化平台,允许我们以传统意义上的很少或几乎没有安装(Docker 外部)的方式运行应用程序。与模拟整个操作系统的虚拟机不同,容器共享主机系统的内核(操作系统的核心部分),并利用主机系统的操作系统功能,同时将应用程序进程和文件系统与主机隔离开。这允许你在单个主机系统上运行多个隔离的应用程序,每个应用程序都有自己的环境和资源限制。下面的图表应该让你对 Docker 运行时和主机之间的关系有所了解。

图 7.1 Docker 利用主机操作系统,同时隔离每个容器。这使得 Docker 容器相比虚拟机更加轻量,因为它们不需要完整的操作系统来运行。

从生产准备性的角度来看,其中一个更令人兴奋的功能是 Docker 更容易运行一些在某种意义上可以自我修复的应用程序。如果它们在运行时失败或崩溃,你可以配置它们在不需要干预的情况下重新启动。在本节中,我们将使用 Copilot 创建文件(称为 Dockerfile),从中构建我们的 Docker 镜像

Docker 镜像

Docker 镜像就像 Docker 容器的蓝图。它们是可移植的,包括应用程序运行所需的所有依赖项(库、环境变量、代码等)。

运行中的 Docker 实例称为 Docker 容器。由于其轻量级特性,您可以在单个主机上运行多个容器而不会出现问题。我们之所以能够这样做,是因为容器化技术共享操作系统内核,以独立的用户空间运行。

注意

最初,您的作者希望将 AWS CodeWhisperer 用作本章的 LLM。考虑到预期的云平台,这似乎是合乎逻辑的。但是,在撰写本文时,AWS CodeWhisperer 仅支持使用编程语言进行编程。它没有基础设施即代码的功能。

我们将从以下提示开始,让 Copilot 为我们起草 Dockerfile。

# Create a Dockerfile for this Python app. The main class is main.py. Use Python 3.10 and install

# the dependencies using the requirements.txt file in this directory. The app should run on port 8080.

您可能会留下一个空文件(除了此注释以外)。基础设施即代码的支持正在不断发展(与 LLM 生态系统一样)。根据 Copilot 聊天的说法,GitHub Copilot 能够为您创建 Dockerfile;但是,您需要按照以下步骤引导它:

  1. Dockerfile 中,键入 FROM python:,然后等待 Copilot 建议使用的 Python 版本。选择您想要使用的版本。

  2. 输入 WORKDIR /app 以设置容器的工作目录。

  3. 输入 COPY . /app 以将项目内容复制到容器中。

  4. 输入 RUN pip install --trusted-host pypi.python.org -r requirements.txt 以安装项目的依赖项。

  5. 输入 EXPOSE 8080 以将端口 8080 暴露给容器。

  6. 输入 CMD ["python", "main.py"] 以指定容器启动时要运行的命令。

或者,您可能希望将先前编写的相同提示复制并粘贴到 Dockerfile 中的 Copilot 聊天提示窗口中。 Copilot 聊天将为您提供所需的 Dockerfille 内容。

列表 7.1 构建 Docker 镜像并准备其运行时生命周期的 Dockerfile 内容
FROM python:3.10-slim-buster
WORKDIR /app
COPY . /app
RUN pip install --trusted-host pypi.python.org -r requirements.txt
EXPOSE 8080
CMD ["python", "main.py"]

使用 Dockerfile,我们将构建一个用于部署和运行我们的应用程序的镜像。我们可以输入以下命令来构建我们的应用程序(从 Dockerfile 所在的目录运行,并且不要忘记末尾的点)。您将需要互联网访问以下载依赖项并创建镜像。

docker build -t itam:latest .

构建 Docker 镜像可能需要几秒钟到几分钟,具体取决于您的系统上安装了哪些镜像和软件包以及您的互联网连接速度。您的耐心很快就会得到回报,因为您很快就会有一个几乎可以安装在任何地方的应用程序,从最普通的商品硬件到您最喜欢的云提供商提供的最大型硬件。然而,在任何地方运行之前,您应该尝试在本地运行它。如果您忘记了命令,Copilot 聊天将乐意并乐于协助。

    docker run -p 8000:8000 -d --name itam itam:latest

您可以通过在命令行中输入此命令来确认您的 Docker 容器正在运行:docker ps | grep itam。您应该看到正在运行的实例。

7.2 通过 Copiloting Terraform 架设基础设施

当您创建和测试应用时,在计算机上使用 Docker 镜像是有用的。但是当到达启动应用程序的时候,我们将需要一台比我们的本地计算机更强大的机器。在本节中,我们将使用 GitHub Copilot 来帮助我们通过让 Copilot 编写用于基础设施即代码工具 Terraform 的必需部署描述符来设置和控制我们的 AWS 基础架构。Terraform 由 HashiCorp 制作,使用领域特定语言(DSL)编写我们希望我们的基础架构看起来像什么的内容。此 DSL 使我们不必理解每个云服务提供商用于配置硬件的所有复杂性和细微差别。此外,它允许我们使用基础设施即代码存储和版本化我们的基础架构。

要开始,我们需要创建一个名为ec2.tf的文件,并添加提示以告知 Copilot 我们打算将其作为 Terraform 文件,并说明我们希望如何建立我们的基础架构。您可能会注意到 Copilot 需要我们在继续之前输入给定行的第一个单词。

列表 7.2 Terraform 文件的示例,包括实例大小和如何构建和配置的说明
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the instance's hostname.

provider "aws" {
 region = "us-west-2"
}
 resource "aws_instance" "example" {
 ami = "ami-0c55b159cbfafe1f0"
 instance_type = "t2.micro"
}

您可能会发现 Copilot 跳过了一个微小但关键的细节:Copilot 没有提供安装和配置 Docker 的代码。鉴于 Docker 是运行我们的应用所必需的,我们需要纠正这个疏忽。事实上,您可能需要手动更新文件以包含安装 Docker 的命令,如下所示:

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

   user_data = <<-EOF
                #!/bin/bash
                sudo yum update -y
                sudo yum install -y docker
                sudo service docker start
                sudo usermod -a -G docker ec2-user
                sudo docker run -d -p 80:80 nginx
                EOF
}

Copilot 应该生成一个完整的 Terraform 文件,类似于列表 7.3 的内容。很可能,代码与此列表不完全匹配,但只要包含关键特性即可:即提供程序、实例、添加 Docker 守护程序的脚本、密钥对和安全组。

列表 7.3 完整列出了用于创建最小 EC2 实例的 Terraform 文件
# Create a Terraform file that provisions an AWS EC2 instance of type t2.micro, installs the Docker daemon, and returns the hostname of the instance.

provider "aws" {
 region = "us-west-2"
}
 resource "aws_instance" "example" {
 ami = "ami-0c55b159cbfafe1f0"
 instance_type = "t2.micro"
 user_data = <<-EOF
 #!/bin/bash
 sudo yum update -y
 sudo yum install -y docker
 sudo service docker start
 sudo usermod -a -G docker ec2-user
 sudo docker run -d -p 80:80 nginx
 EOF
 connection {
 type = "ssh"
 user = "ec2-user"
 private_key = file("~/.ssh/id_rsa")
 host = self.public_ip
 }
 lifecycle {
 create_before_destroy = true
 }
 depends_on = [aws_security_group.allow_http]
}
 resource "aws_security_group" "allow_http" {
 name = "allow_http"
 description = "Allow HTTP inbound traffic"
 vpc_id = "vpc-12345678"
 ingress {
 description = "HTTP from VPC"
 from_port = 80
 to_port = 80
 protocol = "tcp"
 cidr_blocks = ["0.0.0.0/0"]
 }
}
 output "public_dns" {
 value = aws_instance.example.public_dns
 }

如果您使用的是默认的虚拟专用云(VPC),那么第 35 行上的vpc_id条目并不是严格必要的。尽管您会发现 AWS 团队选择的许多默认配置和约定都是有道理的,但如果您有更严格的安全要求,或者如果您想要了解有关您的基础架构的一切而不假设任何内容,那么您可能会考虑使用 Terraform 从头开始设置新的 VPC。您将需要将第 21 行的密钥对条目更改为您可以访问的密钥对。

完成了这个文件后,您应该运行terraform init命令。terraform init命令初始化一个新的或现有的 Terraform 工作目录。此命令下载并安装配置文件中指定的所需提供程序插件和模块。此命令准备就绪。

接下来,您将让 Terraform 解释它打算进行的更改。您可以使用 terraform plan 命令来完成此操作。terraform plan 命令为您的基础架构更改创建执行计划。此命令向您展示了当您应用配置文件时 Terraform 将对基础架构进行的更改。该计划将向您展示将创建、修改或销毁哪些资源,以及将对基础架构进行的任何其他更改。

注意

当您第一次运行 terraform plan 时,可能会出现错误 “Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found。” 您会遇到此错误,是因为 Terraform 尝试连接到 AWS,但无法向 AWS 提供适当的凭据。要解决此问题,您需要创建(或编辑)名为 ~/.aws/credentials 的文件,并添加您的 ITAM AWS 访问密钥 ID 和 AWS 密钥访问密钥凭据。有关如何正确执行此操作的完整详细信息,请参阅 Amazon Web Services in Action, Third Edition4.2.2 配置 CLI 部分。

最后,要应用 Terraform 更改,您将使用 terraform apply 命令。然后,Terraform 将读取当前目录中的配置文件,并将任何更改应用于您的基础架构。如果您自上次运行 terraform apply 以来对配置文件进行了任何更改,例如,如果我们需要启动一个新的数据库实例或更改 EC2 的大小,则 Terraform 将向您显示将要进行的更改的预览,并在应用更改之前提示您确认。

如果您要应用这些更改,那么在几分钟内,您将在您的 VPC 中运行一个全新的 EC2 实例。然而,这只是问题的一半。拥有您手边的计算能力非常棒;然而,您需要某些东西来应用此能力。在这种情况下,我们可以使用这个 EC2 来运行我们的信息系统资产管理系统。以下部分将简要介绍将本地构建的镜像传输到另一台机器的过程。

7.3 移动 Docker 镜像(困难的方法)

首先,我们将从本地计算机导出一个 Docker 镜像,并将其加载到远程计算机上。我们将使用命令 docker saveload 来完成此操作。您可以在本地计算机上使用 docker save 命令将镜像保存到 tar 存档中。以下命令将镜像保存到名为 <image-name>.tar 的 tar 存档中:docker save -o <image-name>.tar <image-name>:<tag>

接下来,您可以使用诸如 SCP 或 SFTP 之类的文件传输协议将 tar 存档传输到远程计算机。您可以在远程计算机上使用 docker load 命令从 tar 存档加载镜像:docker load -i <image-name>.tar. 这将在远程计算机上将镜像加载到本地 Docker 镜像缓存中。一旦镜像加载完成,您可以使用 docker run 命令启动镜像并运行 Docker 容器,就像您构建它后所做的那样。此外,您随后可以将此镜像添加到您的 Docker compose 文件中,其中包含了您的 Postgres 数据库和 Kafka 实例。

注意

Terraform 的前期介绍被大大简化了。当你准备认真学习 Terraform 时,你的首选资源应该是 Scott Winkler 的 Terraform 实战(2021 年 5 月,Manning 出版社)。

本节介绍了如何打包我们的镜像并将其加载到远程主机上。尽管这很容易脚本化,但随着容器注册表的出现,现在更容易地管理部署而无需将它们全部传送到互联网上。在下一节中,我们将探讨其中一个工具 Amazon 的弹性容器注册表(ECR)。

7.4 将 Docker 镜像轻松传送到其他地方

Docker 镜像,我们容器的蓝图,是容器化应用的基本构建块。正确管理它们确保我们保持清洁、高效和有组织的开发和部署工作流程。Amazon ECR 是一个完全托管的 Docker 容器注册表,使开发人员能够轻松地存储、管理和部署 Docker 容器镜像。

首先,让我们深入了解如何将 Docker 镜像推送到 ECR。这个过程对于使您的镜像可以使用和部署是至关重要的。我们将逐步介绍设置您的本地环境、与 ECR 进行身份验证和推送您的镜像。在我们将镜像移到 ECR 之前,我们必须创建一个仓库来存放该镜像。这可以从 AWS 管理控制台中进行,或者我们将要做的就是使用 AWS 命令行界面(CLI)。为我们的镜像创建一个新仓库的命令是:aws ecr create-repository --repository-name itam

接下来,我们需要为我们的 Docker 镜像打上 ECR 仓库的 URL 和镜像名称的标签。我们可能想要将其称为 latest 或使用语义化版本。打标签将使我们能够轻松地回滚或前进系统的版本。我们将使用以下命令为我们的应用镜像打上最新标签:docker tag itam:latest 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest.

然后,我们需要使用 aws ecr get-login-password 命令对 Docker 进行身份验证以访问 ECR 注册表。这将生成一个 Docker 登录命令,您可以使用该命令对 Docker 进行身份验证以访问注册表。登录的命令是 aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com

最后,我们将使用 docker push 命令将 Docker 镜像推送到 ECR 注册表。我们会这样做:docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest

一旦我们的镜像在我们的注册表中,我们的部署选项就大大增加了。例如,我们可以编写一个 bash 脚本,登录到我们的 EC2 实例并执行 docker pull 命令来下载并在该 EC2 实例上运行镜像。或者,我们可能希望采用更可靠的部署模式。在接下来的部分中,我们将逐步介绍在一个名为弹性 Kubernetes 服务(EKS)的强大云服务上设置和启动我们的应用程序的过程。EKS 是由 AWS(亚马逊网络服务)提供的托管 Kubernetes 服务。让我们开始吧!

7.5 将我们的应用程序部署到 AWS 弹性 Kubernetes 服务(EKS)

Kubernetes 相对于简单地在 EC2 实例上运行 Docker 镜像具有许多优势。首先,使用 Kubernetes 管理和扩展我们的应用程序变得更加简单。而且,使用 Kubernetes,我们不必花费大量额外的时间考虑我们的基础架构应该是什么样子。此外,由于其对其镜像生命周期的自动管理(称为 pod),我们的应用程序基本上是自愈的。这意味着如果出现问题,Kubernetes 可以自动修复它,始终保持我们的应用程序平稳运行。

首先,我们需要一个用 YAML(另一种标记语言或 YAML 不是标记语言,这取决于你问的是谁)编写的部署描述符,它将描述我们的信息技术资产管理系统在任何时间点应处于的状态。该文件(通常称为 deployment.yaml)将提供模板,Kubernetes 将根据该模板与当前运行的系统进行比较,并在需要时进行更正。

清单 7.4 用于信息技术资产管理系统的 Kubernetes 部署文件
# Create a Kubernetes deployment file for the itam application. The image name is itam:latest

# The deployment will run on port 8000
 apiVersion: apps/v1
kind: Deployment
metadata:
 name: itam-deployment
 labels:
 app: itam
spec:
 replicas: 1
 selector:
 matchLabels:
 app: itam
 template:
 metadata:
 labels:
 app: itam
 spec:
 containers:
 - name: itam
 image: itam:latest
 imagePullPolicy: Always
 ports:
 - containerPort: 8000

然而,这样是行不通的。Kubernetes 将无法找到我们在部署描述符文件中引用的镜像。为了纠正这一点,我们需要告诉 Kubernetes 使用我们新建的 ECR。幸运的是,这并不像听起来那么困难。我们只需更新文件中的图像条目,指向 ECR 镜像,以及授予 EKS 访问 ECR 的权限(也许有点棘手,但还是可以管理的)。

首先,更新部署 yaml 文件以使用 ECR 镜像。它将类似于这样:image: 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest。然后,我们需要定义一个供 EKS 使用的策略。然后,我们将使用 AWS CLI 或 IAM 管理控制台应用该策略。虽然应用策略略微超出了本书的范围,但您可以使用 Copilot 定义此策略。生成的策略将类似于以下清单。

清单 7.5 允许 EKS 从 ECR 拉取镜像的 IAM 策略

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPull",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<aws_account_id>:role/<role>"
      },
      "Action": [
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:BatchCheckLayerAvailability"
      ],
      "Resource": "arn:aws:ecr:<region>:<aws_account_id>:repository/<repository_name>"
    }
  ]
}

一旦 EKS 能从 ECR 拉取镜像,你会看到一个 Pod 开始运行。然而,你没有办法从外部访问这个 Pod。我们需要创建一个 Service。在 Kubernetes 中,Service 是一种抽象,定义了一组逻辑上的 Pods(在 Kubernetes 对象模型中,你创建或部署的最小、最简单的单位)以及访问它们的策略。

Services 使应用程序的不同部分之间,以及不同应用程序之间能够进行通信。它们通过将 Pods 暴露给网络以及 Kubernetes 内部的其他 Pods 来帮助分配网络流量和进行负载平衡。

7.6 列表:一个 Kubernetes 服务文件,以便为我们的应用程序启用外部访问。
# Please create a service for the application that uses a load balancer type egress
apiVersion: v1
kind: Service
metadata:
  name: itam-service
spec:
  type: LoadBalancer
  selector:
    app: itam
  ports:
  - name: http
    port: 80
    targetPort: 8000

Kubernetes 负责将来自此入口的所有请求通过服务路由到正在运行的 Pods,无论它们运行在哪个主机上。这实现了无缝的故障切换。Kubernetes 预期某些事情会失败。它对此有所准备。因此,分布式系统中的许多最佳实践都被融入了 Kubernetes。进入 Kubernetes 是实现一个可靠、高可用系统的重要第一步。在接下来的部分中,我们将探讨如何减轻将应用程序重复且持续地部署到 Kubernetes 上的负担。我们将探讨使用 GitHub Actions 构建一个小型部署管道。

7.6 章节:在 GitHub Actions 中设置一个持续集成/持续部署管道。

如果发布过程很困难,那么它就不会经常进行。这限制了我们为应用程序和利益相关者增加价值的能力。然而,自动化部署过程显著减少了发布所需的时间。这使得更频繁的发布成为可能,加快了开发的步伐,并加速了向用户交付新功能。持续集成/持续部署(CI/CD)管道可以降低与部署相关的风险。通过进行更小、更频繁的更新,任何出现的问题都可以迅速被隔离和修复,从而将对最终用户的潜在影响降到最低。这些管道促进代码更顺畅地集成并加速部署,简化了软件发布过程。

GitHub Actions 允许我们直接在 GitHub 仓库中构建自定义的 CI/CD 管道。这使得开发工作流程更高效,并使得自动化各种步骤成为可能,让我们能够专注于编写代码,而不是处理集成和部署的后勤工作。

本节提供了使用 GitHub Actions 和 GitHub Copilot 设置 CI/CD 管道的简要介绍。请注意,这不是一份详尽的指南,而是一份概述,介绍了潜在的好处和一般工作流程。这应该作为一个入门教程,让你了解如何使用这些工具来优化你的软件开发过程。

首先,我们将在项目中创建一个文件,路径为:.github/workflows.。注意前导点。我们可以将此文件命名为itam.yaml或任何您想要的名称。在此文件的第一行上,添加以下提示:# 创建一个 GitHub Actions 工作流,每次合并到主分支时都会构建 ITAM 应用程序并将其部署到 EKS。

注意

:像我们在本章中交给 Copilot 的许多基础设施相关任务一样,Copilot 需要大量帮助来为我们创建此文件。我们需要了解此文件的结构以及如何开始每一行。在这种情况下,像向 ChatGPT 或 Copilot Chat 请求构建文件一样是有道理的。

列表 7.7 我们用于构建和部署应用程序的 GitHub Actions 文件的开头
# Create a GitHub Actions workflow that builds the ITAM application on every merge to the main branch and deploys it to EKS.

name: Build and Deploy to EKS
 on:
 push:
 branches:
 - main
jobs:

构建作业首先将代码从我们的 GitHub 存储库中检出。它使用在模块 actions/checkout 版本 2 中编写的代码。类似地,接下来,它将获取 EKS 命令行界面并配置凭据以连接到 EKS。您将注意到 AWS 访问密钥和密码是自动传递到应用程序中的值。GitHub Actions 使用内置的秘密管理系统来存储诸如 API 密钥、密码或证书等敏感数据。此系统已集成到 GitHub 平台中,并允许您在存储库和组织级别添加、删除或更新秘密(和其他敏感数据)。秘密在存储之前已加密,不会显示在日志中,也无法下载。它们仅作为环境变量暴露给 GitHub Actions 运行器,这是处理敏感数据的安全方式。

同样,您可以创建环境参数并在您的操作中使用它们。例如,看看变量ECR_REGISTRY。该变量是使用login-ecr函数的输出创建的。在这种情况下,您仍然需要在您的操作文件中硬编码弹性容器注册表(ECR)。但是,您希望这样做是因为一致性和仅在文件中的一个位置管理它的需要。大多数这些步骤应该对您来说很熟悉,因为我们在整章中都使用过它们。这就是自动化的魔力:它为您做这些事情。

列表 7.8 我们的 GitHub Actions 文件的构建和部署步骤
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v2

    - name: Set up EKS CLI
      uses: aws-actions/amazon-eks-cli@v0.1.0

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: us-west-2

    - name: Build and push Docker image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: itam
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

    - name: Deploy to EKS
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: itam
        IMAGE_TAG: ${{ github.sha }}
      run: |
        envsubst < k8s/deployment.yaml | kubectl apply -f -
        envsubst < k8s/service.yaml | kubectl apply -f -

文件的最后部分将登录到 AWS ECR。操作文件中的步骤调用此部分。完成后,它将输出返回到调用函数。

列表 7.9 我们用于构建和部署应用程序到 EKS 的 GitHub Actions 文件的开头
  login-ecr:
    runs-on: ubuntu-latest
    steps:
    - name: Login to Amazon ECR
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1
      with:
        registry: <your-ecr-registry>
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

总之,探索 Code-as-Infrastructure 使我们能够了解其在任何项目中的重要作用以及如何通过代码更好地管理它。像 Terraform 这样的工具提供了管理基础设施的简化解决方案,而 GitHub 的以代码为中心的功能有助于维护整体工作流程。

引入持续集成和持续部署(CI/CD)流水线,主要通过 GitHub Actions 平台,突出了自动化软件交付流程的重要性。自动化这些流程可以提高软件开发生命周期的速度和可靠性,并减少人为错误的机会。

管理基础设施即代码的旅程不断发展,新工具和实践不断涌现。它需要持续的学习和适应心态。本章为您展示了其好处和可能性。

7.7 摘要

  • 从应用程序开发到产品发布的过渡:讨论了从应用程序准备投入生产到发布上线的过程。

  • 云基础设施的部署策略和最佳实践:详细说明了如何设置您的云基础设施以及实现最佳性能的推荐准则。

  • 使用 Docker 进行应用程序容器化:它解释了 Docker 如何将应用程序及其依赖项捆绑到单个对象中,从而便于部署和管理。

  • 用于基础设施即代码的 Terraform 入门:介绍了 Terraform 如何使您能够使用代码来管理基础设施,从而提高效率并减少错误。

  • 通过 Kubernetes 进行应用部署管理:讨论了 Kubernetes 如何简化容器化应用程序的部署、扩展和管理。

  • 将方法适应不同的云平台或本地基础设施:解释了书中讨论的方法如何适应不同的云平台或本地部署。

  • GitHub Copilot 在创建 Dockerfile 和 Terraform 文件中的作用:讨论了 GitHub Copilot,一个基于人工智能的代码助手,如何帮助您更高效、更准确地创建 Docker 和 Terraform 文件。

  • 将 Docker 镜像从本地导出到远程机器:解释了将 Docker 镜像从本地计算机移动到远程服务器的步骤。

  • 在 AWS 的弹性 Kubernetes 服务(EKS)上部署:讨论了如何在 AWS EKS 上部署容器化应用程序,这是一项托管的 Kubernetes 服务。

  • 创建 Kubernetes YAML 部署描述符:详细说明了如何编写 YAML 格式的 Kubernetes 部署描述符,描述了部署的期望状态。

  • 用于网络流量分发和通信的 Kubernetes 服务形成:解释了如何创建 Kubernetes 服务,这些服务抽象了您与 Pod 的通信和流量路由方式。

  • 将 Docker 镜像推送到亚马逊的弹性容器注册表(ECR):描述了如何将 Docker 镜像上传到 ECR,这是 AWS 提供的完全托管的 Docker 容器注册表。

  • 从本地到使用 GitHub Actions 自动化部署的迁移:讨论了如何使用 GitHub Actions,一个 CI/CD 平台,自动化部署流程。

posted @ 2024-05-02 22:33  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报