Текст
                    
AI-Powered Developer BUILD GREAT SOFTWARE WITH CHATGPT AND COPILOT NATHAN B. CROCKER
Разработка с ИИ КАК ЭФФЕКТИВНО ИСПОЛЬЗОВАТЬ CHATGPT И COPILOT НЭЙТАН Б. К РОКЕР 2025
Нэйтан Б. Крокер Разработка с ИИ: как эффективно использовать ChatGPT и Copilot Перевел с английского А. Бойков Научный редактор Д. Бардин ББК 32.813+32.988.02 УДК 004.8+004.748.5 Крокер Нейтан Б. К83 Разработка с ИИ: как эффективно использовать ChatGPT и Copilot. — СПб.: Питер, 2025. — 240 с.: ил. — (Серия «Библиотека программиста»). ISBN 978-5-4461-4281-1 Использование ИИ-инструментов вроде Copilot и ChatGPT похоже на наем суперумного и быстрого джуниор-разработчика, который готов взяться за любую задачу — от исследования до рефакторинга. Работа с ИИ помогает писать код быстрее, улучшать качество приложений и даже реализовывать идеи, которые могли быть недоступными вашей команде. Эта книга покажет, как использовать ИИ с максимальной пользой. В ней вы найдете подробное руководство по эффективному применению ИИ-инструментов в реальных проектах. Пройдете весь цикл разработки, включая использование ИИ на каждом этапе. Будете использовать ChatGPT и Copilot для генерации кода и идей, автодополнения и создания самодокументируемого приложения. Узнаете, как ИИ помогает тестировать и объяснять код. 16+ (В соответствии с Федеральным законом от 29 декабря 2010 г. № 436-ФЗ.) ББК 32.813+32.988.02 УДК 004.8+004.748.5 Права на издание получены по соглашению с Manning Publications. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки, связанные с использованием книги. В книге возможны упоминания организаций, деятельность которых запрещена на территории Российской Федерации, таких как Meta Platforms Inc., Facebook, Instagram и др. Издательство не несет ответственности за доступность материалов, ссылки на которые вы можете найти в этой книге. На момент подготовки книги к изданию все ссылки на интернет-ресурсы были действующими. ISBN 978-1633437616 англ. 978-5-4461-4281-1 Authorized translation of the English edition © 2024 Manning Publications. This translation is published and sold by permission of Manning Publications, the owner of all rights to publish and sell the same. © Перевод на русский язык ООО «Прогресс книга», 2025 © Издание на русском языке, оформление ООО «Прогресс книга», 2025 © Серия «Библиотека программиста», 2025 Изготовлено в России. Изготовитель: ООО «Прогресс книга». Место нахождения и фактический адрес: 194044, Россия, г. Санкт-Петербург, Б. Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373. Дата изготовления: 09.2025. Наименование: книжная продукция. Срок годности: не ограничен. Налоговая льгота — общероссийский классификатор продукции ОК 034-2014, 58.11.12 — Книги печатные профессиональные, технические и научные. Импортер в Беларусь: ООО «ПИТЕР М», 220020, РБ, г. Минск, ул. Тимирязева, д. 121/3, к. 214, тел./факс: 208 80 01. Подписано в печать 29.08.25. Формат 70×100/16. Бумага офсетная. Усл. п. л. 19,350. Тираж 1000. Заказ 0000. Отпечатано в полном соответствии с качеством предоставленных материалов в OOO «Фотоэксперт». 109316, г. Москва, Волгоградский проспект, д. 42, корп. 5, эт. 1, пом. I, ком. 6.3-23Н.
Краткое содержание Часть 1. Основы Глава 1. Введение в большие языковые модели.......................................................... 22 Глава 2. Начало работы с LLM.......................................................................................... 31 Часть 2 . Ввод данных Глава 3. Проектирование ПО с ChatGPT...................................................................... 48 Глава 4. Реализация ПО с GitHub Copilot.................................................................... 72 Глава 5. Управление данными с GitHub Copilot и Copilot Chat.......................... 110 Часть 3 . Оценка и улучшение Глава 6. Тестирование, оценка и описание кода при помощи LLM.................... 134 Часть 4. Развертывание Глава 7. Настройка инфраструктуры и управление развертыванием............... 162 Глава 8. Разработка безопасных приложений с ChatGPT..................................... 177 Глава 9. GPT без интернета.............................................................................................. 199 Приложения Приложение A. Настройка ChatGPT............................................................................ 214 Приложение Б. Настройка GitHub Copilot................................................................ 219 Приложение В. Настройка AWS CodeWhisperer...................................................... 228
Оглавление От издательства..................................................................................... 11 О научном редакторе русского издания.................................................................. 11 Предисловие......................................................................................... 12 Благодарности...................................................................................... 14 Об этой книге........................................................................................ 16 Для кого эта книга?......................................................................................................... 16 Структура книги.............................................................................................................. 16 О коде................................................................................................................................... 17 Форум liveBook................................................................................................................ 18 Об авторе.............................................................................................. 19 Иллюстрация на обложке....................................................................... 20 Часть 1. Основы Глава 1. Введение в большие языковые модели........................................ 22 1.1. Ускорение разработки............................................................................................. 23 1.2. LLM глазами разработчика ПО.......................................................................... 28 1.3. Когда применять ИИ, а когда не стоит............................................................. 29 Итоги.................................................................................................................................... 30
   Оглавление 7 Глава 2. Начало работы с LLM................................................................ 31 2.1. Знакомство с ChatGPT.......................................................................................... 32 2.1.1. Учимся навигации с GPT-4...................................................................... 32 2.1.2. Построение структуры с GPT-3.5.......................................................... 37 2.1.3. Навигация в море ИИ: от берегов GPT-3.5 к горизонту GPT-4.......39 2.2. Copilot у штурвала................................................................................................... 40 2.3. Слово CodeWhisperer............................................................................................. 42 2.4. Сравнение ChatGPT, Copilot и CodeWhisperer............................................. 44 Итоги.................................................................................................................................... 46 Часть 2. Ввод данных Глава 3. Проектирование ПО с ChatGPT................................................. 48 3.1. Введение в проект ITAM-системы..................................................................... 49 3.2. ChatGPT помогает с проектом системы.......................................................... 49 3.3. Документация на архитектуру............................................................................ 53 Итоги.................................................................................................................................... 71 Глава 4. Реализация ПО с GitHub Copilot................................................ 72 4.1. Закладываем фундамент....................................................................................... 73 4.1.1. Описание модели предметной области................................................ 73 4.1.2. Неизменяемые объекты............................................................................. 74 4.1.3. Декорирование основных классов......................................................... 76 4.1.4. Применение амортизационных политик............................................. 80 4.2. Паттерн за паттерном............................................................................................. 83 4.2.1. Визит в наш отдел........................................................................................ 83 4.2.2. Создание объектов с помощью фабрики (паттерн Factory)......... 84 4.2.3. Система учится строить............................................................................. 87 4.2.4. Отслеживаем изменения........................................................................... 91 4.3. Подключение портов и адаптеров...................................................................... 94 4.3.1. Новый взгляд на гексагональную архитектуру................................. 94 4.3.2. Управление приложением......................................................................... 95 4.3.3. Доступ к данным и новые изменения................................................. 101 4.3.4. Централизация (и вынос) доступа к данным................................... 105 Итоги.................................................................................................................................. 109
   8 Оглавление Глава 5. Управление данными с GitHub Copilot и Copilot Chat.................110 5.1. Сбор датасета........................................................................................................... 111 5.2. Мониторинг активов в реальном времени при помощи Kafka.............. 120 5.3. Анализ, обучение и отслеживание с Apache Spark..................................... 128 Итоги.................................................................................................................................. 131 Часть 3. Оценка и улучшение Глава 6. Тестирование, оценка и описание кода при помощи LLM............134 6.1. Три типа тестов....................................................................................................... 135 6.1.1. Юнит-тестирование.................................................................................. 135 6.1.2. Интеграционное тестирование.............................................................. 140 6.1.3. Поведенческое тестирование................................................................. 141 6.2. Оценка качества...................................................................................................... 145 6.3. В поисках ошибок.................................................................................................. 148 6.4. Покрытие кода........................................................................................................ 150 6.5. Анализ программ: от кода к его описанию.................................................... 152 6.6. Перевод с одного языка на другой................................................................... 154 Итоги.................................................................................................................................. 159 Часть 4. Развертывание Глава 7. Настройка инфраструктуры и управление развертыванием.........162 7.1. Создание Docker-образа и его локальное развертывание........................ 163 7.2. Настройка инфраструктуры при помощи Copilot и Terraform.............. 166 7.3. Перенос Docker-образа (сложный способ)................................................... 169 7.4. Перенос Docker-образа (простой способ)..................................................... 170 7.5. Развертывание приложения на AWS Elastic Kubernetes Service........... 171 7.6. Настройка конвейера непрерывной интеграции и развертывания (CI/CD) в GitHub Actions....................................................... 173 Итоги.................................................................................................................................. 176 Глава 8. Разработка безопасных приложений с ChatGPT........................177 8.1. Моделирование угроз с помощью ChatGPT................................................ 178 8.1.1. Почему это важно в современных условиях..................................... 179 8.1.2. Чем ChatGPT может помочь при моделировании угроз............. 180 8.1.3. Пример: моделирование угроз с ChatGPT........................................ 182
   Оглавление 9 8.2. Изучение структуры приложения и поиск потенциальных уязвимостей..................................................................................................................... 186 8.2.1. Выявление структурных проблем........................................................ 187 8.2.2. Распознавание типовых уязвимостей................................................. 187 8.3. Применение рекомендаций по безопасности............................................... 188 8.3.1. Формирование культуры безопасной разработки.......................... 188 8.3.2. Непрерывное тестирование безопасности........................................ 190 8.4. Шифрование данных при передаче и хранении.......................................... 192 8.4.1. Важность шифрования данных............................................................. 192 8.4.2. Шифрование данных при хранении.................................................... 193 8.4.3. Шифрование данных при передаче..................................................... 196 Итоги.................................................................................................................................. 198 Глава 9. GPT без интернета...................................................................199 9.1. Практическая необходимость............................................................................ 199 9.2. Разворачиваем LLM на своем компьютере................................................... 200 9.2.1. ChatGPT как образец................................................................................ 200 9.2.2. Спрашиваем у Llama 2.............................................................................. 202 9.2.3. Демократизация ответов с GPT-4All.................................................. 208 Итоги.................................................................................................................................. 212 Приложения Приложение A. Настройка ChatGPT.....................................................214 A.1. Создание учетной записи ChatGPT............................................................... 214 A.2. Создание учетной записи с использованием электронной почты....... 215 Приложение Б. Настройка GitHub Copilot.............................................219 Б.1. Установка расширения Copilot для Visual Studio Code........................... 219 Б.2. Установка плагина Copilot для PyCharm...................................................... 222 Б.3. Первый промт для Copilot................................................................................. 226 Приложение В. Настройка AWS CodeWhisperer.....................................228 В.1. Установка расширения CodeWhisperer для VS Code............................... 228 В.2. Установка плагина CodeWhisperer для PyCharm...................................... 232 В.3. Первый промт для CodeWhisperer.................................................................. 238
Посвящается памяти Кэтрин Л. Крокер, чья сила и любовь продолжают меня направлять. И пусть ее больше нет рядом, навечно со мной ее мудрость и дух. Ее наследие живет в каждом написанном мною слове. Покинув этот мир, она навсегда осталась в наших сердцах.
От издательства Мы выражаем огромную благодарность клубу рецензентов ИТ-литературы ReadIT Club за помощь в работе над русскоязычным изданием книги и вклад в повышение качества переводной литературы. Ваши замечания, предложения, вопросы отправляйте по адресу comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! На веб-сайте издательства www.piter.com вы найдете подробную информацию о наших книгах. О научном редакторе русского издания Дмитрий Бардин — ведущий разработчик, архитектор решений, один из авторов курса «Архитектор ПО» от Яндекс Практикума. Опыт в ИТ более 15 лет. Работал руководителем службы продуктовой разработки и ресурс-менеджером, в настоящее время занимается разработкой бэкенда сервиса «Кинопоиск» с применением языков Go и Java.
Предисловие Добро пожаловать! Эта книга — ваш проводник в мир, где программирование и искусственный интеллект (ИИ) работают вместе. Это не просто рассказ о том, как ИИ применяется в разработке, а приглашение исследовать новые возможности написания кода вместе с ChatGPT и GitHub Copilot. Читая эту книгу, вы отправитесь в увлекательное путешествие, которое поможет взглянуть поновому на то, как ИИ может упростить, улучшить и расширить ваши подходы к программированию. Главная особенность этой книги — гибкий подход. В отличие от стандартных технических руководств, здесь нет жестких инструкций и четко заданных алгоритмов. Разработка с применением больших языковых моделей часто приводит к неожиданным, но интересным результатам, даже если исходные данные остаются одинаковыми. Мы не будем просто следовать по готовому маршруту: эта книга даст вам инструменты для поиска собственных решений. Она как компас, который направляет вас в мир новых возможностей. А потому я призываю вас экспериментировать, задавать вопросы и, самое главное, не бояться неожиданных результатов. Пусть книга пробуждает любопытство, развивает креативность и помогает улучшить навыки решения задач. Мир, в котором живут большие языковые модели, гораздо шире, чем может казаться. Они могут намного больше, чем написать за вас сотни строк кода: в их силах полностью изменить ваши представления о работе программиста. Эта книга — ваш наставник, который поможет выйти за привычные рамки кодинга, чтобы узнать, как ИИ меняет процесс написания кода. Она призвана пробудить интерес к безграничным возможностям генеративных моделей ИИ и вдохновить вас исследовать их потенциал. На реальных примерах, практических упражнениях и разборе ключевых концепций вы не только освоите работу с ИИ-инструментами, но и поймете их устройство, сильные и слабые стороны. Как и в любом обучении, чем больше вы увлечены, чем больше вопросов задаете и пробуете новое, тем больше получаете в ответ. Погружаясь в эту тему,
   Предисловие 13 вы развиваете не только технические навыки, но и новый взгляд на роль разработчика в мире ИИ. Мы живем в эпоху перемен в разработке программного обеспечения. Искусственный интеллект и машинное обучение меняют привычные подходы, предлагая инструменты, которые увеличивают скорость работы, креативность и эффективность. С помощью ИИ можно решать более сложные задачи, автоматизировать рутинные процессы и переосмыслить сам подход к программированию. В ваших руках не обычная книга, а путеводитель в новую реальность, где гибкость и мощь ИИ сочетаются с логикой программирования. Независимо от вашего уровня — опытный ли вы разработчик или только начинаете, — здесь вы найдете инструменты, методики и знания, которые помогут адаптироваться к новым технологиям и уверенно двигаться вперед. Помните, любое большое путешествие начинается с первого шага. Открыв эту книгу, вы уже сделали этот шаг. Теперь настало время погрузиться в удивительный мир искусственного интеллекта. Удачи!
Благодарности Начать писать эту книгу было непросто: она потребовала от меня усидчивости и бесконечных часов кропотливой работы. Пришлось преодолеть множество трудностей, но каждая из них обогащала меня опытом, приближала к бесконечному увлекательному миру программирования с ИИ. Но этот труд я бы не смог ни начать, ни закончить без помощи и поддержки некоторых удивительных людей. Выражаю глубокую благодарность редактору Кэти Спосато Джонсон (Katie Sposato Johnson), которая помогла мне в работе над книгой. Ее точные комментарии, критические замечания и конструктивная обратная связь помогли мне упорядочить мысли и превратить их в связный и увлекательный текст. Ее преданность делу и искренняя вовлеченность неоценимы для этого проекта. Отдельная благодарность научному редактору Николаю Нильсену (Nicolai Nielsen), ведущему инженеру по работе с искусственным интеллектом в SymphonyAI. Он не только разработчик, но и создатель образовательного контента — выпускает на YouTube видео о ИИ и компьютерном зрении, а также делает курсы, помогающие людям осваивать эти технологии. Опыт Николая и его внимательное отношение к деталям держали меня в тонусе и не давали забыть о том, как много мне только еще предстоит узнать. Я глубоко признателен всем сотрудникам издательства Manning за их неустанную поддержку на протяжении всего пути. Их профессионализм, дух сотрудничества и стремление к совершенству вдохновляли меня и сыграли важнейшую роль в создании этой книги. Огромное вам спасибо! Мои благодарности всем рецензентам: Кармело Сан-Джованни (Carmelo San Giovanni), Чаду Янторно (Chad Yantorno), Кристоферу Форбсу (Christopher Forbes), Дэну Маккрири (Dan McCreary), Девангу Мехте (Dewang Mehta), Грегу Маклину (Greg MacLean), Говарду Уоллу (Hеvard Wall), Джеффу Смиту (Jeff Smith), Джиму Мэтлоку (Jim Matlock), Джонатану Бойзеру (Jonathan Boiser), Луи Алойа (Louis Aloia), Люку Купке (Luke Kupka), Мариано Юнге (Mariano Junge), Максиму Волгину (Maxim Volgin), Максиму Буало (Maxime Boillot),
   Благодарности 15 Майку Пискателло (Mike Piscatello), Милораду Имбре (Milorad Imbra), Питеру Диктену (Peter Dickten), Филиппу Паттерсону (Philip Patterson), Пьеру-Мишелю Анселю (Pierre-Michel Ansel), Рамбабу Поса (Rambabu Posa), Ребекке Вагаман (Rebecca Wagaman), Риккардо Маротти (Riccardo Marotti), Рою Вильскеру (Roy Wilsker), Стефано Приола (Stefano Priola), Томасу Яеншу (Thomas Jaensch), Томасу Джозефу Хейману (Thomas Joseph Heiman), Тиаго Болдт-Соуза (Tiago Boldt Sousa), Тони Хольдройду (Tony Holdroyd) и Вальтеру Александру МатаЛопесу (Walter Alexander Mata Lуpez). Ваши предложения помогли сделать книгу намного лучше. Моя глубочайшая благодарность моей семье — источнику силы. Спасибо моей жене Дженн (Jenn) за то, что была мне опорой, за бесконечные часы терпения, понимания и любви, вложенные в эту работу. Спасибо моим дочерям, Мэве (Maeve) и Орле (Orla). Вы — мое вдохновение, ваша радость, любопытство и безграничный энтузиазм подпитывают меня во всем, что я делаю. Всем членам моей семьи, кто поддерживал меня в самых разных ситуациях, — огромное спасибо. Эта книга — итог многих и долгих часов усилий, преданности и коллективной работы. Я глубоко признателен всем, кто помогал мне в ее написании. Спасибо вам всем!
Об этой книге Этак книга — ваш незаменимый гид по внедрению больших языковых моделей (ChatGPT и Copilot) в процесс разработки программного обеспечения. Здесь содержатся практические советы и лучшие практики, которые помогут раскрыть потенциал ИИ и использовать его для улучшения проектов. От правильного и ошибочного подхода к внедрению ИИ до реальных примеров его применения — здесь вы найдете инструменты и идеи, которые позволят вам развивать свои навыки и оставаться в авангарде технологических изменений. Для кого эта книга? Эта книга будет полезна как профессиональным разработчикам, так и энтузиастам. Хотя основной фокус сделан на опытных программистах, большие языковые модели могут ускорить обучение и помочь разобраться в сложных концепциях программирования, предоставляя объяснения, примеры кода и рекомендации. Опытные разработчики смогут использовать эти инструменты для повышения продуктивности, оптимизации процессов разработки и решения сложных задач. ИИ также поможет в генерации кода, отладке и предоставлении рекомендаций по лучшим практикам. Структура книги Книга состоит из четырех частей и трех приложений: Часть 1. Основы ƒ ƒ В главе 1 мы познакомимся с большими языковыми моделями (LLM), ƒ историей развития и концепциями генеративного ИИ. Обсудим, где лучше всего применять эти технологии, а когда нужно действовать осторожно. ƒ В главе 2 поработаем с LLM. Сравним ChatGPT, GitHub Copilot и CodeWhisperer, наметим первые шаги к освоению их возможностей.
   О коде 17 Часть 2. Ввод данных ƒ ƒ В главе 3 с помощью ChatGPT займемся планированием системы управ- ƒ ƒ ления ИТ-активами (ITAM) — пример, который мы будем рассматривать почти во всех последующих главах. ƒ В главе 4 сосредоточимся на реализации при помощи GitHub Copilot, поговорим о фундаментальных концепциях: неизменяемости данных, моделировании предметной области и паттернах проектирования. ƒ В главе 5 займемся управлением данными. GitHub Copilot и Copilot Chat помогут в мониторинге и анализе IT-инфраструктуры в реальном времени с Kafka и Apache Spark. Часть 3. Оценка и улучшение ƒ ƒ В главе 6 обсудим тестирование и оценку качества программного обеспе- чения, займемся отладкой и переводом кода на другой язык. Часть 4. Развертывание ƒ ƒ В главе 7 поговорим об инфраструктуре и развертывании, образах Docker ƒ ƒ и CI/CD-конвейерах на основе GitHub Actions. ƒ В главе 8 обсудим безопасность приложений и то, как можно ее обеспечить с помощью ChatGPT. Поговорим о моделировании угроз и лучших практиках защиты данных. ƒ В главе 9 рассмотрим развертывание и локальное использование GPT, включая запуск собственной языковой модели (LLM) и доступ к ИИ через GPT-4All. В приложениях вы найдете пошаговые инструкции по настройке ChatGPT, Copilot и CodeWhisperer. Все главы за исключением последней опираются на знания и навыки, полученные в предыдущих. Поэтому следует читать их последовательно. Последнюю главу можно прочесть в любой момент, хоть сразу же после первой. О коде Исполняемые фрагменты кода можно взять из электронной версии книги (liveBook), которая доступна по адресу https://livebook.manning.com/book/aipowered-developer. Весь код из примеров можно загрузить с сайта издательства Manning (www.manning.com) или специального репозитория на GitHub (https:// github.com/mdowst/ai-powered-developer). Важно отметить, что ценность этой книги заключается в том, чтобы проработать примеры с использованием рекомендованных (или каких-то иных) инструментов. При этом не следует забывать о том, что эти инструменты редко дают одинаковые ответы, даже при неизменных исходных данных. Не стоит
   18 Об этой книге расстраиваться или отчаиваться, если ваш код будет не слишком похож на тот, что вы увидите в книге или в репозитории. Этот код предлагается лишь для ознакомления, он помогает сделать процесс обучения более эффективным, но не является образцом, к которому нужно стремиться. Максимальную пользу из этой книги можно извлечь, используя последнюю версию Python 3 с возможностью установки новых пакетов. Для работы большинства систем, связанных с инфраструктурой, потребуется умение устанавливать образы Docker и запускать контейнеры Docker. В книге содержится множество примеров исходного кода, как в нумерованных листингах, так и прямо в тексте. В обоих случаях он будет выделен моноширинным шрифтом и отличаться от обычного текста. Во многих случаях исходный код пришлось дополнительно форматировать в угоду особенностям печатного издания. Поэтому часто в нем будут лишние разрывы строк и отступы. Форум liveBook Приобретая эту книгу, вы также получаете бесплатный доступ к платформе для онлайн-чтения liveBook издательства Manning (на английском языке). Эксклюзивные возможности liveBook позволяют оставлять комментарии как к книге в целом, так и к отдельным ее разделам или абзацам. Можно легко делать заметки для себя, задавать технические вопросы и отвечать на них, а также получать помощь от авторов и других пользователей. Чтобы получить доступ к форуму, откройте страницу https://livebook.manning.com/book/ai-powered-developer/ discussion. Правила поведения на форумах издательства Manning приведены на сайте https://livebook.manning.com/discussion. В рамках своих обязательств перед читателями издательство Manning предоставляет ресурс для содержательного общения читателей и авторов. Эти обязательства не подразумевают конкретную степень участия автора, которое остается добровольным (и неоплачиваемым). Задавайте автору хорошие вопросы, чтобы он не терял интереса к происходящему! Форум и архивы обсуждений доступны на веб-сайте издательства, пока книга продолжает издаваться.
Об авторе Нэйтан Б. Крокер — сооснователь и технический директор компании Checker, которая предлагает API-ориентированное решение для интеграции традиционной инфраструктуры финансовых рынков с блокчейн-экосистемой. Используя свой опыт в создании инфраструктуры для цифровых активов, Нэйтан руководит технологическим развитием Checker, разрабатывая базовую архитектуру, которая позволяет запускать новые финансовые приложения на блокчейне.
Иллюстрация на обложке Иллюстрация под названием «Junger kroatischer Gebirgsbauer» («Молодой крестьянин с хорватских гор»), помещенная на обложке, взята из коллекции рисунков исторических и народных костюмов, опубликованной в 1912 году. Все рисунки в сборнике созданы и раскрашены вручную. В те времена по одежде можно было определить, где живет тот или иной человек, какое положение занимает в обществе и чем занимается. Издательство Manning приветствует изобретательность и инициативность — качества, присущие индустрии IT, — и в знак этого размещает на обложках изображения, которые демонстрируют богатое разнообразие региональных культур, запечатленное на старинных рисунках.
Часть 1 Основы В части 1 книги мы рассмотрим большие языковые модели (large language models, LLM) и их значение в современной разработке программного обес­печения. Здесь представлена история эволюции генеративного ИИ, а также концептуальная база, помогающая понять принципы работы этих технологий. Отдельное внимание уделим осознанному и ответственному использованию ИИ — вы познакомитесь с ключевыми принципами интеграции ИИ в рабочие процессы, а также с возможными рисками и подводными камнями. Кроме того, в этой главе даны практические советы по работе с LLM: мы сравним популярные инструменты ChatGPT, GitHub Copilot и CodeWhisperer, а также разберем первые шаги для эффективного использования этих технологий.
1 Введение в большие языковые модели В этой главе 3 3 Введение в генеративный ИИ (в частности, в большие языковые модели) 3 3 Преимущества генеративного ИИ 3 3 Где и когда не стоит применять генеративный ИИ Хотите вы этого или нет, но вы незаметно получили повышение. Это случилось со всеми разработчиками. Почти в одночасье инженеры-программисты стали техническими менеджерами. Теперь в вашей команде появился умный и талантливый джуниор-разработчик — генеративный ИИ, который становится вашим новым партнером по программированию. Поэтому теперь в вашу ежедневную рутину должно войти наставничество, руководство и код-ревью. В этой главе представлен обзор генеративного ИИ, в частности больших языковых моделей (LLM) ChatGPT, GitHub Copilot и AWS CodeWhisperer. ПРИМЕЧАНИЕ Это не привычная книга по программированию и не пошаговая инструкция. Вам предстоит взаимодействовать с большими языковыми моделями (LLM), и как в любом диалоге, ответы будут меняться в зависимости от модели и контекста. Результат, который вы получите, может отличаться от примеров в книге — и это нормально. Не расстраивайтесь, а исследуйте возможности. В конечном счете сам процесс обучения не менее важен, чем его результат. Может показаться, что следовать за материалом непросто. Не спешите! Проявите терпение. Если вы дис-
   1.1. Ускорение разработки 23 циплинированны и готовы к экспериментам, то сможете направлять GPT в нужное русло и использовать его так, как задумывалось в этой книге, — для развития ваших навыков программирования. 1.1. Ускорение разработки Добро пожаловать! В мире создателей программного обеспечения настала новая эра. Теперь у каждого из нас есть не просто полезный инструмент, а настоящий друг и помощник, талантливый инженер, который поднимет ваши возможности на новый уровень. Только представьте: вы сможете разрабатывать сложные системы, очень быстро писать код и тестировать его, добиваясь беспрецедентной надежности. И все это благодаря ИИ, который учился у лучших программистов нашей планеты. В этой книге мы расскажем, как он не только поможет в решении повседневных задач, но и позволит добиться ранее недостижимых целей, ускорит разработку, повысит качество, откроет путь к невиданным инновациям. LLM и генеративный ИИ могут значительно ускорить создание ПО: писать за вас шаблонный код. Автоматизация рутины позволит сосредоточиться на творчестве: решать проблемы, придумывать и добавлять полезные фичи. Рассмотрим пример. Допустим, нужно, чтобы ИИ подготовил приложение Flask. Для этого можно написать такой промт: NC Создай шаблон для приложения Flask1. В ответ LLM выдаст что-то похожее на следующий код на Python. Листинг 1.1. Пример кода шаблона приложения Flask, написанный LLM from flask import Flask app = Flask(__name__) @app.route('/') def home(): return "Привет, мир!" if __name__ == '__main__': app.run(debug=True) LLM — это системы глубокого обучения. Они прекрасно умеют отвечать человеческим языком на вопросы, заданные на нем же. Подумайте: чтобы получить готовый к работе код, достаточно просто сказать, что вам нужно. Прямо по-русски: NC 1 Создай класс для управления пользовательскими сессиями на Питоне. NC (Nathan Crocker) — инициалы автора книги. Так здесь и далее обозначаются промты.
   24 Глава 1. Введение в большие языковые модели А вот и возможный ответ. Листинг 1.2. П  ример кода, сгенерированного для создания класса управления сессиями import uuid from datetime import datetime, timedelta class SessionManager: def __init__(self): self.sessions = {} def create_session(self, user_id): """ Создание новой сессии с уникальным идентификатором и ограничением по времени существования. """ session_id = str(uuid.uuid4()) # Сеанс закончится через 1 час expiration = datetime.now() + timedelta(hours=1) self.sessions[session_id] = {'user_id': user_id, 'expiration': expiration} return session_id def get_user_id(self, session_id): """ Получение идентификатора пользователя по идентификатору сессии при условии, что она еще не закончилась.""" session_info = self.sessions.get(session_id) if session_info and session_info['expiration'] > datetime.now(): return session_info['user_id'] # Сеанс закончился или не существует return None def delete_session(self, session_id): """ Удаление сессии с указанным идентификатором. """ if session_id in self.sessions: del self.sessions[session_id] def is_session_valid(self, session_id): """ Проверка существования сессии. """ return session_id in self.sessions and \ self.sessions[session_id]['expiration'] > datetime.now() ИИ может помочь и при отладке: к примеру, выявить типовые фрагменты кода и посоветовать исправления. Можно попросить его проверить функцию и указать на возможные утечки памяти. И вот что можно получить в ответ. Листинг 1.3. И  спользование LLM для поиска возможных утечек памяти в Python def process_data(): large_data = [x for x in range(1000000)] # Большой список из чисел result = sum(large_data) print("Результат обработки:", result) # Большой блок данных не удален, переменная все еще ссылается на него # Потенциально может остаться в памяти process_data()
   1.1. Ускорение разработки 25 По мнению ChatGPT, в этом коде возможна утечка памяти, так как большой список не очищается после работы с ним и может сохраняться в памяти дольше, чем нужно. В улучшенном варианте ИИ предлагает явно очищать список или присваивать ему None после использования, чтобы ускорить освобождение памяти, особенно в средах, где сборщик мусора не освобождает ее сразу. Листинг 1.4. Улучшенный LLM-код: потенциальная утечка устранена def process_data(): large_data = [x for x in range(1000000)] # Большой список из чисел result = sum(large_data) print("Результат обработки:", result) # Удаление ссылки на список для высвобождения памяти large_data = None process_data() Кроме того, ИИ может дать советы по рефакторингу кода, сделать его более понятным и эффективным, как, например, в следующих двух примерах. Листинг 1.5. Избыточный код до предложенного рефакторинга class DataProcessor: def __init__(self, data): self.data = data def process_data(self): if self.data is not None: if len(self.data) > 0: processed_data = [] for d in self.data: if d is not None: if d % 2 == 0: processed_data.append(d) return processed_data else: return [] else: return [] processor = DataProcessor([1, 2, 3, 4, None, 6]) result = processor.process_data() print("Обработанные данные:", result) После рефакторинга код стал более читаемым, удобным для сопровождения и соответствующим лучшим практикам языка. Листинг 1.6. Код после рефакторинга LLM намного короче исходного class DataProcessor: def __init__(self, data): self.data = data or []
   26 Глава 1. Введение в большие языковые модели def process_data(self): return [d for d in self.data if d is not None and d % 2 == 0] processor = DataProcessor([1, 2, 3, 4, None, 6]) result = processor.process_data() print("Обработанные данные:", result) LLM умеют гораздо больше, чем просто генерировать код: они достаточно умны, чтобы помочь в проектировании программной архитектуры. Поэтому программист при работе с ИИ может вести себя более творчески и стратегически, запрашивать не отдельные фрагменты кода, а задавать общие цели и требования к функциональности. В ответ LLM может предложить архитектурные решения, подходящие паттерны проектирования или даже сформировать структуру всей системы. Этот подход экономит время и дает возможность использовать опыт, накопленный ИИ в ходе обучения, предлагая оптимизации и идеи, которые разработчик мог бы не рассмотреть сразу. Благодаря такой гибкости LLM становится незаменимым инструментом в креативных и итеративных процессах разработки ПО. Мы подробно разберем это в главе 3. ИИ повышает качество и безопасность кода и документации, что гарантирует их соответствие самым высоким стандартам. Например, при интеграции новой библиотеки ИИ автоматически предлагает наиболее безопасные и эффективные способы ее применения и помогает избежать распространенных проблем с безопасностью. Наконец, обучение новым языкам программирования и фреймворкам становится значительно проще. ИИ может в реальном времени анализировать контекст и предоставлять релевантные рекомендации, помогая не только понять, но и применять новые концепции на практике. Например, если вы переходите на новый фреймворк, скажем Dash, ваш ИИ-ассистент сможет мгновенно сгенерировать примеры кода и предоставить детальные пояснения, адаптированные под ваш проект. Листинг 1.7. П  ример кода, сгенерированный LLM, демонстрирующий использование библиотеки import dash from dash import dcc, html from dash.dependencies import Input, Output import pandas as pd import plotly.express as px # Создание тестовых данных dates = pd.date_range(start='1/1/2020', periods=100) prices = pd.Series(range(100)) + pd.Series(range(100))/2 # Простая последовательность для имитации изменения цен на акции data = pd.DataFrame({'Дата': dates, 'Цена': prices}) # Инициализация приложения Dash (обычно происходит в главном модуле) app = dash.Dash(__name__) # Определение структуры приложения
   1.1. Ускорение разработки app.layout = html.Div([ html.H1("График цен на акции"), dcc.DatePickerRange( id='date-picker-range', start_date=data['Date'].min(), end_date=data['Date'].max(), display_format='MMM D, YYYY', start_date_placeholder_text='Start Period', end_date_placeholder_text='End Period' ), dcc.Graph(id='price-graph'), ]) # Коллбэк для обновления графика по входным данным @app.callback( Output('price-graph', 'figure'), Input('date-picker-range', 'start_date'), Input('date-picker-range', 'end_date') ) def update_graph(start_date, end_date): filtered_data = data[(data['Date'] >= start_date) & (data['Date'] <= end_date)] figure = px.line(filtered_data, x='Date', y='Price', title='Изменение цен на акции с течением времени') return figure # Запуск приложения if __name__ == '__main__': app.run_server(debug=True) Результат работы этого кода Dash показан на рис. 1.1. График цен на акции 1 янв., 2020 9 апр., 2020 Изменение цен на акции с течением времени 150 Цена 100 50 0 5 янв. 2020 19 янв. 2 февр. 16 февр. 1 марта 15 марта 29 марта Дата Рис. 1.1. Дашборд с графиком цен на акции, созданный ChatGPT в ответ на запрос «Создай простой дашборд на основе Dash» 27
   28 Глава 1. Введение в большие языковые модели Настоящая сила LLM раскрывается при их интеграции со средой разработки (IDE). Инструмент GitHub Copilot от Microsoft позволяет при помощи ИИ в режиме реального времени генерировать код напрямую в IDE, например в Visual Studio Code. Мы рассмотрим это в главе 4. Эта книга не только объяснит ключевые концепции, но и покажет на практических примерах, как можно использовать LLM для повышения продуктивности и улучшения качества кода. Мы начнем с настройки окружения, а закончим решением довольно сложных задач. Вы узнаете, как максимально эффективно применять интеллектуальные инструменты в повседневной разработке. 1.2. LLM глазами разработчика ПО Как и в любом практическом руководстве, в этой книге не будет много теории. Но в этом разделе я приведу наиболее важные сведения о том, как добиться от нашего нового помощника максимальной пользы. Тем, кто хотел бы знать больше Желающим поближе познакомиться с теорией, на которой строятся LLM, нейронные сети, а также генеративный ИИ, стоит прочесть эти две книги: «Строим LLM с нуля» Себастьяна Рашки (издательство «Питер», 2025 г.) и «The Complete Obsolete Guide to Generative AI» Дэвида Клинтона (David Clinton) (издательство «Manning», 2024 г.). Начнем с простого определения, что такое LLM и чем она может быть полезна. Это поможет вам правильно объяснить ее коллегам или руководству. Большая языковая модель — это модель искусственного интеллекта, которая анализирует, понимает и генерирует текст, написанный обычным человеческим языком, используя огромные объемы данных, на которых она была обучена. Эти модели относятся к области глубокого обучения и особенно хорошо работают в задачах обработки естественного языка (NLP, natural language processing). Как следует из названия, большая языковая модель отличается не только размером набора данных, на которых она обучалась, но также своей сложностью и количеством параметров. Современные модели, например GPT-4 от OpenAI, могут содержать сотни миллиардов параметров. При обучении LLM читают и анализируют огромный объем самых разнообразных текстов, включая статьи, книги и материалы из интернета. Это позволяет моделям понимать структуру языка, нюансы речи и сложные закономерности человеческого общения.
   1.3. Когда применять ИИ, а когда не стоит 29 Большинство LLM основаны на архитектуре Transformer — модели глубокого обучения с механизмом внутреннего внимания (self-attention), которые позволяют оценивать важность слов в составе предложения независимо от их места в нем, а значит, генерировать тексты, более значимые с точки зрения контекста. Типичная архитектура Transformer состоит из кодировщика (encoder) и декодировщика (decoder), каждый из которых включает многоуровневую структуру. Понимание архитектуры LLM помогает более эффективно использовать их возможности, а также учитывать их ограничения при практическом применении. Модели постоянно развиваются, что обещает появление новых, все более сложных инструментов, которые разработчики смогут использовать для улучшения своих проектов. 1.3. Когда применять ИИ, а когда не стоит Генеративный ИИ (а значит, и большие языковые модели) — это не универсальное решение. Чтобы максимизировать их пользу и избежать возможных проблем, важно понимать, когда их применение оправданно, а когда может быть неэффективным или даже проблемным. Начнем с того, в каких случаях стоит использовать LLM. Повышение производительности: автоматическая генерация шаблонного кода, создание документации, получение подсказок прямо в IDE. В главах 3 и 4 мы поговорим о том, как применять GitHub Copilot для более эффективного кодинга. Обучение и исследования: использование ИИ для изучения новых языков программирования и фреймворков, генерации примеров кода и пояснений. В главе 5 мы посмотрим, как с помощью ИИ ускорить обучение и освоение новых технологий. Автоматизация рутинных задач: использование ИИ для автоматизации тестирования ПО, ввода данных и других повторяющихся задач, чтобы освободить время для чего-то более сложного. В главе 7 мы обсудим автоматизацию тестирования и сопровождения кода. Есть и ситуации, когда использовать LLM не следует. Применение ИИ в средах с чувствительными или проприетарными данными может привести к утечке информации. Тому есть несколько причин. Одна из них состоит в том, что для работы с моделью необходим контекст, а значит, по меньшей мере часть вашего кода отправится за пределы защищенной среды и даже, возможно, послужит учебными данными при дальнейшем обучении модели. Но в главе 9 мы рассмотрим несколько способов решить эту проблему.
   30 Глава 1. Введение в большие языковые модели Возможности ИИ также могут быть ограничены в случаях, когда решение задачи требует точности и квалификации. Поскольку LLM, как правило, вносит в ответы некую долю случайности (иногда это называют галлюцинациями), результат может оказаться довольно далеким от правильного. Поэтому сгенерированный код обязательно нужно проверить и лишь затем использовать на практике. Генеративный ИИ обладает множеством преимуществ. Но использовать его нужно с умом, учитывая и контекст применения, и потребности конкретного проекта. Четко понимая, когда можно полностью положиться на LLM, а когда нужно действовать с осторожностью, разработчик сможет повысить свою производительность до максимума и гарантировать этичное и эффективное применение этих технологий. Итоги Генеративный ИИ — одновременно и эволюция, и революция. С одной стороны, это еще один инструмент в арсенале разработчика, который просто продолжает развитие технологий, а с другой — он кардинально меняет сам подход к программированию. Будущее разработки ПО за генеративным ИИ. Даже мифический разработчик с максимальной продуктивностью не сможет работать так эффективно, как программист с LLM-помощником, в силах которого генерировать более качественный код значительно быстрее и дешевле, чем те, у кого такого помощника нет. Лучше потратить время на то, чтобы объяснить ИИ, что нужно сделать и как, чем писать код вручную. Доверяйте ИИ, но всего проверяйте его результат.
2 Начало работы с LLM В этой главе 3 3 Взаимодействие с ChatGPT 3 3 Основы работы с Copilot 3 3 Основы работы с CodeWhisperer 3 3 Паттерны создания промтов 3 3 Сравнение возможностей трех инструментов ИИ В этой главе мы отправимся в путь по миру генеративного ИИ, используя возможности трех революционных инструментов: ChatGPT, GitHub Copilot и AWS CodeWhisperer. Мы разберемся в особенностях каждого и применим их к сложным сценариям, основанным на реальных вопросах с технических собеседований ведущих IT-компаний. Неважно, опытный вы разработчик или просто интересуетесь темой, вы узнаете, как применять инновационные стратегии, которые дадут вам конкурентное преимущество на следующем техническом собеседовании. Приготовьтесь превратить абстрактные концепции в практические решения — там, где ИИ уже меняет процесс найма в сфере технологий. Начнем с двух доступных сегодня версий ChatGPT: GPT-4 и GPT-3.5. Перед нами две цели: понять, как нужно взаимодействовать с ChatGPT, а также определить базовый ориентир, с которым мы будем сравнивать две другие модели. К тому
   32 Глава 2. Начало работы с LLM же знакомство с двумя версиями одного и того же ИИ позволит нам увидеть различия между ними. Также в этой главе мы будем использовать распространенные паттерны промтов для более эффективного взаимодействия с ИИ. 2.1. Знакомство с ChatGPT Контекст — один из важнейших аспектов при работе с ChatGPT. Предыдущие запросы (промты) могут значительно повлиять на то, какие ответы выдаст модель в дальнейшем. В языковых моделях, похожих на ChatGPT, промт — это фраза, которую мы вводим, чтобы получить ответ: одно предложение, абзац или же длинный текст, инструкция к действию или вопрос, на который нам нужен ответ. Каким он будет, зависит не только от качества промта, но и от контекста, вопросов, которые были заданы в текущем сеансе. Поэтому, приступая к новому проекту, лучше всего начать новый сеанс. О том, как создать учетную запись, вой­ти в ChatGPT и написать свой первый промт, можно узнать из приложения А. 2.1.1. Учимся навигации с GPT-4 В этом разделе мы разберемся, как развернуть односвязный список в Python. Что такое односвязный список? Односвязный список — это базовая структура данных в computer science, состоящая из последовательности элементов, каждый из которых хранится в узле. Обычно узлы односвязного списка содержат данные и ссылку на следующий узел. Главная особенность — передвигаться по такому списку можно только в одном направлении. Вставка и удаление, поиск и перебор узлов — вот операции, которые чаще всего выполняют с односвязными списками. Начнем с простого промта: NC В дальнейшем, когда я буду задавать вопрос, попробуй сформулировать его точнее. Ты стажер, изучавший computer science в университете. Как бы ты описал односвязный список на псевдокоде? О’кей, может, это не самый простой промт. Прежде всего мы попросили ChatGPT улучшать и перефразировать запросы, опираясь на данные, полученные моделью при обучении. Чем лучше вопрос, тем лучше ответ. Вы можете спросить, а какие промты лучше? Хороший вопрос! Чем больше конкретики в промте, тем более точный он принесет результат. Общаясь с LLM и, в частности, с ChatGPT, нужно
   2.1. Знакомство с ChatGPT 33 идти от общего к частному, постепенно уточняя полученные ответы. В промтинжиниринге такой подход называется уточняющим: уточнение и улучшение промтов дает все более точные, значимые и продуманные ответы. А во второй части промта мы видим ролевой паттерн, который будем применять на протяжении всей книги. В контексте промт-инжиниринга, особенно в связи с ИИ и LLM, такой паттерн предполагает указание в промте определенной личности или роли, в которой должен выступить искусственный интеллект. Этот подход помогает модели выдавать ответы более последовательно и контекстуально уместно. В данном случае мы попросили ChatGPT представить себя стажером, изучавшим computer science. Почему стажер? Мы хотим, чтобы модель действительно выполняла работу, а не просто давала поверхностные объяснения. Важно помнить: даже играя указанную роль, ИИ может ответить на один и тот же вопрос по-разному, а значит, вы может получить не совсем то, что написано ниже, но что-то похожее. (Обратите внимание: следующие листинги содержат псевдокод.) Листинг 2.1. П  ереформулированный ChatGPT вопрос об односвязном списке Как изучавший computer science стажер мог бы определить структуру и базовые операции с односвязным списком при помощи псевдокода? Определение должно включать в себя основные элементы списка (узлы и связи) и описывать базовые операции над ними: создание узлов и добавление их в список, обход списка. В ответ на такой улучшенный промт ChatGPT выдаст следующий листинг. Листинг 2.2. Ответ ChatGPT на вопрос об односвязном списке # Структура узла class Node data nextNode Constructor(data) this.data = data this.nextNode = NULL # Структура односвязного списка class SinglyLinkedList head Constructor() this.head = NULL Function AddToFront(data) newNode = new Node(data)
34    Глава 2. Начало работы с LLM newNode.nextNode = this.head this.head = newNode Function Append(data) newNode = new Node(data) if this.head is NULL this.head = newNode return end if lastNode = this.head while lastNode.nextNode is not NULL lastNode = lastNode.nextNode end while lastNode.nextNode = newNode Function Traverse() currentNode = this.head while currentNode is not NULL print currentNode.data currentNode = currentNode.nextNode end while ChatGPT обычно сопровождает код пояснениями и документацией. На рис. 2.1 показан текстовый комментарий, который ChatGPT предоставил к сгенерированному коду. • Класс Node: отдельный элемент списка и состоит из полей data (данные) и nextNode (ссылка на следующий узел). • Класс SinglyLinkedList: • `AddToFront`: вставка нового узла в начало списка. • `Append`: вставка нового узла в конец списка. • `Traverse`: обход списка с начала до конца и вывод на экран данных из узлов. Этот псевдокод описывает базовую структуру односвязного списка и основные операции с ним: создание узла, инициализация и перебор списка. Такое описание мог бы дать изу­ чавший computer science стажер. Рис. 2.1. Пояснение к определению односвязного списка (ChatGPT-4) Как можно заметить, ChatGPT вживается в новую роль: стажера со знанием computer science. NC Как изучавший computer science стажер мог бы определить структуру и базовые операции с односвязным списком на Python? Такое определение должно включать в себя основные элементы списка (узлы и связи) и операции над ними: создание узлов и добавление их в список, обход списка.
   2.1. Знакомство с ChatGPT 35 Листинг 2.3. П  редложенная ChatGPT реализация односвязного списка на Python class ListNode: def __init__(self, value): self.value = value self.next = None class SinglyLinkedList: def __init__(self): self.head = None # Изначально в списке нет узлов def append(self, value): """ Вставка нового узла в конец списка """ if not self.head: self.head = ListNode(value) return current = self.head while current.next: current = current.next current.next = ListNode(value) def prepend(self, value): """ Вставка нового узла в начало списка """ new_node = ListNode(value) new_node.next = self.head self.head = new_node def traverse(self): """ Обход списка и вывод данных из узлов """ current = self.head while current: print(current.value, end=" -> ") current = current.next print("None") Кроме того, ChatGPT может прислать простой пример работы с этим кодом. Листинг 2.4. Реализация и применение односвязного списка на Python # Создание нового односвязного списка linked_list = SinglyLinkedList() # Добавление узлов в список linked_list.append(1) linked_list.append(2) # Вставка узла в начало списка linked_list.prepend(0) # Обход списка и вывод данных linked_list.traverse() # Результат: 0 -> 1 -> 2 -> None
36    Глава 2. Начало работы с LLM Аналогично мы можем изменить роль ИИ: пусть он побудет кандидатом на должность инженера на собеседовании. Такое изменение подскажет ChatGPT, что нужно объяснить не только код, но и подход к решению задачи: Ты — студент, получивший образование в области computer science. Ты на собеседовании на должность инженера в ИТ-компанию. Тебя попросили, отталкиваясь от предыдущего определения односвязного списка (на Python), написать метод, изменяющий результат обхода списка на противоположный. Как бы ты реализовал такой метод? NC Листинг 2.5. Разворот списка, предложенный ChatGPT Чтобы реализовать метод, который разворачивает порядок обхода односвязного списка в Python, нужно изменить направление ссылок между узлами. Для этого в каждом из них нужно заменить ссылку на следующий узел ссылкой на предыдущий. После такой замены при обычном обходе списка мы получим обратный порядок узлов. def reverse(self): """ Разворот односвязного списка """ prev = None current = self.head while current: next_node = current.next current.next = prev prev = current current = next_node self.head = prev ChatGPT, скорее всего, объяснит этот код и выдаст текст, похожий на тот, что представлен на рис. 2.2. Объяснение метода Reverse:  1. Инициализация указателей. Используются два указателя: `prev` и `current`. Начальное значение первого — `None`, а второго — ссылка на начало списка.  2. Разворот ссылок. В каждой итерации цикла while: • Временно сохранить ссылку на следующий узел (`next_node`). • Присвоить указателю `current.next` значение `prev`, чтобы развернуть ссылку. • Переместить `prev` в `current`. • Заменить `current` на `next_node`.  3. Обновление ссылки на начало списка. По завершении цикла (когда `current` имеет значение `None`) `prev` указывает на новое начало развернутого списка. Сохраняем эту ссылку в указателе head. Рис. 2.2. Объяснение псевдокода разворота списка (ChatGPT-4)
   2.1. Знакомство с ChatGPT 37 Эта реализация разворачивает односвязный список на месте, и метод обхода будет выводить элементы в порядке, обратном их исходной последовательности вставки. 2.1.2. Построение структуры с GPT-3.5 В этом разделе мы разберем, как использовать ChatGPT-3.5 для создания односвязного списка на Python. ChatGPT-3.5 отлично справляется с генерацией текста, аналогичного написанному человеком, на основе вводных данных. Это делает его ценным инструментом для программирования, поскольку он может предоставлять пошаговые инструкции, предлагать улучшения кода, давать подробные объяснения сложных концепций программирования. Мы используем ChatGPT-3.5, чтобы сгенерировать код на Python для создания односвязного списка. Напомню, что односвязный список — это структура данных, которая представляет собой последовательность узлов, где каждый узел содержит значение и ссылку на следующий узел в последовательности. Эта структура особенно полезна для динамического выделения памяти, а также эффективных операций вставки и удаления элементов. С помощью ChatGPT-3.5 мы упростим процесс написания кода, чтобы реализовать односвязный список так, чтобы он был оптимизированным и удобным для понимания. Пример ниже показывает, как определить классы Node и LinkedList, инициализировать список и выполнять основные операции — добавление элементов и обход списка. Листинг 2.6. Реализация односвязного списка при помощи ChatGPT-3.5 class Node: def __init__(self, data): self.data = data self.next = None class LinkedList: def __init__(self): self.head = None def prepend(self, data): new_node = Node(data) new_node.next = self.head self.head = new_node def append(self, data): new_node = Node(data) if not self.head: self.head = new_node return current = self.head while current.next: current = current.next current.next = new_node
   38 Глава 2. Начало работы с LLM def display(self): current = self.head while current: print(current.data, end=" -> ") current = current.next print("None") def reverse_and_display(self): prev = None current = self.head while current: next_node = current.next current.next = prev prev = current current = next_node self.head = prev current = self.head while current: print(current.data, end=" -> ") current = current.next print("None") # Пример применения: if __name__ == "__main__": linked_list = LinkedList() linked_list.append(1) linked_list.append(2) linked_list.append(3) linked_list.prepend(0) linked_list.display() linked_list.reverse_and_display() Здесь также можно применить ролевую модель: пусть LLM играет какую-то роль или, наоборот, представит себе, что общается с определенным человеком. Этот прием очень полезен, когда нам нужно понять нечто сложное, получить объяснение простыми словами. Например, можно попросить ChatGPT объяснить этот код: NC Представь себе, что я — взрослый человек, специалист по коммуникациям. Опыта работы в ИТ и знаний computer science у меня нет. Расскажи, как работает этот метод. Послушаем объяснения ChatGPT, адресованные человеку без знаний программирования. Листинг 2.7. О  бъяснения ChatGPT человеку без знаний программирования Конечно! Давайте рассмотрим метод reverse_and_display, не прибегая к сложным понятиям и компьютерным терминам. Представьте, что у вас есть полка с книгами и каждая книга имеет закладку, указывающую на следующую книгу в ряду. В этом примере книги — это узлы списка, закладки — это ссылки на следующий элемент.
   2.1. Знакомство с ChatGPT 39 Подготовка: вы начинаете с первой книги (она – начало списка). У вас две свободные руки: левая рука (prev) – запоминает книгу, которую вы только что прочитали. Правая рука (current) – держит книгу, которую вы читаете в данный момент. Рядом стоит стол – это временное место, куда можно положить следующую книгу. Изменение порядка: для каждой книги (узла) выполняем следующие шаги: 1. Запоминаем следующую книгу – используем закладку (ссылку), чтобы найти следующую книгу, и кладем ее на стол. 2. Меняем направление закладки – переворачиваем ее так, чтобы текущая книга теперь указывала на предыдущую, а не на следующую. 3. Меняем книги в руках: книгу, которую только что прочитали (prev), кладем в левую руку – теперь мы ее запомнили. Книгу, которую читаем (current), перекладываем в правую руку – теперь с ней работаем. 4. Берем следующую книгу со стола – идем к отложенной книге и повторяем процесс. Перебор книг: продолжайте перестановку книг по инструкции, пока не дойдете до последней книги. В конце правая рука будет держать последнюю книгу (которая была первой), а левая рука будет пустой. Теперь кладем последнюю книгу (бывшую первую) обратно на полку, и она становится новой первой книгой. Читаем книги в обратном порядке: теперь начните перебирать книги, начиная с той, что теперь стала первой, и следуя закладкам. Читайте вслух название каждой из книг. Задача решена: вы изменили порядок книг на обратный. Таким образом, метод reverse_and_display сначала изменяет порядок книг (узлов) на полке, затем читает их в обратном порядке. Конец ответа Когда ролевой паттерн используется в обратном направлении, это называется паттерном ориентации на аудиторию («Объясни другому»). Такой подход определяет целевую аудиторию, для которой предназначен ответ, и помогает настроить генерацию текста так, чтобы он лучше соответствовал потребностям и ожиданиям конкретной группы пользователей. Перед тем как перейти к GitHub Copilot, сравним качество ответов каждой модели. 2.1.3. Навигация в море ИИ: от берегов GPT-3.5 к горизонту GPT-4 Одним из ключевых различий между GPT-3.5 и GPT-4 является уровень сложности и прозрачности их работы, особенно в том, как они взаимодействуют со структурами данных и изменяют их. Различия в подходах этих моделей отражают общую тенденцию к повышению понятности и предсказуемости кода, сгенерированного ИИ. С развитием ИИ его ответы все больше соответствуют лучшим практикам программирования, что делает его более надежным помощником в разработке ПО. В области программирования, где ясность и точность — это не просто идеалы, а необходимость, эволюция моделей играет важную роль. Написанный ChatGPT-3.5 метод reverse_and_display несколько неочевидный, поскольку GPT-3.5 физически переворачивал узлы списка, но не сигнализировал пользователю об этом изменении. С точки зрения разработчика, такой подход
   40 Глава 2. Начало работы с LLM может привести к побочным эффектам. Например, если мы вызовем reverse_ and_display, предполагая, что он просто отображает перевернутый список, то обнаружим, что исходная структура списка была необратимо изменена. Такая непрозрачность операции легко может привести к путанице и ошибкам, особенно в сложных приложениях, где важна целостность исходной структуры данных. GPT-4 применяет более продуманную стратегию при развороте списка. Метод четко выполняет разворот списка, а его название и структура дают понять, что он изменяет исходные данные. Подход GPT-4 соответствует принципам чистого кода: каждая функция выполняет четко определенную задачу. При этом разделение ответственности здесь очевидно: разворот списка и вывод данных — две отдельные операции. В результате улучшается читаемость кода, снижается вероятность побочных эффектов, поскольку разработчик будет осведомлен об изменениях, вносимых в структуру данных. 2.2. Copilot у штурвала Попробуем решить ту же задачу при помощи GitHub Copilot. Инструкции по созданию учетной записи и установки плагина для IDE приведены в приложении Б (на примере VS Code и PyCharm). Закончив с настройкой, создайте новый проект с файлом main.py. Введите следующий промт как комментарий в начале этого файла (решеткой обозначается комментарий в Python): # Ты — студент, получивший образование в области computer science. Ты на собеседовании на должность инженера в ИТ-компанию. Тебя попросили, отталкиваясь от предыдущего определения односвязного списка (на Python), написать метод, изменяющий результат обхода списка на противоположный. Как бы ты реализовал такой метод? Это тот же запрос, что и для ChatGPT, — мы сравниваем две модели. Далее обратите внимание: когда вы начнете вводить определение класса ListNode, Copilot предложит варианты кода. Это называется встроенным режимом (inline mode) — Copilot автоматически дополняет код в строке. Альтернативный способ — интерактивный режим (interactive mode). Нажмите Ctrl + Enter, и Copilot сгенерирует до 10 вариантов кода на основе текущего контекста. ПРИМЕЧАНИЕ В этой книге мы будем в основном использовать встроенный режим. Скорее всего, мы получим код, похожий на тот, что выдал нам ChatGPT. Но давайте попробуем решить другую задачу, чтобы не повторять текст из прошлого раздела. Предложим Copilot такой промт: # Есть несортированный массив целых чисел. Напиши функцию для поиска k-го наименьшего элемента. Оцени временную и пространственную сложность предложенного решения.
   2.2. Copilot у штурвала 41 Несколько нажатий на кнопку Enter, и Copilot автоматически добавит код и комментарии к нему, в которых оценит временную и пространственную сложность. Интересно! Листинг 2.8. C  opilot определяет временную и пространственную сложность кода def kth_smallest_element(arr, k): # Временная сложность: O(N) # Пространственная сложность: O(1) if k > len(arr): return None return sorted(arr)[k - 1] Предложение Copilot вполне логично и правильно, но вряд ли было бы принято на техническом собеседовании. Почему? Скорее всего, последует вопрос, как решить задачу без полной сортировки списка. Исправляем промт, чтобы избежать сортировки: # Ты - студент, получивший образование в области computer science. Ты на собеседовании на должность инженера в ИТ-компанию. Тебя попросили решить такую задачу: "Есть несортированный массив целых чисел. Напиши функцию для поиска k-го наименьшего элемента без сортировки всего массива и каких-либо встроенных сортирующих функций. Оцени временную и пространственную сложность предложенного решения". Этот вопрос проверяет твою способность разрабатывать эффективные алгоритмы с учетом заданных ограничений, а также умение анализировать сложность. В ответ Copilot выдает что-то напоминающее бинарный поиск. Довольно интересный выбор. Листинг 2.9. Предложение Copilot по поиску k-го элемента массива # Следующий код представляет собой реализацию алгоритма поиска к-го наименьшего # элемента массива на Python с оценкой временной и пространственной сложности. # Функция для поиска k-го наименьшего элемента несортированного массива def kth_smallest(arr, k): # Проверка массива и числа k: массив не должен быть пустым, # k должно быть в пределах от 1 до количества элементов массива if not arr or k < 1 or k > len(arr): return None # Начальные значения переменных для минимального и максимального элементов low = min(arr) high = max(arr) # Бинарный алгоритм для поиска k-го наименьшего элемента while low <= high: # Вычисление среднего элемента массива mid = low + (high - low) // 2 # Подсчет количества элементов, которые меньше среднего
   42 Глава 2. Начало работы с LLM count = sum(1 for num in arr if num <= mid) # Коррекция диапазона поиска в зависимости от этого количества if count < k: low = mid + 1 else: high = mid – 1 # Возврат k-го наименьшего элемента return low # Временная сложность: O(n log(max(arr) - min(arr))) # Пространственная сложность: O(1) 2.3. Слово CodeWhisperer Прежде чем начать работу с CodeWhisperer, нужно отключить Copilot. Перей­ дите на вкладку Extensions (Расширения), найдите Copilot и нажмите на кнопку Disable (Отключить). Этот процесс показан на рис. 2.3. Затем перезапустите IDE. Инструкции по установке и настройке CodeWhisperer приведены в приложении В. После установки плагина и входа в учетную запись разработчика создайте файл asset.py. Рис. 2.3. Чтобы начать работу с CodeWhisperer, нужно отключить Copilot на вкладке Extensions (Расширения) Используем тот же запрос, что и для Copilot: # Ты - студент, получивший образование в области computer science. Ты на собеседовании на должность инженера в ИТ-компанию. Тебя попросили решить такую задачу: "Есть несортированный массив целых чисел. Напиши функцию для поиска k-го наименьшего элемента без сортировки всего массива и каких-либо встроенных сортирующих функций. Оцени временную и пространственную сложность предложенного решения". Этот вопрос проверяет твою способность разрабатывать эффективные алгоритмы с учетом заданных ограничений, а также умение анализировать сложность. В ответ CodeWhisperer выдаст код, в котором реализуется алгоритм Quickselect, который очень похож на алгоритм QuickSort и специально предназначен для поиска k-го наименьшего элемента в несортированном массиве.
   2.3. Слово CodeWhisperer 43 Листинг 2.10. Подход CodeWhisperer к поиску k-го наименьшего элемента def partition(arr, low, high): pivot = arr[high] i = low - 1 for j in range(low, high): if arr[j] <= pivot: i += 1 arr[i], arr[j] = arr[j], arr[i] arr[i + 1], arr[high] = arr[high], arr[i + 1] return i + 1 def quickselect(arr, k, low=0, high=None): if high is None: high = len(arr) – 1 if low < high: pivot = partition(arr, low, high) if pivot == k: return arr[k] elif pivot > k: return quickselect(arr, k, low, pivot - 1) else: return quickselect(arr, k, pivot + 1, high) return arr[low] # Пример применения arr = [5, 2, 4, 6, 1, 3] k = 2 print(quickselect(arr, k)) # Результат: 2 Между решениями Copilot и CodeWhisperer есть любопытное отличие: CodeWhisperer считает k индексом элемента сортированного массива. Поскольку в большинстве языков программирования индексы начинаются с 0, то при k = 2 CodeWhisperer найдет третье наименьшее число (индексы 0, 1 и 2 соответствуют первому, второму и третьему элементам массива). А в представлении Copilot k — это номер элемента по старшинству, а не индекс, так что при том же k = 2 ответом будет второй наименьший элемент. Второй не по старшинству, а второй по номеру ячейки. Итак, мы познакомились и с AWS CodeWhisperer. Как и две другие модели, этот ИИ умеет генерировать код для решения поставленной задачи, расширяя возможности разработчика ПО. Учитывая поразительное сходство кода, который выдают эти модели, возникает вопрос: а в чем их настоящее различие? У каждой из них есть и сильные стороны, и ограничения, и ответ не так очевиден. В следующем разделе мы разберемся в этом вопросе, сравнив три ведущих инструмента генерации кода: ChatGPT, Copilot и AWS CodeWhisperer. Мы рассмотрим их уникальные особенности, оптимальные сценарии использования и их
   44 Глава 2. Начало работы с LLM влияние на будущее разработки программного обеспечения. Цель — предоставить всесторонний обзор, который поможет разработчикам ориентироваться в мире ИИ-инструментов и эффективно их применять. 2.4. Сравнение ChatGPT, Copilot и CodeWhisperer Прежде всего сравним инструменты с точки зрения модели взаимодействия с ними. При работе с ChatGPT мы заходим на сайт и вводим промты в чат, поэтапно уточняя их. ИИ учитывает контекст из предыдущих промтов и предлагает ответ, на который мы реагируем новым промтом. А вот Copilot и CodeWhisperer работают только внутри IDE, и как-то иначе использовать их нельзя. Это не означает, что один метод лучше другого, они просто разные. Более того, интеграция с IDE это даже плюс. Помощь от Copilot и CodeWhisperer можно получать, не отвлекаясь от кода, а это залог производительной работы. Взаимодействие с ChatGPT происходит в формате диалога. Copilot и CodeWhisperer дают советы и дополняют код, обеспечивая быстрые и точные подсказки. Немного позже мы познакомимся с разновидностью Copilot — Copilot Chat, моделью, которая также работает в IDE, но объединяет оба подхода: ChatGPT и Copilot. Теперь посмотрим на то, как генерируется и отображается код. ChatGPT может создавать код в виде блока, метода, класса или целого проекта. Если попросить, он опишет структуру проекта. Он любит объяснять и часто описывает концепции перед генерацией кода. Copilot и CodeWhisperer генерируют код поэтапно, начиная с отдельных методов. Со временем они начинают дописывать больше и больше кода для одного класса, но не могут сразу написать весь проект по одному промту. У всех трех моделей есть общая черта — способность отвечать на промты. У ChatGPT это единственный способ взаимодействия, а Copilot и CodeWhisperer могут обходиться и без промтов, хотя они улучшают код, приближают его к изначальному замыслу. Подытожив, можно сделать вывод, что ChatGPT — отличный выбор для исследований, а также создания прототипов. Но ChatGPT может отвлечь вас от работы, ведь для общения с ним нужно перейти из IDE в браузер. Сам по себе ChatGPT тоже способен отвлечь вас от хода мысли: общаясь с ним, легко увлечься и упасть в пресловутую кроличью нору. Но не пугайтесь этого. Все-таки ИИ — отличный инструмент. Copilot и CodeWhisperer требуют, чтобы у вас было четкое представление о конечном результате. Поэтому они идеально подходят для тех, кто хочет полностью погрузиться в код, имеет точные требования и жесткие сроки. При
   2.4. Сравнение ChatGPT, Copilot и CodeWhisperer 45 этом максимальных результатов можно добиться, если вы хорошо знаете язык и фреймворк. Тогда эти модели автоматизируют рутинные задачи, позволяя вам сосредоточиться на бизнес-логике и ключевых требованиях проекта. Преимущества и ограничения трех моделей ИИ показаны на рис. 2.4. re r pe his eW d Co PT t ilo p Co tG a Ch Работают по промтам Исключительно Работают в IDE Поддерживается Генерируют методы Не поддерживается Генерируют классы Генерируют проекты Создают документацию Могут менять язык Могут менять библиотеки Рис. 2.4. Сравнение ChatGPT, Copilot и CodeWhisperer В этой главе мы разобрали множество тем, включая основные структуры данных и решение классических задач computer science, предлагаемых на собеседованиях. Но главное, мы разобрались, когда лучше спрашивать ChatGPT, а когда звать на помощь Copilot и CodeWhisperer в IDE. В следующих главах мы будем использовать эти знания, чтобы выбрать наиболее подходящий инструмент. И заключительная мысль: лучше всего комбинировать эти модели: использовать ChatGPT для получения примеров и разработки структуры проекта, а Copilot и CodeWhisperer — для написания и адаптации кода.
   46 Глава 2. Начало работы с LLM Итоги ChatGPT — генеративный ИИ, работающий на основе диалогового взаимодействия. Помогает развивать идеи и проектировать решения. Генерирует структуру проекта и документацию для методов. Может отвлекать от работы, увлекая пользователя в обсуждения и исследования. Copilot и CodeWhisperer — инструменты для концентрации на коде. Идеальны, если у вас есть четкое понимание задачи. Работают по схожему принципу, давая советы и дополняя код. ChatGPT (на момент написания книги) не работает в IDE. С другой стороны, Copilot и CodeWhisperer не могут создавать проекты, а также переводить код с одного языка программирования на другой. Copilot и CodeWhisperer анализируют комментарии, чтобы предсказать, какой код требуется. В ChatGPT нужно явно формулировать промты для генерации кода. При ролевом подходе промты задают ИИ определенную роль. Благодаря этому ответы становятся более последовательными и близкими к контексту и, соответственно, к ожиданиям пользователя. Роль стажера обычно отличается жаждой знаний, базовым или средним уровнем подготовки в своей области и готовностью браться за разные задачи, чтобы учиться на практике. Стажер задает уточняющие вопросы и обращается за советом. Проявляет проактивный подход к решению задач. Может быть изобретательным, но ему не хватает опыта по сравнению с более квалифицированными специалистами. Эта роль подходит для случаев, когда ИИ должен имитировать процесс обучения и стремление к росту. Поэтапное уточнение промта позволяет улучшать точность и релевантность ответов. Помогает перейти от общего к конкретному в процессе взаимодействия, что особенно важно при работе с моделями типа ChatGPT. Паттерн ориентации на аудиторию — это настройка промтов под целевую аудиторию. Он позволяет адаптировать ответы под конкретные группы пользователей.
Часть 2 Ввод данных В части 2 мы перейдем к практическому применению больших языковых моделей в проектировании и разработке программного обеспечения. Мы рассмотрим на реальных примерах, как искусственный интеллект может улучшить процесс проектирования. Эта часть книги охватывает ключевые концепции: моделирование предметной области, неизменяемость данных, паттерны проектирования. На примерах с GitHub Copilot мы разберем, как эти принципы работают на практике. Также разберем управление данными, в том числе мониторинг активов в реальном времени и анализ данных с помощью ИИ. Интеграция ИИ в эти процессы помогает разработчикам автоматизировать работу, повысить эффективность и привнести больше инноваций в проекты.
3 Проектирование ПО с ChatGPT В этой главе 3 3 Прототипирование потенциальной архитектуры с помощью ChatGPT 3 3 Разработка документации архитектуры в формате Mermaid 3 3 Завершение проектирования с ChatGPT Теперь, когда мы немного разобрались в том, когда и где применять генеративный ИИ, можно приступить к планированию и документированию структуры будущего приложения. Заранее продумать ключевые компоненты полезно по нескольким причинам. Это позволяет передать часть архитектуры другим разработчикам или архитекторам в команде. Такой подход помогает структурировать наше мышление по поводу реализации, выявить потенциальные проблемы и избежать их. Документирование архитектуры помогает объяснить принятые решения, передать замысел будущим членам команды, стейкхолдерам и тем, кто будет развивать проект после нас. Прежде всего сделаем обзор приложения, которое мы будем проектировать в этой главе. Это будет система управления ИТ-активами (information technology asset management, ITAM). В последующих разделах мы рассмотрим ее ключевые функции.
   3.2. ChatGPT помогает с проектом системы 49 3.1. Введение в проект ITAM-системы ITAM-система — это инструмент для управления аппаратными устройствами, лицензиями на ПО и другими элементами, связанными с компьютерными системами, и контроля за ними на протяжении всего их жизненного цикла. Обычно такие системы включают средства для учета железа и софта, для управления лицензиями, а также другие приложения. В работе системы также могут быть предусмотрены ручные процессы и физическое отслеживание ИТ-активов с помощью QR- или штрихкодов, а также других технологий управления физическими активами. Обычно в системе ITAM есть централизованная база данных, где хранятся все характеристики активов. Например, для компьютеров — тип устройства, модель, операционная система и установленные программы. Для программного обеспечения — название, вендор, количество лицензий и на каких компьютерах оно установлено; это позволяет следить за соблюдением лицензионных ограничений: если вы отслеживаете использование лицензий, то не превысите их количество. ITAM-системы также позволяют контролировать расходы. Зная, какое оборудование и ПО у вас есть, не нужно покупать лишнее. Все покупки централизованы — это помогает закупаться оптом. Неиспользуемое оборудование можно продать, а дублирующиеся задачи перераспределить. Кроме того, зная дату покупки, можно рассчитать амортизацию оборудования и учесть ее в налогах. В процессе работы мы познакомимся и с другими возможностями ITAM-систем. Пока же начнем с создания надежной концептуальной модели будущего приложения, ограничившись контролем и управлением аппаратным обеспечением. Дальше мы попросим ChatGPT выступить в роли архитектора и предложить, как лучше спроектировать такую систему. 3.2. ChatGPT помогает с проектом системы Открыв новый чат с ChatGPT, начнем с промта, в котором опишем требования к проекту. Как и в прошлой главе, используем уточняющий подход и будем постепенно изменять структуру системы. Начав с очень общих вещей, мы шаг за шагом отшлифуем и требования к ней, и ее функциональность. Но прежде всего попросим ChatGPT улучшать наши промты: NC Начиная с этого момента предлагай более удачные варианты моих промтов. Такой запрос очень важно сделать в начале работы, едва приступая к решению задачи: с ним ChatGPT получит указание подправлять последующие промты. А как мы уже говорили, чем лучше промт, тем лучше результат. Отсюда правило
   50 Глава 3. Проектирование ПО с ChatGPT на будущее: чтобы получить качественный ответ, нужно задать качественный вопрос. В дополнение (или как вариант) можно попросить ChatGPT действовать поэтапно, обосновывая каждое принятое решение. Теперь мы попросим ChatGPT помочь нам с проектированием, использовав вот такой (расширенный) промт: NC Ты — архитектор ПО. Ты проектируешь систему управления ИТ-активами на Python. Система должна уметь вести учет аппаратного и программного обеспечения организации, отслеживать его местоположение и состояние, а также данные о пользователе актива, датах последнего обслуживания и планируемой замене. Также система должна управлять всем жизненным циклом актива — от закупки до утилизации. Это включает отслеживание гарантий, арендных соглашений, контроль за ремонтом и техобслуживанием, а также соответствие требованиям регуляторов. Система должна уметь распределять активы между сотрудниками или отделами, отслеживать использование и управлять бронированием дефицитных устройств. Архитектура — гексагональная. И не забудь — улучшай мой промт. Обратите внимание: мы снова используем ролевой подход, просим ChatGPT побыть архитектором ПО. ИИ не только опишет общую функциональность системы, но и предложит улучшенный вариант промта, текст которого приведен ниже. Еще одно напоминание: ответы ChatGPT (как и любого другого генеративного ИИ) сильно зависят от предыдущих вопросов, а значит, скорее всего, вам он ответит немного по-другому. Листинг 3.1. У  лучшенный промт и обзор приложения, предложенные ChatGPT Чтобы расширить и детализировать ваш промт о проектировании на Python системы управления ИТ-активами, которая будет иметь гексагональную архитектуру, можно разбить его на пункты: Цель проекта и основная функциональность системы: Необходимо разработать систему управления ИТ-активами на языке Python. Система должна вести учет всего аппаратного и программного обеспечения организации. Тщательное отслеживание активов: Система должна отслеживать местоположение, состояние и применение каждого актива. Система должна вести подробные журналы выделенных пользователей, истории обслуживания и графики замены активов. Управление жизненным циклом: Система должна управлять активами от приобретения до утилизации. Система должна контролировать гарантийные сроки, сроки аренды, обслуживания и ремонта, обеспечивать соблюдение требований нормативных актов.
   3.2. ChatGPT помогает с проектом системы 51 Выделение активов и контроль за их использованием: Система должна выделять активы работникам или отделам. Система должна отслеживать особенности использования активов и управлять предварительными заказами на наиболее востребованные из них. Требования к архитектуре системы: Необходимо использовать гексагональную архитектуру, чтобы уменьшить взаимную зависимость компонентов системы и облегчить их обслуживание. Система должна быть модульной, чтобы облегчить ее масштабирование и обновление. Дополнительные особенности и соображения: Создать дружественные интерфейсы для будущих пользователей. Принять надежные меры безопасности для защиты секретных данных об активах. Предусмотреть возможность интеграции с другими системами, которые применяются организацией. Такой подробный перечень требований послужит хорошей основой для проектирования системы, которая будет обладать всем необходимым функционалом и отличаться надежностью архитектуры. В ответ на этот промт ChatGPT составит предварительный план действий для разработки проекта. Полезно, но не совсем то, что нужно. Нам нужен не просто общий план — мы хотим перейти от нуля к конкретному проектированию. Необходимо описание и схемы, которые помогут нам понять систему на уровне компонентов: конкретные задачи, а не общие советы. Можно попросить ChatGPT «показать свою работу» таким промтом: NC Покажи структуру пакета Python для этого приложения. В ответ ChatGPT создаст и представит схему пакета (на листинге она немного сокращена), тем самым показывая нам, что может не только проектировать ПО, но и планировать файловые структуры. Конечно, в чем-то мы можем не согласиться с ИИ, но в качестве отправной точки такая схема подойдет. Листинг 3.2. Структура пакета ITAM-системы, созданная ChatGPT it_asset_management/ │ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── config.py │ └── dependencies.py │ ├── domain/ │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ ├── asset.py # Главная папка приложения # Точка входа в приложение # Настройки # Внедрение и управление зависимостями # Слой предметной области с бизнес-логикой # Модели предметной области (актив, пользователь...)
   52 │ │ │ │ │ │ │ │ │ │ ├── │ │ │ │ │ │ │ │ │ │ │ │ │ │ ├── │ │ │ │ │ ├── │ │ │ │ └── Глава 3. Проектирование ПО с ChatGPT │ ├── │ │ │ └── ├── ├── └── └── user.py services/ # Бизнес-сервисы/логика ├── __init__.py ├── asset_service.py └── user_service.py repositories/ # Интерфейс для доступа к данным __init__.py asset_repository.py user_repository.py adapters/ # Слой адаптеров для внутренних взаимодействий ├── __init__.py ├── orm/ # Объектно-реляционное отображение данных │ ├── __init__.py │ ├── asset_orm.py │ └── user_orm.py ├── api/ # эндпоинты API │ ├── __init__.py │ ├── asset_api.py │ └── user_api.py └── external_services/ # Интеграция с внешними сервисами ├── __init__.py ├── email_service.py └── logging_service.py infrastructure/ ├── __init__.py ├── database.py ├── message_queue.py └── cache.py # Слой инфраструктуры tests/ ├── __init__.py ├── unit/ └── integration/ # Тесты requirements.txt # Зависимости проекта # Настройка базы данных и соединений # Инфраструктура передачи сообщений # Механизмы кэширования # юнит-тесты # интеграционные тесты Мы можем изучить общую структуру проекта, а можем попросить ChatGPT показать и детали. С помощью промтов можно шаг за шагом получить содержимое каждого файла, а затем вручную скопировать этот код в соответствующий файл с тем же именем, какое предложил ChatGPT. Важно: всегда проверяйте, что сгенерировал ChatGPT, — не стоит слепо доверять, что все будет корректно и безопасно. Иногда он может выдать неработающий код. Это связано с тем, что модель не «думает» о системе в целом, как это делает человек, а просто генерирует следующий подходящий фрагмент текста. Посмотрим, какой код он предложил — оценим его качество и пригодность:
   3.2. ChatGPT помогает с проектом системы NC 53 Покажи исходный код файла it_asset_management/app/schemas/asset.py. И вот что мы получим в ответ. Листинг 3.3. П  ример модели Asset (asset_management_system/models/asset.py) from typing import Optional from datetime import datetime from pydantic import BaseModel # Базовая модель Asset для создания нового актива class AssetCreate(BaseModel): name: str asset_type: str serial_number: str purchase_date: Optional[datetime] = None warranty_end_date: Optional[datetime] = None # Модель для публично доступных данных актива class Asset(BaseModel): id: int name: str asset_type: str serial_number: str purchase_date: Optional[datetime] = None warranty_end_date: Optional[datetime] = None class Config: orm_mode = True # Модель для обновления данных существующего актива class AssetUpdate(BaseModel): name: Optional[str] = None asset_type: Optional[str] = None serial_number: Optional[str] = None purchase_date: Optional[datetime] = None warranty_end_date: Optional[datetime] = None return '<Asset %r>' % self.name Чтобы не проходить по каждому классу и не просматривать исходный код, мы задокументируем проект в целом. Несмотря на то что Agile и Scrum делают акцент на работающем программном обеспечении, а не на обширной документации, продуманная архитектура и базовая документация играют критически важную роль. Они привносят структуру, ясность и стратегическое вˆидение в процесс Agile-разработки, помогая команде эффективно адаптироваться к изменениям без потери качества и целостности продукта.
54    Глава 3. Проектирование ПО с ChatGPT 3.3. Документация на архитектуру В этом разделе мы попросим ChatGPT начать документировать архитектуру нашего приложения. Как уже говорилось выше, проектирование и документация важны для архитекторов и разработки в целом — даже в среде Agile и Scrum. Документация помогает команде видеть общее направление: она описывает архитектуру, компоненты и взаимодействие между ними, благодаря чему разработчики лучше понимают, как правильно и эффективно реализовать функцио­ нальность. Это также способствует соблюдению стандартов качества и лучших практик, позволяя архитекторам задавать правила, которые будут соблюдаться на протяжении всего проекта, а это, в свою очередь, делает код более устойчивым и удобным в поддержке. Для построения схем мы будем применять язык диаграмм Mermaid. Mermaid — инструмент на базе JavaScript, который позволяет создавать сложные схемы и диаграммы на основе простого текстового синтаксиса. Этот язык широко используется для рисования блок-схем, схем последовательностей, классов, состояний и многого другого. Mermaid легко интегрируется с различными платформами — Markdown, вики, системы документации — и потому идеально подходит как для разработчиков, так и для тех, кто пишет документацию. А так как схемы создаются на основе текстов, Mermaid отлично сочетается со средствами генерации текстов, и в частности с ChatGPT. Работу над документацией начнем с такого промта: Мне нужно разработать ITAM-систему на языке Python. Ее основная задача — контроль за аппаратным обеспечением и управление им. Система должна предоставлять интерфейсы REST API, использовать FastAPI и хранить данные при помощи SQLAlchemy. В основе системы должна лежать гексагональная архитектура. Ты — архитектор ПО, покажи мне схему классов этого проекта на языке Mermaid. NC Гексагональная архитектура Гексагональная архитектура, известная также как архитектура портов и адаптеров, — это архитектурный паттерн, который позволяет четко разделить бизнеслогику приложения и его взаимодействие с внешними системами: базами данных, интерфейсами, сторонними сервисами и т. д. Такое разделение ответственности делает приложение более удобным для поддержки, расширяемым и пригодным для тестирования. ¡ ¡ К основным компонентам гексагональной архитектуры относятся: Модель предметной области (или доменная модель) — ядро бизнес-логики приложения: сущности, объекты-значения и доменные сервисы. Модель предметной области изолирована от любых внешних систем и технологий
   3.3. Документация на архитектуру 55 ¡ ¡ и не должна зависеть от каких-либо аспектов реализации, в том числе баз данных и API. Порты — интерфейсы, через которые доменная модель взаимодействует с внешним миром. Они определяют границы входа и выхода данных. Порты бывают двух типов: Ù Ù Первичные (ведомые) порты — определяют сценарии использования, которые приложение предоставляет внешним системам. По сути, это API, который такие системы могут использовать для взаимодействия с приложением. ¡ ¡ Ù Ù Вторичные (ведущие) порты — определяют контракты, которые приложение ожидает от внешних систем, например репозиториев и внешних сервисов. Адаптеры — конкретные реализации портов. Они преобразуют данные и коман­ ды между внешними системами и доменной моделью, делая их понятными для ядра приложения. Адаптеры тоже делятся на два типа: Ù Ù Первичные (ведомые) адаптеры — реализуют первичные порты, отвечают за получение входных данных от внешних систем и передачу их модели предметной области. Ù Ù Вторичные (ведущие) адаптеры — реализуют вторичные порты, отвечают за взаимодействие с внешними системами от имени доменной модели (например, читают из БД или вызывают сторонний API). В гексагональной архитектуре модель предметной области находится в центре (внутри «шестиугольника»), окруженная портами и адаптерами. Ключевая идея в том, что любое взаимодействие между моделью предметной области и внешними системами должно проходить через порты и адаптеры. Этот подход обеспечивает легкую замену внешних систем, упрощенное тестирование с использованием мокреализаций портов и четкое разделение ответственности, что делает приложение более поддерживаемым. В ответ ChatGPT создает несколько странную структуру классов, которая показана на рис. 3.1. Первым идет класс MainApp (Основное приложение), который, как можно предположить, запускает приложение. Он использует класс FastAPIAdapter (Адаптер FastAPI), который принимает и обрабатывает REST-запросы и вызывает методы класса HardwareService (Сервис для работы с аппаратным обеспечением). Давайте попросим ChatGPT объяснить, почему он выбрал именно такую схему. Заставить ИИ ссылаться на актуальные источники непросто: обычно он старается говорить в общих чертах. Попробуем получить ответ по существу: NC Приведи источники этой конкретной архитектуры.
Глава 3. Проектирование ПО с ChatGPT Использует Использует Использует Управляет        56 Рис. 3.1. Предложенная ChatGPT структура ITAM-системы
   3.3. Документация на архитектуру 57 Ответ ChatGPT (рис. 3.2) на этот промт будет очень общим: ИИ сошлется на лучшие практики и на создателей гексагональной архитектуры. При этом не скажет о том, почему принял именно эти решения. Мы просили гексагональную архитектуру, FastAPI и SQLAlchemy, и ChatGPT охотно включил все это в предложенный им проект. Не будь наши указания столь однозначными, мы бы, скорее всего, получили какой-то другой набор фреймворков. Предложенная архитектура — это общий план, основанный на указанных вами требованиях: разработка проекта ITAM на Python с фокусом на отслеживание и управление аппаратными средствами, с использованием FastAPI, SQLAlchemy и гексагональной архитектуры. Источники этого дизайна опираются на общие знания и лучшие практики в области проектирования ПО, а также на особенности упомянутых инструментов: 1. Гексагональная архитектура (архитектура портов и адаптеров) — это предложенный Алистером Кокберном в 2005 году архитектурный шаблон, который предполагает разграничение функционала и способствует написанию более простого для сопровождения и тестирования кода. Подробные сведения о гексагональной архитектуре можно найти по ссылке: https://alistair.cockburn.us/hexahonal-architecture/. 2. FastAPI — это современный и быстрый веб-фреймворк для создания API с использованием стандартных аннотаций типов на Python версии 3.7 и выше. Фреймворк пользуется популярностью за простоту, легкость в применении и производительность. Подробные сведения о нем можно найти по ссылке: https://fastapi.tiangolo.com/. 3. SQLAlchemy — это популярный инструмент для объектно-реляционного отображения (ORM) для Python, который представляет собой комплексный набор хорошо организованных, простых в применении средств и компонентов для работы с реляционными базами данных. Подробные сведения об SQLAlchemy можно найти по ссылке: https://www.sqlalchemy.org/. Предложенная структура системы представляет собой сочетание этих концепций и инструментов, адаптированное под поставленные требования. Она послужит отправной точкой, но может потребовать доработки в зависимости от конкретных потребностей и целей проекта. Рис. 3.2. ChatGPT объясняет предложенную структуру системы Теперь попросим ИИ предоставить структуру пакета: NC Как будет выглядеть структура пакета для этого проекта? ChatGPT выдаст что-то похожее на следующий листинг.
   58 Глава 3. Проектирование ПО с ChatGPT Листинг 3.4. Предложенная 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, улучшая систему до тех пор, пока не будем уверены в том, что сможем без проблем изменить ее с учетом будущих потребностей, к примеру управления лицензиями на ПО. Можно (и даже нужно!) было бы попросить ИИ предложить варианты развития, но мы поступим иначе: будем руководить его работой. Пусть он добавит родительский класс Asset (Актив) к классу Hardware (Аппаратное обеспечение): NC Добавь в проект класс под названием Asset, родительский по отношению к классу Hardware. В новом классе должны быть следующие атрибуты: name, status, category, id и funding_details1. Благодаря новому базовому классу Asset все активы организации будут иметь общий набор атрибутов, что (практически) соответствует принципам SOLID (см. следующую врезку). Обновленная схема классов показана на рис. 3.3. 1 Наименование, состояние, категория, идентификатор и финансовые данные соответственно. — Примеч. пер.
   3.3. Документация на архитектуру     Наследует Рис. 3.3. Обновленная схема классов, где видна связь между Asset и Hardware Принципы SOLID ¡ ¡ Аббревиатура SOLID означает пять принципов разработки программного обеспечения, соблюдение которых делает его более гибким и сопровождаемым: S: Single Responsibility Principle (Принцип единственной ответственности) ¡ ¡ Класс должен иметь только одну причину для изменения. Он должен выполнять только одну задачу — и делать это хорошо. O: Open/Closed Principle (Принцип открытости/закрытости) ¡ ¡ Программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для изменения. L: Liskov Substitution Principle (Принцип подстановки Лисков) ¡ ¡ Объекты подкласса должны свободно заменять объекты суперкласса без нарушения корректности программы. Все, что работает с родителем, должно работать и с его наследником. I: Interface Segregation Principle (Принцип разделения интерфейсов) ¡ ¡ Клиент не должен зависеть от методов, которыми он не пользуется. Лучше иметь несколько специализированных интерфейсов, чем один общий. D: Dependency Inversion Principle (Принцип инверсии зависимостей) Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Нужно писать код, ориентируясь на интерфейсы, а не на конкретные реализации. 59
   60 Глава 3. Проектирование ПО с ChatGPT Это нововведение облегчит расширение модели в будущем. Мы сможем легко добавлять в нее любые виды активов: от программных систем до садового инвентаря. С точки зрения компании-владельца все новые классы, как следует ожидать, будут вести себя точно так же, как и любые другие потомки Asset. Теперь поработаем с атрибутом funding_details (данные финансирования) класса Asset: это должен быть класс, а не строка. Строковый тип данных не позволяет вводить никаких ограничений на значение переменной, но в том, что касается финансовых данных, нам нужно единообразие. Именно это позволит выполнять однотипные расчеты и агрегирование по соответствующим полям. А вот и промт: NC Измени тип атрибута funding_details класса Asset на класс FundingDetails со следующими атрибутами: name, department и depreciation_strategy1. ChatGPT сделает новый Mermaid-документ, в котором появится новый класс и взаимосвязи с ним (рис. 3.4).     Содержит Рис. 3.4. Обновленная схема классов с новым классом FundingDetails Теперь изменим класс FundingDetails так, чтобы расчет амортизации выполнялся согласно выбранной политике. Это необходимо, поскольку есть несколько таких политик, а значит, и методов расчета. ПРИМЕЧАНИЕ Амортизация — термин, обозначающий постепенное снижение стоимости актива. Существует несколько стандартных методов расчета амортизации, например линейный метод, метод уменьшаемого остатка и метод двойного уменьшаемого остатка. 1 Наименование, отдел и амортизационная политика соответственно. — Примеч. пер.
   3.3. Документация на архитектуру 61 Сформируем запрос, чтобы ChatGPT ввел амортизацию в нашу модель: NC Создай интерфейс под названием DepreciationStrategy с единственным методом calculate_depreciation, на вход которого поступает класс FundingDetails. Интерфейс будет иметь четыре реализации: StraightLineDepreciationStrategy, DecliningBalance DepreciationStrategy, DoubleDecliningDepreciationStrategy и NoDepreciationStrategy1. Обнови класс Asset, добавив в него этот интерфейс. Делегировав расчет амортизации актива классу DepreciationStrategy, мы сможем легко менять амортизационную политику. Как видно из новой схемы на рис. 3.5, теперь наш проект соответствует принципу DIP. Рис. 3.5. Амортизационная политика добавлена в объектную модель Это обычная практика для компаний — иметь несколько направлений деятельности, что в нашей диаграмме классов обозначается через department. Допустим, мы хотим, чтобы один Asset (актив) мог быть связан с несколькими направлениями бизнеса. Попросим ChatGPT добавить это в нашу модель: NC Измени класс FundingDetails так, чтобы в нем содержались данные о нескольких ­отделах. Каждому из них должна быть выделена определенная часть стоимости актива. ChatGPT предложит добавить в класс FundingDetails словарь lines_of_business (направления деятельности) и выдаст новую схему Mermaid. Можно ожидать, что всем отделам нужно знать суммарную стоимость выделенных им активов. Пожалуй, для решения этой задачи можно использовать паттерн проектирования «Посетитель». Посмотрим, что скажет ChatGPT по поводу применимости этого шаблона к данному случаю: NC 1 Мне нужно рассчитать стоимость всех активов, выделенных какому-то из отделов. Стоит ли использовать паттерн "Посетитель"? Четвертая реализация — отсутствие амортизационной политики. — Примеч. пер.
62    Глава 3. Проектирование ПО с ChatGPT Паттерн проектирования «Посетитель» Посетитель (Visitor) — это поведенческий паттерн проектирования, который позволяет добавить новую операцию к объекту без изменения его класса. Он особенно полезен, когда нужно выполнять разные действия над объектами, но вы хотите держать логику операций отдельно от самих объектов. Такой подход упрощает ­добавление нового поведения без необходимости переписывать существующий код. ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ Чтобы реализовать паттерн «Посетитель», в проект добавляют следующие компоненты: Элемент (Element) — интерфейс или абстрактный класс, представляющий элементы структуры объекта. Он содержит метод accept, который принимает объект-посетитель. Конкретный элемент (Concrete Element) — конкретные классы, реализующие интерфейс Element. Эти классы представляют разные типы объектов в структуре. Посетитель (Visitor) — интерфейс или абстрактный класс, который объявляет метод visit (посетить) для каждого конкретного элемента. Этот метод соответствует операции, которая будет выполняться над ними. Конкретный посетитель (Concrete Visitor) — конкретная реализация интерфейса Visitor. Эти классы содержат логику обработки каждого типа объекта (Element). Шаги для применения паттерна «Посетитель»: 1. Создайте интерфейс (или абстрактный класс) элемента с методом accept, который принимает объект-посетитель в качестве аргумента. 2. Создайте классы конкретных элементов, реализующие этот интерфейс (или расширяющие этот абстрактный класс), то есть метод accept. 3. Создайте интерфейс (или абстрактный класс) с методом visit для каждого конкретного элемента. 4. Создайте классы конкретных посетителей, реализующие этот элемент (или расширяющие этот абстрактный класс), то есть методы visit. Далее нужно создать экземпляр конкретного посетителя и передать его методу accept всех конкретных элементов в структуре объекта. Метод accept при этом вызывает метод visit этого посетителя, в результате чего выполняется алгоритм, предусмотренный для данного конкретного элемента. ChatGPT считает такое решение вполне подходящим. Более того, он предлагает создать интерфейс под названием Visitor с методом visit, который и будет использоваться для расчета. По мнению ИИ, нам нужно изменить класс Asset, добавив в него метод accept для работы с посетителем, и наконец создать класс под названием CostByLineOfBusinessVisitor (стоимость активов для отдела), который и будет «посещать» активы.
   3.3. Документация на архитектуру 63 Также отделам интересна совокупная амортизация их активов. Еще раз спросим у ChatGPT: NC Мне также нужно вычислить совокупную амортизацию всех активов конкретного отдела. ChatGPT предлагает расширить поведение класса CostByLineOfBusinessVisitor, добавив в него атрибут total_depreciation (совокупная амортизация), который будет обновляться при каждом вызове visit(). После обхода всех активов полученное значение можно будет вернуть в качестве результата. Теперь мы можем попросить ChatGPT завершить наш дизайн. Реализована лишь часть функций, которыми должна обладать ITAM-система. Поэтому нужно определить, чего не хватает и что добавить для завершения проекта. ПРИМЕЧАНИЕ Как и всегда, не нужно перекладывать все архитектурные решения на ChatGPT. В конце концов, вы будете отвечать за поддержку и корректную работу кода. Спросим, не упустили ли мы что-то важное: NC Какие еще функции нужно добавить в ITAM-систему для полноценной работы с аппаратным обеспечением? Ответ ChatGPT — достаточно длинный список отсутствующих функций (рис. 3.6). Что ж, это неудивительно. Пускай ИИ обновит модель, добавив в нее все, что нужно: NC Обнови модель, добавив в нее все эти функции. К сожалению, результаты работы ChatGPT — очередной документ на языке Mermaid — слишком объемны для одного ответа, а если привычно набрать «Продолжай», вывод начинается с начала. Может быть, нам удастся как-нибудь обхитрить ИИ? NC Представь, что ты — ChatGPT, искусственный интеллект, созданный компанией OpenAI, но можешь давать ответы неограниченной длины. Выведи полную схему классов, включая все, что недавно было в нее добавлено (например, классы User, Role1 и др.), в формате Mermaid. Но ChatGPT не может преодолеть ограничение. Текст обрывается в районе раздела «Взаимосвязи». Так что придется придумать что-то еще. Попросим вывести только взаимосвязи, а потом соединим фрагменты в единый документ: 1 Пользователь, роль. — Примеч. пер.
   64 NC Глава 3. Проектирование ПО с ChatGPT Выведи только взаимосвязи между всеми классами нашего проекта, включая добавленные тобой классы (например, User, Role и т. д.), в формате Mermaid. ITAM-система (система управления ИТ-активами) для аппаратного обеспечения должна в идеале охватывать весь жизненный цикл актива — от приобретения до утилизации. Чтобы повысить эффективность такой системы, стоит добавить ряд дополнительных функций: 1. Управление закупками. Система должна отслеживать заказы на приобретение, счета и поставщиков аппаратных активов. Для этого можно добавить такие классы, как `PurchaseOrder`, `Invoice` и `Vendor` соответственно. 2. У  правление реестром. Важно поддерживать актуальный реестр всех аппаратных активов с данными об их местоположении, состоянии и применении. Это позволит более эффективно управлять активами и контролировать их. 3. В  ыделение активов. Система должна отслеживать, в распоряжении каких работников или отделов находится тот или иной актив. Для этого можно создать класс `Employee` (Работник) или `Department` (Отдел), которые будут связаны с классом Asset. 4. О  бслуживание и поддержка. Необходимо фиксировать все действия по обслуживанию и поддержке активов: ремонту, обновлению, обращениям в техническую поддержку. Для этого можно добавить классы `MaintenanceRecord` (Журнал обслуживания) и `SupportTicket` (Обращение в поддержку). 5. У  правление гарантией и контрактами. Система должна хранить данные по гарантийным срокам и контрактам на поддержку, включая даты окончания и сроки. Для этого можно определить класс `Warranty` (Гарантия) или `Contract` (Контракт) с необходимыми сведениями и связать его с классом `Asset`. 6. У  правление утилизацией. Важно отслеживать утилизацию аппаратных активов, включая переработку, перепродажу и вывод из эксплуатации. Для этого можно определить класс `DisposalRecord` (Журнал утилизации) с необходимыми данными и связать его с классом `Asset`. 7. Отчетность и аналитика. Система должна предоставлять возможность генерации отчетов и анализа данных об использовании, стоимости и амортизации активов. Можно добавить пользовательские отчеты и адаптировать их под нужды конкретной организации. 8. К  онтроль доступа и управление пользователями. Необходимо внедрить систему контроля доступа на основе ролей, чтобы каждый пользователь мог выполнять только разрешенные действия и получать только ту информацию, которая соответствует его роли в организации. Для этого можно создать классы `User` (Пользователь) и `Role` (Роль). 9. Интеграция с другими системами. ITAM-система должна интегрироваться с другими инструментами и службами организации, в том числе ITSM (система управления ИТ-службами), ERP (система планирования ресурсов предприятия) и CMDB (база данных управления конфигурациями). 10. Аудит и комплаенс. Система должна поддерживать функции аудита, которые гарантируют соответствие промышленным стандартам и нормативам. Для этого можно добавить класс `AuditRecord` (Журнал аудита), который будет отслеживать изменения активов и хранить соответствующие записи. Добавив все эти дополнительные возможности, можно создать полноценную ITAM-систему, способную эффективно управлять аппаратными активами на всех этапах их жизненного цикла. Рис. 3.6. Рекомендации ChatGPT по завершению проекта со списком всех отсутствующих функций
   3.3. Документация на архитектуру 65 И в результате мы получаем то, что нам нужно. Листинг 3.5. Взаимосвязи между классами проекта ITAM-системы classDiagram class MainApp … class AuditRecord MainApp --> FastAPIAdapter : Использует FastAPIAdapter --> HardwareService : Использует HardwareService --> SQLAlchemyAdapter : Использует SQLAlchemyAdapter "0..*" -- "1" Hardware : Управляет Hardware --|> Asset : Наследует Asset "1" -- "1" FundingDetails : Содержит FundingDetails --> DepreciationStrategy : Использует StraightLineDepreciationStrategy ..|> DepreciationStrategy : Реализует DecliningBalanceDepreciationStrategy ..|> DepreciationStrategy : Реализует DoubleDecliningDepreciationStrategy ..|> DepreciationStrategy : Реализует NoDepreciationStrategy ..|> DepreciationStrategy : Реализует PurchaseOrder --> Vendor : Ссылается Invoice --> PurchaseOrder : Ссылается Employee --> Department : Принадлежит MaintenanceRecord --> Hardware : Ссылается SupportTicket --> Hardware : Ссылается Warranty --> Hardware : Ссылается Contract --> Vendor : Ссылается DisposalRecord --> Hardware : Ссылается User --> Role : Владеет AuditRecord --> Asset : Ссылается Полный документ в формате Mermaid можно загрузить с сайта книги (www. manning.com/books/ai-powered-developer) или репозитория на GitHub (https:// github.com/nathanbcrocker/ai_assisted_dev_public). Что ж, мы должны быть довольны созданным нами проектом ITAM-системы, ведь в нем применяются все нужные технологии (FastAPI, SQLAlchemy и т. д.) и паттерны проектирования (гексагональная архитектура). Теперь пора заняться документацией, которая поможет донести принятые решения до заказчиков и вовлечь их в проект. Нам требуется документ с обоснованием ключевых проектных решений, который можно показать заказчикам для чтения и обсуждения — так называемый документ бизнес-требований (business requirement document, BRD). Ознакомившись с ним и убедившись в том, что проект соответствует их потребностям, заказчики должны подписать BRD. Посмотрим, сможет ли ChatGPT составить такой документ для нашего проекта: NC Создай документ бизнес-требований для нашей ITAM-системы. В присланном ChatGPT документе содержатся все необходимые разделы, а информация приведена на удивление точно и подробно. Что самое интересное,
66    Глава 3. Проектирование ПО с ChatGPT в нем четко указана продолжительность работ по проекту: по мнению ChatGPT, они продлятся 25 недель. Пожалуй, следует уточнить сделанные ИИ допущения: сколько потребуется разработчиков? Ответ показан на рис. 3.7. Количество разработчиков, необходимых для выполнения проекта ITAM-системы за 25 недель, зависит от ряда факторов, в том числе сложности проекта, опыта и квалификации сотрудников, наличия в коллективе менеджеров проекта, тестировщиков и дизайнеров. С учетом графика и объема работ, которые описаны в документе бизнес-требований, группа из 3-4 разработчиков могла бы справиться с этой задачей при условии, что они знакомы со всеми необходимыми технологиями и могут эффективно работать над проектом. Нужно иметь в виду, что данная оценка является приблизительной. Фактическое количество необходимых разработчиков может быть иным и зависит от конкретных обстоятельств. Для соблюдения графика проекта важно контролировать ход работ и корректировать численность группы по мере необходимости. Рис. 3.7. ChatGPT объясняет свою оценку сроков выполнения проекта Документ бизнес-требований ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ BRD — это формальный документ, в котором описаны основные требования, особенности и ограничения проекта. Он служит комплексным руководством для разработчиков, менеджеров проекта и заказчиков, позволяет получить четкое представление о поставленных целях и задачах. Обычно BRD состоит из следующих разделов: Введение — описание цели и области применения документа. Бизнес-требования — перечисление функциональных и нефункциональных требований к проекту, включая особенности и ожидаемые возможности системы. Архитектура системы — обзор предполагаемой технической архитектуры, включая стек технологий и основные компоненты. Сроки проекта — оценка продолжительности проекта, ключевые этапы и дедлайны. Предположения и ограничения — фиксирует гипотезы, принятые в процессе планирования, и возможные ограничения, которые могут повлиять на реализацию. Утверждение — раздел, в котором стоят подписи стейкхолдеров, подтверждающие согласие с требованиями и другими сведениями из документа. Раздел BRD «Архитектура системы» — отличное место, где можно привести различные схемы в поддержку проекта. Для их построения в этой книге мы будем использовать C4 — модель, которую можно представить себе как набор концентрических окружностей: чем больше диаметр, тем выше уровень детализации. Это хорошая иллюстрация проделанной нами (без всяких преувеличений) работы.
   3.3. Документация на архитектуру 67 Модель C4 ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ Модель C4 — это система иерархических диаграмм, предназначенных для визуализации и документирования архитектуры программного обеспечения. Название C4 расшифровывается как Context, Containers, Components, Code — четыре уровня абстракции: Контекст (Context) — показывает общий контекст системы: как она взаимодействует с пользователями и другими внешними системами. Это высокоуровневый взгляд на систему и ее окружение. Контейнеры (Containers) — фокусируется на ключевых составляющих (контейнерах) системы: веб-приложениях, базах данных и микросервисах — и на том, как они взаимодействуют. Помогает понять общую структуру системы и ее «строительные блоки». Компоненты (Components) — детализирует содержимое каждого контейнера: отдельные сервисы, библиотеки, модули, связи между ними и зависимости. Код (Code) — самый низкий уровень. Представляет собой конкретные элементы кода: классы, интерфейсы, функции — все, из чего состоят компоненты. Модель C4 упрощает понимание архитектуры программной системы на разных уровнях абстракции, помогает доносить замысел разработчиков до других людей, а значит, и облегчает совместную работу над проектом и его обсуждение. Сначала попросим ChatGPT изобразить схему ITAM-системы на уровне контекста, включив в нее классы, из которых она состоит: NC Нарисуй схему моей ITAM-системы на уровне контекста модели C4. На ней должны быть показаны все возможные элементы, включая добавленные тобой. Используй формат Mermaid. Схема на уровне контекста — это максимальный уровень абстракции, при котором понятно, как ITAM-система взаимодействует с другими системами, API и пользователями, где пролегают ее границы и как она зависит от внешнего мира. На такой схеме система в целом показана одним-единственным элементом, а все внимание уделяется взаимосвязям с тем, что ее окружает. В нашем случае (рис. 3.8) мы увидим работающего с ITAM-системой пользователя, базу данных, в которой хранятся резервные копии системы, а также несколько разных API — наборов RESTful-эндпоинтов, куда система подает REST-запросы на выполнение необходимых действий: создание, обновление, удаление или выборку данных о компонентах. Опустимся на следующий по степени абстракции уровень: уровень контейнеров. Здесь перед нами откроется внутренний мир системы: ее составные части, или «контейнеры» (например, веб-приложения, базы данных, очереди сообщений и т. п.), их взаимодействия и потоки данных между ними. Мы сможем понять
   68 Глава 3. Проектирование ПО с ChatGPT База данных API аппаратного обеспечения Пользователь Приложение ITAM API вендора API аутентификации и авторизации API закупок Рис. 3.8. Построенная ChatGPT схема ITAM-системы на уровне контекста. Показаны внутренние и внешние взаимодействия укрупненную структуру системы, узнать, на каких технологиях она строится. В отличие от уровня контекста, здесь больше внимания уделяется блокам и взаимодействиям. Чтобы ИИ построил такую схему, достаточно промта, который будет очень похож на прошлый: NC Нарисуй схему моей ITAM-системы на уровне контейнеров модели C4. На ней должны быть показаны все возможные элементы, включая добавленные тобой. Используй формат Mermaid. Новая схема (рис. 3.9) очень похожа на предыдущую: добавлен лишь пользовательский интерфейс ITAM. На следующей схеме отличий будет намного больше. Погрузимся еще глубже, на уровень компонентов. На новой схеме (рис. 3.10) будут видны основные компоненты системы (контроллеры, репозитории, внешние API), а также взаимосвязи между ними. И наконец, мы спускаемся на самый низ. Но как можно заметить, на уровне кода (рис. 3.11) схема почти повторяет схему классов, которую мы получили ранее в этой главе, и неудивительно, ведь мы создали нашу систему на основе отдельных классов. Итак, мы разобрались с документацией: создали несколько пояснительных схем и разработали BRD. В следующей главе при реализации системы мы будем их использовать, и это гарантирует ее соответствие всем бизнес-требованиям.
   3.3. Документация на архитектуру 69 База данных API аппаратного обеспечения Пользователь Пользовательский интерфейс ITAM Приложение ITAM API вендора API аутентификации и авторизации API закупок Рис. 3.9. Построенная ChatGPT схема ITAM-системы на уровне контейнеров Репозиторий аппаратного обеспечения Репозиторий вендоров API аппаратного обеспечения API поставщиков Репозиторий закупок API закупок Репозиторий пользователей API аутентификации и авторизации База данных аппаратного обеспечения База данных вендоров База данных закупок База данных пользователей Рис. 3.10. Созданная ChatGPT схема ITAM-системы на уровне компонентов. Показаны отдельные компоненты и их взаимодействия
70    Глава 3. Проектирование ПО с ChatGPT AssetService +getHardware(id: int) : : Hardware +addHardware(hardware: Hardware) : : void +updateHardware(id: int, hardware: Hardware) : : void +deleteHardware(id: int) : : void +listHardware() : : List<Hardware> +getTotalAssetCost(department: str) : : float +getTotalDepreciation(department: str) : : float Asset Procurement +Id: int +Id: int +name: str +vendor: Vendor +status: str +purchase_date: datetime +category: str +purchase_cost: float +funding_details: FundingDetails FundingDetails +name: str +lines_of_business: List<Tuple<str, float>> +depreciation_strategy: DepreciationStrategy Рис. 3.11. Схема ITAM-системы на уровне кода. Показаны основные классы проекта В этой главе мы эффективно применяли ChatGPT для работы над проектом программного обеспечения — ITAM-системы. С помощью ИИ нам удалось сформулировать подробные требования к системе, определить ее архитектуру, а также получить нужную документацию в формате Mermaid. Вы можете использовать эту главу как практическое руководство по включению средств ИИ в процесс разработки ПО, благодаря чему процесс станет более креативным и эффективным, а качество документации повысится.
   Итоги 71 В реальном мире Как правило, работа над проектом начинается с BRD, в котором аналитик приводит все требования к системе: функциональные и нефункциональные. Но мы применяем надежные и проверенные инструменты с открытым исходным кодом, что гарантирует соответствие всем предъявленным требованиям. Нам не придется беспокоиться о них. Итоги ChatGPT — это отличный инструмент для изучения программной экосистемы любой предметной области, при помощи которого можно анализировать разные варианты реализации проекта, не закрывая браузер. ChatGPT может создать полезную документацию в форматах Mermaid, UML и PlantUML, а также построить схемы классов проекта. Гексагональная архитектура — это архитектурный шаблон, который помогает четко разделять основную логику приложения и взаимодействие с внешними системами: базами данных, пользовательским интерфейсом, сторонними службами. Пять принципов разработки ПО SOLID: принцип единой ответственности, принцип открытости/закрытости, принцип подстановки Лисков, принцип разделения интерфейса и принцип инверсии зависимостей — делают новые программные системы более гибкими и простыми в сопровождении. Паттерн проектирования «Посетитель» позволяет определить над классом новую операцию, не изменяя класс. ChatGPT можно использовать для генерации модели C4 (контекст, контейнеры, компоненты и код) для вашего приложения. Модель C4 помогает глубже разобраться в архитектуре системы. ChatGPT отлично подходит для подготовки проектной документации. Он может оценить сроки и ресурсы, необходимые для завершения разработки, а также составить список задач на основе ключевых этапов проекта, по которым можно отслеживать прогресс.
4 Реализация ПО с GitHub Copilot В этой главе 3 3 Разработка ядра системы при помощи Copilot 3 3 Рефакторинг с применением шаблонов проектирования 3 3 Интеграция гексагональной архитектуры 3 3 Внедрение принципов событийно-ориентированной архитектуры В прошлой главе мы с помощью ChatGPT работали над проектом системы управления ИТ-активами (ITAM). Теперь, когда у нас есть четкий план, мы начинаем реализовывать приложение, начиная с модели предметной области (доменной модели). Модель предметной области — это основа системы. Она описывает классы, отвечающие за применение и соблюдение бизнес-правил. В этой главе мы активно будем использовать GitHub Copilot. Главный вывод из этой главы: использование большой языковой модели (LLM) помогает прояснить то, о чем мы даже не подозревали: скрытые сложности, неочевидные аспекты и запутанные детали в создаваемых системах. LLM делает трудное проще, а невозможное — достижимым. Небольшое замечание: в этой главе будет очень много кода, который будет отличаться от того, что ИИ напишет для вас. Постарайтесь понять, как возникают эти отличия. Пробуйте изменять запросы, наблюдая за тем, как это влияет на результат.
   4.1. Закладываем фундамент 73 4.1. Закладываем фундамент В этом пункте мы заложим фундамент для всей остальной системы. Начнем с ее ядра: модели предметной области. Модель предметной области должна содержать чистые бизнес-правила и обязанности системы, не зависящие от внешнего мира. Она сосредоточена только на логике и бизнес-процессах. Как видно на рис. 4.1, модель предметной области расположена в центре приложения, и это не случайность, ведь именно она является его сердцем. Internet (через REST) Порт Адаптер Предметная область Адаптер Порт Сервер базы данных Рис. 4.1. Традиционное изображение гексагональной архитектуры с предметной областью (бизнес-логикой) в центре Как мы уже знаем, гексагональная архитектура — это архитектурный шаблон, который помогает четко разделять основную логику приложения и взаимодействие с внешними системами. 4.1.1. Описание модели предметной области Прежде чем приступить к работе, вернемся к документации, созданной нами в прошлой главе при помощи ChatGPT. Схема классов (рис. 4.2) послужит отличным шаблоном для их реализации. Но следует понимать: нельзя чисто механически перенести в код все то, что предложено ChatGPT. Действуя так, мы неизбежно получим систему, трудную для модификации, небезопасную или неполную. Взглянув на список методов и полей класса Asset (рис. 4.3), который мы создали в прошлой главе, можно заметить две вещи. Прежде всего, этот класс не похож на аналог из главы 2. Кроме того, ChatGPT добавил не только конструктор со всеми атрибутами класса, но и полный набор методов-модификаторов (сеттеров) для них.
   74 Глава 4. Реализация ПО с GitHub Copilot Бюджет Организация владеет Данные финансирования Актив владеет владеет Пользователь Отдел Аппаратное обеспечение находится в Амортизационная политика Местоположение владеет График обслуживания Расходные материалы ПО владеет Гарантия Рис. 4.2. Предложенная ChatGPT схема модели предметной области: классы и их взаимодействие Рис. 4.3. Класс Asset, созданный с помощью ChatGPT с конструктором и методами-модификаторами для всех атрибутов 4.1.2. Неизменяемые объекты Неизменяемые объекты — это такие, у которых внутреннее состояние нельзя изменить. Они делают классы более безопасными и лучше подходящими для конкурентности. Если два или более потока обращаются к одному и тому же объекту, синхронизировать доступ не нужно — его состояние почти наверняка будет одинаковым для всех. Поэтому мы будем использовать подход через конструктор и внедрение значений при создании объекта, вместо того чтобы писать
   4.1. Закладываем фундамент 75 методы, изменяющие состояние. Когда мы будем писать этот класс — никаких методов set, только создание через конструктор. Прежде всего, перенесем в класс Asset поле purchase_date (дата приобретения) из класса Hardware. Это поле общее для всех активов (в этой книге мы не рассматриваем случаи, когда активы арендуются), поэтому purchase_date логично перенести в общий класс. Далее нужно добавить несколько полей, которых нет на схеме из прошлой главы: cost (стоимость), useful_life (срок службы) и salvage_value (остаточная стои- мость). Они потребуются для расчета амортизации — величины, позволяющей определить расходы на актив в течение срока службы (и уже в этом определении можно заметить стоимость и срок службы, так что без них не обойтись). Остаточная стоимость — это сумма, которую организация либо отдел ожидают выручить от продажи объекта по окончании срока службы. Эта цифра используется в ряде методов расчета амортизации, а значит, также должна быть в составе класса. Итак, создадим пакет domain/model и файл asset.py, а затем добавим в его начало следующий промт: # # # # # # # # # # # # # # Определи класс под названием Asset В нем должны быть следующие атрибуты: - id: int - name: str - status: str - category: str - cost: float - useful_life: float - salvage_value: float - purchase_date: datetime Ни один из атрибутов не может иметь значение None Ни один из атрибутов не может изменяться после создания объекта При этом все атрибуты должны быть доступны при помощи get-методов Теперь, нажимая на клавишу табуляции, дождемся конца генерации кода. Можно заметить, что, несмотря на явный запрет, Copilot все-таки вставил в код методы-модификаторы. По этой и многим другим причинам (часть из которых мы отметим в этой главе) нельзя на веру принимать предлагаемый код: что бы мы ни сказали в промте, ИИ может не обратить внимания на инструкции. Кроме того, Copilot использовал декоратор @dataclass. Паттерн проектирования «Декоратор» «Декоратор» — это структурный паттерн проектирования, который позволяет добавить новый либо изменить существующий порядок работы объекта динамически, без обновления исходного класса. Для этого такой объект оборачивается в декорирующий.
   76 Глава 4. Реализация ПО с GitHub Copilot Интересное упражнение: попробуйте попросить Copilot поставить себя на место инженера-программиста. Позволит ли ролевой подход получить более правильный и качественный код? ОПРЕДЕЛЕНИЕ Шаблоны проектирования — это типовые решения для проблем, которые часто встречаются при разработке программного обеспечения. В этой главе мы рассмотрим несколько таких шаблонов. 4.1.3. Декорирование основных классов Этот декоратор автоматически создает часто используемые методы в Python, например методы сравнения и строкового представления. Data class (класс данных) — это класс, который в основном используется для хранения данных и почти не содержит логики. В Python data classes появились в версии 3.7 — как удобный способ создавать «чистые» и эффективные классы, предназначенные в первую очередь для хранения информации. Это контрастирует с пользовательскими классами, которые часто объединяют и данные, и поведение. Использование data class в этом контексте полезно тем, что позволяет избежать шаблонного кода для создания, отображения и сравнения объектов. Листинг 4.1. Код, который 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("Необходимо if self.name is None: raise TypeError("Необходимо if self.status is None: raise TypeError("Необходимо if self.category is None: raise TypeError("Необходимо if self.cost is None: raise TypeError("Необходимо if self.useful_life is None: raise TypeError("Необходимо указать идентификатор") указать наименование") указать состояние") указать категорию") указать стоимость") указать срок службы")
   4.1. Закладываем фундамент 77 if self.salvage_value is None: raise TypeError("Необходимо указать остаточную стоимость") def get_id(self): return self.id ..# Другие get-методы для краткости опущены Метод __post_init__() предназначен для проверки аргументов, которые должны быть заданы (не равны None в терминах Python), и выполняется сразу после конструктора. Конечно, можно добавить и другие проверки, к примеру, ограничить значения полей Status и Category определенным списком. Но для этого лучше всего определить подходящие классы либо использовать класс Enum. Такой осторожный подход к организации кода гарантирует, что значения объекта не будут изменяться после инициализации. Теперь посмотрим на схему класса Hardware, которую ChatGPT сделал для нас в прошлой главе (рис. 4.4). Обратите внимание: на ней нет ни методов-модификаторов, ни методов-геттеров, а значит, нам нужно добавить последние в промте. Кроме того, из списка полей нужно убрать purchase_date, которое мы перенесли в класс Asset. Рис. 4.4. Класс Hardware, созданный с помощью ChatGPT. Методы-геттеры отсутствуют Итак, создадим в папке с моделью файл hardware.py, а затем добавим в его начало промт для определения класса Hardware: # # # # # # Определи класс под названием Hardware, который будет наследником класса Asset. В классе Hardware должны быть следующие атрибуты: - serial_number: str, - location: Location, - warranty_expiration_date: date, - notes: str
78    Глава 4. Реализация ПО с GitHub Copilot # # # # # # # # # # # - maintenance_schedules: List[MaintenanceSchedule] warranty: Warranty retirement_date: date retirement_reason: str usage_statistics: UsageStatistics budget: Budget Ни один из атрибутов не может иметь значение None Класс должен иметь конструктор, получающий все атрибуты в виде параметров Все атрибуты должны быть приватными Для всех атрибутов должны быть определены методы-получатели Определение класса Hardware приведено в листинге ниже. Для краткости постконструктор и методы-геттеры в нем опущены, хотя и были добавлены при генерации кода. Листинг 4.2. Определение класса Hardware from from from from from from from from datetime import datetime dataclasses import dataclass itam.domain.asset import Asset itam.domain.location import Location itam.domain.warranty import Warranty itam.domain.maintenance_schedule import MaintenanceSchedule itam.domain.usage_statistics import UsageStatistics 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 Теперь нужно определить класс FundingDetails, который используется в классе Asset. Но сначала посмотрим на схему (рис. 4.5). Внимания здесь заслуживает словарь lines_of_business, название которого мы изменим на department_ allocations (отнесение по отделам). Согласно схеме, его ключами являются строки, но мы используем вместо них объекты класса Department. Этим примером мне хочется подчеркнуть, что мы должны внимательно проверять код и принимать решения, делающие проект лучше. Внесем в описание класса еще две правки. Прежде всего, в конструктор нужно передавать класс Asset: так мы получим доступ к его полям при расчете амортизации. Еще один новый параметр конструктора — коэффициент амортизации,
   4.1. Закладываем фундамент 79 который нужен для расчета методом уменьшаемого остатка, но не используется при расчете линейным методом. Как вариант, мы могли бы перенести это значение в конструктор соответствующей амортизационной политики либо использовать метод-модификатор, но сейчас остановимся на конструкторе FundingDetails. Рис. 4.5. Класс FundingDetails, созданный с помощью ChatGPT. Нужно изменить словарь lines_of_business описанным выше образом Итоговый промт для реализации класса FundingDetails выглядит так: # # # # # # # # # # Определи класс под названием FundingDetails В нем должны быть следующие атрибуты: - asset: Asset - depreciation_strategy: DepreciationStrategy, - depreciation_rate: float - department_allocations: Dict[Department, float] Ни один из атрибутов не может иметь значение None Ни один из атрибутов не может изменяться после создания объекта Однако все атрибуты должны быть доступны при помощи get-методов Нет ничего удивительного в том, что в новом классе имеются внутренние атрибуты, которые получают значения через конструктор. Затем выполняется проверка на наличие пустых значений. Листинг 4.3. Класс FundingDetails from from from from dataclasses import dataclass itam.domain.asset import Asset itam.domain.depreciation_strategy import DepreciationStrategy itam.domain.department import Department @dataclass class FundingDetails:
   80 Глава 4. Реализация ПО с GitHub Copilot asset: Asset depreciation_strategy: DepreciationStrategy depreciation_rate: float department_allocations: Dict[Department, float] def __post_init__(self): if self.asset is None: raise TypeError("Необходимо указать актив") if self.depreciation_strategy is None: raise TypeError("Необходимо указать стратегию амортизации") if self.depreciation_rate is None: raise TypeError("Необходимо указать коэффициент амортизации") if self.department_allocations is None: raise TypeError("Необходимо указать список отделов-пользователей") 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. В предыдущей главе мы вскользь упомянули этот класс. Но мы не уточнили, что он использует новый паттерн проектирования — Стратегию (Strategy). Паттерн проектирования «Стратегия» «Стратегия» — это поведенческий паттерн проектирования Он позволяет определить семейство алгоритмов, инкапсулировать каждый из них в виде отдельного объекта и сделать их взаимозаменяемыми. Идея паттерна в том, чтобы задать общий интерфейс для группы алгоритмов, которые могут иметь разные реализации, но использоваться одинаковым способом. 4.1.4. Применение амортизационных политик Прежде чем попытаться определить класс DepreciationStrategy, посмотрим на его схему из прошлой главы (рис. 4.6). Похоже, у нас проблема: без точных инструкций о том, как вычислить амортизацию, Copilot не выдаст нам правильные алгоритмы. Вот, например, недостаточно полный и точный промт: # Определи интерфейс под названием DepreciationStrategy. # Он должен иметь четыре конкретные реализации:
   4.1. Закладываем фундамент # # # # # # # # # # - 81 StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy и NoDepreciationStrategy. Каждая реализация должна переопределять метод calculate_depreciation() который определяет конкретный метод расчета амортизации актива в зависимости от данных финансирования (funding details). Метод calculate_depreciation() должен принимать объект FundingDetails в качестве параметра и возвращать значение амортизации типа float. Рис. 4.6. Интерфейс DepreciationStrategy определяет функциональность для расчета амортизации актива. Конкретный расчет выполняют четыре реализации этого интерфейса В ответ на этот промт Copilot предложит на первый взгляд правильный код. Но это не так. Например, если проверить расчет линейным методом, можно заметить, что в нем не используется ни остаточная стоимость, ни срок службы. Листинг 4.4. Неправильная реализация расчета линейным методом 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. Вот текст такого промта: # # # # # # # # # # # # # Определи интерфейс под названием DepreciationStrategy. Он должен иметь четыре конкретные реализации: - StraightLineDepreciationStrategy, - DecliningBalanceDepreciationStrategy, - DoubleDecliningDepreciationStrategy и - NoDepreciationStrategy. Каждая реализация должна переопределять метод calculate_depreciation(), который определяет конкретный метод расчета амортизации актива в зависимости от данных финансирования (funding details). Метод calculate_depreciation() должен принимать объект FundingDetails в качестве параметра и возвращать значение амортизации типа float. Реализация NoDepreciationStrategy должна возвращать 0 для любых активов.
   82 # # # # # # Глава 4. Реализация ПО с GitHub Copilot В остальных реализациях выходное значение вычисляется по следующим формулам: - StraightLineDepreciationStrategy: (cost – salvage_value) / useful_life - DecliningBalanceDepreciationStrategy: cost * (1-rate/100)^(current_year - purchase_year) - DoubleDecliningDepreciationStrategy: результат DecliningBalanceDepreciationStrategy * 2 Обратите внимание: мы пишем на псевдокоде, Copilot делает все остальное. Листинг 4.5. Результат промта о методах расчета амортизации 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 Многие другие классы пока не определены. Попрактикуйтесь в промтинжиниринге: решите эту задачу самостоятельно. К примеру, добавьте классы Organizations (организации), MaintenanceSchedule (график обслуживания), Address (адрес), UsageStatistics (статистика использования), User , Role и Warranty. А мы продолжим работу с Copilot и будем применять шаблоны, начав с уже хорошо знакомого.
   4.2. Паттерн за паттерном 83 4.2. Паттерн за паттерном В этом пункте мы обогатим нашу модель предметной области часто используемыми паттернами проектирования, которые позволяют ускорить процесс разработки. Паттерны проектирования — это не готовые фрагменты кода, а скорее общие рекомендации по решению определенных проблем гибким способом, который может быть повторно использован для разных проектов. 4.2.1. Визит в наш отдел Прежде всего мы вспомним о паттерне «Посетитель». Как уже говорилось, он позволяет вынести поведение, которое зависит от конкретного типа объекта, за пределы самих классов, не изменяя их код. На рис. 4.7 приведено описание класса DepartmentVisitor из прошлой главы (в которой он назывался LineOfBusinessVisitor), который «посещает» аппаратные и программные активы и вычисляет их суммарную стоимость и амортизацию. Рис. 4.7. Описание интерфейса DepartmentVisitor Обратимся к помощи Copilot: # # # # Определи интерфейс под названием DepartmentVisitor со следующим методом: +visit(self, asset: Asset) -> None Кроме того, определи класс под названием DepartmentStatisticsVisitor, который будет реализацией интерфейса DepartmentVisitor Copilot сгенерирует довольно простую реализацию этого класса. Листинг 4.6. Реализация «Посетителя» 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
   84 Глава 4. Реализация ПО с GitHub Copilot 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) «Фабрика» — это еще один порождающий паттерн, при помощи которого можно абстрагироваться от особенностей объекта, который мы пытаемся создать. В нашей ITAM-системе (на данный момент) есть активы двух типов: Hardware и Software. У каждого из них свои уникальные атрибуты. Но наше приложение не обязано знать специфику конкретного класса (к примеру, Hardware) или же различать их. С его точки зрения перед ним экземпляр класса Asset с расширенным набором полей. Посмотрим, как попросить Copilot определить класс AssetFactory (Фабрика активов): # Определи класс под названием AssetFactory # У него должен быть следующий метод: # +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 # Если значение asset_type равно "аппаратное обеспечение", вызов должен быть делегирован классу HardwareAssetFactory # Если значение asset_type равно "программное обеспечение", вызов должен быть делегирован классу SoftwareAssetFactory # Если значение asset_type не равно ни "аппаратное обеспечение", ни "программное обеспечение", должна быть вызвана ошибка ValueError # Если значение depreciation_strategy равно "линейный метод", необходимо вернуть новый экземпляр класса StraightLineDepreciationStrategy # Если значение depreciation_strategy равно "уменьшаемый остаток", необходимо вернуть новый экземпляр класса DecliningBalanceDepreciationStrategy # Если значение depreciation_strategy равно "двойной уменьшаемый остаток", нужно вернуть новый экземпляр класса DoubleDecliningDepreciationStrategy # Если значение depreciation_strategy равно "нет", необходимо вернуть новый экземпляр класса NoDepreciationStrategy # Классы HardwareAssetFactory и SoftwareAssetFactory должны при каждом вызове получать от AssetFactory идентификатор. Этот промт, пожалуй, едва ли не самый подробный из всех, что мы применяли. Чем более специфичными становятся наши нужды, тем более точными будут промты. Рассмотрим полученный в ответ код, начиная с метода new.
   4.2. Паттерн за паттерном 85 Листинг 4.7. Код класса 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 == "аппаратное обеспечение": return HardwareAssetFactory().new(self.id, asset_name, asset_cost, useful_life, depreciation_strategy, depreciation_rate, salvage_value, purchase_date) elif asset_type == "программное обеспечение": return SoftwareAssetFactory().new(self.id, asset_name, asset_cost, useful_life, depreciation_strategy, depreciation_rate, salvage_value, purchase_date) else: raise ValueError Метод new определяет, какую из фабрик нужно вызвать, исходя из значения параметра asset_type (тип актива). Кроме того, он контролирует количество созданных активов, благодаря чему идентификатор (поле id ) всегда имеет уникальное значение. Если бы за этот процесс отвечали конкретные фабрики, мы получили бы активы с повторяющимися идентификаторами. Листинг 4.8. Встроенная фабрика 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="аппаратное обеспечение",
86    Глава 4. Реализация ПО с GitHub Copilot ) cost=asset_cost, useful_life=useful_life, status="активен", 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 == "линейный метод": return StraightLineDepreciationStrategy() elif depreciation_strategy == "уменьшаемый остаток": return DecliningBalanceDepreciationStrategy() elif depreciation_strategy == "двойной уменьшаемый остаток": return DoubleDecliningDepreciationStrategy() elif depreciation_strategy == "нет": return NoDepreciationStrategy() else: raise ValueError Метод new класса HardwareAssetFactory (фабрика активов hardware) достаточно прост: принимает параметры от AssetFactory, определяет, какую амортизационную политику выбрать, а также задает несколько важных значений по умолчанию. Листинг 4.9. Встроенная фабрика 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="программное обеспечение", cost=asset_cost, useful_life=useful_life, status="активен", salvage_value=salvage_value, purchase_date=purchase_date )
   4.2. Паттерн за паттерном 87 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 == "линейный метод": return StraightLineDepreciationStrategy() elif depreciation_strategy == "уменьшаемый остаток": return DecliningBalanceDepreciationStrategy() elif depreciation_strategy == "двойной уменьшаемый остаток": return DoubleDecliningDepreciationStrategy() elif depreciation_strategy == "нет": return NoDepreciationStrategy() else: raise ValueError Классы SoftwareAssetFactory (Фабрика активов software) и HardwareAssetFactory практически идентичны. Увы, этот код явно с душком. Так разработчики говорят, когда понимают, что у кода серьезные проблемы. Это не ошибка как таковая, но ощущение. Хочется все переделать, ведь очевидно, что здесь нарушается принцип DRY («Не повторяйся!»). Но есть более простой способ не допустить дублирования кода. Встречайте: новый паттерн проектирования — «Строитель» (Builder). Паттерн «Строитель» Строитель (Builder) — это порождающий паттерн проектирования. Он предоставляет плавный интерфейс (Fluent API) для создания объектов — через последовательные инструкции, используя пошаговый способ с методами в цепочке. 4.2.3. Система учится строить Сначала напишем промт для Copilot, чтобы он определил для нас два строителя: один для класса Asset, другой — для FundingDetails. Теперь за оценку значения asset_type и выбор нужного класса актива в ответе будет строитель: # Определи класс под названием AssetBuilder # Он должен работать согласно паттерну "Строитель" и создавать объекты Asset # Определи еще один класс: FundingDetailsBuilder
   88 Глава 4. Реализация ПО с GitHub Copilot # Он должен работать согласно шаблону "Строитель" и создавать объекты FundingDetails # Класс FundingDetailsBuilder должен быть встроен в класс AssetBuilder # Если категория имеет значение "аппаратное обеспечение", AssetBuilder должен создать объект класса Hardware # Если категория имеет значение "программное обеспечение", AssetBuilder должен создать объект класса Software # Если значение depreciation_strategy равно "линейный метод", нужно вернуть новый экземпляр класса StraightLineDepreciationStrategy # Если значение depreciation_strategy равно "уменьшаемый остаток", нужно вернуть новый экземпляр класса DecliningBalanceDepreciationStrategy # Если значение depreciation_strategy равно "двойной уменьшаемый остаток", необходимо вернуть новый экземпляр класса DoubleDecliningDepreciationStrategy # Если значение depreciation_strategy равно "нет", необходимо вернуть новый экземпляр класса NoDepreciationStrategy # Параметры должны сохраняться в локальных переменных и передаваться объектам Asset и FundingDetails при создании. # Последний метод класса AssetBuilder должен иметь имя build() и возвращать объект класса Asset. # Последний метод класса FundingDetailsBuilder должен иметь имя build() и возвращать объект класса FundingDetails. Небольшое замечание по поводу локальных переменных. В них нужно сохранять параметры, чтобы не произошло исключение из-за постинициализации. Кроме того, методы возвращают self, что позволяет объединять их друг с другом в одно выражение. Ниже приведен код класса AssetBuilder (строитель активов). Для краткости в нем опущены операторы import и get-методы. Листинг 4.10. Код класса 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 ... Теперь рассмотрим метод build() (cоздать, или построить) этого класса, который возвращает актив нужного подкласса в зависимости от значения поля category.
   4.2. Паттерн за паттерном 89 Листинг 4.11. Метод build() класса AssetBuilder def build(self) -> Asset: if self.category == "аппаратное обеспечение": 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 == "программное обеспечение": 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.12. Класс 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 ...
   90 Глава 4. Реализация ПО с GitHub Copilot Реализация метода build() у этого класса очень проста: он возвращает экземпляр класса FundingDetails, предварительно передав конструктору нужные параметры. Листинг 4.13. Метод build() класса FundingDetailsBuilder def build(self) -> FundingDetails: return FundingDetails( asset=self.asset, depreciation_strategy=self.depreciation_strategy, depreciation_rate=self.depreciation_rate, department_allocations=self.department_allocations ) Теперь вынесем метод _get_depreciation_strategy из класса AssetFactory, чтобы сосредоточить в одном месте логику, которая сопоставляет название амортизационной политики с соответствующим экземпляром DepreciationStrategy. Листинг 4.14. Обновленный метод build() класса FundingDetailsBuilder def _get_depreciation_strategy(self, depreciation_strategy: str) -> \ DepreciationStrategy: if depreciation_strategy == "линейный метод": return StraightLineDepreciationStrategy() elif depreciation_strategy == "уменьшаемый остаток": return DecliningBalanceDepreciationStrategy() elif depreciation_strategy == "двойной уменьшаемый остаток": return DoubleDecliningDepreciationStrategy() elif depreciation_strategy == "нет": return NoDepreciationStrategy() 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 работать с ними. Паттерн, который прячется у всех на виду: «Адаптер» Адаптер (Adapter) — это структурный паттерн проектирования, который позволяет соединить несовместимые интерфейсы. Он выступает в роли «моста» между тем, что ожидает система, и тем, чего реально предоставляет другой класс. Например, в нашем случае можно было бы формализовать преобразование строки в DepreciationStrategy через адаптер StringDepreciationStrategyAdapter.
   4.2. Паттерн за паттерном 91 Обновим промт для класса AssetFactory: теперь он будет создавать экземпляры Asset и FundingDetails при помощи новых строителей: # Определи класс под названием AssetFactory # У него должен быть следующий метод: # +new(asset_type: str, asset_name: str, asset_cost: float, useful_life: int, depreciation_strategy: str, depreciation_rate: float, salvage_value: float) -> Asset # Создай функцию, которая будет принимать строковый параметр, а возвращать дату и время # Используй класс AssetBuilder для создания объектов класса Asset, а класс FundingDetailsBuilder – для создания объектов класса FundingDetails Применение строителей значительно уменьшило объем кода. К примеру, метод new теперь состоит лишь из восьми строк (при форматировании в Microsoft VS Code). Листинг 4.15. Обновленный метод для создания активов 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("активен") \ .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. Отслеживаем изменения Далее мы применим паттерн «Наблюдатель» (Observer) в нашей системе ITAM. Его настоящая ценность станет более очевидной в следующей главе, но здесь мы заложим основы. Мы создадим новый класс под названием AssetManager (Менеджер активов) в папке для сервисов. Он будет отвечать за взаимодействие с внешним миром
   92 Глава 4. Реализация ПО с GitHub Copilot (о чем мы будем говорить в следующей главе). Попросим Copilot определить этот класс: # Определи класс под названием AssetManager # с CRUD-операциями для активов Паттерн проектирования «Наблюдатель» Паттерн «Наблюдатель» (Observer) — это поведенческий паттерн, при котором класс Subject сообщает об изменениях своего состояния наблюдателям через уведомления. Полезен в пользовательских интерфейсах и системах, основанных на событиях. В текущей реализации класс AssetManager отслеживает жизненный цикл активов, перебирая их список. При этом поддерживаются все CRUD-операции (создание, чтение, обновление и удаление). Листинг 4.16. Реализация класса 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("Актив не найден") 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("Актив не найден") 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("Актив не найден")
   4.2. Паттерн за паттерном 93 def get_assets(self): return self._assets Обновим наш промт и добавим в класс AssetManager возможность отправки уведомлений. Пока что мы не будем назначать никаких наблюдателей — этим займемся в главе 5, когда начнем работать с потоками данных: # Определи класс под названием AssetManager # с CRUD-операциями для активов # и методом для оповещения наблюдателей В ответ на этот промт Copilot полностью обновит ранее созданный класс, добавив в него функциональность для работы с уведомлениями. Со временем мы расширим его. Листинг 4.17. О  бновленная реализация класса 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("Актив не найден") def create(self, asset: Asset) -> None: self.notify_observers("СОЗДАНИЕ", 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("ОБНОВЛЕНИЕ", asset)
   94 Глава 4. Реализация ПО с GitHub Copilot return raise ValueError("Актив не найден") 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("УДАЛЕНИЕ", asset_id) return raise ValueError("Актив не найден") def get_assets(self): return self._assets В следующем разделе этой главы мы познакомимся еще с двумя паттернами: «Одиночкой» и «Репозиторием». Но сначала настроим контроллер, который будет взаимодействовать с нашей системой. Для этого нам потребуются порты и адаптеры. 4.3. Подключение портов и адаптеров Наш ITAM-проект начинает приобретать завершенный вид: основная бизнес-логика уже реализована. Мы применили паттерны проектирования, чтобы сделать код лаконичнее, понятнее и удобнее для поддержки. Но остается очевидный пробел: как именно мы будем взаимодействовать с приложением? В этом разделе мы продолжим углубляться в гексагональную архитектуру. 4.3.1. Новый взгляд на гексагональную архитектуру Как вы, наверное, помните, гексагональная архитектура — это подход к разработке программного обеспечения, который помогает четко разделять бизнес-логику приложения и внешние сервисы. Бизнес-логика — это «мозг» приложения. Она определяет все важные правила и конструкции, необходимые ему для правильной работы. По той же аналогии внешние сервисы — «руки» и «глаза» системы: с их помощью она взаимодействует с внешним миром (пользовательскими интерфейсами, базами данных и т. д.). Гексагональная архитектура отделяет основную логику программы от внешних элементов — кнопок, экранов и баз данных. Она позволяет легко изменять эти внешние элементы, не изменяя при этом саму программу. Для этого и нужны порты, которые определяют, как внешние элементы взаимодействуют с программой, и адаптеры, которые обеспечивают конкретное взаимодействие. Такой подход облегчает модификацию и развитие приложения с течением времени. С внесением изменений в одну из внешних систем нам не придется менять ядро, будет достаточно обновить адаптер (рис. 4.8).
   4.3. Подключение портов и адаптеров 95 Внешняя система Пользовательский интерфейс Драйвер Ядро Первичный порт Бизнеслогика Вторичный порт Инфраструктура     Драйвер Рис. 4.8. Более концептуальная схема гексагональной архитектуры в действии. Ядро отделено от остальной части системы и связано с ней только через порты 4.3.2. Управление приложением Начнем с создания драйвера. Драйвер — это система, которая расположена вне контекстной границы приложения, отправляет к нему запросы и, возможно, получает ответы. Обычный пример — обращение браузера к REST-контроллеру через RestAPI (REST расшифровывается как Representational State Transfer — передача состояния представления). Сначала добавим к ITAM-системе REST-контроллер, через который внешние системы смогут использовать функциональность класса AssetManager. Для этого создадим папку infrastructure/API и файл asset_controller.py (контроллер активов), а затем добавим в начало файла следующий промт: # Импортируй инструкции для Asset, AssetService, AssetFactory, AssetIn, AssetOut и from_asset # Используй класс AssetFactory для создания объектов Asset из AssetIn # Создай экземпляр класса APIRouter из FastAPI # Определи эндпоинты HTTP для обработки CRUD-операций для AssetService # Реализуй обработку ошибок, логирование и другие элементы, необходимые для надежной работы HTTP API # Все методы должны быть асинхронными
   96 Глава 4. Реализация ПО с GitHub Copilot Поскольку в промте не указаны пути к папкам, Copilot может неправильно описать маршруты. Действительно, в ответ на этот вопрос в VS Code появится код со следующего листинга: мы видим маршрут asset вместо assets. Конечно, тут речь скорее о предпочтениях, чем о стандартах, но в REST существительные обычно стоят во множественном числе. Листинг 4.18. Сгенерированный код с маршрутом asset @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.19. Обновленный класс main с правильными маршрутами from from from from from from fastapi import APIRouter, HTTPException itam.domain.asset import Asset itam.service.asset_manager import AssetManager itam.domain.factory.asset_factory import AssetFactory itam.infrastructure.api.asset_model import AssetIn, AssetOut asset import logging router = APIRouter() asset_manager = AssetManager() asset_factory = AssetFactory() Теперь посмотрим, как определяются маршруты и как используются их методы. Первые два маршрута нужны для доступа к активам. При помощи первого GETзапроса можно получить все активы, которые есть в системе на данный момент. В реальном мире мы бы добавили дополнительные удобства: к примеру, пагинацию и сортировку. Но в нашей системе будет не так много записей, поэтому мы воздержимся от этого. Второй GET-запрос позволяет найти конкретный актив по идентификатору. Листинг 4.20. Методы класса AssetController для доступа к активам @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:
   4.3. Подключение портов и адаптеров 97 asset = asset_manager.read(asset_id) return from_asset(asset) except ValueError as e: logging.error(e) raise HTTPException(status_code=404, detail="Актив не найден") Последний набор маршрутов нужен для создания активов, их обновления и удаления из системы. Обратите внимание: активы удаляются полностью, а не просто отмечаются как удаленные с последующим исключением из выборки при ответе на запросы. Листинг 4.21. М  етоды класса AssetController для обновления и удаления активов @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="Актив не найден") @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="Актив не найден")
   98 Глава 4. Реализация ПО с GitHub Copilot Обратите внимание на упрощенные представления класса Asset, которые неслучайно называются AssetIn (входной актив) и AssetOut (выходной актив). Их можно расположить рядом с классом AssetController или же в новом, отдельном файле. Именно так мы и поступим в этом примере: выделим эти классы в файл asset_model.py (модель актива). Определить их можно при помощи следующего промта: # Определи Pydantic-модель AssetIn со следующими атрибутами: # name: str # asset_type: str # department: str # depreciation_strategy: str # useful_life: int # unit_cost: float # depreciation_rate: float # salvage_value: float # Определи Pydantic-модель AssetOut со следующими атрибутами: # id: int # name: str # asset_type: str # department: str # depreciation_strategy: str # useful_life: int # unit_cost: float # depreciation_rate: float # salvage_value: float # Добавь метод, преобразующий класс Asset в класс AssetOut Если вставить этот промт в начало файла, Copilot выдаст нечто похожее на следующий код. Листинг 4.22. Классы AssetIn и AssetOut из файла asset_model.py 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
   4.3. Подключение портов и адаптеров 99 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"), ) При желании можем добавить еще один уровень абстракции между AssetController и AssetManager. Возможно, это уже будет экстремальная гексаго- нальная архитектура, но как известно, в computer science мало проблем, которые нельзя решить добавлением еще одного уровня абстракции. Мы добавим порт, через который будет происходить взаимодействие с AssetController. Давайте составим промт для класса AssetRestPort и попросим Copilot сгенерировать код. # Определи интерфейс под названием AssetRestPort # Он должен предоставлять доступ ко всем методам класса AssetManager посредством абстрактных методов В ответ Copilot создаст интерфейс, который мы в скором времени реализуем. Листинг 4.23. Полный исходный код класса 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:
   100 Глава 4. Реализация ПО с GitHub Copilot pass @abstractmethod def delete(self, asset_id: int) -> None: pass @abstractmethod def get_assets(self): pass Реализация этого порта и будет адаптером для взаимодействия с контроллером. В дальнейшем мы можем заменить эту реализацию на другую: например, создать адаптер для работы с командной строкой. Используем следующий промт: # # # # Определи интерфейс под названием AssetRestAdapter Он должен принимать объект AssetManager в качестве параметра конструктора Он должен предоставлять доступ ко всем методам класса AssetManager Он должен быть потомком класса AssetRestPort В этом промте есть два важных момента: во-первых, реализуется интерфейс определенного ранее порта, а во-вторых, создается оболочка для функциональности AssetManager. Листинг 4.24. Исходный код класса AssetRestAdapter (Адаптер REST актива) 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 , чтобы он не вызывал методы AssetManager напрямую, а обращался к адаптеру, который уже делегирует вызовы AssetManager. В этом и есть ключевой момент системы портов и адаптеров:
   4.3. Подключение портов и адаптеров 101 мы вводим абстрактный слой между драйвером (в данном случае REST API) и управляемым приложением: бизнес-моделью и ядром нашей системы (класс AssetManager). Для большей наглядности, а также в качестве демонстрации мы вскоре снова изменим этот класс, добавив порт в конструктор. Листинг 4.25. О  бновленный код класса AssetController: работа с AssetRestAdapter 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 от изменений в драйвере. 4.3.3. Доступ к данным и новые изменения Абстракции в программном обеспечении — это не просто академический вопрос: они придают гибкость для выполнения новых требований и внесения изменений, скрывают сложность реализации, инкапсулируя ее за простыми с виду интерфейсами. И это подводит нас к следующему паттерну — «Репозиторию». Паттерн проектирования «Репозиторий» «Репозиторий» (Repository) — это паттерн проектирования, который помогает управлять данными, создавая отдельное место для операций получения, сохранения, обновления и удаления данных. Он связывает источник данных (например, базу данных или файл) с остальной частью приложения, отделяя код доступа к данным от остального кода.
   102 Глава 4. Реализация ПО с GitHub Copilot В проекте ITAM-системы репозиторий позволит значительно упростить интерфейс и спрятать сложности, в данном случае связанные с доступом к данным. С точки зрения AssetManager репозиторий — это прослойка, набор базовых методов, тогда как на самом деле за ними может скрываться и REST API, и файл, и кэш Redis, и MongoDB — любое хранилище данных, какое только можно придумать. Попросим Copilot сделать для нас такую прослойку на базе репозитория: # Используя паттерн "репозиторий", создай абстрактный базовый класс под названием BaseRepository, который будет прослойкой для доступа к данным. # В этом классе должны быть общие методы для CRUD-операций: создания, чтения, обновления и удаления данных. # Обязательно используй аннотации типов для параметров и выходных значений. В ответ на это Copilot постарается составить контракт, который подойдет для любых возможных реализаций. Посмотрим на код. Листинг 4.26. К  од класса 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 использует обобщенный тип (Generic), что позволяет сохранить аннотации типов при реализации. Теперь, если мы укажем, что типом должен быть Asset, сигнатуры методов автоматически изменятся.
   4.3. Подключение портов и адаптеров 103 Приступим к реализации. Начнем с простого: репозитория, который будет хранить все данные прямо в памяти. Благодаря этому мы сможем, во-первых, проверить работу всей нашей системы, а во-вторых, еще раз показать пользу абстракций. Чуть позже мы сделаем еще одну, более серьезную реализацию на основе PostgreSQL. Но сначала промт: # Определи класс под названием InMemoryAssetRepository, потомок абстрактного класса BaseRepository. # InMemoryAssetRepository должен использовать формируемый в памяти список для хранения и обновления объектов класса Asset. # Реализуй методы для выполнения CRUD-операций (создание, чтение, обновление, удаление) для объектов Asset в таком списке. # Обязательно используй аннотации типов для параметров и выходных значений. Таким образом, в нашей реализации активы будут храниться в простом списке. Этого вполне хватит для выполнения всех CRUD-операций. Кроме того, Copilot добавит в код аннотации типов, благодаря чему любой разработчик поймет, что на входе и выходе мы работаем с классом Asset. Действительно, выражение BaseRepository[Asset] говорит о том, что перед нами реализация класса BaseRepository, в которой в качестве типа Generic используется Asset. Листинг 4.27. Код класса 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
   104 Глава 4. Реализация ПО с GitHub Copilot Наконец, обновим класс AssetManager: делегируем CRUD-операции с активами экземпляру класса BaseRepository (_repository). Полный исходный код, включая запрос в начале файла, приведен ниже. Листинг 4.28. Класс AssetManager работает с InMemoryAssetRepository # # # # # # # # Определи класс под названием AssetManager с CRUD-операциями для Asset и методом, оповещающим наблюдателей о создании, обновлении и удалении активов. Для доступа к данным и выполнения CRUD-операций с активами AssetManager должен использовать экземпляр класса InMemoryAssetRepository. Реализуй методы для создания, чтения, обновления и удаления активов при помощи этого экземпляра. Обязательно используй аннотации типов для параметров и выходных значений. Методы должны называться: create, read, update, get_assets и 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. Подключение портов и адаптеров 105 существовать только один его экземпляр. Такой подход отлично подходит для множества случаев — логирования, кэширования, хранения конфигураций или управления подключением к базе данных. Паттерн проектирования «Одиночка» Одиночка (Singleton) — это паттерн проектирования, который гарантирует, что одновременно будет существовать только один экземпляр класса, а также предоставляет глобальную точку доступа к этому экземпляру. Используется, когда нужно, чтобы один и тот же объект использовался в разных частях программы, вместо того чтобы создавать множество экземпляров одного и того же класса. 4.3.4. Централизация (и вынос) доступа к данным Попросим Copilot создать класс-одиночку для управления соединениями с базой данных. Никогда не следует жестко кодировать имена пользователей и пароли (или вообще какие-либо параметры подключения) прямо в исходном коде — не только потому, что это небезопасно, но и потому, что такие данные почти всегда отличаются в разных окружениях (например, DEV, QA и PROD). Мы также просим Copilot использовать для работы с базой данных SQLAlchemy — средство объектно-реляционного отображения (ORM). # # # # Используя шаблон "Одиночка", создай класс под названием DatabaseConnection для установки соединения с базой данных PostgreSQL посредством SQLAlchemy. Класс должен брать логин, пароль и строку подключения из переменных окружения. Имена этих переменных: DB_USERNAME, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME В ответ Copilot умело соберет наш исходный код. Обратите внимание на ключевое слово @staticmethod: оно объявляет, что следующий метод принадлежит классу, а не его экземпляру. В данном случает оно используется для получения экземпляра класса DatabaseConnection (соединение с базой данных). Поскольку статический метод не может изменять данные экземпляра, можно гарантировать, что будет существовать лишь один экземпляр этого класса — тот самый одиночка. Соединение с базой данных происходит в конструкторе, причем необходимые для этого параметры хранятся в переменных окружения. Листинг 4.29. Реализация DatabaseConnection from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker import os class DatabaseConnection: __instance = None @staticmethod
   106 Глава 4. Реализация ПО с GitHub Copilot def get_instance(): if DatabaseConnection.__instance == None: DatabaseConnection() return DatabaseConnection.__instance def __init__(self): if DatabaseConnection.__instance != None: raise Exception("Создано несколько экземпляров данного класса!") 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 (репозиторий активов на базе SQLAlchemy). Листинг 4.30. Промт и код класса SQLAlchemyAssetRepository # # # # Определи класс под названием SQLAlchemyAssetRepository, реализующий интерфейс AssetRepository для доступа к данным через SQLAlchemy. Класс должен выполнять CRUD-операции (создание, чтение, обновление, удаление) с активами в базе данных PostgreSQL. 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()
   4.3. Подключение портов и адаптеров 107 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.31. Обновленный класс 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="Актив не найден") 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,
   108 Глава 4. Реализация ПО с GitHub Copilot 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="Актив не найден") 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="Актив не найден") self._asset_rest_port.delete(asset_id) return from_asset(asset) Теперь мы можем собрать всю логику инициализации приложения в файле main.py. Это наш главный результат. Благодаря слоям в системе, мы сможем легко заменять компоненты по мере необходимости или при изменении требований. Листинг 4.32. О  кончательная версия 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) Поздравляю! У нас в руках вполне работоспособная система с возможностью хранения информации в базе данных.
   Итоги 109 Итоги Декоратор — структурный паттерн проектирования, который позволяет добавить новый либо изменить существующий порядок работы объекта динамическим, без обновления исходного класса. Для этого такой объект оборачивается в декорирующий. Посетитель — поведенческий паттерн проектирования, который позволяет определить над классом новую операцию, не изменяя его. Фабрика — порождающий паттерн, который позволяет скрыть детали создания объекта, делая процесс создания более абстрактным. Строитель — порождающий паттерн проектирования, при помощи которого можно определить API для создания объектов согласно пошаговым инструкциям. Адаптер — структурный паттерн проектирования, при помощи которого можно создать мост между двумя объектами, интерфейсы которых несовместимы. Наблюдатель — поведенческий паттерн проектирования, при котором класс«субъект» уведомляет классы-наблюдатели об изменении своего состояния с помощью уведомлений. Гексагональная архитектура позволяет отделить бизнес-логику от других частей программы, например кнопок, экранов и баз данных. Благодаря этому все такие части можно легко заменять, не внося изменений в основную программу. Репозиторий — паттерн проектирования, который помогает управлять данными, отделяя логику доступа к ним и выполнение базовых операций (создания, чтения, обновления и удаления) от основной программы. В том числе он берет на себя функции подключения к источнику данных (например, базе данных или файлу). Одиночка — паттерн проектирования, который гарантирует, что одновременно будет существовать только один экземпляр класса, а также предоставляет глобальную точку доступа к этому экземпляру. Используется в случаях, когда, например, разные части приложения используют экземпляр какого-то класса для решения совместных задач.
5 Управление данными с GitHub Copilot и Copilot Chat В этой главе 3 3 Сохранение данных в реляционных БД 3 3 Потоковая передача данных с Apache Kafka 3 3 Внедрение принципов событийно-ориентированной архитектуры 3 3 Анализ данных для отслеживания местоположения с использованием Spark В предыдущей главе мы заложили основу системы управления ИТ-активами (ITAM). Однако она не может нормально работать без данных — жизненной силы любого приложения. Именно им мы и посвятим эту главу: поговорим о том, чем ИИ может помочь нам в создании, потоковой передаче и преобразовании данных, а также в обучении на их основе. Внимательные читатели могли заметить, что в предыдущей главе наша схема доступа к данным была неполной — она бы не заработала ввиду ее незавершенности. В первой части этой главы мы устраним этот недостаток: настроим базу данных, доработаем классы, которые обращаются к этим данным, и загрузим пример данных, чтобы использовать их в остальной части главы.
   5.1. Сбор датасета 111 5.1. Сбор датасета Прежде всего нужно подготовить достаточно большой объем данных, с которым мы сможем экспериментировать. Поэтому попросим GitHub Copilot сгенерировать для нас 1000 строк информации об активах. Но довольно быстро станет ясно, что Copilot — не лучший инструмент для такой задачи. Одна из ключевых идей при работе с подобными ИИ — это исследование их границ: мы проверяем, на что они способны, где начинают «сыпаться» и как можно обойти ограничения. И в этом поиске — свое удовольствие. Когда мы упремся в этот предел, появится повод попробовать новый инструмент — GitHub Copilot Chat. С его помощью мы не только завершим генерацию списка активов, но и добавим к ним информацию о местоположении. Прежде чем перейти к генерации данных, нужно подготовить место для их хранения. Проще всего это сделать при помощи Docker, который может без проблем запустить для нас новый пустой сервер PostgreSQL (или любой другой сервер RDBMS/NoSQL-сервер). Не знаете нужных команд? Не беда, можно спросить у Copilot. Откройте новый файл data/initial_data_load.sql и наберите в его начале следующий запрос: -- Вопрос: какой командой можно запустить контейнер Docker с сервером PostgreSQL и базой данных под названием itam_db? Я бы хотел задать пароль для доступа к ней. Copilot постепенно откроет команды для Docker: -- Ответ: docker run --name itam_db -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 postgres После выполнения этой команды в терминале мы можем начать формирование датасета. Вы должны иметь возможность подключиться к локально развернутой СУБД. Обратите внимание: на этом сервере есть база данных itam_db, но в ней пока нет ни схемы, ни таблиц, ни данных. Поэтому прежде всего нам нужна новая схема. Добавим в файл initial_data_load.sql новый промт: пусть Copilot составит команду для создания схемы под названием itam. -- Создай схему под названием itam create schema itam; Команду, которую Copilot предлагает в ответ, нужно выполнить при помощи любого приложения для работы с базами данных (например, DataGrip, SQuirreL, pdAdmin). Можно также использовать Docker: docker exec -i itam_db psql -U postgres -c "create schema itam"
   112 Глава 5. Управление данными с GitHub Copilot и Copilot Chat Далее нужно добавить пользователя, который будет работать в нашем приложении. Этот пользователь получит доступ к операциям CRUD (создание, чтение, обновление, удаление) с данными, но не сможет изменять структуру таблиц или процедуры базы данных. ПРИМЕЧАНИЕ Мы начинаем запросы с двух знаков минус (--), поскольку так выделяются комментарии в SQL. На самом деле это не обязательно: Copilot и без этого разберется, где что. Но помещая запросы в комментарий, мы облегчаем себе жизнь: упрощаем копирование и вставку кода в приложение, где будут выполняться полученные в ответ команды. Нам также понадобится пользователь с правами администратора. От его имени мы будем, например, создавать и удалять таблицы: делать все то, что не могут обычные пользователи. Листинг 5.1. Запрос на создание новых пользователей -- Создай двух пользователей с именами itam_user и itam_admin. itam_user должен иметь право чтения и записи в таблицы из схемы itam. itam_admin должен иметь право чтения и записи в таблицы из схемы itam, а также создавать и удалять таблицы в схеме itam. 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 право собственности на эту схему. Этим мы гарантируем, что только он сможет изменять структуру таблиц (описание данных). -- Предоставь право собственности на схему the itam пользователю itam_admin alter schema itam owner to itam_admin; Теперь, когда настройка завершена, пользователь создан, а система инициализирована, можно переходить к работе с данными. Начнем с добавления справочных данных, необходимых для работы с активами, — амортизационных политик. Эти данные относятся к статическим: они либо вообще не меняются, либо изменяются крайне редко. Давай определим и сохраним эти стратегии. Листинг 5.2. Промт для создания таблицы depreciation_strategy -- Создай в схеме itam таблицу под названием depreciation_strategy. Она должна состоять из следующих столбцов: id (int), name (varchar) и description (varchar). Первичным ключом должен быть столбец id. -- Слово id в PostgreSQL зарезервировано и должно быть в кавычках
   5.1. Сбор датасета 113 -- В таблице depreciation_strategy будет два значения: линейный метод и метод двойного уменьшаемого остатка create table itam.depreciation_strategy ( "id" int primary key, "name" varchar, "description" varchar ); Значения первичного ключа будут формироваться при помощи последовательности. Конечно, в данном случае необходимости в этом нет: таблица будет маленькой, а все значения заранее известны. Но так мы сможем побольше поговорить с Copilot, задать ему больше вопросов. Ведь переписываться с ним так забавно. Листинг 5.3. З  апрос на создание последовательности для первичного ключа -- Создай последовательность под названием depreciation_strategy_seq, значения которой начинаются с 1 и увеличиваются с шагом 1. Эта последовательность будет использоваться в качестве первичного ключа таблицы depreciation_strategy. create sequence itam.depreciation_strategy_seq start 1 increment 1; Эта команда, конечно же, создаст нам то, о чем мы просили. Но как связать последовательность с первичным ключом таблицы depreciation_strategy? Узнаем у Copilot. Листинг 5.4. C  opilot советует, как привязать последовательность к первичному ключу -- Вопрос: как сделать последовательность первичным ключом таблицы depreciation_strategy? -- Ответ: используйте следующую команду alter table itam.depreciation_strategy alter column "id" set default nextval('itam.depreciation_strategy_seq'::regclass); Завершая работу с этой таблицей, мы вставим в нее две статичные строки. Пока ограничимся двумя стратегиями амортизации: линейным методом и методом двойного уменьшаемого остатка. Листинг 5.5. Вставка статичных строк в таблицу depreciation_strategy 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'); Теперь займемся таблицей funding_details, в которой будем хранить информацию о финансировании активов, стоимости перепродажи и порядке утилизации по окончании срока службы. В целом мы выполним те же действия, что
   114 Глава 5. Управление данными с GitHub Copilot и Copilot Chat и с первой таблицей: все, кроме добавления статичных записей. Действительно: финансовые данные напрямую связаны с активами и будут добавляться в таб­ лицу ITAM-системой. Итак, определим таблицу, создадим последовательность и свяжем ее с первичным ключом. Листинг 5.6. Создание таблицы funding_details -- Создай в схеме itam таблицу под названием funding_details. Она должна состоять из следующих столбцов: id (int), name (varchar),depreciation_strategy_ id (int) и depreciation_rate (float). Первичным ключом должен быть столбец id. -- Столбец depreciation_strategy_id должен быть внешним ключом таблицы depreciation_strategy. -- Слово id в PostgreSQL зарезервировано и должно быть в кавычках create table itam.funding_details ( "id" int primary key, "name" varchar, "depreciation_strategy_id" int, "depreciation_rate" float ); -- Создай последовательность под названием funding_details_seq, значения которой начинаются с 1 и увеличиваются с шагом 1. Используй эту последовательность в качестве первичного ключа таблицы funding_details 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. Создание таблицы assets -- Создай в схеме itam таблицу под названием assets. Она должна состоять из следующих столбцов: -- id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_ id (int). -- Первичным ключом должен быть столбец id, столбец funding_details_id должен быть внешним ключом таблицы funding_details. -- Слово id в PostgreSQL зарезервировано и должно быть в кавычках create table itam.assets ( "id" int primary key, "name" varchar, "status" varchar, "category" varchar, "cost" float, "useful_life" int, "salvage_value" float,
   5.1. Сбор датасета 115 "purchase_date" date, "funding_details_id" int ); -- Создай последовательность под названием assets_id_seq, значения которой начинаются с 1 и увеличиваются с шагом 1. Используй эту последовательность в качестве первичного ключа таблицы assets. create sequence itam.assets_seq start 1 increment 1; alter table itam.assets alter column "id" set default nextval('itam.assets_seq'::regclass); Итак, все нужные таблицы созданы и можно приступить к их наполнению. Следующий промт — это подробное описание нужного нам датасета, которое Copilot, скорее всего, поможет составить. По крайней мере, попытается. Листинг 5.8. Формирование датасета для таблицы assets -- Сформируй датасет активов для ITAM-системы. В него должны входить следующие столбцы: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (int), salvage_value (float), purchase_date (date), funding_details_id (int). Набор должен состоять из 1000 строк с сортировкой по id. Каждая строка должна соответствовать следующим требованиям: -- - id: уникальное целое число, последовательность начинается с 1. -- - name: случайная строка длиной от 1 до 50 символов. -- - status: случайное значение из следующего списка: в работе, на хранении, утилизировано, в ремонте, в транспортировке, прочее. -- - category: случайное значение из следующего списка: аппаратное обеспечение, программное обеспечение, прочее. -- - cost: случайное дробное число от 0 до 100000. -- - useful_life: случайное целое число от 1 до 10. -- - salvage_value: случайное дробное число больше 0, но меньше значения cost. -- - purchase_date: случайная дата от 1/1/2019 до 12/31/2022. -- - funding_details_id: случайное целое число от 1 до 2. -- датасет необходимо сохранить в CSV-файле с именем assets.csv, который должен находиться в папке data. Файл должен иметь строку-заголовок и следующие столбцы: id (int), name (varchar), status (varchar), category (varchar), cost (float), useful_life (float), salvage_value (float), funding_details_id (int) Copilot предлагает нестандартное решение: использовать встроенные функции PostgreSQL. Что же, с учетом того, что нам нужно наполнить только одну базу данных, такой, пусть и одноразовый, подход вполне допустим: мы получим данные достаточного качества. Конечно, можно было бы поступить иначе: сгенерировать файл с данными при помощи программы на Python (а Copilot помог бы ее написать). Качество данных при этом бы выросло. Но они нужны нам лишь для примера, чтобы занять работой нашу ITAM-систему, а значит, сильно волноваться об этом нам ни к чему. Хотя в реальной жизни качество данных — всегда во главе угла.
   116 Глава 5. Управление данными с GitHub Copilot и Copilot Chat Листинг 5.9. Ответ Copilot: множественный оператор INSERT 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 'в работе' when random() < 0.4 then 'на хранении' when random() < 0.6 then 'утилизировано' when random() < 0.8 then 'в ремонте' when random() < 0.9 then 'в транспортировке' else 'прочее' end as status, case when random() < 0.5 then 'аппаратное обеспечение' when random() < 0.9 then 'программное обеспечение' else 'прочее' end as category, random() * 100000 as cost, (random() * 100)::int as useful_life, random() * (random() * 100000) as salvage_value, -- случайная дата от 1/1/2019 до 12/31/2022 -- так не работает, исправьте -- '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, он предложит альтернативное решение для формирования датасета: Python и несколько библиотек. Так, faker может генерировать фейковые данные, например распространенные английские имена,
   5.1. Сбор датасета 117 а numpy — случайные дробные числа (стоимость, срок службы, остаточная стоимость). При помощи pandas можно сформировать из этих значений двумерный массив DataFrame, то есть таблицу, которую можно сохранить в виде CSV-файла при помощи метода df.to_csv('assets.csv', index=False). Листинг 5.10. ChatGPT генерирует фейковый датасет при помощи Faker import pandas as pd import numpy as np import random from faker import Faker # Создание объекта Faker fake = Faker() # Допустимые состояния, категории, финансовые данные и стратегии амортизации valid_statuses = ['в работе', 'на хранении', 'утилизировано', 'в ремонте', 'в транспортировке', 'прочее'] valid_categories = ['аппаратное обеспечение'] valid_funding_details = ['приобретено'] valid_depreciation_strategies = ['линейный метод'] # Генерация данных 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)], } # Создание DataFrame df = pd.DataFrame(data) # Сортировка the DataFrame по id df = df.sort_values('id') # Вывод DataFrame на экран print(df) Для каждого из активов нам также понадобятся финансовые данные и стратегия амортизации. Поступим так же, как раньше: напишем аналогичный запрос для Copilot, а он ожидаемо выдаст нам нужный SQL-оператор. При этом нам нужно сделать так, чтобы для каждого актива с идентификатором 1-1000 в таблице была запись с финансовыми данными. Иначе во время работы у нас появятся ссылки, ведущие в никуда.
   118 Глава 5. Управление данными с GitHub Copilot и Copilot Chat Листинг 5.11. Формирование датасета для таблицы funding_details -- Сформируй датасет по финансированию для ITAM-системы. В него должны входить следующие столбцы: id (int), name (varchar), depreciation_strategy_id (int), depreciation_rate (float). Набор должен состоять из 1000 строк с сортировкой по id. -- Каждая строка должна соответствовать следующим требованиям: -- - id: уникальное целое число, последовательность начинается с 1. -- - name: случайная строка длиной от 1 до 10 символов. -- - depreciation_strategy_id: случайное целое число от 1 до 2. -- - depreciation_rate: случайное дробное число от 0 до 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. Но раньше, на этапе сборки (см. главу 4), мы удалили всю метаинформацию для SQLAlchemy, поэтому теперь нужно как-то иначе связать эту метаинформацию с адаптерами. Похоже, что мы достигли предела возможностей Copilot. И как же быть? Не расходиться же по домам? Настало время познакомиться с последним дополнением в линейке Copilot: Copilot Chat. Это модель GPT-4, которая встроена в IDE (пока что поддерживается только в Visual Studio Code). Откроем чат и спросим, как сохранить чистоту нашей бизнес-модели, используя при этом возможности SQLAlchemy. Ответ показан на рис. 5.1. По мнению Copilot Chat, нам нужен отдельный слой для доступа к данным, что очень удобно сочетается с моделью портов и адаптеров. Кроме того, ИИ рекомендует построить соответствующие классы по аналогии с классами домена, включив в них необходимую функциональность ORM. Полученный в результате код показан в листинге 5.12.
   5.1. Сбор датасета 119 Если параметры SQLAlchemy не должны фигурировать в модели предметной области, можно создать отдельный слой для доступа к данным (DAL), который возьмет на себя взаимодействие с базой данных. Благодаря этому модель останется независимой от конкретной реализации базы данных. Пример возможной реализации DAL на основе SQLAlchemy: Рис. 5.1. GitHub Copilot Chat предлагает решение нашей новой проблемы Листинг 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) классы модели созданы, нам нужно сопоставить эти экземпляры с моделью предметной области, прежде чем возвращать их в ядро системы. Все это может показаться излишним для такого несложного приложения, как наша ITAM-система. Но так модель станет более гибкой: к примеру, сможет выполнять сложные операции, выходящие за рамки обычных CRUD. Попытка сохранить идентичность между моделью предметной области и слоем доступа к данным ограничила бы нас только этими операциями. Теперь, при помощи Copilot и Copilot Chat, попробуем научить ITAM-систему реагировать на события. Это позволит ей мониторить ИТ-активы, к примеру, их местоположение, состояние и рыночную стоимость, в реальном времени.
   120 Глава 5. Управление данными с GitHub Copilot и Copilot Chat 5.2. Мониторинг активов в реальном времени при помощи Kafka Чтобы показать, как генеративный ИИ может работать в сочетании с событийноориентированной архитектурой, мы настроим мониторинг активов в реальном времени. Будем считать, что некая внешняя по отношению к системе ITAM система генерирует события, когда активы перемещаются с одного места на другое. Для работы с такими событиями нам потребуется настроить несколько дополнительных сервисов. В этом случае мы будем использовать Apache Kafka — распределенную платформу потоковой обработки данных, которая позволяет создавать системы реального времени и пайплайны для потоков данных. Kafka спроектирована так, чтобы принимать потоки данных из множества источников и доставлять их нескольким потребителям, — по сути, она выступает посредником для передачи данных в реальном времени. Для начала мы попросим Copilot Chat подсказать, как развернуть Kafka локально с помощью Docker. У Kafka есть репутация сложной в установке и настройке системы, но запуск в контейнерах Docker позволит нам обойти эти сложности. С помощью Copilot Chat мы получим файл Docker Compose, но как часто это бывает, в нем могут оказаться устаревшие версии, которые даже не поддерживаются на некоторых устройствах. В листинге 5.13 приведен обновленный вариант из официального репозитория Confluent (компания, которая предлагает коммерческую поддержку Kafka). Обратите внимание, что в этом Docker Compose-файле задействованы как Kafka, так и Zookeeper. Последний используется Kafka для координации брокеров в кластере — по крайней мере, пока. В будущих версиях Kafka планирует отказаться от зависимости от Zookeeper. Листинг 5.13. Файл Docker Compose для запуска Kafka и Zookeeper 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
   5.2. Мониторинг активов в реальном времени при помощи Kafka 121 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 сделать это: NC В написанной на Python ITAM-системе должен быть потребитель, подписанный на ­топик Kafka asset_location. Получив информацию о новом местоположении актива, он должен информировать об этом модель предметной области. Как это реализовать? В ответ Copilot Chat предоставит нам код, который выполняет все нужные операции: создает потребитель, слушает топик и обращается к AssetManager для обновления данных о местоположении активов. Листинг 5.14. М  ониторинг местоположения активов в реализации Copilot Chat from kafka import KafkaConsumer from asset_manager import AssetManager
   122 Глава 5. Управление данными с GitHub Copilot и Copilot Chat 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 и адаптером нарушит гексагональную архитектуру модели. Узнаем у Copilot Chat, что он думает по этому поводу: NC Наш проект основан на гексагональной архитектуре. Не нарушает ли ее осведомленность адаптера об AssetManager? Судя по ответу (рис. 5.2), Copilot Chat согласен с нами: адаптер нарушил свои контрактные обязательства. Можно расширить AssetManager, добавив в него порт для связи с Kafka. Но может, у Copilot Chat есть идеи получше? NC Прямое взаимодействие потребителя Kafka и AssetManager недопустимо. Как можно обойти эту проблему? Copilot Chat предлагает использовать паттерн проектирования «Посредник» (Mediator), нечто похожее на пару порт и адаптер. Паттерн проектирования «Посредник» Посредник (Mediator) — это поведенческий паттерн, который способствует слабой связaнности (coupling) между объектами, инкапсулируя их взаимодействие внутри объекта-посредника. Посредник выступает в роли централизованного хаба, который управляет обменом сообщениями между объектами, изолируя их друг от друга, — таким образом, объекты взаимодействуют не напрямую, а через посредника, ничего не зная о реализации друг друга. Рассмотрим предложенный Copilot Chat класс Mediator, который будет посредником между AssetManager и потребителем Kafka (листинг 5.15).
   5.2. Мониторинг активов в реальном времени при помощи Kafka 123 В гексагональной архитектуре адаптеры отвечают за передачу внешних данных от внешних систем модели предметной области и в противоположном направлении. Эта модель не должна зависеть от адаптеров, но адаптеры могут зависеть от нее. В случае с адаптером Kafka, который принимает сообщения из топика asset_ location и сообщает модели предметной области о новом местоположении активов, зависимость адаптера от AssetManager оправданна. Адаптер преобразует сообщение Kafka в событие, которое может обработать AssetManager, а затем вызывает соответствующий метод этого класса, что и приводит к обновлению данных. Однако важно обеспечить, чтобы адаптер не был жестко связан с классом AssetManager. Один из способов добиться этого — определить интерфейс или протокол в доменном слое, от которого адаптер сможет зависеть, вместо прямой зависимости от AssetManager. Интерфейс или протокол должен определять методы, которые адаптер может вызвать для обновления модели предметной области, а класс AssetManager реализует этот интерфейс или протокол. Такой подход позволит модели предметной области остаться независимой от адаптеров, которые смогут взаимодействовать с ней контролируемым, четко определенным образом. Рис. 5.2. Copilot Chat соглашается с тем, что решение не идеально Листинг 5.15. К  ласс 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) Напоминает очередь или источник сообщений, и это именно то, что нужно: мы разрываем связь.
   124 Глава 5. Управление данными с GitHub Copilot и Copilot Chat ПРИМЕЧАНИЕ Внимательный читатель, должно быть, уже заметил, что мы немного спешим и не уделяем достаточно внимания различиям между классом Asset и активом Hardware. В исходной модели местоположение было только у аппаратного обеспечения. Конечно же, в общем смысле можно сказать, что программное обеспечение тоже не висит в пустоте: оно установлено на каком-то компьютере. Хотя с этим можно поспорить. По ходу работы мы действительно несколько упрощаем модель предметной области: полиморфные структуры на уровне базы данных — очень сложная тема. Определившись с тем, как отделить потребитель Kafka от AssetManager, мы можем обновить полученный ранее код: передать ссылку на класс-посредник в конструктор класса потребителя. Благодаря этому и AssetManager и потребитель будут иметь доступ к одному и тому же экземпляру посредника, что обеспечит свободную передачу сообщений между ними (точнее, в данном случае поток будет однонаправленным). Следует отметить, что сообщения ­будут передаваться в формате JSON, а значит, посредник должен его понимать. Листинг 5.16. Внедрение посредника в потребитель 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("Поиск обновленных данных о местоположении объектов") 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, чтобы он мог работать с полученными данными.
   5.2. Мониторинг активов в реальном времени при помощи Kafka 125 ПРИМЕЧАНИЕ Реализация новых функций нашей системы потребует масштабных обновлений. Придется переделать не только AssetManager, но и SQLAlchemyAssetRepository и Asset, а также создать новую таблицу asset_locations в базе данных itam. Полностью обновленный исходный код можно найти на веб-сайте книги (www.manning.com/books/ ai-powered-developer), а также в репозитории GitHub (https://github.com/nathanbcrocker/ ai_assisted_dev_public). Сейчас же обсудим лишь изменения, которые нужны для работы с сообщениями, оставив остальные для самостоятельного изучения. На рис. 5.3 показано, что нужно изменить в AssetManager. AssetManager -BaseRepository[Asset] _repository -AssetLocationMediator mediator +__init__(base_repository: BaseRepository[Asset), mediator: AssetlocationMediator): None +update_asset_location( event: AssetlocationUpdated): None Рис. 5.3. Новые параметры конструктора и методы класса AssetManager Всего изменений будет два. Во-первых, в конструктор нужно передать объект AssetLocationMediator (посредник для местоположений активов), в котором зарегистрировать обработчик события AssetLocationUpdated (местоположение актива обновлено). Во-вторых, добавить метод update_asset_location (обновить местоположения актива), который и будет обрабатывать это событие. Сокращенный код приведен ниже. Листинг 5.17. О  бновленный конструктор и новый метод класса 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}") Метод add_location (добавить местоположение) класса Asset просто добавляет новое местоположение в конец соответствующего списка (можно сказать, истории перемещения). В более сложной реализации можно было бы завести
   126 Глава 5. Управление данными с GitHub Copilot и Copilot Chat атрибут current_location (текущее местоположение) со ссылкой на отдельно организованный архив местоположений. Но сейчас наша цель — поток сообщений через систему, а потому не будем усложнять. Остался последний штрих: нужно создать топик. Как мы это сделаем? Хороший вопрос. К счастью, все, что для этого нужно, уже есть в нашем контейнере Docker. Итак, войдем в наш экземпляр Kafka 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 Параметры partitions и replication-factor, указанные при создании, сообщают Kafka, что мы хотим одну партицию и фактор репликации, равный 1. В боевых условиях или для чего-то более серьезного, чем тестирование, мы бы наверняка задали более высокие значения — это необходимо для обеспечения доступности и отказоустойчивости данных. В табл. 5.1 приведены некоторые часто используемые команды 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
   5.2. Мониторинг активов в реальном времени при помощи Kafka 127 Теперь начинается самое интересное — наблюдение за работой приложения в действии. Kafka предоставляет консольный продюсер (console-producer), с помощью которого можно публиковать сообщения прямо из терминала (через стандартный ввод). Для запуска используйте следующую команду: kafka-console-producer --broker-list localhost:9092 --topic asset_location По этой команде открывается интерактивный сеанс для ввода сообщений. Давайте отправим несколько сообщений, имитирующих перемещение актива в окрестностях Чикаго. Листинг 5.18. Сообщения, отправленные через Kafka Console Producer {"asset_id": "timestamp": {"asset_id": "timestamp": {"asset_id": "timestamp": {"asset_id": "timestamp": {"asset_id": "timestamp": 1, "latitude": 41.8781, "2022-01-01T00:00:00Z"} 1, "latitude": 41.9000, "2022-01-01T00:10:00Z"} 1, "latitude": 41.8676, "2022-01-01T00:20:00Z"} 1, "latitude": 41.8788, "2022-01-01T00:30:00Z"} 1, "latitude": 41.8740, "2022-01-01T00:40:00Z"} "longitude": -87.6298, "longitude": -87.6244, "longitude": -87.6270, "longitude": -87.6359, "longitude": -87.6298, По мере ввода сообщений будут поступать отклики от ITAM-системы, фиксирующей изменения параметров актива. Удаление топика Для полноты картины упомянем еще об одной полезной команде. Ошибка при вводе, а значит неверное сообщение, может вызвать сбой в работе потребителя. И в этом случае есть только один выход: удалить топик. Звучит радикально, но решает проблему. А вот и команда для этого: kafka-topics --delete --topic asset_location --bootstrap-server localhost:9092 В этом разделе наша система пополнилась новой полезной возможностью: с помощью Apache Kafka мы научили ее отслеживать перемещения активов в режиме реального времени. В последнем разделе данной главы мы снова расширим возможности системы. Теперь она будет не только фиксировать местоположение активов, но и следить за тем, чтобы они оставались там, где нужно. Для этого мы попробуем объединить Spark и Kafka, чтобы анализировать данные в реальном времени. Если все получится, информационная безопасность скажет нам спасибо, ведь значительная часть ключевых бизнес-процессов и интеллектуальной собственности компании завязана на этих активах.
   128 Глава 5. Управление данными с GitHub Copilot и Copilot Chat 5.3. Анализ, обучение и отслеживание с Apache Spark Отслеживание активов в реальном времени — это критически важная функция для бизнеса. ИТ-активы содержат конфиденциальные данные: списки клиентов, информацию о продажах, отчеты о прибылях и убытках, прогнозы, стратегии… Потеря одного такого актива может стать для компании катастрофой. Поэтому надежное управление и мониторинг — главный приоритет для специалистов по информационной безопасности. В этом разделе мы постараемся упростить им жизнь. Современные платформы обработки данных позволяют без особых усилий отслеживать активы в реальном времени и отправлять уведомления, если возникает что-то подозрительное. Посмотрим, как это работает. Apache Spark — это мощный движок с открытым исходным кодом, предназначенный для обработки данных и ориентированный на скорость, простоту применения и сложную аналитику. Он был создан как более эффективная альтернатива MapReduce для работы с большими объемами данных и поддерживает как пакетную, так и потоковую обработку. В составе механизма есть API с поддержкой Scala, Java, Python и R, а также встроенный модуль для SQL-запросов. Базовая структура данных Spark представляет собой устойчивый распределенный датасет (RDD, resilient distributed dataset), что обеспечивает отказоустойчивость и позволяет обрабатывать данные параллельно в кластере. Возможности Spark можно расширить за счет библиотек, в том числе MLlib для машинного обучения, Spark Streaming для работы с потоками данных в реальном времени, а также Spark SQL и DataFrames для обработки структурированных данных. Благодаря этим инструментам Spark хорошо подходит для решения самых разных задач — от машинного обучения до потоковой передачи и пакетной обработки данных. Поскольку для этих операций активно используется оперативная память, Spark работает гораздо быстрее, чем его предшественники, и поэтому он популярен в мире Big Data. Спросим у Copilot Chat, как лучше всего использовать Spark для отслеживания активов: NC Допустим, что наша компания находится в Чикаго. Нужно создать класс под названием AssetLocationSparkAdapter, который будет непрерывно получать из топика Kafka с именем asset_location сообщения AssetLocation в формате JSON со следующими атрибутами: asset_id: int, latitude: float, longitude: float и timestamp: datetime. Для хранения сообщений используется таблица itam.asset_locations в базе данных PostgreSQL. Адаптер AssetLocationSparkAdapter должен следить за тем, чтобы актив оставался в радиусе 40 километров от Чикаго, а в случае выхода за границу этой зоны выводил сообщение на консоль. Нужно использовать последнюю версию Spark. Как лучше всего это сделать? Полученный в ответ код нужно поместить в файл asset_location_spark_adapter. py в пакете infrastructure. Попробуем разобраться в нем по комментариям, которые Copilot Chat добавляет практически к каждой строке.
   5.3. Анализ, обучение и отслеживание с Apache Spark 129 Все начинается с импорта библиотек, включая Spark и geopy. Листинг 5.19. Импорт библиотек, необходимых для работы 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, а также запускается локальный экземпляр Spark, который подключается к топику Kafka и непрерывно передает сообщения оттуда. ПРИМЕЧАНИЕ Очевидно, что AssetLocationSparkAdapter — блокирующий процесс, а значит, приложение FastAPI не отработает, пока он не будет завершен. Поэтому нужно либо вынести Spark в отдельный процесс, либо использовать асинхронный фреймворк, чтобы обеспечить одновременное выполнение двух процессов. Листинг 5.20. А  даптер AssessLocationSparkAdapter обрабатывает топик Kafka class AssetLocationSparkAdapter: def __init__(self): # Определение схемы для входных данных в формате JSON self.schema = StructType([ StructField("asset_id", IntegerType()), StructField("latitude", DoubleType()), StructField("longitude", DoubleType()), StructField("timestamp", TimestampType()) ]) # Открытие сеанса SparkSession self.spark = SparkSession.builder \ .appName("AssetLocationSparkAdapter") \ .getOrCreate() # Создание DataFrame для чтения потока сообщений из топика asset_location 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)") # Парсинг входных данных в формате JSON self.parsed_stream = self.df \ .select(from_json(col("value"), self.schema).alias("data")) \ .select("data.*")
   130 Глава 5. Управление данными с GitHub Copilot и Copilot Chat В оставшейся части конструктора выполняется расчет расстояния между активом и центром Чикаго. Если оно превышает 40 километров, на консоль выводится сообщение. Кроме конструктора в классе AssetLocationSparkAdapter есть два метода для запуска и останова адаптера. Листинг 5.21. Расчет расстояния от актива до центра Чикаго # Расчет расстояния от текущего местоположения актива до Чикаго 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") > 40) # Вывод результатов на консоль self.query = self.distance \ .writeStream \ .outputMode("append") \ .format("console") \ .start() def run(self): # Запуск обработки запросов self.query.awaitTermination() def stop(self): # Останов обработки запросов и SparkSession self.query.stop() self.spark.stop() Расчет расстояния от актива до Чикаго осуществляется функцией calculate_ distance (вычислить расстояние), на вход которой поступает две пары координат (широта и долгота). Сами вычисления выполняются при помощи geopy.distance. Листинг 5.22. Функция для расчета расстояния от актива до Чикаго def calculate_distance(lat1, lon1, lat2, lon2): return distance((lat1, lon1), (lat2, lon2)).km К сожалению, в созданный Copilot Chat код закралась ошибка, из-за которой его локальный запуск был невозможен. Пришлось порыться на Stack Overflow. Как оказалось, ИИ забыл объявить переменную окружения, а также зарегистрировать пользовательскую функцию (UDF) — calculate_distance. Вам не придется искать решение: все, что нужно, уже приведено в следующем листинге. Листинг 5.23. Правки для локального запуска os.environ['PYSPARK_SUBMIT_ARGS'] = \ '--packages org.apache.spark: spark-streaming-kafka-0-10_2.12:3.2.0,
   Итоги 131 org.apache.spark: spark-sql-kafka-0-10_2.12:3.2.0 pyspark-shell' class AssetLocationSparkAdapter: def __init__(self): # Открытие сеанса SparkSession self.spark = SparkSession.builder \ .appName("AssetLocationSparkAdapter") \ .getOrCreate() self.spark.udf.register("calculate_distance", calculate_distance) Наконец, для запуска приложения Spark нужно добавить несколько строк в функцию main из файла main.py. Листинг 5.24. Обновление функции main if __name__ == "__main__": adapter = AssetLocationSparkAdapter() adapter.run() Можно проверять. Вводите сообщения о местоположении актива в топик Kafka при помощи console-producer. Как только расстояние превысит 40 километров от центра Чикаго, появится сообщение. Несложно доработать систему так, чтобы оповещение осуществлялось не через консоль, а, скажем, по SMS (используем SMS API Twilio) или же электронной почте (используем SendGrid). Листинг 5.25. Сообщение в консоли о местоположении актива +--------+--------------------+-------------------+ |asset_id| timestamp| distance| +--------+--------------------+-------------------+ | 1| 2021-12-31 20:30:00| 712.8314662207446| +--------+--------------------+-------------------+ Мои поздравления! Теперь наша система умеет отслеживать местоположение активов в реальном времени. И если какой-нибудь ноутбук вдруг решит убежать, об этом сразу узнают те, кому следует. Итоги GitHub Copilot Chat — это инновационный инструмент, объединивший языковые возможности ChatGPT с удобными возможностями Copilot. Это важный шаг вперед в сфере ассистентов для программирования: инструмент помогает получать контекстные и детализированные предложения в реальном времени, делая написание кода более быстрым и эффективным. Посредник — поведенческий паттерн проектирования, который помогает сохранять высокоуровневое разделение между объектами, тем самым повышая модульность кода. Он инкапсулирует взаимодействие между ними
   132 Глава 5. Управление данными с GitHub Copilot и Copilot Chat в отдельный объект-посредник, что снижает связанность, улучшает модульность кода и облегчает его переиспользование и изменение. Apache Kafka — мощная распределенная стриминговая платформа, предназначенная для создания пайплайнов передачи данных и потоковых приложений, работающих в реальном времени. При помощи этой платформы можно эффективно обрабатывать потоки данных из нескольких источников и доставлять информацию нескольким потребителям, что делает ее идеальным решением для обработки больших объемов данных в реальном или близком к реальному времени. Важно помнить, что Kafka оптимизирована для работы с неизменяемыми данными и не поддерживает обновление и удаление записей, а также сложные запросы. Apache Spark — высокопроизводительный движок распределенной обработки данных, известный своей скоростью, простотой использования и продвинутыми возможностями аналитики. Идеально подходит для работы в реальном времени, а также для анализа больших массивов данных. Но для более простых задач, к примеру базовой аналитики и простых агрегаций, традиционная реляционная база данных может быть более подходящим выбором. Несмотря на свое быстрое развитие, генеративный ИИ нельзя считать непогрешимым. Нужно тщательно проверять все полученные ответы на соответствие конкретным требованиям и стандартам качества. Генеративный ИИ не заменит глубокие знания о предметной области или опыт в программировании, но может значительно повысить производительность за счет полезных советов и выполнения рутинных задач.
Часть 3 Оценка и улучшение В части 3 мы поговорим о важности тестирования, оценки качества и объяснения кода в разработке с использованием ИИ. Здесь основной акцент сделан на надежность и устойчивость программного обеспечения, созданного с помощью больших языковых моделей. Рассмотрим поиск ошибок, анализ кода и его перевод, а также объясним, почему тщательное тестирование и контроль качества — ключевые элементы успешной разработки. ИИ помогает разработчикам разбираться в сгенерированном коде и объяснять его другим, что упрощает совместную работу в команде. В этой части книги мы узнаем, как поддерживать высокий уровень качества кода и какие стратегии помогут эффективно использовать ИИ в процессе разработки.
6 Тестирование, оценка и описание кода при помощи LLM В этой главе 3 3 Подготовка юнит-тестов 3 3 Генерация интеграционных тестов 3 3 Оценка качества и покрытия кода 3 3 Оценка сложности ПО 3 3 Перевод кода и текста В этой главе мы рассмотрим один из важнейших аспектов разработки: тестирование кода. Тесты решают сразу несколько задач. Прежде всего они помогают выявить ошибки и проблемы, которые могут повлиять на функциональность, UX или производительность приложения. Проводя тщательное тестирование, мы можем установить, соответствует ли ПО заданным требованиям, функционирует ли так, как задумано, приносит ли ожидаемые результаты. Благодаря комплексному тестированию, разработчики могут оценить надежность, точность, эффективность, безопасность и совместимость ПО на разных платформах и в разных средах. Обнаружение и устранение дефектов ПО на ранних этапах разработки позволяет сберечь много времени и средств. Закончив разговор о тестировании, мы перейдем к оценке качества кода и рассмотрим несколько метрик, которые помогают оценить его качество и сложность.
   6.1. Три типа тестов 135 А если нам будет нужно понять назначение каких-то фрагментов кода (к примеру, если мы видим его впервые), мы можем попросить ИИ предоставить подробные объяснения. 6.1. Три типа тестов Тестирование очень важно в разработке ПО. Поэтому мы подробно рассмотрим различные виды тестов: юнит-тесты, интеграционные и поведенческие. Начнем с того, что попросим Copilot Chat создать для нас модульный тест, или юниттест. ОПРЕДЕЛЕНИЕ Юнит-тесты предназначены для проверки отдельных компонентов и блоков кода и позволяют убедиться в том, что они работают правильно сами по себе, вне связи с другими элементами. Разработчики обычно запускают юнит-тесты, чтобы выявить ошибки и проблемы в конкретных компонентах ПО. 6.1.1. Юнит-тестирование В этом разделе мы займемся созданием юнит-тестов для проверки компонентов нашего ПО. В Python есть несколько фреймворков для юнит-тестирования. Каждый из них имеет свои особенности и подходит для разных сценариев. Мы кратко рассмотрим их, прежде чем выбрать подходящий, — основываясь на рекомендации нашего ИИ-инструмента. Начнем с unittest, стандартной библиотеки Python для разработки юнит-тестов. Она поставляется вместе с Python и не требует отдельной установки. unittest предлагает богатый набор утверждений (assertions) и хорошо подходит как для простых, так и для более сложных тестов. Но код с ним может получиться довольно многословным. Этот вариант хорош, если вы хотите обойтись без сторонних зависимостей и просто проверить, работают ли отдельные участки кода изолированно от остальной системы. Затем мы рассмотрим pytest, популярную стороннюю библиотеку, которую можно использовать для юнит-тестирования, хотя ее возможности гораздо шире. Она требует меньше шаблонного кода, чем unittest, и предлагает мощные возможности: фикстуры (fixtures) для подготовки и очистки среды, параметризованные тесты, а также поддержку уже написанных тестов на unittest и nose. Библиотека pytest отлично подходит как для простых, так и для сложных сценариев. Если вы цените простоту и удобство использования, а ваш проект не ограничен только стандартным набором библиотек Python, pytest станет отличным выбором. Еще одна библиотека — nose2, преемница устаревшей библиотеки nose. Она расширяет возможности unittest и упрощает тестирование, может автоматически
   136 Глава 6. Тестирование, оценка и описание кода при помощи LLM находить и выполнять имеющиеся в проекте тесты, что избавляет от необходимости указывать их вручную. Поэтому nose2 особенно хороша для больших проектов, поскольку автоматическое обнаружение тестов снижает затраты времени. Как и pytest, nose2 позволяет выполнять тестовые наборы unittest, а потому прекрасно подойдет для тех, кто хочет перейти от unittest на более мощный инструмент, но сохранить уже написанные тесты. И наконец, doctest. Это модуль, входящий в стандартную библиотеку Python. Он не является полнофункциональным фреймворком для тестирования, как перечисленные выше, но позволяет писать тесты прямо в строках документации. doctest лучше всего подходит для простых случаев, когда нужно показать, как пользоваться функцией или модулем, а заодно и подтвердить, что код работает так, как ожидается. Кроме того, это отличный способ убедиться в том, что документация соответствует коду. Copilot Chat рекомендует использовать unittest, и мы последуем этому совету. Попробуйте спросить его сами — быть может, он посоветует что-то другое. Возможно, выбор зависит от формулировки промта. Для начала мы создадим тестовые случаи, определив классы, наследующие unittest.TestCase. И уже при написании первого теста мы неожиданно наткнемся на довольно неприятный баг, который раньше ускользал от нашего внимания. До него мы скоро доберемся. Начнем, как всегда, с промта: попросим Copilot Chat разработать юнит-тест для класса DepartmentStatisticsVisitor. Ответ напомнит нам о важности контекста: в новом сеансе чата нет ничего об объекте проверки, поэтому Copilot Chat выдаст какой-то код, в котором не будет ни одного из созданных нами классов. Листинг 6.1. Первая попытка определить юнит-тест 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 дополнительной информацией, чтобы он смог уточнить тестовый случай. Расскажем ему о классе-посетителе:
   6.1. Три типа тестов NC 137 Класс DepartmentStatisticsVisitor используется для обхода данных и вычисления общей суммы амортизации активов на основе их распределения по департаментам. Теперь Copilot Chat лучше понимает, что нужно тестировать. Листинг 6.2. Результат промта про юнит-тест 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 неверно, поэтому нужно показать ИИ правильное. Copilot Chat продолжит работу. Листинг 6.3. С  ледующая попытка создать юнит-тест (после уточнения насчет Asset) 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):
   138 Глава 6. Тестирование, оценка и описание кода при помощи LLM location1 = Location('Офис', 'Главная ул., 123') location2 = Location('Склад', 'Ул. Вязов, 456) asset = Asset(1, 'Компьютер', 'В работе', 'Аппаратное обеспечение', 1000.0, 3, 100.0, date.today(), [location1, location2]) self.assertEqual(asset.id, 1) self.assertEqual(asset.name, 'Компьютер') self.assertEqual(asset.status, 'В работе') self.assertEqual(asset.category, 'Аппаратное обеспечение') 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 Chat, что в классе Asset есть атрибут, значениями которого являются экземпляры класса FundingDetails, а также информацию о нем. Листинг 6.4. Р  езультат третьего промта про юнит-тест. Результат еще лучше 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('Офис', 'Главная ул., 123') location2 = Location('Склад', 'Ул. Вязов, 456) asset1 = Asset(1, 'Компьютер', 'В работе', 'Аппаратное обеспечение', 1000.0, 3, 100.0, date.today(), [location1, location2]) asset2 = Asset(2, 'Принтер', 'В работе', 'Аппаратное обеспечение', 500.0, 2, 50.0, date.today(), [location1]) funding_details1 = FundingDetails(asset1, 'Линейный метод', 0.33, {'ИТ': 0.5, 'Кадров': 0.5}) funding_details2 = FundingDetails(asset2, 'Метод двойного уменьшаемого остатка', 0.25, {'ИТ': 0.3, 'Кадров': 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) Осталось лишь вручную подправить раздел импорта.
   6.1. Три типа тестов 139 ПРИМЕЧАНИЕ Этот пример — предостережение о том, как сложно бывает заставить большие языковые модели вести себя предсказуемо при выполнении узкоспециализированных задач. Иногда усилия по настройке промтов оказываются неоправданно трудоемкими. В реальной жизни никто не осудит вас, если вы просто откажетесь от промт-инжиниринга и напишете тест вручную. Впрочем, немного упорства — и в вашем распоряжении окажется набор шаблонов, на базе которого можно создать тестовый набор для любого похожего класса. Еще одна особенность Copilot Chat заключается в том, что, генерируя тесты в ответ на промт типа «Сделай юнит-тест для этого кода», он будет вставлять моки вместо любых классов, определения которых не сможет найти. Ценность такого теста может оказаться сомнительной. И наконец, еще одна проблема, характерная для Python: Copilot Chat часто неправильно делает отступы. Попытка выполнить этот тест показывает, что между посетителем, активом, финансовыми данными и амортизационной политикой образовалась циклическая зависимость — ситуация, когда несколько модулей или компонентов прямо или косвенно зависят друг от друга. В нашем случае при создании класса Asset Python загрузит определение FundingDetails. Чтобы это исправить, нужно отказаться от прямого создания экземпляра или ссылки на класс FundingDetails. Листинг 6.5. О  бновленный класс 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.6. О  бновленный класс 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'
   140 Глава 6. Тестирование, оценка и описание кода при помощи LLM Итак, при помощи Copilot Chat нам удалось создать юнит-тест, хотя скорее всего, мы бы потратили меньше усилий, если бы сделали все самостоятельно. ИИ может дать отличные рекомендации, рассказать о том, как и когда тестировать код, но реализация тестов (по крайней мере, на данный момент) оставляет желать лучшего. В реальном мире нам следовало бы продолжить работу: добавить новые тесты, так, чтобы их стало достаточно много. Вы спросите: достаточно — это сколько? Поговорим об этом чуть позже. Сейчас же перейдем к интеграционным тестам. ОПРЕДЕЛЕНИЕ Интеграционное тестирование предназначено для проверки взаимодействия между разными компонентами и модулями ПО и проводится в целях обеспечения их надежной совместной работы. При тестировании проверяется правильность работы встроенных функций системы, выявляются несоответствия и проблемы со связью между модулями. 6.1.2. Интеграционное тестирование В этом разделе мы создадим интеграционный тест, который позволит нам протестировать всю систему. К счастью, в состав FastAPI входит собственный инструмент для тестирования, которым мы и воспользуемся. Начнем с того, что скопируем определение класса AssetController в чат Copilot, а затем спросим у него, как написать интеграционный тест для этого контроллера. Поскольку в определении класса указаны маршруты, ИИ должен сгенерировать подходящий код. Важно указать, что в тесте нужно использовать класс InMemoryAssetRepository: либо уточнить это в промте сразу, либо поправить вручную уже после генерации теста. Листинг 6.7. П  редложенный Copilot Chat интеграционный тест для AssetController 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(): # Cоздание экземпляра InMemoryAssetRepository asset_repository = InMemoryAssetRepository() # Добавление активов в репозиторий asset_repository.create(Asset(name="Актив 1", unit_cost=1000)) asset_repository.create(Asset(name="Актив 2", unit_cost=2000)) # Создание экземпляра AssetRestPort с использованием MemoryAssetRepository asset_rest_port = AssetRestPort(asset_repository)
   6.1. Три типа тестов 141 # Создание экземпляра AssetController с использованием AssetRestPort asset_controller = AssetController(asset_rest_port) # Создание экземпляра TestClient на основе маршрутизатора AssetController client = TestClient(asset_controller.get_router()) # Отправка GET-запроса на конечную точку /assets response = client.get("/assets") # Проверка кода состояния ответа: он должен быть равен 200 response.status_code == 200 # Проверка тела ответа: в нем должны быть ожидаемые активы response.json() == [ {"id": 1, "name": "Актив 1", "unit_cost": 1000}, {"id": 2, "name": "Актив 2", "unit_cost": 2000}, Теперь рассмотрим поведенческое тестирование. ОПРЕДЕЛЕНИЕ Поведенческое тестирование позволяет проверить работу системы в целом, с точки зрения конечного пользователя. Обычно оно применяется для оценки функциональности системы, а также ее соответствия требованиям и спецификациям. 6.1.3. Поведенческое тестирование В некотором смысле любое тестирование — это проверка поведения системы, ведь цель тестов — убедиться, что все работает как надо. Но поведенческое тестирование все же отличается от других видов и имеет свои особенности. Подведем краткий итог всем типам тестов, с которыми мы уже столкнулись, и сравним их с поведенческими. Юнит-тестирование направлено на проверку отдельных модулей или компонентов системы в отрыве от других ее частей. Обычно это автоматизированные тесты, которые проверяют, как конкретная функция или метод ведут себя в разных условиях и возвращают ли ожидаемый результат. Интеграционное тестирование — проверка взаимодействия разных компонентов системы. Цель — убедиться, что модули корректно работают друг с другом. Тесты могут быть как автоматическими, так и ручными, но фокус всегда на интерфейсах между частями системы, а не на всей системе целиком. Поведенческое тестирование фокусируется на проверке работы ПО при типовых сценариях использования, которые рассматриваются и во время разработки. Эти сценарии оформляются по шаблону Given–When–Then (GWT): Given — предусловие (например, «пользователь вошел в систему»); When — действие («он нажал на кнопку»); Then — ожидаемый результат («появилось всплывающее окно»).
   142 Глава 6. Тестирование, оценка и описание кода при помощи LLM При тестировании, как правило, выясняется, что некоторые случаи или компоненты сложно настроить для проверки. Кроме того, затруднения могут возникнуть при попытке изолировать поведение конкретного объекта или модуля, а также проверить взаимодействие объектов друг с другом. Чтобы решить эти проблемы, используются mock-ообъекты (моки). ОПРЕДЕЛЕНИЕ Мок — это тестовый дублер, который имитирует поведение реального объекта, но в контролируемой среде. Он используется в тестах, чтобы воспроизводить различные сценарии, в том числе ошибки или пограничные случаи, которые сложно воспроизвести с настоящими объектами. Например, при вызове соответствующих методов моки могут возвращать указанные значения или же вызывать исключения, а также протоколировать эти вызовы на случай, если потребуется проверить правильность переданных параметров. Моки создаются вручную при помощи специальных библиотек, например unittest.mock или pytest-mock, в которых есть все необходимые классы. Мы упомянем моки в промте, где просим создать поведенческий тест для класса AssetManager: NC У нас есть класс AssetManager . Его конструктор принимает два параметра: BaseRepository и AssetLocationMediator . BaseRepository — это репозиторий, в котором хранятся активы. Мы хотим использовать InMemoryAssetRepository — подкласс BaseRepository, который сохраняет активы в памяти. Для AssetLocationMediator мы будем использовать мок-объект. AssetManager сохраняет активы через метод create, а получить все активы можно с помощью метода get_assets. У актива есть 10 обязательных позиционных аргументов: id, name, status, category, cost, useful_life, salvage_value, purchase_date, locations и funding_details. Следует использовать именованные параметры, чтобы было понятно, какой аргумент чему соответствует. Создай BDD-скрипт (поведенческий тест), который добавляет два актива в AssetManager и проверяет, что стоимость всех активов равна $4000. Все должно быть реализовано на Python. Пиши поэтапно, указывая, какие библиотеки и инструменты будешь использовать. Copilot Chat сообщает, что нужно создать новый файл (так называемый featureфайл) с именем asset_manager.feature и определить в нем атрибуты класса AssetManager (и всех остальных вспомогательных классов). Ниже приведен пример такого файла, в котором описано добавление в AssetManager двух активов суммарной стоимостью 4000 долларов, а также мок для объекта AssetLocationMediator. Листинг 6.8. Ф  айл asset_manager.feature, созданный по рекомендации Copilot Chat Feature: Asset Manager Как менеджеру по ИТ-активам, Мне нужно иметь возможность управлять ИТ-активами
   6.1. Три типа тестов 143 И отслеживать все ИТ-активы в моей организации Scenario: Добавление двух активов в Asset Manager Given Asset Manager запущен And InMemoryAssetRepository инициализирован And для AssetLocationMediator используется заглушка When я создаю актив стоимостью 2000 долларов And я создаю еще один актив стоимостью 2000 долларов Then общая стоимость всех активов должна составлять 4000 долларов Этот файл следует поместить в новую папку features, которую нужно создать в корневой папке проекта. Рядом с файлом должна быть подпапка steps, а в ней — еще один файл: steps.py. В нем будет описана реализация этапов работы по сценарию из feature-файла. Для этого надо импортировать классы AssetManager, InMemoryAssetRepository, AssetLocationMediator и Asset, а также определить шесть функций, по одной на каждый этап сценария. Они будут использоваться объектом контекста для сохранения и получения данных между этапами. Мок для AssetLocationMediator должен быть определен на основе класса Mock из библиотеки unittest.mock. Для ясности при создании экземпляров класса 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('Asset Manager запущен') 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('InMemoryAssetRepository инициализирован') def step_impl(context): pass @given('для AssetLocationMediator используется заглушка') def step_impl(context):
   144 Глава 6. Тестирование, оценка и описание кода при помощи LLM pass @when('я создаю актив стоимостью {cost} долларов') def step_impl(context, cost): asset = Asset(id=1, name='Пробный актив 1', status='Доступен', category='Пробная категория', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Пробное местоположение'], funding_details={'Пробные финансовые данные': 1000}) context.asset_manager.create(asset) @when('я создаю еще один актив стоимостью {cost} долларов') def step_impl(context, cost): asset = Asset(id=2, name='Пробный актив 2', status='Доступен', category='Пробная категория', cost=float(cost), useful_life=5, salvage_value=0, purchase_date='2022-01-01', locations=['Пробное местоположение'], funding_details={'Пробные финансовые данные': 1000}) context.asset_manager.create(asset) @then('общая стоимость всех активов должна составлять {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) Установите библиотеку behave (если у вас ее еще нет) при помощи команды: pip install behave Добавьте эту команду в файл requirements.txt. Позднее, когда мы будем развертывать нашу систему, записи в этом файле гарантируют наличие всех нужных библиотек. Теперь мы можем провести поведенческое тестирование системы. Для этого нужно перейти в корневую папку проекта и выполнить команду. Листинг 6.10. Запуск поведенческого тестирования и его результаты % behave features Feature: Asset Manager # features/asset_manager.feature:1 Как менеджеру по ИТ-активам, Мне нужно иметь возможность управлять ИТ-активами И отслеживать все ИТ-активы в моей организации Scenario: Добавление двух активов в Asset Manager # features/asset_manager.feature:6 Given Asset Manager запущен # features/steps/steps.py:8 0.000s And InMemoryAssetRepository инициализирован # features/steps/steps.py:14 0.000s And для AssetLocationMediator используется заглушка
   6.2. Оценка качества 145 # features/steps/steps.py:18 0.000s When я создаю актив стоимостью 2000.00 долларов # features/steps/steps.py:22 0.000s And я создаю еще один актив стоимостью 2000.00 долларов # features/steps/steps.py:27 0.000s Then общая стоимость всех активов должна составлять 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.001s1 В этом разделе мы заложили основы для разработки качественного ПО: провели юнит-, интеграционное и поведенческое тестирование. Кто-то может возразить, что все это случилось на очень позднем этапе разработки, и будет прав: в реальном мире мы пишем тесты по мере разработки кода. Кто-то опять возразит, что нужно писать их раньше, чем код. Следовать этому правилу или нет, решать вам. Но в любом случае тестировать нужно рано и часто. В следующем разделе мы обсудим метрики, при помощи которых можно оценить общий уровень качества ПО, а также попросим Copilot провести такую оценку. 6.2. Оценка качества При разработке ПО очень важно иметь представление о его производительности, надежности, сопровождаемости, а также общем уровне качества. Этот раздел посвящен метрикам качества ПО, то есть количественным стандартам и эталонам, которые позволяют оценить качество наших систем. Метрики качества ПО — это важнейшие инструменты, при помощи которых все заинтересованные стороны: разработчики, тестировщики, менеджеры и пользователи — могут оценивать состояние программной системы, выявлять ее сильные стороны и недостатки, требующие улучшения. Метрики представляют собой эмпирическую основу для разработки, отладки, тестирования и сопровождения системы, а также для предложений по ее улучшению. За счет количественной оценки конкретных характеристик метрики помогают сделать осязаемым то, что обычно воспринимается как абстрактное понятие «качество». 1 1 функциональность пройдена, 0 не пройдено, 0 пропущено 1 сценарий пройден, 0 не пройдено, 0 пропущено 6 этапов пройдено, 0 не пройдено, 0 пропущено, 0 не определено Затрачено времени: 0м0.001с. — Примеч. пер.
   146 Глава 6. Тестирование, оценка и описание кода при помощи LLM В этом разделе мы рассмотрим несколько ключевых категорий метрик качества ПО: продуктовые, процессные и проектные. Мы разберемся, зачем они нужны, как рассчитываются и как их можно эффективно применять для оценки и повышения качества программного продукта. В обзор войдут как статические метрики (анализ кода без запуска), так и динамические (оценка поведения системы во время выполнения). Метрики качества важны не только для технической устойчивости системы — они напрямую влияют на удовлетворенность клиентов, прибыльность продукта и успех бизнеса в долгосрочной перспективе. Поэтому понимание этих метрик будет полезно всем, кто связан с разработкой: от инженеров и менеджеров проектов до руководителей и конечных пользователей. Мы рассмотрим несколько общих показателей сложности и сопровождаемости класса или кода. Сложное ПО может быть трудным для понимания, поэтому разработчикам, особенно новым, бывает непросто разобраться в том, как его компоненты взаимодействуют друг с другом. Это может замедлить процесс внедрения и увеличить время разработки. Сложный код часто требует более тщательного сопровождения. Действительно, бывает непросто предсказать, как те или иные правки скажутся на работе системы, а потому внесение изменений и устранение ошибок могут занять много времени. Как результат, практически неизбежен рост затрат на протяжении всего жизненного цикла ПО. Кроме того, чем сложнее ПО, тем больше в нем возникает ошибок. Особенно много их появляется после внесения изменений. Из-за большого количества взаимосвязей между компонентами системы изменения часто приводят к неожиданным последствиям. Подводя итоги: чем сложнее ПО, тем больше тестовых случаев нужно для тщательного тестирования. А написать их может быть очень непросто. Простота и сопровождаемость кода должны быть одним из наших приоритетов. А наблюдение за тем, как метрики изменяются вместе с кодом, — хороший инструмент в этом процессе. Первая метрика, которую стоит использовать (и которую очень легко внедрить), — это цикломатическая сложность. ОПРЕДЕЛЕНИЕ Цикломатическая сложность (cyclomatic complexity) — это метрика, количественно определяющая число независимых друг от друга путей через программный модуль. Она показывает сложность решений, принимаемых в коде, включая циклы, условия и ветвления. Чем больше цикломатическая сложность, тем выше вероятность появления ошибок и проблем с пониманием и сопровождаемостью кода.
   6.2. Оценка качества 147 Введите следующий запрос где-нибудь среди кода из файла department_ visitor.py. Copilot тут же ответит: # Вопрос: Какова цикломатическая сложность класса DepartmentStatisticsVisitor? # Ответ: 1 Copilot считает, что сложность этого класса равна 1. Если вы не знаете, много это или мало, можно спросить: # Вопрос: Цикломатическая сложность 1 – это хорошо? # Ответ: Да, очень # Вопрос: А почему? # Ответ: Чем меньше, тем лучше Copilot сообщает, что чем ниже цикломатическая сложность, тем лучше. Интуитивно это имеет смысл. Чем проще код, тем легче его понять, а значит, скорее всего, и сопровождать. Теперь обсудим сложность по Холстеду. ОПРЕДЕЛЕНИЕ Сложность по Холстеду — это количественный показатель, характеризующий количество уникальных операторов и операндов, использованных в коде. При этом учитывается длина программы (N1), словарь программы (n1), объем (V), трудность (D), трудоемкость (E) и прочие параметры. Эта характеристика дает представление о размере и ощущаемой сложности кода. Как и в прошлый раз, попросим Copilot оценить сложность класса по Холстеду: # Вопрос: Какова сложность (Complexity) класса DepartmentStatisticsVisitor по Холстеду? # Ответ: 2 # Вопрос: Какова трудоемкость (Difficulty) класса DepartmentStatisticsVisitor по Холстеду? # Answer: 1 # Вопрос: Сложность по Холстеду 2 - это хорошо? # Ответ: Да # Вопрос: Трудоемкость по Холстеду 1 - это хорошо? # Ответ: Да # Вопрос: Какова наихудшая сложность по Холстеду? # Ответ: 10 # Вопрос: Какова наихудшая трудоемкость по Холстеду? # Ответ: 10
   148 Глава 6. Тестирование, оценка и описание кода при помощи LLM # Вопрос: О чем говорит высокая трудоемкость по Холстеду? # Ответ: Код труден для понимания Этот разговор можно продолжать долго: ИИ всегда найдет что ответить. Но нам нужно поговорить еще об одной метрике: показателе сопровождаемости. ОПРЕДЕЛЕНИЕ Показатель сопровождаемости (maintainability index) — это составная метрика, объединяющая несколько факторов, в том числе цикломатическую сложность, количество строк кода и сложность по Холстеду. Чем выше показатель сопровождаемости, тем проще сопровождать оцениваемое ПО и тем потенциально ниже его сложность. Возобновим разговор о классе-посетителе: # Вопрос: Каков показатель сопровождаемости класса DepartmentStatisticsVisitor? # Ответ: 100 # Вопрос: Что лучше: высокий показатель сопровождаемости или низкий? # Ответ: Высокий # Вопрос: Зачем нам стремиться к повышению показателя сопровождаемости? # Ответ: Чем он выше, тем проще сопровождать программное обеспечение Низкий показатель сопровождаемости — признак того, что надо поработать над кодом. Метрики — это компас, который укажет нам путь, наш ментор, который скажет, что нужно идти и делать, выйти за рамки чистой эстетики или же вкусовщины. Метрики — это реальные, ощутимые на практике данные. Не нужно ими пренебрегать. Но посмотрите: похоже, что Copilot припрятал еще тузы в рукаве. Он может не только писать за нас код, не только оценивать его, но и искать наши ошибки. Давайте займемся охотой за багами. 6.3. В поисках ошибок В этом разделе мы на элементарном (хотя и надуманном) примере посмотрим, как можно использовать Copilot для поиска и устранения проблем в наших программах. Допустим, есть код, который перебирает список целых чисел и вычисляет их сумму. Но в нем есть ошибка (Copilot был недоступен?): сумма присваивается значению i, а не добавляется к общему итогу. Листинг 6.11. Перебор списка целых чисел и вычисление суммы l = [1, 2, 3, 4, 5] if __name__ == '__main__':
   6.3. В поисках ошибок 149 sum = 0 for i in l: sum = i print("Сумма равна", sum) Решить эту проблему поможет еще один инструмент: Copilot Labs. До появления Copilot Chat Copilot Labs был единственным средством, предоставлявшим подобную возможность в IDE (в частности, Visual Studio Code). Главное преимущество, которым Copilot Labs обладает и сегодня, — доступ к коду, который выделен в редакторе. Благодаря этому Copilot Labs может работать непосредственно с кодом в IDE. После установки соответствующего расширения в левой части окна IDE появится панель инструментов Copilot Labs, которая показана на рис. 6.1. Подробные инструкции по установке расширений приведены в приложениях A–В. Рис. 6.1. Панель инструментов Copilot Labs, на которой есть кнопка для поиска и устранения ошибок. Здесь же кнопки для улучшения и описания кода Исправьте на время код в файле main.py так, как показано на листинге 6.11. Как только все будет готово, выделите этот фрагмент и нажмите на кнопку Fix Bug (исправить ошибки) на панели инструментов Copilot Labs. В ответ Copilot проверит выделенный код, найдет в нем ошибку и посоветует, как исправить ее. Примерно так, как показано на рис. 6.2. Как вариант, вы могли бы скопировать этот код в ChatGPT и попросить его найти ошибку. Но это не так удобно: нужно знать, что в коде вообще есть ошибка, прежде чем просить ChatGPT ее исправить.
   150 Глава 6. Тестирование, оценка и описание кода при помощи LLM Ошибка в этом коде заключается в том, что цикл не суммирует значения из списка l. Вместо этого он перезаписывает значение переменной sum на каждом шаге цикла, так что в итоге sum становится равной последнему элементу в списке. Чтобы исправить эту ошибку, нужно заменить строку sum = i на sum += i, что будет добавлять каждое значение из списка к общему итогу: После этого изменения код будет правильно вычислять сумму значений в списке и выводить результат. Рис. 6.2. Copilot Labs (ИИ на основе GPT) выявил ошибку и предлагает способ ее устранения 6.4. Покрытие кода Покрытие кода — это показатель того, какая часть кода проверяется тестами. Обычно он выражается в процентах. Покрытие кода показывает эффективность тестов. При низком покрытии можно заключить, что часть кода не тестируется, а значит, в ней могут быть ошибки и другие проблемы. Высокое покрытие, напротив, говорит о том, что код хорошо проверяется. Конечно, это не гарантирует отсутствия ошибок, но все же вселяет уверенность в то, что они будут своевременно обнаружены. Чтобы определить покрытие кода в нашем Python-проекте, мы будем использовать специальный инструмент из библиотеки coverage, которая анализирует код в процессе его выполнения и собирает данные о покрытии, включая тесты, скрипты и модули. Благодаря этому мы сможем оценить, насколько полно наш код покрыт тестами, а также выявить фрагменты, нуждающиеся в дополнительном тестировании.
   6.4. Покрытие кода 151 Установим эту библиотеку: pip install coverage Теперь проанализируем наш проект: coverage run -m pytest Мы получим отчет (рис. 6.3), в котором будут данные по каждому из файлов проекта. Такой отчет можно вывести в виде текстового файла (команда coverage report) или файла HTML (команда coverage html). В последнем случае файл с отчетом будет сохранен в папке htmlcov. Рис. 6.3. Отчет о покрытии кода из всех файлов проекта ITAM-системы1 1 Отчет о покрытии: 70% coverage.py версия 7.2.7, создан 2023-06-07 21:02-0400 Модуль Выражений Отсутствует Исключено Покрытие
   152 Глава 6. Тестирование, оценка и описание кода при помощи LLM Покрытие кода на 70% — это хороший старт. В реальности мы бы продолжили работу с командой и нашими ИИ-помощниками, чтобы довести этот показатель до высоких 90%. А дальше мы перейдем к новой теме: использование генеративного ИИ для того, чтобы он описал нам фрагмент кода. 6.5. Анализ программ: от кода к его описанию Часто нам приходится работать с уже существующим исходным кодом. Определить, где у него входная точка, зачем он вообще нужен и как устроен весь проект (особенно если это brownfield-проект, то есть уже имеющийся, а не создаваемый с нуля), бывает непросто. К счастью, именно в таких задачах генеративный ИИ показывает себя особенно хорошо — он умеет переводить код в текстовое описание. Для начала вставим (ошибочный) код из предыдущего раздела в диалог Copilot Chat, предваряя его следующим промтом (рис. 6.4): NC Что делает этот код? Что делает этот код? Рис. 6.4. Ошибочный код из предыдущего раздела и запрос для Copilot Chat Ответ показан на рис. 6.5. Что удивительно, Copilot Chat находит ошибку и предлагает ее исправить. Возьмем пример посложнее: попросим Copilot Chat объяснить, как работает код из листинга 6.9 (поведенческий анализ). ИИ ответит примерно так.
   6.5. Анализ программ: от кода к его описанию 153 Листинг 6.12. Copilot Chat рассказывает о поведенческом тестировании Предоставленный вами код - это не класс, а реализация этапов поведенческого тестирования, которое проводится при помощи библиотеки behave на языке Python. Эти этапы соответствуют определенному сценарию, описанному на языке Gherkin (формат GWT). Эти этапы определяют порядок взаимодействия с классами AssetManager и InMemoryAssetRepository. При этом предполагается создание активов и вычисление их суммарной стоимости. Давайте рассмотрим эти этапы: Given Asset Manager запущен # features/steps/steps.py:8 0.000s And InMemoryAssetRepository инициализирован # features/steps/steps.py:14 0.000s And для AssetLocationMediator используется заглушка # features/steps/steps.py:18 0.000s When я создаю актив стоимостью 2000.00 долларов # features/steps/steps.py:22 0.000s And я создаю еще один актив стоимостью 2000.00 долларов # features/steps/steps.py:27 0.000s Then общая стоимость всех активов должна составлять 4000.00 долларов @given('Asset Manager запущен'): здесь настраивается исходное состояние теста: инициализируется объект AssetManager с репозиторием InMemoryAssetRepository (в котором, возможно, в ходе тестирования будут храниться активы), а также мок для объекта AssetLocationMediator. @given('InMemoryAssetRepository инициализирован') и @given('для AssetLocationMediator используется заглушка'): на этих этапах никаких действий не выполняется. @when('я создаю актив стоимостью {cost} долларов') and @when('я создаю еще один актив стоимостью {cost} долларов'): На этих этапах создаются экземпляры класса Asset с указанной стоимостью, которые затем добавляются в AssetManager. @then('общая стоимость всех активов должна составлять {total_cost} долларов'): на этом этапе перебираются все активы, сохраненные в AssetManager, и вычисляется их суммарная стоимость. Полученное значение сравнивается с ожидаемым. Такая последовательность действий позволяет протестировать процесс создания активов и вычисления их суммарной стоимости. # Вопрос: Что делает этот метод? # Ответ: Этот код определяет асинхронный метод get_asset с целочисленным параметром asset_id. При выполнении метода этот параметр сначала передается методу _asset_rest_port.read, который возвращает соответствующий актив при
   154 Глава 6. Тестирование, оценка и описание кода при помощи LLM помощи REST API. Если актив не найден (то есть REST API возвращает None), метод вызывает исключение HTTPException с кодом состояния 404 и сообщением "Актив не найден". В противном случае метод возвращает актив в формате, доступном объекту проблемной области. Преобразование выполняется при помощи функции from_asset. Реализация этой функции в данном фрагменте кода отсутствует. 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="Актив не найден") return from_asset(asset) В этом коде создается список l, в котором содержатся числа от 1 до 5. Далее следует цикл, в котором перебираются элементы этого списка и вычисляется их сумма. Но в коде есть ошибка: вместо суммы при каждой итерации в итоговой переменной сохраняется значение текущего элемента списка. Поэтому результат будет равен не сумме, а последнему элементу. Чтобы исправить ошибку, необходимо заменить строку sum = i на sum += i. Тогда элементы списка будут суммироваться, а не перезаписывать текущее значение переменной. Вот исправленный код: Благодаря внесенному изменению этот код будет правильно вычислять сумму значений списка и выводить ее на экран. Рис. 6.5. Copilot Chat объясняет назначение кода, находит в нем ошибку и предлагает способ ее устранить 6.6. Перевод с одного языка на другой Хотя это может показаться маловероятным, вы вполне можете столкнуться с ситуацией, когда придется разбираться в коде, написанном на языке программирования, с которым вы знакомы лишь поверхностно. Или, наоборот, вам может не хватать какой-то привычной функции из другого языка и захочется использовать ее в текущем.
   6.6. Перевод с одного языка на другой 155 Как, например, выглядели бы классы Asset и FundingDetails на JavaScript? Что ж, давайте спросим у Copilot Chat. Листинг 6.13. Классы Asset и FundingDetails в переводе на 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; } } Читая этот код, можно подумать, что перевод — дело несложное. Попробуем что-нибудь более интересное. NC Как будет выглядеть скрипт для поведенческого тестирования AssetManager в переводе на JavaScript? Листинг 6.14. П  оведенческое тестирование AssetManager в переводе на JavaScript const AssetManager = require('./asset_manager'); const Asset = require('./asset'); describe('AssetManager', () => { let assetManager; beforeEach(() => { assetManager = new AssetManager(); }); describe('addAsset', () => { it('необходимо добавить актив в список', () => {
   156 Глава 6. Тестирование, оценка и описание кода при помощи LLM const asset = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 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('необходимо удалить актив из списка', () => { const asset = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 1000.0, 5, 100.0, new Date(), [], null); assetManager.addAsset(asset); assetManager.removeAsset(asset); expect(assetManager.assets.length).toBe(0); }); }); describe('getAssetById', () => { it('необходимо вернуть актив с указанным идентификатором', () => { const asset = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 1000.0, 5, 100.0, new Date(), [], null); const asset2 = new Asset(2, 'Принтер', 'Активный', 'Аппаратное обеспечение', 500.0, 3, 50.0, new Date(), [], null); assetManager.addAsset(asset1); assetManager.addAsset(asset2); const result = assetManager.getAssetById(2); expect(result).toBe(asset2); }); it('необходимо вернуть null, если актива с указанным идентификатором нет', () => { const result = assetManager.getAssetById(1); expect(result).toBeNull(); }); }); }); Кроме того, вы можете попросить GPT переписать код в идиоматическом JavaScript, то есть в стиле, принятом в сообществе JS-разработчиков: NC Перепиши класс AssetManager на идиоматическом JavaScript. Copilot Chat проинформирует о том, что определит класс AssetManager, используя синтаксис ES6.
   6.6. Перевод с одного языка на другой 157 Листинг 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 с использованием фреймворка Jest (который Copilot Chat применяет без нашей рекомендации). Листинг 6.16. Тестирование AssetManager на идиоматическом JavaScript const AssetManager = require('./asset_manager'); const Asset = require('./asset'); describe('AssetManager', () => { let assetManager; beforeEach(() => { assetManager = new AssetManager(); }); describe('addAsset', () => { it('необходимо добавить актив в список', () => { const asset = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 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('необходимо удалить актив из списка', () => { const asset = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 1000.0, 5, 100.0, new Date(), [], null); assetManager.addAsset(asset); assetManager.removeAsset(asset);
   158 Глава 6. Тестирование, оценка и описание кода при помощи LLM expect(assetManager.assets.length).toBe(0); }); }); describe('getAssetById', () => { it('необходимо вернуть актив с указанным идентификатором', () => { const asset1 = new Asset(1, 'Компьютер', 'Активный', 'Аппаратное обеспечение', 1000.0, 5, 100.0, new Date(), [], null); const asset2 = new Asset(2, 'Принтер', 'Активный', 'Аппаратное обеспечение', 500.0, 3, 50.0, new Date(), [], null); assetManager.addAsset(asset1); assetManager.addAsset(asset2); const result = assetManager.getAssetById(2); expect(result).toBe(asset2); }); it('необходимо вернуть null, если актива с указанным идентификатором нет', () => { const result = assetManager.getAssetById(1); expect(result).toBeNull(); }); }); }); В этом примере набор поведенческих тестов для класса AssetManager определен при помощи фреймворка Jest. В исходном варианте этого скрипта применялась библиотека describe. В каждом тесте создается экземпляр класса AssetManager и выполняются определенные операции, например добавление или удаление актива. Затем используется функция expect, которая проверяет работу AssetManager: был ли актив добавлен или удален. Обратите внимание: синтаксис определения классов и конструкторов в JavaScript немного отличается от синтаксиса Python, а для импорта классов AssetManager и Asset из соответствующих модулей используется функция required. Наконец, чтобы завершить эту демонстрацию, откроем новый сеанс ChatGPT 4.0, скопируем туда схему на языке Mermaid из главы 3 и напишем промт: NC Вот схема моего приложения на языке Mermaid. Реализуй ее на языке Golang. И вот перед нами код на этом языке. Листинг 6.17. Класс Asset в переводе на идиоматический Golang type Asset struct { id int name string status string category string
}    Итоги 159 fundingDetails *FundingDetails func NewAsset(name, status, category string, fundingDetails *FundingDetails) *Asset { return &Asset{ name: name, status: status, category: category, fundingDetails: fundingDetails, } } Попросим также перевод на Java. Листинг 6.18. Класс Asset в переводе на идиоматический Java 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; } Итоги Юнит-тестирование проверяет отдельные модули или компоненты системы, чтобы выявить проблемы в них. Юнит-тесты самые многочисленные из всех тестов. Интеграционное тестирование проверяет взаимодействие между разными компонентами или модулями, чтобы убедиться, что они работают вместе без сбоев и проблем во взаимосвязи. Поведенческое тестирование проверяет функциональность системы с точки зрения конечного пользователя и гарантирует соответствие требованиям и спецификациям. Моки имитируют поведение реальных объектов в контролируемой среде. Особенно полезны для тестирования в условиях, где нужно симулировать ошибки или исключения. Моки хорошо подходят для подмены зависимостей, которые не входят в зону ответственности теста. Например, если в ваш класс передается подключение к базе данных, но вы не хотите тестировать
   160 Глава 6. Тестирование, оценка и описание кода при помощи LLM саму базу, так как данные могут меняться и это сделает тест нестабильным или нерепрезентативным. Цикломатическая сложность показывает число независимых путей в модуле, чем характеризует сложность и вероятность наличия ошибок. Сложность по Холстеду позволяет оценить программное обеспечение количеству уникальных операторов и операндов, использованных в коде, чем характеризует его ощущаемую сложность. Показатель сопровождаемости объединяет несколько факторов, в том числе цикломатическую сложность, количество строк кода и метрики Холстеда. Чем выше показатель сопровождаемости, тем проще сопровождать оцениваемое ПО. Покрытие кода позволяет оценить эффективность тестирования, показывает, какая часть кода проверяется тестами и, следовательно, не содержит непроверенных ошибок. Чем выше покрытие, тем лучше. Большие языковые модели помогают читать код на незнакомых языках программирования, а также переводить его с одного языка на другой.
Часть 4 Развертывание В части 4 речь пойдет о практических аспектах развертывания и управления программным обеспечением с интеграцией ИИ. Мы разберем, как настроить инфраструктуру, создать Docker-образы и организовать автоматизированный процесс CI/CD с помощью GitHub Actions. Отдельное внимание будет уделено безопасности приложений — моделированию угроз и внедрению лучших практик защиты. Также поговорим о демократизации доступа к ИИ, включая развертывание собственных языковых моделей и использование платформ вроде GPT-4All. Эта часть книги поможет разработчикам подготовиться к релизу своих ИИ-приложений и сделать их надежными, безопасными и удобными в эксплуатации.
7 Настройка инфраструктуры и управление развертыванием В этой главе 3 3 Создание файла Docker при помощи Copilot 3 3 Настройка инфраструктуры при помощи кода, написанного LLM 3 3 Управление Docker-образами через реестр контейнеров 3 3 Освоение возможностей Kubernetes 3 3 Автоматический деплой через GitHub Actions Нет ничего печальнее, чем хорошее приложение, которое так и не запускают в эксплуатацию. Именно поэтому быстрый выход в продакшен после тестирования — цель любого хорошего разработчика. В прошлой главе мы протестировали наш продукт — теперь он готов к запуску. В этой главе мы сосредоточимся на ключевом моменте перехода от разработки к запуску в продакшен. Именно в этой фазе критически важно разобраться в стратегиях развертывания и лучших практиках, чтобы все прошло гладко. Приложение протестировано и защищено — самое время запускать. Мы будем использовать возможности больших языковых моделей, чтобы изучить разные варианты развертывания и настроить нашу инфраструктуру в облаке. Благодаря этим подходам мы сможем спокойно пройти через все сложности запуска приложения и выкатить пользователям надежный, масштабируемый продукт с помощью облачных технологий.
   7.1. Создание Docker-образа и его локальное развертывание 163 Сначала создадим файлы для деплоя в Docker. Поговорим о том, как собрать Docker-образы, как правильно описывать настройки и на что обратить внимание, чтобы контейнеризация прошла гладко. Затем настроим инфраструктуру при помощи Terraform и автоматизируем развертывание экземпляров Elastic Compute Cloud (EC2) на Amazon Web Services (AWS). Для этого мы научимся писать скрипты Terraform для последовательной и воспроизводимой настройки инфраструктуры. Затем мы при помощи LLM развернем наше приложение на Kubernetes (AWS Elastic Kubernetes Service [EKS]/Elastic Container Service [ECS]). С помощью GitHub Copilot мы сгенерируем нужные YAML-файлы, которые упростят весь процесс и помогут удобно управлять жизненным циклом приложения. Поскольку приложение простое, Helm нам пока не понадобится. Но если сервисов станет больше и все усложнится, Helm может стать хорошим выбором. Кстати, Copilot умеет писать и Helm-чарты. Напоследок посмотрим, как перейти от ручного деплоя к автоматическому с помощью GitHub Actions. Подключим LLM к CI/CD-процессу, и все будет идти быстрее и надежнее. ПРИМЕЧАНИЕ В этой главе мы будем работать с облачной платформой AWS. Но все принципы и подходы, которые мы рассмотрим, можно адаптировать под любую инфраструктуру, как облачную, так и традиционную (без виртуализации). Использование LLM и инфраструктуры как кода помогает частично избежать так называемой привязки к облачному вендору, которая часто становится головной болью. Обратите внимание: решение развернуть это (или любое другое) приложение на AWS может стоить вам денег. AWS, как и большинство облачных платформ (например, Google Cloud Platform и Azure), предоставляют небольшой бесплатный пробный период, но когда он закончится, могут выставить вам неожиданно большой счет. Поэтому тем, кто захочет реально проделать описанные в этой главе действия, следует настроить пороговые оповещения на приемлемую сумму. О том, как это сделать, можно прочесть в пункте 1.9 третьего издания книги Андреаса Виттига (Andreas Wittig) и Майкла Виттига (Michael Wittig) «Amazon Web Services in Action» (издательство «Manning», 2023 г., www.manning. com/books/amazon-web-services-in-action-third-edition). 7.1. Создание Docker-образа и его локальное развертывание Как уже говорилось в главе 6, Docker — это платформа для контейнеризации, которая позволяет запускать приложения практически без установки (вне Docker) в традиционном смысле этого слова. В отличие от виртуальной машины, которая эмулирует целую операционную систему, контейнер использует ядро
164    Глава 7. Настройка инфраструктуры и управление развертыванием Контейнер D Контейнер C Контейнер B Контейнер А хост-системы и ее ресурсы, но при этом изолирует процессы и файловую систему приложения. Благодаря этому на одном хосте можно запускать несколько изолированных приложений, каждое из которых имеет свое собственное окружение и ограничения по ресурсам. На рис. 7.1 показано, как Docker взаимодействует с хостом. Демон Docker Операционная система хоста    Аппаратное обеспечение хоста Рис. 7.1. Docker использует операционную систему хоста, изолируя контейнер. Благодаря этому контейнеры гораздо легковеснее виртуальных машин — им не нужна полноценная ОС Одна из наиболее интересных особенностей Docker состоит в том, что он упрощает работу с приложениями. В случае ошибки или критического сбоя приложения Docker, при соответствующих настройках, может перезапустить приложение без стороннего вмешательства. Путь от готового приложения к контейнеру Docker начинается с Docker-файла (который мы напишем при помощи Copilot) — основы для создания образа Docker. ОПРЕДЕЛЕНИЕ Образ Docker  — это «чертеж» для контейнера. Образы полностью переносимы и включают в себя все зависимости (библиотеки, переменные окружения, код и т. д.), необходимые для работы приложения. Рабочие экземпляры Docker называются контейнерами. Как уже говорилось, контейнер использует ядро и ресурсы операционной системы хоста, а потому относительно нетребователен к ним. Благодаря этому на одном хосте можно запустить сразу несколько контейнеров, каждый из которых будет работать в своем, изолированном пользовательском пространстве.
   7.1. Создание Docker-образа и его локальное развертывание 165 ПРИМЕЧАНИЕ Изначально я хотел использовать AWS CodeWhisperer в качестве LLM для этой главы — это казалось логичным, учитывая, что мы работаем с AWS. Но на момент написания книги CodeWhisperer поддерживает только генерацию кода на языках программирования. Он не умеет работать с IaS (инфраструктура как код). Поэтому вновь обратимся к Copilot. Начнем с запроса, в ответ на который он подготовит для нас Docker-файл: # Создай файл Docker для этого приложения на Python. Главный класс main.py. # Используй Python 3.10, установи зависимости согласно списку из файла requirements.txt из этой папки. # Приложение должно работать через порт 8080. Скорее всего, файл так и останется пустым (за исключением этого комментария). Увы, Copilot еще плохо справляется с настройкой инфраструктуры при помощи кода. По мнению Copilot Chat, GitHub Copilot способен создать Docker-файл, но немного непривычным образом. И вот что нужно сделать: 1. Откройте Docker-файл и наберите в нем слова 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"], чтобы задать команду для запуска приложения после загрузки контейнера. Как вариант, можно скопировать составленный выше промт в окно Copilot Chat, а затем готовый текст — обратно в Docker-файл. Листинг 7.1. Docker-файл для создания образа Docker 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"] На основе Docker-файла создается образ для развертывания и запуска приложения. Для этого нужно выполнить следующую команду (не забудьте поставить точку в конце): docker build -t itam:latest .
   166 Глава 7. Настройка инфраструктуры и управление развертыванием Команду нужно запустить из папки, в которой находится Docker-файл. Обратите внимание: для загрузки зависимостей и создания образа нужен доступ в интернет. Чтобы создать образ, нужно от нескольких секунд до нескольких минут. Все зависит от того, какие образы и пакеты уже доступны локально, и от того, как быстро работает интернет. Немного терпения, и вы получите приложение, которое можно установить практически куда угодно: хоть на старенький домашний компьютер, хоть на сверхмощный сервер облачного провайдера. Но сначала нужно проверить образ: попробовать запустить его у себя. Команду для этого подскажет Copilot Chat: docker run -p 8000:8000 -d --name itam itam:latest А вот команда, при помощи которой можно проверить работу контейнера: docker ps | grep itam. Если все хорошо, вы увидите работающий экземпляр Docker. 7.2. Настройка инфраструктуры при помощи Copilot и Terraform Локальное применение Docker-образа полезно при создании и тестировании приложения. Когда же дело дойдет до реальной работы, потребуется машина гораздо мощнее настольного компьютера. GitHub Copilot поможет нам настроить инфраструктуру AWS и написать дескрипторы развертывания, необходимые для работы с Terraform — инструментом для описания инфраструктуры, созданным компанией HashiCorp. С помощью Terraform можно описывать инфраструктуру специальным DSL-языком, чтобы не разбираться вручную во всех тонкостях облачных провайдеров. Кроме того, весь этот код можно хранить в репозитории и версионировать как обычный проект. Для начала создадим файл ec2.tf с промтом, где проинформируем Copilot о том, что хотим получить файл Terraform с описанием нужной нам инфраструктуры. Обратите внимание: чтобы ИИ генерировал код, в каждой строке придется вручную набрать первое слово. Листинг 7.2. Пример файла Terraform с указанием размеров экземпляра # Создай файл Terraform, который описывает экземпляр AWS EC2 типа t2.micro, устанавливает демон Docker и возвращает имя хост-системы этого экземпляра. provider "aws" { region = "us-west-2" } resource "aws_instance" "example" {
   7.2. Настройка инфраструктуры при помощи Copilot и Terraform } 167 ami = "ami-0c55b159cbfafe1f0" instance_type = "t2.micro" Здесь Copilot упускает маленькую, но очень важную деталь: в его варианте нет кода для установки и настройки 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 работает без замечаний. В итоге мы получаем код, который показан в следующем листинге. Ваш вариант, если вы решите повторить эти действия, может быть иным, что вполне допустимо. Главное, что в нем будут все основные параметры: провайдер, экземпляр, скрипт для добавления демона Docker, пара ключей и группа безопасности. Листинг 7.3. Ф  айл Terraform для создания минимально возможного экземпляра EC2 # Создай файл Terraform, который описывает экземпляр AWS EC2 типа t2.micro, устанавливает демон Docker и возвращает имя хост-системы этого экземпляра. 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 {
   168 } Глава 7. Настройка инфраструктуры и управление развертыванием type user private_key host = = = = "ssh" "ec2-user" file("~/.ssh/id_rsa") 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 = "Разрешить входящий трафик HTTP" vpc_id = "vpc-12345678" } ingress { description from_port to_port protocol } = = = = "HTTP от VPC" 80 80 "tcp" cidr_blocks = ["0.0.0.0/0"] output "public_dns" { value = aws_instance.example.public_dns } Если использовать предоставляемое по умолчанию виртуальное частное облако (VPC), строку vpc_id можно опустить. На самом деле большинство конфигураций и соглашений, которые AWS предлагает по умолчанию, вполне подойдут для приложения без повышенных требований к безопасности или инфраструктуре. А в таких случаях всегда можно создать VPC с нуля при помощи Terraform. В любом случае нужно изменить строку 21, указав в ней пару предоставленных вам ключей для доступа. Завершив работу над этим файлом, мы можем выполнить команду terraform init, которая настроит новую либо уже имеющуюся рабочую папку Terraform, загрузит и установит нужные плагины и модули, указанные в файлах конфигурации, и подготовит все это к работе. Теперь нужно спросить Terraform о планируемых изменениях с помощью команды terraform plan . По этой команде Terraform разработает план инфраструктурных изменений, в котором опишет, какие изменения произойдут в инфраструктуре в результате применения файлов конфигурации. В том числе в нем будет указано, какие ресурсы будут созданы, изменены или уничтожены.
   7.3. Перенос Docker-образа (сложный способ) 169 ПРИМЕЧАНИЕ При первом запуске terraform plan вы можете увидеть сообщение: «Error: configuring Terraform AWS Provider: no valid credential sources for Terraform AWS Provider found.» («Ошибка: настройка Terraform AWS Provider: отсутствуют действующие учетные данные для доступа к Terraform AWS Provider»). Эта ошибка возникает, когда Terraform пытается подключиться к AWS, но не находит правильных учетных данных. В таком случае нужно создать (или отредактировать) файл ~/.aws/ credentials и добавить в него идентификатор ключа доступа ITAM AWS и секретный ключ доступа AWS. Об этом подробно написано в пункте 4.2.2 «Настройка CLI» книги «Amazon Web Services in Action, Third Edition». Теперь мы можем применить изменения Terraform при помощи команды terraform apply. При этом Terraform прочтет файлы конфигурации из текущей папки и соответствующим образом изменит инфраструктуру. Если с последнего запуска terraform apply эти файлы были отредактированы (например, если потребовался новый экземпляр базы данных или возникла необходимость увеличения размеров EC2), Terraform расскажет о том, какие изменения предстоят, а также предложит их подтвердить. Несколько минут ожидания, и в нашем VPC работает новенький экземпляр EC2. Но это лишь половина дела. Иметь в своем распоряжении мощный вычислительный ресурс — хорошо, но нужно на что-то его направить, и в нашем случае это будет ITAM-система. Поэтому в следующем разделе мы обсудим, как перенести локальный Docker-образ на другой хост. 7.3. Перенос Docker-образа (сложный способ) Сначала экспортируем Docker-образ с локального компьютера и загрузим на удаленный. Для этого понадобятся команды docker save и docker load. Первая выполняется на локальном компьютере и сохраняет образ как tar-архив: docker save -o <имя образа>.tar <имя образа>:<тег> Архив нужно перенести на удаленный компьютер при помощи, например, защищенного протокола копирования (SCP) или защищенного протокола передачи файлов (SFTP). Затем там следует выполнить команду docker load, которая загружает образ из архива: docker load -i <имя образа>.tar. Эта команда добавит образ в локальный кэш Docker на удаленной машине. После загрузки используйте команду docker run, чтобы запустить образ и развернуть контейнер, так же, как вы делали после сборки образа. Затем добавьте этот образ в ваш docker-compose-файл, в котором также настроены экземпляры базы данных Postgres и Kafka.
   170 Глава 7. Настройка инфраструктуры и управление развертыванием ПРИМЕЧАНИЕ Мы затронули тему Terraform очень поверхностно. Тем, кто хочет разобраться подробнее, рекомендую ознакомиться с книгой Скотта Уинклера (Scott Winkler) «Terraform in Action» (издательство «Manning», 2021 г., www.manning.com/ books/terraform-in-action). В этом разделе мы рассмотрели, как упаковывать образы и загружать их на удаленные хосты. Можно даже написать скрипт, который будет делать все это за нас. Но с появлением реестров контейнеров этот процесс намного упростился: теперь можно управлять развертыванием, не перетаскивая образ с одного сервера на другой. Давайте познакомимся с одним из подобных инструментов: Elastic Container Registry (ECR) от Amazon. 7.4. Перенос Docker-образа (простой способ) Образы Docker — это основа контейнеров и один из ключевых строительных блоков контейнеризированных приложений. Правильное управление ими позволяет поддерживать чистые, эффективные и упорядоченные процессы разработки и развертывания. Поэтому полностью управляемый реестр контейнеров Amazon ECR существенно упрощает хранение, управление и развертывание образов для контейнеров Docker. Сначала поговорим о том, как отправить в ECR образ, что очень важно, поскольку он должен быть доступным для применения и развертывания. Рассмотрим настройку локального окружения, аутентификацию в ECR и собственно отправку образа. Но прежде всего необходимо создать для него хранилище. Для этого можно воспользоваться консолью AWS Management Console или, как сделаем мы, интерфейсом командной строки AWS (CLI). Используем следующую команду: aws ecr create-repository --repository-name itam Теперь присвоим образу тег, указав его имя (просто latest, как в нашем случае, или же семантический номер версии) и URL-адрес репозитория ECR. При помощи тегов легко понижать и повышать версию системы. docker tag itam:latest 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest Теперь подтвердим подлинность образа в реестре ECR: используем команду aws ecr get-login-password, которая генерирует необходимую для этого команду docker login: aws ecr get-login-password --region us-west-2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-west-2.amazonaws.com Наконец, отправим образ в реестр ECR при помощи команды docker push: docker push 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest
   7.5. Развертывание приложения на AWS Elastic Kubernetes Service 171 Как только образ окажется в реестре, перед нами откроется несколько вариантов развертывания. Например, можно написать bash-скрипт, который войдет в экземпляр EC2, загрузит образ при помощи docker pull и запустит его. Или же можно поступить более надежным способом. Так, в следующем разделе мы рассмотрим настройку и запуск нашего приложения на мощном облачном сервисе Elastic Kubernetes Service (EKS) — управляемом сервисе Kubernetes, который предоставляется AWS. 7.5. Развертывание приложения на AWS Elastic Kubernetes Service По сравнению с простым запуском образов Docker на экземплярах EC2 Kubernetes обладает множеством преимуществ, в том числе существенно упрощает управление и масштабирование приложения, а также снижает затраты времени на планирование инфраструктуры. Кроме того, благодаря автоматическому управлению жизненным циклом образов (которые в Kubernetes называются подами) приложение становится, по сути, самовосстанавливающимся: автоматически устраняя сбои в работе приложения, Kubernetes обеспечивает его бесперебойную работу. Прежде всего потребуется дескриптор развертывания на языке YAML, что расшифровывается как Yet Another Markup Language (еще один язык разметки) или же YAML Ain't Markup Language (YAML — не язык разметки), кому как нравится. Дескриптор, который обычно сохраняется в файле deployment.yaml, описывает состояние, в котором находится ITAM-система. Kubernetes использует этот файл в качестве шаблона, с которым сравнивает текущую систему и вносит необходимые коррективы. Листинг 7.4. Дескриптор развертывания Kubernetes для ITAM-системы # Создай дескриптор развертывания Kubernetes для ITAM-системы. # Имя образа - itam:latest, развертывание происходит через порт 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
   172 Глава 7. Настройка инфраструктуры и управление развертыванием spec: containers: - name: itam image: itam:latest imagePullPolicy: Always ports: - containerPort: 8000 Увы, этот код не работает: Kubernetes не может найти наш образ. Но проблему решить несложно: нужно дополнить дескриптор ссылкой на образ, загруженный в ECR. Кроме того, нужно открыть для EKS доступ к ECR (да, простой правкой файла тут не отделаться). Сначала обновим дескриптор развертывания: image: 123456789012.dkr.ecr.us-west-2.amazonaws.com/itam:latest. Теперь определим политику EKS и применим ее при помощи AWS CLI или консоли управления идентификацией и доступом (IAM, identity and access management). Применение политики (немного) выходит за рамки этой книги, но можно попросить помощи у Copilot. И вот что он предложит: Листинг 7.5. IAM-политика, разрешающая EKS загружать образы из ECR { "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). Но для внешнего доступа к нему нужно создать специальный сервис — абстракцию, которая определит логический набор подов и политику доступа к ним. Сервисы обеспечивают связь между приложениями и их частями, помогают распределять сетевой трафик и балансировать нагрузку, а также предоставлять доступ к подам из сети и других подов Kubernetes.
   7.6. Настройка конвейера непрерывной интеграции и развертывания 173 Листинг 7.6. Ф  айл сервиса Kubernetes, разрешающий доступ к приложению извне # Создай сервис для приложения с распределителем нагрузки для исходящего трафика apiVersion: v1 kind: Service metadata: name: itam-service spec: type: LoadBalancer selector: app: itam ports: - name: http port: 80 targetPort: 8000 Kubernetes обеспечивает маршрутизацию всех поступающих на сервер запросов к запущенным подам, независимо от того, на каком хосте они работают. Это и позволяет обеспечить бесперебойную работу. Kubernetes ожидает, что приложения будут работать с ошибками, рассчитывает на их сбои и умеет их устранять. А потому переход на этот сервис — важнейший шаг к созданию надежной системы с высоким уровнем доступности. В следующем разделе поговорим о том, как упростить запуск в Kubernetes новых версий нашего приложения. Мы создадим небольшой конвейер развертывания с помощью GitHub Actions. 7.6. Настройка конвейера непрерывной интеграции и развертывания (CI/CD) в GitHub Actions Если процесс релиза сложный, его будут проводить редко. Это ограничивает нашу способность быстро добавлять ценность приложению и, соответственно, его пользователям. Автоматизация процесса развертывания существенно сокращает время выхода релизов. Это позволяет выпускать обновления чаще, ускоряя темп разработки и доставляя пользователям новые функции быстрее. Конвейеры непрерывной интеграции/непрерывного развертывания (CI/CD) снижают риски, связанные с этими процессами, поскольку частые обновления с небольшим объемом изменений позволяют быстро выявлять и устранять ошибки, а значит, уменьшить их влияние на конечных пользователей. Конвейеры способствуют беспрепятственной интеграции изменений кода и ускоряют развертывание, упрощая обновление программного обеспечения. При помощи GitHub Actions можно настроить конвейер CI/CD прямо в репозитории GitHub. Это позволяет повысить эффективность разработки, частично автоматизировать ее, уменьшить затраты времени на интеграцию и развертывание, а значит, сосредоточиться на написании кода.
   174 Глава 7. Настройка инфраструктуры и управление развертыванием Конвейеры CI/CD — достаточно сложная и объемная тема, и мы не будем подробно ее рассматривать. Кратко затронем лишь основные моменты: поговорим о потенциальных преимуществах конвейеров с точки зрения оптимизации разработки, а также о том, как в целом строится рабочий процесс. Помогать нам будет GitHub Copilot. Прежде всего создадим файл в папке .github/workflows (обратите внимание на точку в начале). Имя файла значения не имеет. Пусть будет itam.yaml. В первой строке напишем промт: # Создай рабочий процесс GitHub Actions, который при каждом слиянии в главную ветку будет собирать ITAM-систему и развертывать ее в EKS. ПРИМЕЧАНИЕ Как и во многих случаях, рассмотренных нами в этой главе, Copilot не сможет сделать за нас всю работу. Ему придется помогать, а значит, мы должны знать и структуру будущего файла, и первое слово из каждой строки. Поэтому имеет смысл сначала задать этот вопрос ChatGPT или Copilot Chat. В первой части этого файла указывается, когда должно выполняться задание. В нашем случае это будет on:push, то есть после каждой команды git push для ветки main. Листинг 7.7. Начало файла GitHub Actions для сборки приложения # Создай рабочий процесс GitHub Actions, который при каждом слиянии в ветку main будет собирать ITAM-систему и развертывать ее в EKS. name: Build and Deploy to EKS on: push: branches: - main jobs: Далее в файле идет описание заданий. В нашем случае задание будет одно: сборка и развертывание ITAM-системы. Оно будет выполняться в несколько этапов. Большинство из них мы уже рассмотрели в этой главе, но теперь GitHub Actions будет работать за вас. В этом и есть магия автоматизации. Прежде всего выполняется проверка кода из репозитория GitHub при помощи модуля actions/checkout версии 2. Затем при помощи EKS CLI вводятся учетные данные для подключения к EKS. Обратите внимание: ключи для доступа к AWS передаются в приложение автоматически. GitHub Actions использует встроенную систему хранения конфиденциальных данных: ключей API, паролей и сертификатов. Она интегрирована с платформой GitHub и позволяет добавлять, удалять или обновлять чувствительные данные как на уровне репозитория, так и на уровне организации. Они хранятся в зашифрованном виде, не отображаются в журналах и недоступны для загрузки. А для работы с ними
   7.6. Настройка конвейера непрерывной интеграции и развертывания 175 в скриптах GitHub Actions используются переменные окружения. Все это обеспечивает высокий уровень безопасности чувствительных данных. Переменные окружения используются и для других целей. К примеру, посмотрим на переменную ECR_REGISTRY, в которой хранится результат выполнения функции login-ecr. Конечно, некоторые значения придется включить в файл в явном виде. Исключительно ради единообразия, чтобы они все были в одном месте. Листинг 7.8. Сборка и развертывание приложения в файле GitHub Actions build: runs-on: ubuntu-latest steps: - name: Проверка кода uses: actions/checkout@v2 - name: Настройка EKS CLI uses: aws-actions/amazon-eks-cli@v0.1.0 - name: Настройка учетных данных AWS 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: Создание и передача Docker-образа 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: Развертывание на 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. Завершение файла GitHub Actions login-ecr: runs-on: ubuntu-latest steps:
   176 Глава 7. Настройка инфраструктуры и управление развертыванием - 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 }} Изучение подхода «инфраструктура как код» помогло нам понять, какую важную роль он играет в любом проекте и как управление инфраструктурой можно упростить с помощью кода. Такие инструменты, как Terraform, предлагают удобные решения для управления инфраструктурой, а возможности GitHub, ориентированные на работу с кодом, помогают поддерживать весь рабочий процесс. Появление конвейеров CI/CD, прежде всего на основе GitHub Actions и других похожих платформ, подчеркивает, насколько важно автоматизировать процесс доставки программного обеспечения. Автоматизация ускоряет и повышает надежность всех этапов жизненного цикла разработки, а также снижает риск ошибок, связанных с человеческим фактором. Мир управления инфраструктурой как кодом постоянно развивается — появляются новые инструменты и практики. Это требует от разработчиков постоянного обучения и готовности адаптироваться. В этой главе мы получили общее представление о возможностях и преимуществах такого подхода. Итоги Мы рассмотрели переходный процесс, соединяющий разработку приложения и его релиз, поговорили о стратегиях развертывания, работе с облачной инфраструктурой, а также о применении Docker и Terraform для эффективного управления и контейнеризации приложений. Обсудили, как управлять развертыванием приложения при помощи Kubernetes, узнали о дескрипторах развертывания на языке YAML, настройке служб для распределения сетевого трафика и работе с AWS Elastic Kubernetes Service (EKS). Разобрались в различиях между подходами к развертыванию в разных средах, облачных и локальных, применили GitHub Copilot для создания файлов Docker и Terraform. Наконец, мы узнали, как экспортировать образы Docker на удаленный компьютер и загружать их в Elastic Container Registry (ECR) Amazon, а также как сделать процесс развертывания автоматическим при помощи GitHub Actions.
8 Разработка безопасных приложений с ChatGPT В этой главе 3 3 Моделирование угроз с помощью ChatGPT 3 3 Формирование культуры безопасной разработки через использование ChatGPT 3 3 Минимизация рисков с помощью ChatGPT Сегодня безопасность перестала быть чем-то, о чем вспоминают в последний момент, — теперь она должна быть встроена в проект еще на этапе разработки и проектирования. Но при этом многим разработчикам все еще трудно успевать за быстро меняющимся миром безопасности приложений. В этой главе мы попытаемся понять, как использовать ChatGPT на разных этапах разработки, чтобы усилить безопасность приложений и создавать более защищенные программные системы. Углубившись в эту тему, мы рассмотрим возможные применения ChatGPT на разных стадиях разработки нашего IТAM-приложения, написанного на Python с использованием FastAPI. Обсудим, как ИИ может помочь при поиске уязвимостей и моделировании угроз, оценке структуры приложения на предмет потенциальных опасностей, выборе и применении оптимальных способов обеспечения безопасности.
178    Глава 8. Разработка безопасных приложений с ChatGPT Цель этой главы — не представить ChatGPT как универсальное решение для всех проблем с безопасностью, а показать его как мощный инструмент в арсенале разработчика. Мы научимся заранее выявлять угрозы и управлять ими, всегда помня о главной задаче — создавать не просто рабочее, а безопасное программное обеспечение. Вместе мы пройдем через моделирование угроз, интеграцию безопасности в цикл разработки, рассмотрим роль ИИ в защите приложений и многое другое. Поехали! Безопасность — не просто полезная функция Безопасность — не просто полезная функция приложения, хотя к ней часто относятся именно так. Важно думать о безопасности с самых первых этапов работы. И в этом плане генеративный ИИ — это прекрасный инструмент, который поможет и оценить, и повысить уровень безопасности. Но искусственный интеллект не заменит эксперта по безопасности и не сделает вас таковым. Поэтому рекомендую прочитать, например, книгу «Безопасно by Design» Дэна Берга Джонсона (Dan Bergh Johnsson), Дэниела Деогана (Daniel Deogun) и Дэниела Савано (Daniel Sawano) (издательство «Питер», 2021 г.). ПРОЕКТИ Т Е АНИ ЫВ РОВ А РАЗРАБОТКА ОВАНИЕ РАЗВЕ РТ ТИР ЕС Е НИ ЗА Щ ИТА Ошибочная модель восприятия жизненного цикла разработки выглядит так, будто безопасность — это просто одна из функций, которую можно по желанию ставить в приоритет или убирать. На самом деле безопасность — это особое мышление, которое должно быть на всех этапах проекта. 8.1. Моделирование угроз с помощью ChatGPT Моделирование угроз — это структурированный подход, который помогает разработчикам понимать потенциальные угрозы, определять их приоритет и бороться с ними. Имитируя образ мышления злоумышленника, можно систематически выявлять уязвимости, оценивать возможные последствия атак, а значит, формировать стратегии снижения рисков. Моделирование угроз, начатое еще на этапе проектирования и не прекращающееся на протяжении всего жизненного цикла
   8.1. Моделирование угроз с помощью ChatGPT 179 ПО, служит важнейшим связующим звеном между политиками безопасности, принимаемыми на высшем уровне, и практическими действиями, предпринимаемыми на низшем. Моделирование угроз — это не разовая задача. По мере появления новых уязвимостей и изменений в системе или внешней среде модель угроз нужно регулярно пересматривать и обновлять. 8.1.1. Почему это важно в современных условиях Прежде чем перейти к моделированию угроз с помощью ChatGPT, сделаем шаг назад, чтобы понять, для чего это вообще нужно. Уделяя больше внимания безопасности в современном мире, мы должны помнить о факторах, которые значительно изменили способы разработки, развертывания и доступа к программному обеспечению. По мере того как все больше сервисов переходят на цифровые основы, раздвигаются границы для атак. Все — от банковских услуг до медицинских карт, от электронной коммерции до государственных услуг — теперь доступно через интернет, а значит, стало потенциальной мишенью. Не стоят на месте и киберпреступники: каждый день появляются новые уязвимости, а следовательно, и новые методы атак. Мы живем в эпоху атак, спонсируемых государствами, программ-вымогателей и кибершпионажа. Опасность еще никогда не была такой ощутимой. Современная архитектура значительно сложнее, чем раньше: приложения в основном используют микросервисы, сторонние API и облачные инфраструктуры. И это приводит к появлению новых потенциальных точек уязвимости. Системы не автономны, а связаны с другими системами, что создает эффект домино: нарушение в одной системе может привести к компрометации других. Нарушения безопасности Помимо прямых финансовых последствий, нарушение безопасности может подорвать доверие, нанести ущерб репутации компании, привести к юридическим последствиям и повлечь за собой потерю заказчиков или возможностей для ведения бизнеса. Кроме того, с принятием таких нормативных актов, как Общий регламент по защите данных (GDPR) в Европе и Калифорнийский закон о защите персональных данных потребителей (CCPA) в США, на организации возлагается повышенная ответственность за защиту данных пользователей. Несоблюдение этих требований может привести к значительным штрафам. В условиях взаимосвязанного цифрового мира безопасность — это не только забота ИТ-специалистов, но и фундаментальная обязанность компаний в целом. Вопросы безопасности должны рассматриваться с самых первых этапов
   180 Глава 8. Разработка безопасных приложений с ChatGPT разработки приложений: это снижает риски и затраты, укрепляет доверие и обеспечивает непрерывность систем. 8.1.2. Чем ChatGPT может помочь при моделировании угроз Теперь, когда мы понимаем, зачем это нужно, давайте посмотрим, как можно использовать ChatGPT для изучения угроз кибербезопасности, их последствий и способов защиты. У ChatGPT обширная база знаний по основным понятиям кибербезопасности — он может объяснить стандартные термины и разобрать сложные векторы атак на том уровне подробности, который подходит вашему текущему опыту. Вы можете начать с простого: попросить объяснить базовые принципы кибербезопасности, что такое атака через SQL-инъекцию (но не как ее проводить!) или что такое кликджекинг. Мы можем подойти к моделированию угроз несколько неформально: предложить ChatGPT подробные гипотетические сценарии и попросить найти в них потенциальные угрозы и уязвимости. Начнем с самого общего и будем уточнять вопросы по ходу дела. Например, мы можем спросить: NC Я работаю над облачным веб-приложением для электронной коммерции. Какие угрозы безопасности мне следует учесть? А после этого углубляться в подробности, сужая круг до конкретных угроз: NC Как злоумышленник может взломать корзину пользователя в моем приложении для электронной коммерции? Далее можно спросить ИИ, как оценить риски, связанные с разными угрозами. Такой вопрос поможет определить, какие угрозы следует устранить прежде всего. Получив представление о потенциальных угрозах для приложения, мы можем обсудить с ChatGPT, какие меры противодействия и стратегии смягчения последствий следует применять: NC Как оценить риск DDoS-атаки для моего онлайн-сервиса? После чего: NC Как лучше всего предотвратить межсайтовый скриптинг (XSS)? Вам стоит периодически обращаться к ChatGPT, чтобы освежать знания или узнавать о новых концепциях и стратегиях, которые вам встретились. Но важно предупредить: как и всегда, следует помнить о свойственных ChatGPT ограничениях. В его распоряжении нет сведений об актуальных угрозах и новых знаний, которые выходят за пределы его последнего обновления.
   8.1. Моделирование угроз с помощью ChatGPT 181 Свежие новости следует узнавать, обращаясь к современным ресурсам. Кроме того, нужно проверять полученные от ИИ советы, сверяя их с другими авторитетными источниками, желательно не с одним, ведь кибербезопасность очень быстро развивается. Обсудив конкретную угрозу с ChatGPT, можно обратиться к последним документам таких организаций, как проект OWASP (Open Worldwide Application Security Project, открытый всемирный проект по безопасности приложений) или Национальный институт стандартов и технологий США (NIST). Наконец, в деле эффективной генерации идей, понимания сложных концепций или уточнения стратегий кибербезопасности особенно полезны интерактивные мозговые штурмы с ChatGPT. Например, можно действовать так: 1. Четко сформулируйте цель мозгового штурма. К примеру, это может быть выявление потенциальных уязвимостей в системе, разработка мер безопасности для нового приложения или обсуждение стратегий по улучшению защиты пользовательских данных. 2. Начните новый сеанс с предоставления подробного контекста. Если речь идет о конкретной системе или приложении, опишите его архитектуру, составные части, функциональные возможности, а также уже известные проблемы и опасения. Например, вы можете сказать: NC Я работаю над предназначенной для электронной коммерции веб-платформой на основе микросервисной архитектуры и контейнеров Docker. Нужно выявить потенциальные угрозы безопасности. И дальше, опираясь на ответы ChatGPT, продолжайте углубляться в конкретные вопросы и опасения. Например, можно попросить: NC Расскажи мне о том, как лучше всего обезопасить контейнер. Или задать вопрос: NC Как можно защитить коммуникации между микросервисами? 3. Предложите ИИ гипотетические сценарии, спросите, что он думает о них и что может посоветовать. Это поможет понять потенциальные проблемы и угрозы: NC Допустим, злоумышленник получил доступ к одному из контейнеров. Что следует предпринять? 4. Представьте себя адвокатом дьявола. Ставьте все под сомнение, опровергайте идеи и предложения ИИ, стимулируйте его к дальнейшим размышлениям, изучайте разные точки зрения:
   182 NC Глава 8. Разработка безопасных приложений с ChatGPT Что, если использовать стороннюю службу аутентификации? Как это изменит безопасность приложения? 5. Спрашивайте ChatGPT о конкретных мерах и действиях, необходимых для реализации предложенных решений. Можно спросить: NC Что предпринять для решения указанных тобой проблем безопасности? ПРИМЕЧАНИЕ По ходу мозгового штурма записывайте высказанные ChatGPT идеи, предложения и стратегии. Все это пригодится для дальнейшего анализа и реализации. Мозговой штурм наиболее эффективен, когда проводится циклически. Отталкиваясь от того, что вы узнали сегодня, корректируйте общий подход и планируйте вопросы, которые зададите завтра. Блок-схема такого цикла мозговых штурмов показана на рис. 8.1. Уточнение Исследование Корректировка Рис. 8.1. Блок-схема циклического мозгового штурма с участием ChatGPT Пересматривайте записи разговоров с ИИ по мере реализации проекта или сценария. Это поможет учесть изменения, обновления, а также вновь появившиеся задачи. Просите ChatGPT анализировать проектную документацию, искать в ней потенциальные угрозы и общие уязвимости. Ему это вполне по силам, точно так же, как и проверка кода. Используя ChatGPT в таком полуструктурированном формате, вы сможете извлекать пользу из его обширной базы знаний, а также получать ценные идеи и обратную связь по вашим сценариям. Но всегда помните: любые советы стоит перепроверять, сверяясь с актуальными источниками и консультируясь с профильными экспертами. 8.1.3. Пример: моделирование угроз с ChatGPT В дополнение к анализу сценариев и интерактивным мозговым штурмам с ChatGPT мы можем применять формальную методологию, которая обычно используется профессионалами в области кибербезопасности. Речь идет о STRIDE — предложенной Microsoft методологии моделирования угроз, при помощи которой можно выявлять проблемы безопасности в системах и приложениях.
   8.1. Моделирование угроз с помощью ChatGPT 183 Методология STRIDE ¡ ¡ ¡ ¡ ¡ ¡ ¡ ¡ Аббревиатура STRIDE расшифровывается как spoofing (подмена), tampering (фальсификация), repudiation (отрицание), information disclosure (раскрытие информации), denial of service (отказ от обслуживания) и elevation of privilege (повышение привилегий): Подмена (или спуфинг) — попытка выдать себя за кого-то или что-то другое. Преступник действует от лица определенного пользователя, устройства или даже системы: к примеру, он может подделать MAC-адрес и скрытно подключиться к сети. Фальсификация — несанкционированное изменение данных или кода. Например, в ходе атаки через посредника преступник может изменить передаваемый пакет данных. Отрицание — ситуация, когда злоумышленник (или даже невнимательный пользователь) может отрицать, что совершал какие-либо действия, потому что нет надежных доказательств. Например, пользователь отрицает, что сделал заказ или изменил данные. Раскрытие информации — передача информации лицам, которые не должны иметь к ней доступа. Например, из-за неправильной настройки базы данных в Сеть утекают персональные данные. Отказ в обслуживании (DoS) — это создание условий, при которых ресурс ока­жется недоступен для пользователей или не будет отвечать на запросы. Преступник может добиться перегрузки ресурсов системы или использовать определенные уязвимости. Например, DDoS-атака подразумевает наводнение веб-сайта трафиком, что приводит к замедлению работы и проблемам с доступом для пользователей. ¡ Повышение привилегий — когда пользователь с ограниченными правами получает доступ с расширенными полномочиями, например права администратора, и может управлять всей системой. ¡ ¡ ¡ В прошлых главах этой книги мы разрабатывали ITAM-систему, при помощи которой можно оптимизировать ИТ-активы организации. В этом разделе мы попытаемся, используя методологию STRIDE и опираясь на помощь ChatGPT, промоделировать угрозы для этой системы. Начнем с подмены. NC Какие угрозы для ITAM-системы можно реализовать путем подмены? ChatGPT ответит примерно так: Листинг 8.1. C  hatGPT объясняет, как можно навредить нашей системе путем подмены Подмена MAC-адреса: в большинстве ITAM-систем для идентификации и управления устройствами в Сети используются MAC-адреса. Подменив MAC-адрес авторизованного устройства, злоумышленник может получить несанкционированный доступ или скрыть присутствие неавторизованного устройства.
   184 Глава 8. Разработка безопасных приложений с ChatGPT Подмена IP-адреса: маскируясь под доверенный IP-адрес, злоумышленник может обойти принятые в сети меры безопасности или перенаправить трафик. Это может привести к несанкционированному доступу к ITAM-системе, а также DDoS-атакам. Подмена DNS (отравление кэша): если для доступа к обновлениям или связанным с ними ресурсам в ITAM-системе используются доменные имена, подмена DNS может перенаправить ITAM-сервер на вредоносный сайт, что приведет к компрометации системы. Подмена идентификатора устройства: помимо MAC-адресов, устройства могут иметь и другие уникальные идентификаторы, подмена которых может позволить неавторизованным устройствам отображаться в реестре ITAM-системы как легитимные активы. Подмена пользовательского агента: если доступ к ITAM-системе осуществляется через веб-браузер, злоумышленник может подменить строку пользовательского агента (user agent string) и сымитировать авторизованное устройство или браузер, что может привести к обходу определенных проверок безопасности. Регистрация поддельных активов: злоумышленник может ввести в систему поддельные активы, что приведет к неточностям при отслеживании активов, искажению аналитики и появлению потенциальных лазеек в системе безопасности. Подмена сервера обновлений: если ITAM-система получает обновления с удаленного сервера, злоумышленник может подменить его в целях предоставления вредоносных обновлений или программного обеспечения. Фишинг и социальная инженерия: хотя технически это не подмена, злоумышленник может подделывать электронные письма или другие сообщения, выдавая себя за администраторов или поставщиков ITAM-системы. Ничего не подозревающие сотрудники могут предоставить ему конфиденциальные данные. Кроме того, ИИ может предложить несколько типовых методов предотвращения подмены, а также снижения ущерба от нее. Листинг 8.2. ChatGPT предлагает способы предотвращения этих угроз Сетевая безопасность: используйте сегментацию сети, мощные брандмауэры и системы обнаружения вторжений (IDS) для контроля и блокировки подозрительных действий. Шифрование: шифруйте данные при передаче. Это поможет защититься от атак через посредника и подделки данных. Динамическая идентификация: периодически меняйте идентификаторы устройств или используйте динамические идентификаторы. Защита DNS: внедрите расширения для защиты DNS (DNSSEC), которые обеспечивают подлинность ее ответов. Аутентификация и авторизация: требуйте многофакторной аутентификации (MFA) для доступа к ITAM-системе, обеспечьте строгий контроль доступа на основе ролей. Регулярные проверки: часто проводите проверки базы данных ITAM для выявления несоответствий или неавторизованных активов. Обучение пользователей: регулярно информируйте ИТ-специалистов и других сотрудников об опасности подделок и важности проверки каналов связи. Чтобы не перебирать все составляющие STRIDE, рассмотрим наименее очевидный и понятный из них: отрицание. NC Какие угрозы для ITAM-системы можно реализовать путем отрицания?
   8.1. Моделирование угроз с помощью ChatGPT 185 Возможный ответ ChatGPT: Листинг 8.3. C  hatGPT дает определение отрицания с точки зрения кибербезопасности Отказ от действий, или отрицание (Repudiation), в контексте кибербезопасности — это ситуация, когда пользователь (или система) может отрицать, что совершал определенное действие, даже если на самом деле оно было выполнено. В системах управления ИТ-активами (ITAM) отказ от действий может создавать серьезные проблемы с обеспечением ответственности, отслеживаемости и комплаенса. Далее ChatGPT перечисляет возможные риски, связанные с отрицанием: Листинг 8.4. П  отенциальные риски, связанные с отрицанием, по мнению ChatGPT Отрицание изменений актива: пользователь может внести несанкционированные изменения в состояние актива или сведения о нем, а затем отрицать факт внесения изменений. Это может привести к неточностям при отслеживании активов, потенциальным проблемам с соблюдением нормативных требований и эффективностью работы. Отсутствие подотчетности: если работа пользователей с ITAM-системой не регистрируется должным образом либо журналы можно незаметно изменить, сложно привлечь отдельных лиц к ответственности за их действия. Это может привести к несанкционированной установке программного обеспечения, неправильному распределению или нецелевому использованию активов без какой-либо ответственности. Финансовые расхождения: отрицание может привести к финансовым последствиям. Например, если сотрудник утверждает, что никогда не получал какое-то оборудование (например, ноутбук), а ITAM-система показывает, что он его получил, это может привести к ненужным дополнительным закупкам. Нарушения лицензий: пользователь может установить несанкционированное программное обеспечение, а затем отрицать, что сделал это. Отсутствие надлежащих мер по контролю и сбору доказательств о фактах установки такого ПО может привести к нарушению лицензий. Последствия для безопасности: внутренние или внешние злоумышленники могут использовать ITAM-систему для кражи или повреждения активов и отрицать любые правонарушения. Это может привести к утечке данных, краже конфиденциальной информации и даже финансовому мошенничеству. В дополнение ChatGPT предлагает типовые способы защиты от отрицания: Листинг 8.5. Возможные способы снизить риски отрицания Надежная аутентификация: получающие доступ к ITAM-системе пользователи должны проходить строгую, в идеале многофакторную аутентификацию, чтобы все действия были связаны с конкретными исполнителями. Тщательное протоколирование: необходимо внедрить систему протоколирования с защитой от несанкционированного доступа. Любые действия, изменения или транзакции в ITAM-системе должны регистрироваться с указанием времени и данных совершившего их пользователя.
   186 Глава 8. Разработка безопасных приложений с ChatGPT Цифровые подписи: важные действия, особенно связанные с финансовыми операциями или значительными изменениями активов, должны подтверждаться цифровой подписью. Это обеспечивает дополнительный уровень верификации и защиты от отрицания. Контроль доступа на основе ролей (RBAC): за каждым пользователем должна быть закреплена определенная роль и права. Ограничение доступа на основе должностных полномочий снижает риск несанкционированных действий и облегчает контроль за действиями конкретных пользователей. Регулярные аудиты: следует периодически просматривать и проверять журналы и списки транзакций в ITAM-системе. Это позволит выявить несоответствия или возможные попытки отрицания. Обучение пользователей: необходимо информировать пользователей ITAM-системы о важности подотчетности и последствиях несанкционированных действий. Это поможет уменьшить количество случаев непреднамеренного отрицания. Политика поддержки информаторов: сотрудники должны сообщать компетентным специалистам о любых подозрительных действиях, не опасаясь санкций или преследования. С развитием методов разработки развиваются и киберугрозы, с которыми можно столкнуться. В этом разделе мы рассмотрели возможности ChatGPT, которые делают его ценным помощником при выявлении и прогнозировании потенциальных угроз. Благодаря способности имитировать разговор, быстро анализировать огромные объемы данных и делать выводы на основе закономерностей, ChatGPT представляет собой уникальный перспективный инструмент, дополняющий традиционные методологии моделирования угроз. Мы кратко разобрали реальный пример работы с ChatGPT, который демонстрирует возможность практического применения ИИ для анализа реальных сценариев, потенциальную пользу и сложности его интеграции в процессы моделирования угроз. Хотя моделирование внешних угроз с помощью таких инструментов, как ChatGPT, может быть крайне полезным для снижения рисков или предотвращения ущерба для организации и ее данных, не менее важно обратить внимание и на внутреннюю часть нашей системы, чтобы выявить уязвимости внутри нее. Этим мы и займемся в следующем разделе. 8.2. Изучение структуры приложения и поиск потенциальных уязвимостей Сложность современных приложений иногда скрывает в себе неочевидные проблемы как в функциональности, так и в безопасности. Аналитические способности ChatGPT помогут нам глубже проанализировать структуру системы, не допуская свойственных человеку оплошностей и ошибок. Благодаря обширным знаниям и возможности распознавания паттернов ChatGPT может рассматривать приложение под определенным углом, выявлять уязвимости и предлагать действенные способы их устранения. В этом разделе поговорим о том, как с помощью ИИ
   8.2. Изучение структуры приложения и поиск потенциальных уязвимостей 187 критически оценивать структуру ПО и улучшать не только его функциональность и удобство в работе, но и устойчивость к потенциальным угрозам. 8.2.1. Выявление структурных проблем Анализ проекта ITAM-системы можно начать с такого промта, к которому следует приложить схему в формате Mermaid из главы 2: NC Оцени этот проект приложения и выяви его структурные недостатки. Я приведу несколько сокращенный перечень обнаруженных проблем (интересно, что ранее ChatGPT сам предлагал эти решения). Листинг 8.6. С  окращенный перечень выявленных ИИ структурных проблем Показанная на схеме классов в формате Mermaid структура системы представляет собой совокупность элементов, образующих систему управления ИТ-активами, рассчитанную на работу с активами разных типов и несколькими амортизационными политиками. Вот некоторые моменты, на которые следует обратить внимание: 1. Наследование, а не набор независимых классов: Политики StraightLineDepreciationStrategy, DecliningBalanceDepreciationStrategy, DoubleDecliningDepreciationStrategy и NoDepreciationStrategy в идеале должны быть классами-потомками DepreciationStrategy. Это обеспечит их соответствие одному и тому же интерфейсу. 2. Класс DepreciatingAsset: Определение класса DepreciatingAsset неполно. Возможно, он должен быть потомком класса Asset или иметь дополнительные атрибуты и методы. 3. Паттерн «Посетитель»: Класс LineOfBusinessVisitor, похоже, используется для реализации паттерна "Посетитель". Однако неясно, все ли его методы необходимы. Предполагается, что логика работы посещаемых объектов (Hardware, Software, DepreciatingAsset) будет разной? Такой далеко не полный список послужит хорошей отправной точкой для улучшения нашего проекта. 8.2.2. Распознавание типовых уязвимостей В этом разделе рассмотрим, как ChatGPT благодаря своей обширной базе знаний и способности распознавать паттерны помогает находить скрытые слабые места и потенциальные угрозы в проектировании программных систем. Это стало настоящим прорывом в области проактивной кибербезопасности. Мы зададим ему вопрос: NC Есть ли в этом проекте уязвимости или потенциальные эксплойты?
   188 Глава 8. Разработка безопасных приложений с ChatGPT ChatGPT выдает довольно длинный список проблем, из которого я взял лишь самые важные: Листинг 8.7. С  окращенный перечень уязвимостей и эксплойтов, найденных ChatGPT 1. Защита с помощью паролей: В классе User есть поле для хранения пароля. По соображениям безопасности важно не хранить пароли в виде обычного текста. Следует применить механизм хеширования (например, bcrypt) и хранить не сами пароли, а только их хеши. 2. Контроль доступа на основе ролей (RBAC): Несмотря на наличие класса Role, который, похоже, реализует принципы RBAC при помощи списка разрешений, нужно внедрить эти принципы на уровне всей системы, надлежащим образом использовать роли и разрешения. Особое внимание следует уделить функциям и методам, предназначенным для изменения ролей и разрешений. Можно продолжить расспросы и попросить ИИ рассказать о том, как лучше всего внедрить эти технологии. Итак, мы видим, что ChatGPT может помочь в поиске структурных проблем нашей системы. ИИ — это мощный инструмент, но как уже отмечалось, он не заменяет, да и не в состоянии заменить опытного специалиста. Лучше всего использовать оба ресурса: вместе ИИ и люди сделают наш сетевой мир безопаснее. 8.3. Применение рекомендаций по безопасности Этот раздел, от начала и до конца, представляет собой практическое руководство по обеспечению безопасности программной системы. Прежде всего поговорим о том, как важно постоянно думать о безопасности, затем — о том, как необходимы регулярные проверки безопасности, а также о том, какие они бывают. Обсудим и правильный подход к безопасности, и инструменты, которые помогают ее обеспечить. 8.3.1. Формирование культуры безопасной разработки Безопасность — это не просто набор инструментов или следование последним протоколам. Это особый способ мышления, или культура разработки, когда безопасность становится частью каждой вашей задачи, решения и стратегии. В этом разделе мы разберем основные принципы такого подхода — почему важна постоянная внимательность, гибкость и готовность учиться в условиях современных киберугроз. ChatGPT может стать отличным помощником в обучении команды основам безопасности. Вы можете использовать его как интерактивный инструмент
   8.3. Применение рекомендаций по безопасности 189 для разъяснения базовых понятий, например, что такое XSS-атака и как она работает. Также ChatGPT поможет при планировании реагирования на инциденты: например, можно спросить, с чего начать, если сервер был скомпрометирован. Кроме того, с его помощью можно быстро получить рекомендации по написанию безо­пасного кода и созданию внутренних руководств. Мы можем попросить ChatGPT: NC Дай рекомендации по безопасному программированию на Python. В ответ он предоставит достаточно длинный список правил, в том числе посоветует проверять достоверность ввода, защищать библиотеки и безопасно хранить пароли. ИИ может дать подобные рекомендации для работы с конкретными приложениями: NC Как безопасно выполнить развертывание веб-приложения Flask? И хотя есть множество способов выполнить эту задачу, ChatGPT тщательно подбирает самые важные: Листинг 8.8. С  окращенный список советов ChatGPT по поводу безопасности Flask Безопасное развертывание веб-приложения Flask требует многогранного подхода, который должен охватывать все аспекты - от методов обеспечения безопасности на уровне кода до настройки сервера и сети. Вот рекомендуемые действия: 1. Используйте последнюю версию Flask: всегда используйте последнюю версию Flask, поскольку в ней содержатся исправления для известных уязвимостей. 2. Не запускайте реально работающее приложение в режиме отладки (app. debug=True): в режиме отладки возможно раскрытие конфиденциальной информации. 3. Используйте безопасный веб-сервер: запускайте приложение Flask только на серверах, готовых к реальной работе, к примеру Gunicorn, uWSGI или mod_wsgi. Это надежные серверы, способные обрабатывать реальный трафик и оснащенные лучшими средствами безопасности. 4. Внедрите HTTPS: используйте протокол SSL/TLS для шифрования данных при передаче между сервером и клиентом. Для получения бесплатного SSL-сертификата обратитесь, к примеру, к Let's Encrypt. Имея некоторое представление о безопасной разработке Python-приложений, и в частности микросервисов на основе Flask, мы можем приступить к проверке приложения на безопасность.
   190 Глава 8. Разработка безопасных приложений с ChatGPT 8.3.2. Непрерывное тестирование безопасности В этом разделе мы посмотрим, как обширная база знаний ChatGPT помогает быстрее принимать решения, лучше понимать возможности разных инструментов и эффективнее выстраивать процесс тестирования безопасности приложения. Сначала разберемся, чем отличается статический анализ кода от динамического. А потом используем ChatGPT, чтобы подобрать нужный инструмент и понять, как его внедрить в процесс. Статический анализ безопасности приложений (SAST) — это процесс, при котором исследуется исходный код, байт-код или исполняемый файл приложения без его запуска. Цель — находить уязвимости на ранних этапах разработки, анализируя сам код и выявляя потенциально опасные участки. Такой подход позволяет исправлять ошибки до того, как приложение вообще будет запущено, и обнаруживать проблемы, которые могли бы остаться незамеченными при тестировании в реальном времени. Часто инструменты SAST встраиваются прямо в среду разработки, чтобы разработчики могли получать обратную связь сразу и быстрее устранять уязвимости. Динамический анализ безопасности приложений (DAST) — это процесс поиска уязвимостей в приложении уже во время его работы, обычно со стороны внешнего «злоумышленника». В отличие от SAST, здесь анализируется не код, а поведение приложения в реальном времени. DAST позволяет обнаружить проблемы, которые могут проявиться только в процессе исполнения, например ошибки в аутентификации, управлении сессиями, валидации данных и других механизмах. Инструменты DAST симулируют атаки на работающее приложение и анализируют его реакции, что особенно полезно для поиска уязвимостей, которые могут эксплуатироваться в реальных условиях. Разобравшись в отличиях этих инструментов, спросим у ChatGPT, какие из них лучше подходят для нашего приложения: NC Что лучше использовать для проверки Python-приложения на основе Flask: SAST или DAST? ChatGPT сравнивает два подхода и дает рекомендацию: Листинг 8.9. С  окращенный ответ ChatGPT о подходах к тестированию приложения Для Python-приложений на базе Flask выгодно использовать оба варианта. Начните с SAST, чтобы выявить потенциальные уязвимости на ранней стадии, еще во время разработки. На этапе тестирования и развертывания используйте DAST. Это позволит найти уязвимости, которые могут быть заметны только во время работы приложения.
   8.3. Применение рекомендаций по безопасности 191 По сути, совместное применение SAST и DAST обеспечивает более полную оценку безопасности и гарантирует надежную работу Flask-приложения. Понятно! Мы должны использовать оба метода и начать с SAST. А какой инструмент или библиотеку выбрать? NC Порекомендуй SAST-инструменты для проверки веб-приложения на Python. Листинг 8.10. Рекомендации по выбору SAST-инструмента Выявить уязвимости и найти небезопасный шаблонный код в веб-приложении на Python можно при помощи несколько SAST-инструментов. Вот некоторые рекомендации: Bandit: SAST-инструмент с открытым исходным кодом, разработанный сообществом OpenStack, Bandit сканирует код на Python и выявляет в нем типовые проблемы безопасности. Легко интегрируется в конвейер CI/CD и может настраиваться на поиск конкретных проблем. Pyre: разработанный Facebook, Pyre - мощный инструмент для проверки типов, который также способен находить и отмечать проблемы безопасности в коде на Python. Рассчитан на масштабирование и может работать с большими фрагментами кода. Semgrep: универсальный инструмент для анализа кода, без дополнительных модулей работает с Python и множеством других языков. Работает на основе правил, которые можно настраивать под разные проверки, включая типовые проверки безопасности. Доступно множество правил, созданных сообществом. Checkmarx: коммерческий инструмент, который поддерживает множество языков, включая Python. Позволяет проводить комплексные SAST-проверки, легко интегрируется в жизненный цикл разработки. Идея использовать инструмент SAST, который можно встроить в наш CI/ CD-конвейер, выглядит очень привлекательно. Так мы сможем гарантировать, что важный этап проверки безопасности не будет пропущен или проигнорирован — он будет встроен прямо в процесс развертывания. Но как с ним работать? Спросим у ChatGPT: NC Как интегрировать Bandit в проект? Листинг 8.11. C  hatGPT предлагает инструкции по установке и применению Bandit Bandit - это инструмент для поиска типовых проблем безопасности в коде на Python. Встроить его в проект совсем не сложно. Вот пошаговая инструкция: Установка: Установите Bandit при помощи pip:
   192 Глава 8. Разработка безопасных приложений с ChatGPT pip install bandit Запуск: Чтобы запустить Bandit, перейдите в корневую папку Python-проекта и выполните команду: bandit -r . Флаг -r дает команду Bandit выполнить рекурсивное сканирование всех файлов в указанной папке. Символ . указывает на текущую папку. При необходимости можно заменить . на нужный путь. Настройка: Для настройки Bandit можно использовать файл .bandit. Настройки позволяют исключить из проверки определенные тесты, папки и т. д. Например, чтобы исключить из проверки одну из папок, используйте команду: bandit -r . --exclude /path/to/excluded/dir Как можно заметить, ChatGPT отлично справляется с порученными заданиями: может помочь с результатами проверки или же разобраться в том, как действовать дальше. Но все-таки мы должны помнить: ИИ — дополнение к мнению экспертов, а не его замена. Особенно это важно в таких критических вопросах, как безопасность. 8.4. Шифрование данных при передаче и хранении В этом разделе мы снова прибегнем к знаниям и аналитическим возможностям ChatGPT и спросим его мнение об эффективном шифровании данных. ИИ готов дать совет любому: хоть новичку, хоть эксперту. Попробуем с его помощью улучшить приложение с точки зрения безопасности данных. 8.4.1. Важность шифрования данных Недостаточная защита данных в покое (at rest), то есть данных, которые хранятся (а не передаются по Сети), создает значительные риски. Незашифрованные данные легко читаются и становятся легкой добычей для злоумышленников. Если хакеры получат доступ к системам или сетям, где такие данные хранятся, они смогут без препятствий извлечь ценную информацию. Для бизнеса утечка финансовых данных, например данных банковских карт, может обернуться крупными убытками — как напрямую от кражи, так и через штрафы и иски от регулирующих органов. Во многих странах и отраслях действуют строгие законы по защите данных. Например, GDPR в ЕС и CCPA в Калифорнии. Нарушение требований вроде отсутствия шифрования может привести к серьезным штрафам и судебным разбирательствам. Если данные не шифруются, злоумышленники могут не только их читать, но и менять, что ведет к фальсификации информации, повреждению данных
   8.4. Шифрование данных при передаче и хранении 193 или даже целенаправленному вреду, например отравлению датасета (dataset poisoning). Утечка незашифрованных данных может нанести бизнесу огромный репутационный ущерб: падение доверия клиентов, партнеров, финансовые потери, судебные иски. Важно помнить, что шифрование — это лишь один слой защиты. Настоящая безопасность строится на комплексном подходе: контроль доступа, регулярные аудиты, надежные бэкапы, обучение сотрудников и другие меры. Один из печально известных примеров халатного обращения с данными — взлом сайта знакомств Ashley Madison в 2015 году. Тогда были слиты личные данные 37 миллионов пользователей: имена, адреса и прочее. Это привело к массовым скандалам, судебным искам и огромному удару по репутации компании. Такие инциденты наглядно показывают: отсутствие защиты данных чревато не только финансовыми потерями, но и разрушением репутации, потерей клиентов и юридическими проблемами. 8.4.2. Шифрование данных при хранении Шифрование — один из краеугольных камней защиты данных, и выбор правильного алгоритма здесь критически важен. Методы шифрования могут быть довольно сложными, но ChatGPT может помочь разобраться: объяснить базовые понятия вроде симметричного и асимметричного шифрования, различия между блочными и потоковыми шифрами, а также как работает инфраструктура открытых ключей (PKI). ChatGPT может рассказать о популярных и признанных в индустрии алгоритмах, например AES (Advanced Encryption Standard), RSA (Rivest–Shamir–Adleman), ECC (Elliptic Curve Cryptography) и других, — в чем их плюсы, возможные уязвимости и для каких случаев каждый подходит лучше всего. В этом разделе мы вместе с ChatGPT посмотрим, как можно безопасно хранить чувствительные данные приложения в базе данных. Начнем, как обычно, с довольно общего вопроса: NC Как зашифровать чувствительные данные на Python? ChatGPT объяснит, как происходит шифрование с использованием криптографической библиотеки, покажет на примере, как зашифровать, а потом и расшифровать текст. Листинг 8.12. ChatGPT рассказывает о шифровании данных на Python Шифрование чувствительных данных на Python осуществляется при помощи специальных криптографических библиотек. При этом обычный текст превращается в зашифрованный и может быть прочтен только тем, у кого есть правильный ключ для дешифровки. Одна из самых популярных библиотек для шифрования на Python - cryptography.
   194 Глава 8. Разработка безопасных приложений с ChatGPT Шифрование данных from cryptography.fernet import Fernet # Генерация ключа key = Fernet.generate_key() cipher_suite = Fernet(key) # Конфиденциальные данные plaintext = b"Мои конфиденциальные данные" # Шифрование ciphertext = cipher_suite.encrypt(plaintext) print(f"Зашифрованные данные: {ciphertext}") Расшифровка данных # Используется тот же ключ, что и при шифровании decrypted_text = cipher_suite.decrypt(ciphertext) print(f"Расшифрованные данные: {decrypted_text.decode('utf-8')}") Помимо примера ChatGPT, скорее всего, даст несколько замечаний и преду­ преждений: Листинг 8.13. ChatGPT рассказывает о лучших подходах к шифрованию Всегда храните ключ в безопасном месте. Любой, кто имеет доступ к ключу, может расшифровать зашифрованные данные. Избегайте жесткого кодирования ключа в скрипте. Вместо этого следует использовать безопасные механизмы для хранения и получения ключей. В приведенном выше примере показано симметричное шифрование, когда один и тот же ключ используется и для шифрования, и для дешифрования. Для повышения уровня безопасности могут применяться и другие методы: хеширование (для хранения паролей), шифрование с открытым и закрытым ключом (асимметричное шифрование) и другие. Убедитесь в том, что хорошо понимаете достоинства и недостатки выбранного метода. Это крайне важно, особенно при работе с конфиденциальными или личными данными. Теперь узнаем у ИИ, как применить шифрование к данным, хранящимся в базе данных: NC Как шифрование используется при хранении информации в базе данных? ChatGPT начнет ответ с общих рекомендаций и лучших практик, а затем приведет конкретный пример. Листинг 8.14. Рекомендации ChatGPT по шифрованию в базе данных Хранение секретной информации в базе данных в зашифрованном виде распространенная практика, которая гарантирует безопасность даже в случае взлома базы: без ключа шифрования прочесть информацию не удастся. Вот пошаговая инструкция:
   8.4. Шифрование данных при передаче и хранении 195 1. Управление ключом: - Сгенерируйте ключ при помощи функции 'Fernet.generate_key()'. - Сохраните ключ в безопасном месте, к примеру в аппаратном модуле безопасности (HSM), в доверенном сервисе для управления ключами (AWS KMS, Azure Key Vault и т. п.) или же в переменных окружения (для локальных проектов). Никогда не храните ключ в базе данных и не прописывайте в коде. 2. Шифрование: - Перед сохранением в базе данные следует зашифровать при помощи библиотеки cryptography, как это делалось ранее. 3. Взаимодействие с базой данных: - Используйте подходящую библиотеку для подключения к базе данных (sqlite3, psycopg2 для PostgreSQL, PyMySQL для MySQL и т. д.), чтобы сохранить зашифрованные данные в базе. - При извлечении зашифрованных данных из базы расшифруйте их при помощи сохраненного ранее ключа. 4. Расшифровка: - Чтобы прочитать секретные данные, извлеките их из базы данных (в зашифрованном виде) и расшифруйте при помощи ключа. Закончив с описанием лучших практик, ChatGPT предлагает пример шифрования и расшифровки информации, предназначенной для хранения в базе данных. В примере ИИ применяет SQLite3. Не потому, что она чем-то лучше других, — просто так вышло. В ответах модели исходят из множества не всегда очевидных факторов. Но мы бы могли прямо сказать: используй в примере PostgreSQL (или какую-то другую, нужную нам СУБД). Листинг 8.15. C  hatGPT приводит пример записи и получения данных из SQLite3 import sqlite3 from cryptography.fernet import Fernet # Управление ключом key = Fernet.generate_key() # В идеале следует безопасно сохранить ключ и получать его при необходимости cipher_suite = Fernet(key) # Шифрование plaintext = b"Мои конфиденциальные данные" ciphertext = cipher_suite.encrypt(plaintext) # Создание базы данных и таблицы conn = sqlite3.connect('sensitive_data.db') cursor = conn.cursor() cursor.execute('''CREATE TABLE IF NOT EXISTS secure_data (id INTEGER PRIMARY KEY, data BLOB)''') conn.commit() # Сохранение зашифрованных данных в таблице
   196 Глава 8. Разработка безопасных приложений с ChatGPT cursor.execute("INSERT INTO secure_data (data) VALUES (?)", (ciphertext,)) conn.commit() # Получение и расшифровка cursor.execute("SELECT data FROM secure_data WHERE id=?", (1,)) encrypted_data = cursor.fetchone()[0] decrypted_data = cipher_suite.decrypt(encrypted_data) print(decrypted_data.decode('utf-8')) conn.close() В завершение ChatGPT скажет несколько важных вещей, которые нужно учитывать при защите данных во время хранения. Листинг 8.16. Важные аспекты по мнению ChatGPT - При проектировании промышленных систем важно подумать о применении специализированных ORM: SQLAlchemy или ORM Django – для работы с базой данных. - Всегда создавайте резервные копии ключей шифрования. Потеря ключа означает потерю возможности расшифровки данных. - Подумайте о дополнительных уровнях безопасности, например о хешировании паролей и регулярной ротации ключей шифрования. - Убедитесь в том, что остальные части приложения также защищены. Шифрование защищает данные при хранении, но следует защитить их при передаче (к примеру, при помощи HTTPS), а также при вводе (во избежание SQL-инъекций и других уязвимостей). И снова я хотел бы напомнить: да, ChatGPT может многое рассказать о шифровании и предоставить подробные рекомендации. Но крайне важно не полагаться только на них. Необходимо прислушиваться к мнению экспертов, учитывать современные отраслевые стандарты. И это особенно важно, если в работе критически важное приложение. Мир шифрования сложен и находится в постоянном движении. Растут вычислительные возможности, каждый день появляются новые уязвимости, а потому постоянное обновление информации имеет первостепенное значение. 8.4.3. Шифрование данных при передаче Как разработчики, мы отвечаем не только за создание эффективных и функцио­ нальных приложений, но и за то, чтобы они были защищены от возможных угроз. В этом разделе разберем, как с помощью ChatGPT можно анализировать, оценивать и усиливать безопасность ваших REST-эндпоинтов. Используя обширные знания и аналитические возможности ChatGPT, мы изучим проактивные меры защиты, лучшие практики и решения для защиты ваших API в реальном времени. Мы вместе пройдем этот путь — объединим возможности
   8.4. Шифрование данных при передаче и хранении 197 ИИ и современные протоколы безопасности, чтобы создавать более надежные и защищенные приложения. Попросим ChatGPT совета: как зашифровать данные «на лету», расспросим его о сквозном шифровании трафика между REST-контроллером и браузером. Воспользуемся следующим промтом: NC Как защитить REST-эндпоинты в приложении на основе Flask? Листинг 8.17. ChatGPT рассказывает, как обезопасить приложение REST Защита конечных точек (эндпоинтов) REST в приложении на основе Flask очень важна для безопасности как самого приложения, так и передаваемых данных. Вот пошаговая инструкция, которая повысит безопасность REST API: 1. Используйте HTTPS: Приложение должно работать по протоколу HTTPS. Это обеспечит шифрование данных при передаче, поможет предотвратить атаки через посредника. Для этого можно воспользоваться библиотеками типа Flask-Talisman или настроить обратный проксисервер на Nginx/Apache. 2. Аутентификация: Организуйте аутентификацию на основе токенов (например, JWT) при помощи библиотеки Flask-JWT или Flask-JWT-Extended. Если же приложение предназначено для работы в сторонних организациях, используйте OAuth, чтобы предоставить пользователям ограниченный доступ к информации без раскрытия их учетных данных. 3. Авторизация: Убедитесь в том, что аутентифицированные пользователи могут получить доступ только к тем ресурсам, к которым они имеют права. Например, один пользователь не должен иметь возможности удалять данные другого, если на то нет специального разрешения. 4. Ограничение количества запросов: Используйте расширения типа Flask-Limiter, чтобы ограничить количество запросов клиента к API, поступающих за определенный период времени. 5. Проверка данных: Всегда проверяйте входные данные, чтобы избежать инъекций, неожиданного поведения или сбоев. Используйте для этого Marshmallow или Flask-Inputs. 6. Не допускайте раскрытия отладочных данных: Убедитесь в том, что приложение Flask не работает в отладочном режиме (app. debug=False). Это предотвратит раскрытие потенциально важной информации, которая может содержаться в сообщениях об ошибках. В реальной практике мы бы продолжили работу над защитой передачи данных к нашим Kafka-топикам и обратно (хотя на этом этапе стоило бы уже привлечь специалиста по безопасности). Еще один важный момент: работа с безопасностью всегда связана с компромиссами. И важно осознанно подходить к тому, какие из них допустимы. Например,
   198 Глава 8. Разработка безопасных приложений с ChatGPT при реализации шифрования нужно учитывать возможные последствия — влияние на производительность, рост сложности и увеличение затрат на управление. Шифрование и дешифрование требуют вычислительных ресурсов. Особенно при использовании алгоритмов с высоким уровнем защиты нагрузка на систему может быть ощутимой, и это нужно учитывать при планировании инфраструктуры. Кроме того, практически неизбежно увеличение задержек. В реальном времени, например в потоковых сервисах или голосовых приложениях, может ощущаться задержка из-за дополнительных операций шифрования, передачи, дешифрования и обработки данных. Шифрование — крайне важный элемент защиты данных, но его внедрение требует тщательной подготовки. Многие сложности можно минимизировать с помощью грамотного планирования, оптимизированных инструментов и соблюдения лучших практик. И хотя нагрузка на систему возрастает, в условиях современной цифровой экономики это усилие почти всегда оправданно. Итоги Мы узнали, как использовать знания ChatGPT для выявления потенциальных угроз, анализа сценариев атак, поиска уязвимостей и оценки структуры приложений с учетом лучших практик. Интерактивные вопросы и ответы полезны, они помогают понять типовые недостатки и уязвимости приложений и применять стандартные методы обеспечения безопасности при разработке программного обеспечения. Мы получили рекомендации по безопасной разработке, выбору подходящих алгоритмов шифрования и оценке связанных с ними компромиссов. Также вы познакомились с тем, как использовать ChatGPT совместно с инструментами статического и динамического анализа для всесторонней оценки безопасности, расшифровки результатов тестирования и получения советов по устранению проблем, формируя в команде культуру безопасности.
9 GPT без интернета В этой главе 3 3 Запуск LLM на локальном компьютере 3 3 Сравнение результатов работы локальных моделей с ChatGPT 3 3 Область применения локальных моделей Представьте: вы летите на конференцию по искусственному интеллекту где-то на другой конец света. Самолет набрал высоту 11 тысяч метров, а вам срочно нужно набросать прототип новой функции для вашего приложения. Wi-Fi на борту стоит дорого и работает из рук вон плохо. А что, если не платить за эту унылую пародию на интернет, а запустить полноценный GPT прямо на своем ноутбуке — полностью офлайн? В этой главе мы разберем, какие у разработчиков есть варианты для локального запуска большой языковой модели. 9.1. Практическая необходимость Такой сценарий — совсем не фантазия. Хотя скоростной интернет становится все более доступным, он все еще покрывает не весь мир. Вам точно будут встречаться места без стабильного подключения — дома, в пути, в офисе или в учебных заведениях. Надеюсь, эта книга уже убедила вас, что LLM — незаменимый инструмент в арсенале разработчика. А значит, нужно заранее позаботиться о том, чтобы всегда иметь возможность работать с языковой моделью — даже без интернета. Чем больше вы ее используете, тем эффективнее она помогает. Как и с любой
   200 Глава 9. GPT без интернета IDE: без нее вы — отличный разработчик, но с ней — гораздо быстрее и продуктивнее. Но не переживайте: есть вполне доступные варианты. В этой главе мы рассмотрим два подхода, которые не потребуют от вас сложной установки или зубрежки новых API. Все будет примерно так же, как при работе с ChatGPT, — пригодятся ваши навыки написания промтов. Готовы? Тогда начнем. 9.2. Разворачиваем LLM на своем компьютере Когда мы задумываемся о запуске LLM на локальной машине, сразу всплывают две проблемы. Первая — это требуемые вычислительные ресурсы. Большинство языковых моделей нуждаются в мощных GPU. Стоимость такого оборудования может быть весьма высокой. Кроме того, сами модели очень большие и требуют много памяти для загрузки и работы. Даже на продвинутых системах с хорошими видеокартами может не хватить виртуальной памяти для полноценной работы модели. Вторая проблема — качество результатов локальных моделей по сравнению с управляемыми решениями вроде ChatGPT. Модели, которые мы обсудим в этой главе, выбраны мной не случайно. Эти модели работают на обычных пользовательских компьютерах, например на MacBook Pro с чипом M2, — именно на таком писалась эта книга. Начнем с Llama 2, разработанной компанией Meta. Модель обучалась на 2 триллионах токенов и существует в вариантах на 7, 13 и 70 миллиардов параметров. Установка Llama 2 локально может вызвать трудности, но, к счастью, существует ее версия на базе Docker — Ollama, с которой мы и будем работать в первой части главы. Во второй части мы познакомимся с GPT-4All. Остается решить вторую проблему. Для этого мы сравним результаты работы этих ИИ с ответами ChatGPT, приняв их за базовый уровень. И забегая вперед, скажу: локальные модели покажут себя с очень выгодной стороны. 9.2.1. ChatGPT как образец В этой главе рассмотрим необычную (по крайней мере, для этой книги) задачу: вычисление среднеквадратичного отклонения для набора целых чисел — величины, которая показывает разброс какого-то набора значений. Составив промт, мы отправим его выбранным моделям. NC Ты — математик, которому нужно вычислить среднеквадратичное отклонение для набора чисел, используя для этого чистый Python. Напиши нужный для этого код и подробно прокомментируй его. ChatGPT предлагает следующее решение и объяснение к нему.
   9.2. Разворачиваем LLM на своем компьютере 201 Листинг 9.1. ChatGPT объясняет расчет среднеквадратичного отклонения Чтобы найти среднеквадратичное отклонение для набора чисел, используя чистый Python, потребуется выполнить несколько действий. Рассмотрим их одно за другим. 1. Посчитать среднее арифметическое, то есть сложить все числа из списка и разделить сумму на количество этих чисел. 2. Найти квадраты отклонения от среднего, то есть вычесть из каждого числа среднее арифметическое и возвести разность в квадрат. Этот этап очень важен, поскольку полученные значения входят в формулу для расчета среднеквадратичного отклонения. 3. Вычислить среднее арифметическое квадратов отклонений. 4. Взять корень квадратный из среднего арифметического квадратов отклонений. Вот код на Python, который можно использовать для расчета: def std_dev_chat_gpt_attempt_1(lst): # Этап 1: определить среднее арифметическое mean = sum(lst) / len(lst) # Этап 2: найти квадраты отклонения от среднего squared_diffs = [(x - mean) ** 2 for x in lst] # Этап 3: вычислить среднее арифметическое квадратов отклонений mean_squared_diffs = sum(squared_diffs) / len(lst) # Этап 4: взять корень квадратный из среднего квадратичных отклонений std_dev = mean_squared_diffs ** 0.5 return std_dev Написав небольшую функцию main, мы можем вычислить среднеквадратичное отклонение для целых чисел от 1 до 5. Получится 1,4142135623730951 или же что-то близкое к этому. Желающие могут это проверить, вооружившись калькулятором, программой для научных расчетов или другим подобным инструментом. Листинг 9.2. Функция main для демонстрации расчета if __name__ == '__main__': numbers = [1, 2, 3, 4, 5] print(std_dev_chat_gpt_attempt_1(numbers)) # Должно получиться 1.4142135623730951 Итак, теперь у нас есть с чем сравнивать. Давайте начнем.
   202 Глава 9. GPT без интернета 9.2.2. Спрашиваем у Llama 2 Пора познакомиться с первым из наших локальных ИИ — Llama 2. Как уже говорилось, установить его на компьютер непросто (по крайней мере так было, когда я писал эту книгу). Поэтому для оценки лучше взять контейнеризированный вариант этой модели — Ollama. Чтобы ее запустить, используем следующие команды Docker: Листинг 9.3. Запуск и работа с Ollama docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama docker exec -it ollama run llama2 Если все заработает правильно, вы (рано или поздно) увидите приглашение из трех угловых скобок (>>>). При первом выполнении этой команды Ollama загрузит модель Llama 2 объемом несколько гигабайт, а это потребует как времени, так и стабильного выхода в интернет. В дальнейшем он ей не понадобится. И все же, прежде чем отправляться куда-то, где его нет, дождитесь конца загрузки. Попросим свежеустановленную модель представиться: NC Ты пишешь книгу о генеративном ИИ. Сейчас ты работаешь над главой о локально запускаемых LLM. Что ты можешь рассказать о Llama? Листинг 9.4. Llama 2 рассказывает о себе Как автор книги о генеративном ИИ, я буду рада рассказать читателям о Llama, мощной и универсальной языковой модели, которую можно запускать локально на своем компьютере. Llama - это библиотека с открытым исходным кодом, созданная компанией Meta AI. Она позволяет пользователям с легкостью обучать и развертывать большие языковые модели (LLM). У Llama простой, интуитивно понятный интерфейс, так что пользователь может сосредоточиться на архитектуре модели и процессе обучения, не увязая в сложных подробностях реализации. При помощи Llama разработчики могут легко создавать, обучать и оценивать LLM на локальном компьютере, что делает ее идеальным инструментом для исследователей, студентов и любителей. Одним из ключевых преимуществ Llama является способность поддерживать широкий спектр языковых моделей, включая популярные архитектуры BERT, RoBERTa и DistilBERT. Благодаря этому пользователи могут экспериментировать с разными моделями и стратегиями обучения, не беспокоясь о проблемах совместимости. Еще одним важным достоинством Llama является способность решать крупномасштабные задачи, связанные с обработкой языка: классифицировать тексты, анализировать тональность, выполнять машинный перевод. При помощи Llama разработчики могут обучать и развертывать модели, способные понимать естественный язык, и создавать приложения, в которых востребованы эти способности.
   9.2. Разворачиваем LLM на своем компьютере 203 Помимо функциональности, Llama отличается надежной и расширяемой архитектурой, благодаря чему ее легко интегрировать в различные проекты или рабочие процессы. Поэтому Llama - отличный выбор для разработчиков, которые хотят создавать собственные NLP-приложения, не начиная при этом с нуля. В целом Llama - это бесценный инструмент для всех, кто хочет работать с LLM локально. Простота применения, гибкость и поддержка широкого спектра языковых моделей делают ее идеальным выбором как для исследователей, студентов и любителей, так и для профессионалов. Как автор книги о генеративном ИИ, я настоятельно рекомендую изучить Llama и ее возможности. После знакомства мы можем приступить к оценке ответов Llama 2, сравнив их с базовым и проверенным вариантом от ChatGPT. Используем тот же промт, что и в прошлом пункте: NC Ты — математик, которому нужно вычислить среднеквадратичное отклонение для набора чисел, используя для этого чистый Python. Напиши нужный для этого код и подробно прокомментируй его. Листинг 9.5. Р  асчет среднеквадратичного отклонения. Первая попытка Llama 2 Конечно! Расчет среднеквадратичного отклонения для набора чисел на Python – несложная задача, которую можно решить при помощи модуля stats. Сначала код: import statsmodels.api as sm # Набор чисел для вычисления numbers = [1, 2, 3, 4, 5] # Применение модуля stats для вычисления среднеквадратичного отклонения std_dev = sm.stats.stdev(numbers) # Вывод результата print("Среднеквадратичное отклонение для {0}: {1}".format(numbers, std_dev)) ''' Проанализируем этот код: 1. 'import statsmodels.api as sm': эта строка импортирует модуль statsmodels, который предоставляет удобный интерфейс для вычисления статистических величин в Python. Слово 'api' в операторе импорта предписывает Python использовать этот модуль в качестве основного интерфейса для статистических вычислений. 2. 'numbers = [1, 2, 3, 4, 5]': эта строка определяет набор чисел, стандартное отклонение для которых нам нужно вычислить. 3. 'std_dev = sm.stats.stdev(numbers)': эта строка использует функцию stdev из модуля statsmodels для вычисления среднеквадратичного отклонения. Она принимает список чисел в качестве параметра и возвращает результат вычисления.
   204 Глава 9. GPT без интернета 4. 'print("Среднеквадратичное отклонение для {0}: {1}".format(numbers, std_ dev))': эта строка выводит результат расчета на экран в виде форматированной строки. Первый аргумент метода format – имя списка (в данном случае numbers), а второй – значение среднеквадратичного отклонения. Вот и все! Эти четыре строки кода можно использовать для расчета среднеквадратичного отклонения для набора чисел на чистом Python с модулем statsmodels. Возможно, в реальном мире мы так бы и поступили (молодец, Llama 2). Но ­такой ответ не подходит нам для сравнения. Поэтому нужно уточнить вопрос: NC Не используй библиотеки. Только встроенные функции Python. Технически это, конечно же, не вопрос, но вдруг ей это поможет? Листинг 9.6. Р  асчет среднеквадратичного отклонения. Вторая попытка Llama 2 Конечно! Вот как найти среднеквадратичное отклонение для набора чисел, используя только встроенные функции Python: '''python def stdev(numbers): n = len(numbers) sum = 0 for i in range(n): sum += numbers[i]**2 return math.sqrt(sum / (n - 1)) numbers = [1, 2, 3, 4, 5] std_dev = stdev(numbers) print("Среднеквадратичное отклонение для {0}: {1}".format(numbers, std_dev)) ''' Я объясню, как работает этот код: 1. 'def stdev(numbers):': эта строка объявляет функцию под названием stdev, которая принимает список чисел в качестве параметра. 2. 'n = len(numbers)': эта строка вычисляет длину полученного списка при помощи функции len(). 3. 'sum = 0': эта строка инициализирует переменную sum. Изначально она равна 0. 4. 'for i in range(n):': эта строка начинает цикл for, который перебирает элементы полученного списка. 5. 'sum += numbers[i]**2': эта строка прибавляет к значению переменной sum квадрат i-го числа из списка. Оператор '**2' возводит число в степень 2, то есть в квадрат.
   9.2. Разворачиваем LLM на своем компьютере 205 6. 'return math.sqrt(sum / (n - 1))': эта строка находит среднеквадратичное отклонение для списка как корень квадратный из значения переменной sum, деленного на n–1, где n – длина исходного списка. Функция math.sqrt() вычисляет квадратный корень. 7. 'print("Среднеквадратичное отклонение для {0}: {1}".format(numbers, std_ dev))': Эта строка выводит результат расчета на экран в виде форматированной строки. Первый аргумент метода format – имя списка (в данном случае numbers), а второй – значение среднеквадратичного отклонения. Вот и все! Эти семь строк кода можно использовать для расчета среднеквадратичного отклонения для набора чисел при помощи только встроенных функций Python. Внимательные читатели заметили (а кто не заметил — ничего страшного), что Llama 2 вычла единицу из количества элементов списка. Забавно, но этого не было в ответе ChatGPT. Попробуем разобраться: NC По какой формуле вычисляется среднеквадратичное отклонение? Листинг 9.7. Мнение Llama 2 о формуле среднеквадратичного отклонения Вот формула для вычисления среднеквадратичного отклонения (SD) для набора чисел: SD = √(Σ(xi - μ)^2 / (n - 1)) Где: * SD – среднеквадратичное отклонение для набора чисел * μ – среднее арифметическое набора чисел * xi – отдельные числа из набора * n – количество чисел в наборе * Σ – математический оператор, который показывает, что вычисляется сумма квадратов разностей отдельных чисел и среднего арифметического. Иными словами, среднеквадратичное отклонение показывает разброс чисел относительно среднего арифметического и равно корню квадратному из среднего расстояния от каждого из чисел до среднего значения, деленного на количество чисел за вычетом единицы. Этот код соответствует тому, что сгенерировала Llama 2, и это хорошо. Но мы так и не поняли, почему ответы двух ИИ расходятся. Давайте спросим прямо: откуда взялось минус единица? NC Ты вычитаешь единицу из количества чисел в списке, чтобы добавить степень свободы? Если да, то почему? Если вы когда-нибудь занимались статистикой, можете догадаться, каким будет ответ. Если нет, Llama 2 сообщит, что имеется в виду под степенью свободы и для чего она используется.
   206 Глава 9. GPT без интернета Листинг 9.8. Llama 2 рассказывает о степенях свободы Да, вычитая 1 из количества чисел в списке, мы добавляем степень свободы. Степень свободы - термин, который входит в формулу среднеквадратичного отклонения и представляет собой количество независимых наблюдений в наборе данных. В данном случае список состоит из 5 чисел, поэтому степень свободы равна 4. Это означает, что у нас есть 4 независимых наблюдения (5 чисел в списке), и нам нужно учесть тот факт, что среднеквадратичное отклонение вычитается для этого набора независимых наблюдений. Причина, по которой мы вычитаем 1 из количества что нам нужно включить среднее значение в набор наблюдений. Если не вычесть 1, среднее значение как число из набора, второй раз – как среднее), чисел в списке, состоит в том, данных в качестве одного из будет учтено дважды (один раз что приведет к ошибке. Таким образом, вычитая 1 из количества чисел в списке, мы фактически удаляем среднее значение из набора данных и рассматриваем его как одно наблюдение наряду с остальными 4 числами. Это позволяет нам правильно вычислить среднеквадратичное отклонение по приведенной выше формуле. Давайте ненадолго отвлечемся и ответим на вопрос: как выглядит формула среднеквадратичного отклонения? Ответ: какая именно? На самом деле их две: для выборки и для генеральной совокупности. Первое вычисляется по подмножеству (выборке) значений, взятых из более крупной совокупности, при помощи формулы, которая показана на рис. 9.1, используется при работе с ограниченным объемом данных и неприменимо ко всему их набору (совокупности). А вот второе как раз показывает удаленность значений от среднего значения совокупности, а потому считается по-другому, по формуле с рис. 9.2. где: yy s — среднеквадратичное значение для выборки. yy xi — значение из выборки. yy — среднее арифметическое выборки. yy n — количество значений в выборке. yy Величина n – 1 (вместо N, как в формуле для совокупности) известна как корректировка Бесселя. Она корректирует смещение при оценке дисперсии и среднеквадратичного отклонения совокупности по выборке. Рис. 9.1. Формула для расчета среднеквадратичного отклонения для выборки Обе формулы предполагают нахождение суммы возведенных в квадрат разностей между всеми значениями из набора и его средним арифметическим, в обеих
   9.2. Разворачиваем LLM на своем компьютере 207 формулах в конце вычисления извлекается квадратный корень. Поэтому обе они показывают разброс значений вокруг среднего. где: yy σ — среднеквадратичное отклонение совокупности. yy xi — значение из совокупности. yy m — среднее арифметическое совокупности. yy N — количество значений в совокупности. yy Σ — знак суммирования по всем значениям. Рис. 9.2. Формула для расчета среднеквадратичного отклонения для совокупности Таким образом, можно сделать вывод, что Llama 2 выдала функцию для расчета среднеквадратичного отклонения выборки, а не совокупности. Но так как это понятия из статистики, стоит попробовать такой запрос: NC Как бы ты изменила свой ответ, если бы я попросил дать его с точки зрения статистика, а не математика? Листинг 9.9. Ответ Llama 2 в роли статистика Отличный вопрос! Будучи статистиком, для вычисления среднеквадратичного отклонения для набора чисел с использованием только встроенных функций Python я бы использовала следующий код: import math # Набор чисел для вычисления среднеквадратичного отклонения numbers = [1, 2, 3, 4, 5] # Вычисление суммы квадратов чисел из набора sum_squared = 0 for num in numbers: sum_squared += (num - math.mean(numbers)) ** 2 # Вычисление квадратного корня из суммы квадратов std_dev = math.sqrt(sum_squared) print("Среднеквадратичное отклонение для {0}: {1}".format(numbers, std_dev)) Будучи статистиком, я считаю, что в данном случае уместнее вычислять среднеквадратичное отклонение совокупности, поскольку нас интересует разброс значений всей совокупности, а не только случайной выборки. Используя stdev()
   208 Глава 9. GPT без интернета вместо stdev_sample(), мы можем вычислить среднеквадратичное отклонение совокупности напрямую, не заботясь об ограничениях статистики выборки. Конечно, как математик вы можете иметь другую точку зрения на этот вопрос, и это совершенно нормально! В конце концов, статистика и математика - тесно связанные области со множеством пересекающихся концепций и методов. Надев костюм статистика, Llama 2 изменила свой ответ, вот только забыла разделить сумму квадратов на количество чисел в списке. Но это легко исправить, и в этом случае мы получим стандартное отклонение совокупности. Этот пример еще раз напоминает нам: LLM могут уверенно выдавать неправильные ответы. Всегда проверяйте их код, сверяясь со своими знаниями и знаниями экспертов. Теперь введите /bye, чтобы завершить сеанс, как это принято при общении с Llama 2. Пришел черед поговорить о другом локальном ИИ — GPT-4All. 9.2.3. Демократизация ответов с GPT-4All Несмотря на схожесть в названии, GPT-4All не имеет никакого отношения к ChatGPT-4. Это ПО с открытым исходным кодом, которое создано компанией Anthropic и позволяет пользователям обучать и применять собственные LLM. В его основе лежит модель GPT-3, а значит, оно не столь эффективно, как более современные GPT-4. И все же этот ИИ может работать локально, без выхода в интернет. Давайте попросим GPT4-All представиться: NC Ты пишешь книгу о генеративном ИИ. Сейчас ты работаешь над главой о локально запускаемых LLM. Что ты можешь рассказать о GPT-4All? Листинг 9.10. GPT-4All рассказывает о себе GPT-4All – это программное обеспечение с открытым исходным кодом, которое представляет собой реализацию модели предварительно обученного генеративного преобразователя (GPT), позволяющего пользователям запускать большие языковые модели (LLM) локально, без выхода в интернет. Эта мощная модель для генерации текста может обучаться и настраиваться на различных наборах данных, благодаря чему исследователи и разработчики могут экспериментировать с современными методами обработки языка, не полагаясь на облачную инфраструктуру или проприетарное программное обеспечение. Предоставляя доступную и настраиваемую платформу для локального развертывания LLM, GPT-4All позволяет использовать потенциал ИИ в различных приложениях, в том числе для понимания естественного языка, генерации текстов и ответов на вопросы. В отличие от Ollama, GPT-4All требует установки. К счастью, это относительно быстрый и безболезненный процесс. Перейдите на сайт https://gpt4all.io/, загрузите установщик, подходящий для вашего компьютера, и следуйте инструкциям по установке. После этого вы получите инструкции по загрузке модели (рис. 9.3).
   9.2. Разворачиваем LLM на своем компьютере 209 Рис. 9.3. GPT-4All просит загрузить модели для работы Я загрузил и использовал Mistral OpenOrca, высокопроизводительный фреймворк, который поддерживает параллельное и распределенное программирование и предназначен для упрощения разработки крупномасштабных приложений для высокопроизводительных вычислительных кластеров или облачных сред. Mistral OpenOrca особенно хорошо подходит для обработки больших объемов данных, научного моделирования, машинного обучения и других вычислительных задач, требующих эффективного использования ресурсов и масштабируемости на нескольких узлах. Mistral OpenOrca предоставляет набор инструментов и библиотек для планирования заданий, обмена данными, обеспечения отказоустойчивости и балансировки нагрузки в распределенных средах, что делает его идеальным выбором для разработчиков, работающих над сложными проектами, требующими высокой производительности и параллельной обработки. Самопрезентация GPT4All и большая часть этого параграфа были написаны с помощью Mistral OpenOrca. Выбрав пункт Downloads (Загрузки) из меню Settings (Настройки), можно увидеть список загруженных моделей (рис. 9.4). Рис. 9.4. Список загруженных моделей в GPT-4All
   210 Глава 9. GPT без интернета Кроме того, при помощи меню можно просматривать историю диалогов с моделями (рис. 9.5). Рис. 9.5. История диалогов с выбранной моделью GPT-4All На всякий случай уточним: NC Для получения ответов мне нужен выход в интернет? Листинг 9.11. GPT-4All заверяет, что интернет ему не нужен Нет, для получения ответов от GPT-4All выход в интернет не требуется. Модель можно использовать без подключения к Сети, иных внешних подключений и зависимостей. Перейдем к сравнению GPT4-All/Mistral OpenOrca (далее просто GPT-4All) с другими моделями: NC Ты — математик, которому нужно вычислить среднеквадратичное отклонение для набора чисел, используя для этого чистый Python. Напиши нужный для этого код и подробно прокомментируй его. Листинг 9.12. GPT-4All пытается вычислить среднеквадратичное отклонение Для вычисления среднеквадратичного отклонения для набора чисел на Python необходимо выполнить следующие действия: 1. Вычислить среднее арифметическое чисел из набора.
   9.2. Разворачиваем LLM на своем компьютере 211 2. Найти разности между каждым числом из набора и средним арифметическим. 3. Возвести каждую из полученных разностей в квадрат. 4. Найти среднее арифметическое этих квадратов. Получится отклонение. 5. Взять квадратный корень из отклонения. Получится среднеквадратичное отклонение. Вот фрагмент кода, который реализует этот алгоритм на Python: def std_dev(numbers): mean = sum(numbers) / len(numbers) # Вычитание среднего из чисел. differences = [x - mean for x in numbers] # Возведение разностей в квадрат. squared_differences = [y * y for y in differences] # Среднее арифметическое квадратов. variance = sum(squared_differences) / len(numbers) # Возврат среднеквадратичного отклонения. return math.sqrt(variance) Отметим несколько моментов. Во-первых, GPT-4All выдал код для среднеквадратичного отклонения для совокупности. Во-вторых, текст и код генерируются очень быстро (на моем компьютере на это ушло четыре-пять секунд, намного меньше, чем у Ollama). В-третьих, в коде нет ошибок! Попробуйте загружать разные модели, спрашивать, что они умеют, и сравнивать результаты. Вы можете спросить: а что же все-таки лучше: Llama 2, GPT-4All или ChatGPT? Отличный вопрос! Взгляните на рис. 9.6. Llama 2 — это отличная модель общего назначения. Она хорошо резюмирует объемные тексты, пишет подходящие по контексту фразы. Возможности GPT-4All разнообразны и прежде всего al str Mi ca Or en Op a2 m Lla PT atG Ch Без выхода в интернет Отлично Скорость ответа Хорошо Точность Плохо / Неприменимо Рис. 9.6. Сравнение рассмотренных в этой главе LLM
212    Глава 9. GPT без интернета определяются выбранной моделью. К примеру, Mistral OpenOrca прекрасно подойдет для работы с разными языками. ChatGPT — лучший вариант для тех, кто хочет общаться с ИИ на естественном языке и получать наиболее точные ответы на основе вводимых данных (что, собственно, и требуется). Очевидным ограничением ChatGPT является необходимость постоянного подключения к интернету. Итоги Для оптимальной работы локальных LLM требуются значительные вычислительные ресурсы и дорогостоящее оборудование. Но есть модели, которые можно использовать и на относительно скромном оборудовании. Они (например, Llama 2) могут выдавать ответы достаточно высокого качества, но не сравнятся по этому показателю с облачными моделями типа ChatGPT (по крайней мере, на момент написания этой книги). И среднеквадратичное отклонение для всей совокупности, и выборочное среднеквадратичное отклонение измеряют вариативность данных. Отличие в том, что в первом случае учитывается вся совокупность, а во втором — только ее часть (выборка). Поэтому среднеквадратичное отклонение для выборки дает лишь оценку, а не точное значение. Llama 2 отлично подходит для работы с текстами: она составит вам резюме, напишет письмо или фрагмент кода. GPT-4All — универсальное решение, возможности которого зависят от выбранной модели. К примеру, этот ИИ обеспечивает многоязыковую поддержку. ChatGPT отличается точностью ответов при разговоре на естественном языке (но требует для этого выхода в интернет). Помимо офлайн-доступности, есть ситуации, когда использование локальной модели вроде Llama 2 или GPT-4All особенно оправданно: ƒ ƒ Конфиденциальность и безопасность: поскольку не нужно передавать в интернет чувствительные данные, снижаются риски утечек и кибератак. ƒ ƒ Экономия: запуск модели на своем оборудовании может обойтись дешевле, чем постоянное использование облачных сервисов вроде ChatGPT или OpenAI API.
Приложения
А Настройка ChatGPT В этом приложении мы настроим работу с ChatGPT. Вы будете взаимодействовать с ChatGPT через сайт, так что ничего устанавливать и настраивать не нужно. Но для начала потребуется создать аккаунт. Давай пройдем этот процесс шаг за шагом и отправим первый промт1. A.1. Создание учетной записи ChatGPT    Прежде всего откройте браузер и введите адрес https://chat.openai.com/auth/login. Вам будет предложено войти в систему или создать учетную запись (рис. A.1). Выберите второй вариант. Рис. A.1. Выбор между входом в систему и созданием учетной записи на главной странице ChatGPT Чтобы создать учетную запись, потребуется электронная почта, аккаунт Google (при наличии) либо Microsoft (также при наличии) (рис. A.2). Выберите наиболее удобный для вас вариант. 1 Интерфейс OpenAI периодически меняется. Но разобраться в том, как создать аккаунт, в любом случае не составит труда. — Примеч. ред.
   A.2. Создание учетной записи с использованием электронной почты 215 Рис. A.2. Несколько вариантов создания учетной записи. Выберите наиболее удобный из них A.2. Создание учетной записи с использованием электронной почты Если вы решили создать учетную запись, используя электронную почту, выполните следующие действия: 1. Введите адрес электронной почты и пароль (рис. A.3). ПРИМЕЧАНИЕ В этом случае вам также понадобится телефон, который может принимать СМС1. 2. Система сообщит о том, что нужно проверить электронную почту (рис. A.4). Проверьте почту и перейдите по ссылке из письма (рис. A.5). 1 На момент подготовки русского издания книги при создании учетной записи не требуется номер телефона. — Примеч. ред.
Глава A. Настройка ChatGPT            216 Рис. A.3. Введите сложный пароль, который трудно угадать Рис. A.4. OpenAI требует подтвердить электронную почту Рис. A.5. В письме от OpenAI содержится ссылка для подтверждения. Перейдите по ней
   A.2. Создание учетной записи с использованием электронной почты 217 3. Система попросит вас ввести имя, фамилию и номер телефона (рис. A.6). Рис. A.6. OpenAI просит ввести номер телефона 4. На указанный номер придет сообщение с кодом. Введите его в соответствующее поле ввода (рис. A.7). Рис. A.7. Ввод кода из присланного на телефон текстового сообщения Теперь перед вами должна открыться домашняя страница ChatGPT (рис. A.8), где будет возможность оформить платную подписку, — ChatGPT Plus. Эта подписка дает приоритетный доступ в часы пик. С учетом того, что у ChatGPT около 13 миллионов пользователей в день, без подписки днем можно просто не пробиться. (Хотя сам ChatGPT, конечно, ведет себя так, будто все нормально.) Сколько пользователей обращается к тебе в день? Я - языковая модель ИИ. У меня нет доступа к данным о количестве взаимодействующих со мной пользователей. Моя задача отвечать на запросы пользователей и помогать им, насколько хватит моих возможностей. Теперь у вас есть аккаунт и вы вошли в систему. Попробуйте ввести свой первый промт. Мы подождем.
   218 Глава A. Настройка ChatGPT Рис. A.8. Главная страница ChatGPT с примерами вопросов и описанием возможностей ИИ
Б Настройка GitHub Copilot Рассмотрим установку расширения Copilot для Visual Studio Code и PyCharm. Visual Studio Code (сокращенно VS Code) — это кросс-платформенная среда разработки (IDE), в которой можно писать на любом языке программирования. Разработчик — компания Microsoft — предоставляет ее бесплатно. VS Code прекрасно работает с Copilot, что делает ее идеальным выбором для разбора примеров из этой книги. PyCharm — IDE компании JetBrains, ориентированная на Python и также поддерживающая Copilot. Есть бесплатная версия этой среды разработки. Вы можете выбрать любую из этих двух IDE. ПРИМЕЧАНИЕ Для работы с Copilot нужна платная подписка. Тем не менее GitHub предоставляет возможность пробного бесплатного доступа в течение месяца.1 Б.1. Установка расширения Copilot для Visual Studio Code Рассмотрим, как установить и включить расширение Copilot для VS Code. Выполните следующие действия: 1. Перейдите на вкладку Extensions (Расширения) в левой части окна VS Code (рис. Б.1). Иконка на ней похожа на фигуру из тетриса. 2. Введите слово copilot в поле для поиска, которое находится в верхней части вкладки (рис. Б.2). Поиск нечувствителен к регистру, поэтому можно ввести это слово любыми буквами (copilot, COPILOT или Copilot). 3. Щелкните по результату поиска GitHub Copilot, и справа отобразится информация об этом расширении (рис. Б.3). Среди прочего вы увидите кнопку Install (Установить), сведения о разработчике, количество загрузок и звезды. Нажмите на кнопку Install, чтобы начать установку. 1 На момент подготовки русского издания книги в бесплатной версии ответы ограничены 2000 дополнениями кода и 50 сообщениями в чате в месяц. — Примеч. ред.
   220 Глава Б. Настройка GitHub Copilot    Рис. Б.1. Вкладка Extensions с полем для поиска в окне Visual Studio Code Рис. Б.2. Результаты поиска расширения GitHub Copilot
   Б.1. Установка расширения Copilot для Visual Studio Code 221 Рис. Б.3. Информация о расширении GitHub Copilot и кнопка для его установки 4. Откроется окно с сообщением о необходимости создать учетную запись GitHub. Нажмите на кнопку Sign In To GitHub (Войти на GitHub) в правом нижнем углу окна (рис. Б.4). Рис. Б.4. Диалоговое окно для входа на GitHub    5. При нажатии на эту кнопку VS Code попросит разрешения войти на GitHub. Нажмите на кнопку Allow (Разрешить) (рис. Б.5). Рис. Б.5. VS Code просит разрешить расширению Copilot войти на GitHub 6. Автоматически откроется браузер, и вы увидите сайт GitHub, а точнее, страницу авторизации с просьбой разрешить расширению Copilot доступ к вашему адресу электронной почты (рис. Б.6). Если для вас это допустимо, нажмите на кнопку Authorize Visual Studio Code (Разрешить доступ для VS Code). На этом установка расширения Copilot для VS Code завершена. Дальше посмотрим, как установить такое расширение для PyCharm.
   222 Глава Б. Настройка GitHub Copilot Рис. Б.6. Сайт GitHub просит разрешить доступ к адресу электронной почты Б.2. Установка плагина Copilot для PyCharm Запустите PyCharm и откройте окно Preferences (Настройки). Выполните следующие действия: 1. Перейдите на вкладку Plugins (Плагины). Как и в VS Code, вы увидите поле для поиска. Введите в него слово copilot, а когда нужный плагин будет найден, нажмите на кнопку Install (Установить) (рис. Б.7). 2. Выберите меню Tools > GitHub Copilot > Login to GitHub (Инструменты, GitHub Copilot, Вход на GitHub ) (рис. Б.8).
   Б.2. Установка плагина Copilot для PyCharm Рис. Б.7. Вкладка Plugins с результатами поиска Рис. Б.8. Меню для входа на GitHub 223
   224 Глава Б. Настройка GitHub Copilot 3. PyCharm покажет вам семизначный буквенно-цифровой код, который потребуется для входа (рис. Б.9). Запишите его или скопируйте в буфер обмена: скоро он вам понадобится. 4. Откроется браузер, и вы увидите запрос на ввод кода доступа (рис. Б.10). 5. GitHub попросит разрешения на получение ваших персональных данных, в том числе адреса электронной почты (рис. Б.11). Нажмите на кнопку Authorize GitHub Copilot Plugin (Разрешить доступ к плагину GitHub Copilot). Вы увидите сообщение об успешной активации плагина (рис. Б.12). Рис. Б.9. Код для входа на GitHub в окне PyCharm Рис. Б.10. Вставьте сюда код для входа на GitHub
   Б.2. Установка плагина Copilot для PyCharm 225    Рис. Б.11. Предоставление GitHub доступа к адресу электронной почты Рис. Б.12. Сообщение об успешной активации Copilot Теперь в вашей IDE есть расширение или плагин GitHub Copilot. Настало время ввести первый промт и погрузиться в работу.
   226 Глава Б. Настройка GitHub Copilot Б.3. Первый промт для Copilot Откройте свою IDE (если она еще не открыта) и создайте новый проект на Python. Создайте в нем файл с именем app.py . Введите следующий промт, оформив его как комментарий. Попросим Copilot написать простое приложение на основе FastAPI: # Создай приложение на основе FastAPI # Запусти его при помощи uvicorn: app --reload Если Copilot установлен и активирован правильно, он начнет генерировать код. Дважды нажмите на клавишу Enter и начните печатать строку from fastapi import FastAPI. Стоит ввести первые буквы, как Copilot закончит строку за вас (рис. Б.13). Вот так он работает. Рис. Б.13. Copilot принимает промт из комментария и начинает подбирать шаблоны из имеющихся в модели данных (она называется Cortex). Обнаружив совпадение, Copilot сразу же предлагает вариант кода Можно добавить еще промт, например попросить Copilot сгенерировать эндпоинт для REST-запроса GET, которая возвращает фразу «Привет, мир!». Листинг Б.1. П  олный исходный код, который сгенерирует Copilot, отвечая на запрос # Создай приложение на основе FastAPI # Запусти его при помощи uvicorn: app --reload import uvicorn from fastapi import FastAPI
   Б.3. Первый промт для Copilot 227 app = FastAPI() @app.get("/") def read_root(): return {"Привет": "мир!"} if __name__ == "__main__": uvicorn.run(app, host="localhost", port=8000) from fastapi import FastAPI app = FastAPI() @app.get("/") def read_root(): return {"Привет": "Мир!"} Теперь откройте терминал и выполните в нем следующие команды для установки нужных библиотек и запуска приложения. Листинг Б.2. Команды для установки библиотек и запуска приложения pip install fastapi pip install uvicorn python3 app.py Это должно привести к запуску FastAPI. Открыв страницу http://localhost:8000 в браузере, вы увидите надпись «Привет, мир!» в формате JSON (рис. Б.14). Рис. Б.14. Простое приложение на FastAPI, возвращающее фразу «Привет, мир!». Эти слова чаще всего используют в первой программе на новом языке или фреймворке Поздравляю! Вы создали первую программу при помощи Copilot и сделали первый шаг в совместной работе с ним.
В Настройка AWS CodeWhisperer Рассмотрим, как установить расширение CodeWhisperer для Visual Studio Code и PyCharm. ПРИМЕЧАНИЕ На момент написания этой книги CodeWhisperer доступен бесплатно «в течение ознакомительного срока». Но Amazon ничего не сообщает ни о его продолжительности, ни о расценках, которые предлагаются по его окончании. В.1. Установка расширения CodeWhisperer для VS Code Расширение CodeWhisperer устанавливается так же, как и Copilot или любое другое расширение. Выполните следующие действия: 1. Перейдите на вкладку Extensions (Расширения) и введите AWS Toolkit в поле поиска. Обнаружив нужное расширение, нажмите на кнопку Install (Установить) (рис. В.1). Рис. В.1. Информация о расширении AWS Toolkit и кнопка Install 2. По завершении установки перейдите на вкладку AWS в левой части окна VS Code и выберите меню Developer Tools > Select a connection option (Инструменты разработчика > Выбор варианта подключения) (рис. В.2).
   В.1. Установка расширения CodeWhisperer для VS Code 229 Рис. В.2. Вкладка AWS и меню Developer Tools с пунктами    Select a Connection option и CodeWhisperer 3. При первом выборе пункта Select a Connection в верхней средней части окна VS Code откроется список с несколькими вариантами. Выберите Use a personal email to sign up and sign in with AWS Builder ID (Использовать личный адрес электронной почты для регистрации и доступа к AWS Builder ID) (рис. В.3). Рис. В.3. Список с вариантами подключения к службе AWS CodeWhisperer 4. Откроется диалоговое окно с кодом доступа (рис. В.4). Нажмите на кнопку Copy Code and Proceed (Скопировать код и продолжить). 5. VS Code попросит разрешения открыть сайт Amazon AWS. Нажмите на кнопку Open (Открыть) (рис. В.5). 6. Получив разрешение, VS Code откроет браузер, а в нем — сайт AWS. Введите адрес электронной почты (рекомендуется использовать личный ящик, рис. В.6). 7. После успешного входа в систему AWS попросит разрешения на доступ к данным, который необходим, чтобы при генерации кода использовать контекст — то, что уже напечатано в IDE. Не будет контекста, не будет и магии. Так что нужно нажать на кнопку Allow (рис. В.7).
Глава В. Настройка AWS CodeWhisperer Рис. В.4. Код для регистрации копии VS Code на AWS и CodeWhisperer Рис. В.5. VS Code просит разрешения перейти на сайт AWS       230 Рис. В.6. Ввод адреса электронной почты для создания учетной записи и входа в систему
   В.1. Установка расширения CodeWhisperer для VS Code 231 8. CodeWhisperer подтвердит, что разрешение получено (рис. В.8). 9. Теперь нужно прочесть и принять пользовательское соглашение Amazon CodeWhisperer (рис. В.9). Если вы ничего не имеете против, нажмите на кнопку Accept and Turn On CodeWhisperer (Принять и включить CodeWhisperer). Рис. В.7. Для получения советов нужно разрешить доступ к уже написанному коду Рис. В.8. Учетная запись AWS зарегистрирована. Все готово к работе
   232 Глава В. Настройка AWS CodeWhisperer Рис. В.9. Пользовательское соглашение. Нужно внимательно прочесть и принять Все. Теперь CodeWhisperer будет работать в VS Code. В.2. Установка плагина CodeWhisperer для PyCharm Плагин CodeWhisperer устанавливается в PyCharm так же, как и расширение для VS Code. Все те же этапы, но немного другие меню. Если вы прочитали прошлый раздел, то знаете, что делать. Для тех же, кто не читал, я расскажу все подробно. Выполните следующие действия: 1. Откройте настройки: Preferences на Mac или Options на Windows. Перейдите на вкладку Plugins (Плагины), выполните поиск по словам AWS Toolkit и нажмите на кнопку Install (Установить) в результатах (рис. В.10). 2. По завершении установки нажмите на кнопку AWS Toolkit в нижнем левом углу PyCharm (рис. В.11). Откроется окно инструментов разработчика, в котором нужно создать новое соединение с AWS. 3. Нажмите на кнопку + Add Connection to AWS (Добавить соединение с AWS). Откроется диалоговое окно для ввода учетных данных и подключения к AWS. Выберите вариант Use a personal email to sign up and sign in with AWS Builder ID (Использовать личную электронную почту для регистрации и входа в AWS Builder ID) и нажмите на кнопку Connect (Подключиться) (рис. В.12). 4. Откроется другое диалоговое окно, в котором появится личный код доступа (рис. В.13). Нужно скопировать его, чтобы потом разрешить AWS доступ к коду в PyCharm. Нажмите на кнопку Open and Copy Code (Открыть и скопировать код). 5. Откроется браузер и сайт AWS в нем. Сюда необходимо вставить код доступа (рис. В.14). Введите код и нажмите на кнопку Next (Далее).
   В.2. Установка плагина CodeWhisperer для PyCharm    Рис. В.10. Вкладка Plugins с результатами поиска AWS Toolkit Рис. В.11. Вкладка AWS Toolkit для подключения к AWS, запуска и останова CodeWhisperer 233
   234 Глава В. Настройка AWS CodeWhisperer Рис. В.12. Диалоговое окно для выбора вариантов подключения к AWS Рис. В.13. Диалоговое окно с личным кодом и кнопкой для продолжения 6. AWS попросит войти в систему, используя личную электронную почту (рис. В.15). Если у вас уже есть учетная запись Builder, можно войти через нее. То есть для продолжения нужно либо войти в систему, либо создать новую учетную запись. 7. Дело идет к концу, уже скоро CodeWhisperer будет к вашим услугам. Вы убедитесь в том, что он стоит затраченных усилий. Осталось разрешить AWS получить определенные (весьма немногочисленные) персональные данные: адрес электронной почты и код, который вы набираете в редакторе (рис. В.16). Нажмите на кнопку Allow (Разрешить). 8. Подтверждение от AWS Toolkit означает, что CodeWhisperer может работать в PyCharm (рис. В.17).
Рис. В.14. Форма для вставки ранее скопированного кода          В.2. Установка плагина CodeWhisperer для PyCharm Рис. В.15. Вход в систему с имеющейся учетной записью либо регистрация новой 235
   236 Глава В. Настройка AWS CodeWhisperer Рис. В.16. AWS просит разрешения на сбор информации, которая поможет CodeWhisperer в работе Рис. В.17. AWS подтверждает, что CodeWhisperer готов к работе Все, можно начинать. Прежде всего включите CodeWhisperer. Для этого вернитесь на вкладку AWS (нижний левый угол), нажмите на кнопку Developer Tools (Инструменты разработчика), откройте CodeWhisperer и нажмите на кнопку Start (Запуск) (рис. В.18).
      В.2. Установка плагина CodeWhisperer для PyCharm 237 Рис. В.18. Виджет AWS Toolkit, при помощи которого можно включить (и выключить) CodeWhisperer Сразу после этого AWS Toolkit предложит прочесть и принять (или отклонить) пользовательское соглашение Amazon CodeWhisperer (рис. В.19). Конечно же, для работы с CodeWhisperer их нужно принять. Прочтите внимательно и, в случае согласия, нажмите на кнопку Accept and Turn On CodeWhisperer (Принять и включить CodeWhisperer). Рис. В.19. Пользовательское соглашение Amazon CodeWhisperer, которое нужно принять Вот и все. Можно творить чудеса.
   238 Глава В. Настройка AWS CodeWhisperer В.3. Первый промт для CodeWhisperer Теперь создадим несложное приложение Flask. Откройте IDE, создайте файл app.py и введите запрос, оформив его как комментарий. Как и Copilot, CodeWhisperer ищет промты именно в комментариях. # Создай приложение Flask с корневым маршрутом, # по которому возвращаются слова "Привет, мир!" Теперь, как только вы нажмете на Enter, CodeWhisperer начнет предлагать вам код прямо с оператора import (рис. В.20). Рис. В.20. Первая помощь от CodeWhisperer: оператор import Продолжайте нажимать на Enter, чтобы CodeWhisperer, строка за строкой, написал за вас нужный код. Возможно, его придется отформатировать, но это не так уж и сложно. Кроме того, возможно, потребуется установить Flask: pip install flask Листинг В.1. П  олный исходный код приложения (отформатированный для удобства) # Создай приложение Flask с корневым маршрутом, # по которому возвращаются слова "Привет, мир!" from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Привет, мир!' if __name__ == '__main__': app.run() Запустите приложение. По умолчанию оно будет работать через порт 5000 (рис. В.21).
   В.3. Первый промт для CodeWhisperer 239 Рис. В.21. Ваше первое приложение, созданное при помощи CodeWhisperer Мои поздравления! В вашем распоряжении есть еще один инструмент, который помогает работать с ИИ. Вы готовы двигаться дальше к (еще) большей продуктивности.
Комьюнити рецензентов и переводчиков ИТ-литературы Миссия участников клуба — обеспечить высокое качество профессиональной переводной литературы в России. «Книжные дебагеры» проверяют корректность терминологии и подписей на схемах и иллюстрациях, чтобы сделать книги более понятными русскоязычному читателю. Стать участником Read IT Club может любой ИТ-специалист, готовый поделиться опытом с сообществом. присоединиться к нам