Вайб-кодинг или осознанная разработка? Я выбираю второе
С приходом ИИ сильно вырос соблазн заниматься простым человеческим вайб-кодингом: пишешь себе промпты, копируешь готовый код, вставляешь в проект - и готово! И бизнес рад, и времени меньше уходит… В чем подвох? Пожалуй, в том, что таким образом все меньше полезной информации оседает в голове.
В этой статье я хочу поделиться своим методом, с помощью которого я внедряю новые фичи в проект, над которым работаю, при этом получая новые знания и опыт (как в старые добрые времена), затрачивая гораздо меньше времени.
В настоящее время я занимаюсь разработкой фронтенда в над starpx.com - это приложение, позволяющее просматривать и редактировать Deepsky-фотографии, поэтому для статьи взял оттуда пару личных кейсов.
Возможно, описываемые в статье технологии покажутся кому-то "детскими" и простыми, но главное - это принцип взаимодействия с ними человека, который про них только слышал, но не использовал.
TL;DR
Мой процесс внедрения новых (в первую очередь - для меня) фичей выглядит так:
Тренируй насмотренность
Изучи с чем имеешь дело
Спроси ИИ и проанализируй ответ
Внедряй
1. Тренируй насмотренность
Во все времена одним из признаков хорошего специалиста является насмотренность. Быть в тренде, читать про новые технологии - это как правило хорошего тона любого инженера. Ибо если ты сам себе землекоп с лопатой, то, не прочитав про экскаватор, ты не будешь его применять.
Для ознакомления с тем, “чем живет” твоя предметная область, не нужно много времени и сил, достаточно посмотреть доклад с конференции или почитать статью вместо листания ленты в кофе-брейк - чтобы хотя бы было представление.
И вот тебе прилетает новая задача, при виде которой обычный человек скорее всего пойдет гуглить на пару часов, далее пару дней обдумывания, дейлики, созвоны, асинхронные консультации и тд и тп. Но не в нашем случае - у нас есть насмотренность.
Личный кейс № 1: тайлинг
Задача: сделать разделение изображения на фрагменты небольшого разрешения (тайлы), которые при размещении рядом друг с другом составляют само изображение. При этом, должно поддерживаться неограниченное количество уровней тайлинга, фрагменты должны подгружаться по мере увеличения степени зумирования, а итоговое изображение должно собираться в SVG (на этом срезалось пару библиотек, которые строят изображение в canvas).
Джуном я бы побежал решать задачу "в лоб", нагородил бы условий и кастомных событий и, скорее всего забыл бы про оптимизацию.
Теперь, в более зрелом возрасте и имея насмотренность, я могу добавить к условиям оптимизацию: при увеличении или перемещении области просмотра должны подгружаться только те тайлы, которые находятся на экране в данный момент. Как это отследить? Ну, я слышал про такую штуку, как IntersectionObserver, но ни разу ее не применял. Было бы круто поработать с ней!
Личный кейс № 2: диаграммы
Вряд ли человек, не увлекающийся астрономией или астрофотографией, знает, что звезды имеют свои характеристики и даже диаграммы, их описывающие. Так, диаграмма Герцшпрунга-Рассела отражает зависимость между спектральными классами или показателями цветовой температуры звёзд и их абсолютными звёздными величинами, а диаграмма светимости показывает зависимость силы излучения для определенных частот световых волн. Разумеется, уважающий себя сервис должен уметь отображать эти диаграммы.
Учитывая, что в проекте для отрисовки overlay-слоев уже использовалась библиотека D3.js, глупо было бы использовать еще одну отдельно для графиков.
Личный кейс № 3: ускорить загрузку информации об объектах звездного неба
При открытии пост-обработанного изображения у нас есть overlay-фигуры, размечающие различные космические объекты: галактики, туманности, звезды и т.д. Разумеется, разметить абсолютно все объекты даже на отдельном участке звездного неба невозможно - будет каша. Но самые значительные видны и подписаны. Мало того: при нажатии на них можно посмотреть более подробную информацию, в том числе диаграммы.
Пожалуй, любому придет в голову идея с кешированием. И, не имея насмотренности, я бы скорее всего побежал бы костылить сохранение этой информации в localStorage.
Под действием алкоголя насмотренности же, я вспомнил про такие классные штуки, как:
IndexedDB - идеально, чтобы сохранить информацию об объектах звездного неба в табличном виде.
Worker'ы - чтобы делать это в отдельном потоке, невидимо для пользователя.
Сразу в голове звучит: “О! Так я же читал про эту библиотеку/API на прошлой неделе, можно применить!” Ну и продолжение - “…вобью-ка в промпт…” В редких случаях это сработает, но лучше…
2. Изучи с чем имеешь дело
Гораздо более эффективным будет сначала изучить технологию, которую собираешься использовать, на достаточном уровне, чтобы а) можно было сделать все самому - чтобы в голове отложилось б) поправить код ИИ - чтобы не тратить время на бесконечные копипасты говнокода, который в лучшем случае работать не будет, а в худшем будет немасштабируемым и неотлаживаемым. Найдите туториал, посмотрите рабочие примеры, пройдите мини-курс. Наша задача - изучить на ДОСТАТОЧНОМ уровне, не погружаясь в детали на несколько недель. Здесь уместно вспомнить про принцип Парето (80/20).
Для первого кейса с тайлингом, который фактически не зависел от других библиотек моего проекта, я сделал мини-полигон в виде большого VueJS компонента, единственного отображаемого на странице. Остальные так или иначе использовали подключенные библиотеки приложения.
В изучении IntersectionObserver мне хорошо помог проект doka.guide - я немного потренировался, понял, что к чему, и "зачел" себе это знание.
Ознакомление с библиотекой D3.js проходил с использованием руководства здесь - я не большой фанат обучающих видео, мне ближе классические учебники с примерами.
По IndexedDB и Worker API я прошелся по официальной документации и статьям на хабре (благодарю всех авторов, коих я, к сожалению, не запомнил).
После того, как я потрогал все нужные технологии руками и убедился, что могу все сделать сам, я...не стал все делать сам, а пошел к нейросетям.
3. Спроси ИИ и проанализируй ответ
Как только чувствуешь, что можешь сделать самостоятельно, но при этом ясно представляешь, что это будет долго и нудно - можешь подключать ИИ-ассистента. Скорее всего, он быстро накидает “скелет” того, что тебе нужно, а благодаря своим знаниям, приобретенным на предыдущем шаге, ты легко сможешь переделать сгенерированный шаблон в рабочий код, который станет частью твоего проекта. Если есть возможность не вставлять код сразу в проект - лучше этого не делать. Вместо этого, сделать полностью рабочий прототип, возможно используя отдельное окружение. В целом код, сгенерированный ИИ, похож на полуфабрикат, типа пельменей: вряд ли их можно съесть, не сварив. Так же с кодом: не работает - “вари”, работает - внедряй.
К сожалению, данную статью я решил написать уже после успешного внедрения описываемых решений, поэтому прикрепить код, выданный LLM, не могу - он ушел в историю. Могу сказать только, что с моими задачами на удивление неплохо справлялся GigaChat от Сбера - и это очень приятный момент, что отечественные разработки становятся по-настоящему конкурентоспособными.
4. Внедряй
На последнем этапе переходим к внедрению уже готового кода в наш проект. Здесь мы подключаем зависимости, переводим имена переменных в Namespace проекта (в идеале это сделать уже в п. 3), пишем необходимые экспорты и импорты.
В кейсе с тайлингом я получил почти сразу рабочую обертку IntersectionObserver для тайлов, которые были на экране. Все, что мне оставалось, это проверить работу моего компонента и просто перенести его в проект.
Код диаграмм работал, но не совсем корректно - были проблемы с масштабом диаграммы и с использованием неподдерживаемых методов библиотеки D3.js, которые были заменены в новой версии библиотеки. К счастью, я изучил библиотеку D3.js и легко смог поправить код, чтобы он работал так, как надо, и рисовал то, что требуется.
В работе с IndexedDB LLM выдал мне рабочий composable-костяк для работы с IndexedDB, в котором править пришлось разве что имена базы данных и хранилища:
export interface StarData {
id: string
info: any // тут секрет =)
}
const DB_NAME = 'starDB'
const DB_VERSION = 1
const STORE_NAME = 'stars_info'
function openDB(): Promise {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION)
request. => reject(request.error)
request.onsuccess = () => {
resolve(request.result)
}
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' })
}
}
})
}
export async function getStarData(id: string): Promise {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly')
const store = tx.objectStore(STORE_NAME)
const request = store.get(id)
request.onsuccess = () => resolve(request.result)
request. => reject(request.error)
})
}
export async function setStarData(data: StarData): Promise {
const db = await openDB()
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite')
const store = tx.objectStore(STORE_NAME)
const request = store.put(data)
request.onsuccess = () => resolve()
request. => reject(request.error)
})
}
Аналогичная ситуация с Worker'ом - почти с первого раза рабочий код, редактировал только обмен информацией между потоками и идентификацию объектов для записи в БД:
/// <reference lib="webworker" />
import { getStarData, setStarData, StarData } from './IndexedDB'
import fastHash from '@/core/utils/FastHash'
const fetchAdditionalInfo = async (url: string) => {
const response = await fetch(url)
const info = await response.json()
return info
}
interface Star {
id: string
// rest fields
}
interface WorkerRequest {
stars: Star[]
}
interface WorkerResponse {
id: string
// info?: any - здесь прилетает контент
ok?: boolean
error?: string
}
self.addEventListener('message', async (event: MessageEvent<WorkerRequest>) => {
const { stars } = event.data
for (const star of stars) {
const id = fastHash(star.id)
try {
let starData = await getStarData(id)
if (!starData) {
const info = await fetchAdditionalInfo()
starData = { id, info }
await setStarData(starData)
}
const response: WorkerResponse = { id, ok: true }
self.postMessage(response)
} catch (error: any) {
const response: WorkerResponse = { id, ok: false, error: error.message }
self.postMessage(response)
}
}
})
Выводы
В описанном подходе я вижу следующие плюсы:
Вы приобретаете опыт и знания, а не просто копипастите ответы LLM к себе в проект
Вы не тратите время на рутину - ее за вас делает ИИ, на вас - архитектура и внедрение
Вы тратите гораздо меньше времени на отладку, благодаря тому, что понимаете, как работает технология, которую вы хотите внедрить.
Конечно, можно (и даже нужно) добавить к перечисленным шагам этап ревью, но как минимум это лучше, чем бездумные Ctrl+C - Ctrl-V.
Спасибо, что дочитали, буду рад конструктивным комментариям!