Последние месяцы невозможно открыть новости и не прочитать что-нибудь о том, как AI снова научился делать что-то. Если смотреть на мир глазами ленты новостей, можно и вправду подумать, что программисту в IT больше места нет.
Однако же, очередной раз разрабатывая и оптимизируя архитектуру не вполне стандартного проекта, для чего у AI, ожидаемо, нет готового шаблона, а только «лоскутки» чужих решений, решил зафиксировать свой ход мыслей и последовательность этапов в решении задачи. Чтобы абстрактную программистскую «чуйку» зафиксировать в более чёткий и осознанный «свод правил».
Добавлю немного контекста. Представьте, что у вас есть некая система, которая работает определённым образом. У неё есть свой API, свои архитектурные принципы, рекомендуемые подходы к разработке и т.п. С другой стороны, у вас у самого есть видение своего будущего проекта, его архитектуры и того, как бы вы хотели управлять функционалом, предоставляемым «некой системой». Видение есть, а вот уверенности, что так можно – нету. Может можно, но нужно доработать внешнюю библиотеку как-то… а, может, и нельзя даже, но, чтобы узнать наверняка – надо погрузиться и попробовать.
Вот, для понимания, несколько примеров, правда тоже довольно абстрактных:
У вас есть императивный API, а вы хотите работать через декларативный
Внешняя система много работает через кодогенерацию, а у вас в планах управлять её работой через конфиги или даже пользовательский интерфейс.
Вам нужно подменить какие-то отдельные алгоритмы системы, не затронув все остальные. Сохранить совместимость. Избежать необходимость делать и поддерживать форк.
Вот своим подходом к такому погружению я и хотел бы поделиться, вынести, так сказать, на обсуждение, поскольку такие ситуации у меня возникали неоднократно. Мой опыт в профессии – чуть более 10 лет, вообще в разработке – чуть больше 15и, в общем, ещё не гуру, но уже и не совсем дебил, так что надеюсь оказаться полезным.
Первые шаги
Итак, вы попали в ситуацию. Что делать? Прежде всего, нужно понимать, что вам потребуется двигаться по «лабиринту» сразу с двух сторон: с одной стороны пытаться использовать внешнюю систему так, как вам это нужно, с другой – подстраивать идеальный образ своей системы под продукт, который используете. Отсюда и первые несколько шагов:
1. Нарисовать в голове и формализировать «на бумаге» образ своей системы, которую хотите написать. Это может быть верхнеуровневая архитектурная схема, может быть набор интерфейсов или список функций публичного API – зависит от вашей прикладной задачи. Но какая-то штука, на которую в минуту беспамятства вы могли бы взглянуть и вспомнить, куда вам надо двигаться, должна быть.
Важное замечание – при этом не нужно вдаваться в скурпулёзную разработку детальной архитектуры, вплоть до незначительных функций. Сейчас слишком рано, т.к. риски, что всё придётся переделывать, ещё слишком высоки.

2. На основе этого «образа» выделить ключевые критерии, которым должны отвечать внешние библиотеки – допустим, у вас есть выбор между различными решениями, а не только какое-то одно.
3. Анализируем внешние библиотеки по выделенным критериям: смотрим документацию, примеры кода, читаем отзывы… пока бегло, нам нужно только составить «шорт-лист», с чем мы готовы пробовать работать, а с чем иметь дело точно не хочется.
4. Более детально анализируем библиотеки из шорт-листа. Начиная с этого этапа важно не только ориентироваться на документацию, но и смотреть исходный код. В документации автор описывает то, как ОН видит использование своей системы, вам же нужно понять, сможете ли ВЫ навязать системе свой алгоритм работы, и не потребует ли это кардинальной её переработки. Такое в доках редко пишут, ответы на эти вопросы находятся часто только в исходниках. Се ля ви.

Проверка гипотезы
Итак, вы выбрали библиотеку / набор библиотек, с которыми готовы дальше работать. Что, теперь можно рисовать финальную архитектуру и начать делать конкретные фичи? Нет! Ни в коем случае! «Стрижка только началась».
5. Из всего планируемого функционала системы нужно выбрать одну/несколько, самых показательных, которые помогут выявить, насколько внешняя система подходит для адаптации под ваши нужды. Это может быть какой-то пользовательский бизнес-процесс, если он не очень сложный, либо набор каких-то «технических примеров», без реального рабочего продукта.
По сути, на этом шаге вам нужно выбрать, на примере какой задачи вы будете пытаться подружить свой «идеальный образ системы» из п. 1 с выбранной в п. 4 библиотекой.
Выбрать и приступить к реализации.
На этом шаге основная ваша цель – не только изучить предоставленное API и научиться им пользоваться, но и найти точки внедрения собственной логики там, где API не позволяет достичь желаемого. Собственно, поиск и реализация точек внедрения – самая основная и сложная часть, т.к. если вам этого не требуется, то и проект ваш, скорее всего, не является «нетиповым», а мой пост не про этот кейс.
Этот шаг самый коварный. На нем важно не зарыться и случайно не отступить от цели, которая у нас, напомню, одна: убедиться, что требуемый подход принципиально реализуем. На этом пути вас будут ждать препятствия, представлю поимённо самых злостных врагов:
Попытка реализовать законченный бизнес-функционал. Нет, тем, что вы сейчас напишете – никто пользоваться не будет. Это исключительно инструмент достижения понимания, не более. Максимум сделанный вами код пригодится как шпаргалка – относитесь к нему соответственно.
Попытка скрупулёзно следовать каким-то архитектурным паттернам и гайдлайнам по коду. Да забейте, код вы пишете на выброс.
Брат-близнец предыдущего пункта: желание рефакторить. Если вас по ходу работы посетили какие-то озарения и идеи по поводу будущей организации кода – конечно, запишите их, чтобы не терять. Но бежать причёсывать то, чему, возможно, не суждено работать – явно преждевременно. Не тратьте на это время, оно ещё вам пригодится потом.
Единственное исключение здесь – если текущее состояние вашего «спагетти» уже вам самому мешает разобраться в причинно-следственных связях. В таком случае, конечно, не нужно откладывать, т.к. главный критерий сейчас – это ваше собственное понимание, что происходит, и ничто не должно быть ему помехой.Наше любимое: заложить «функционал на будущее». Как уже неоднократно замечал, будущего у нашего кода может и не случиться, поэтому закладывать что-либо сейчас бессмысленно. Запись в «блокнотик» - максимум.
Сорваться на какой-то второстепенный функционал, попытаться реализовать его или «вылизать до идеала». Связан исключительно с потерей фокуса на цель. Не теряйте его. Как обычно, есть идея – кидаем в бэклог, сиречь «блокнотик» - и движемся дальше.
Поиск точек внедрения логики
Как я уже отмечал выше, эта статья – про тот случай, когда выбранная библиотека почти позволяет делать то, что вам требуется, но не совсем. И тут два варианта:
Форк и доработка непосредственно в коде библиотеки.
Расширение функциональности путём наследования / внедрения зависимости (если есть) – в общем, написание ещё одного слоя поверх.
В своей практике я до последнего стараюсь избегать первый сценарий, даже если у меня в планах есть законтрибьютить решение в опенсорс. Причин здесь несколько:
Не хочется отвечать за актуальность ещё одного репозитория. Вместо апдейта зависимостей в проекте придётся отдельно ещё заливать обновления в дополнительный реп, и лишняя работа удручает, её и без того много.
Обратная совместимость и избегание конфликтов. Не хочется случайно наделать таких изменений, что каждый новый git pull будет приводить к неработоспособности кода, вызывать конфликты и необходимость разбираться, чего там разработчики такого поменяли. Одно простое изменение форматирования кода на чьей-либо стороне может отнять время на разруливание конфликта. При написании «слоя поверх» у вас, конечно, тоже сохраняются риски, что какие-то изменения затронут и ваш слой, но разруливать конфликты на каждом апдейте уж точно не придётся.
Даже если вы планируете контрибьютить сделанное решение, сперва реализовать его не внутри форка, а как отдельный слой – хороший выбор, т.к. более наглядно видно, какой код вы реализовали сами, а где вынуждены будете что-то переписать в исходной библиотеке. Классы, которые вы наследуете, чтобы изменить функциональность исходных – можно сразу выделять в отдельный каталог/неймспейс. Не исключено, что к моменту решения задачи надобность в некоторых из них у вас отпадёт, либо «вендор» решит часть проблемы самостоятельно. Кроме того, вы сразу видите, где логика, решающая именно вашу проблему, а где – системная. Потом самому будет легче разобраться и аргументировать изменения перед майнтейнером. Да-да, кто не пробовал – это тоже, скорее всего, придётся делать. Ваш пулл-реквест с крутым работающим функционалом ещё могут даже и не принять, если вы там кучу всего наворотили необъяснимого, потенциально влияющего на другие, незамеченные вами части системы.
В продолжение предыдущего пункта: когда точки внедрения в проект наглядно обозначены, проще контролировать, в верном ли направлении вы движетесь. К примеру, если для достижения желаемого вам достаточно поменять пару классов, то это отличный результат… а вот если пару десятков/сотен классов – тогда встаёт вопрос «зачем всё», если пришлось перелопатить библиотеку почти целиком. Либо точки внедрения не самые оптимальные найдены, либо выбор библиотеки стоит пересмотреть…
Порой бывают ситуации, когда авторы библиотеки практически не оставляют выбора, закрывая всё нужное нам API через private и final… причины могут быть разными, от просто недальновидности до следования каким-то сложносоченённым паттернам. Честно, даже в этой ситуации я предпочитаю воспользоваться чем-то вроде этого, чтобы не форкать и не играть по усложнённым правилам автора. Цель всё та же – экономия времени, а играться в идеальную архитектуру будем потом, когда гипотеза будет подтверждена.
Неудача - тоже результат
Допустим вы преодолели все искушения, и достигли желаемой цели – подтвердили работоспособность гипотезы, пригодность стороннего решения для использования в ваших целях. Этот кейс детально мы разберем в следующей главе.
Но что если у вас ничего не получилось? Внешняя либа не гибкая, много чего «прибито гвоздями» и т.п…. Что ж, есть два пути:
Ваше видение архитектуры из п. 1 было слишком идеалистичным / требовательным. Возможно, если пересмотреть какие-то «постулаты», то удастся малой кровью и деревянного «буратино» адаптировать к вашему «балету»?
Не зря мы составляли шорт-лист решений в п. 4. Вернитесь к нему. Возможно даже, стоит вернуться к п. 3 и пересоставить шортлист заново, учитывая сложности, с которыми столкнулись.
Конечно, и это не гарантия того, что всё получится. В подобных историях вообще не бывает гарантий, так что не исключено, что вам придётся в итоге смириться с какими-то существенными ограничениями или вовсе с невозможностью реализовать желаемое выбранным способом.
Работа с подтверждённой гипотезой
Итак, мы выяснили, что задача принципиально выполнима: выбранную в п. 4 систему возможно использовать в соответствии с подходом, описанным в п. 1. В подтверждение есть вам одному ведомо как работающий (говно-)код из п. 5, и «блокнотик» с заметками и нереализованными идеями.
Вот теперь можно смело двигаться к цели, не опасаясь, что она была иллюзорной.
6. Разработка более детализированной архитектуры решения. Вот теперь можно 😊 Ваши заметки в «блокнотике» и код, проверяющий гипотезу, скорее всего, дадут какие-то подсказки о том, как стоит изменить и адаптировать ваш абстрактный-абстрактный API из п. 1 в просто абстрактный.
7. Если ваш код получился «не совсем на выброс» - можно начать разработку функционала с его рефакторинга. Хотя в ряде случаев на моей практике было проще удалить всё и переписать с нуля, просто «подглядывая в шпаргалку», чтобы не тянуть за собой наследие неудачных экспериментов и временных допущений.
Несколько важных замечаний для этого этапа:
Пусть вы более детально проработали архитектуру, но всё же не отказывайтесь от гибкости, меняйте её, если видите, что где-то стоит поступиться идеалом.
Самоограничение по рефакторингу: основная цель данного этапа – уже реализация функционала, чтобы у вас был работающий продукт. Если вас преследует идея следовать каким-то сложным паттернам, внедрить 15 слоёв необязательной абстракции и т.п. – остановитесь, возможно будет достаточно сделать только 5, а остальные 10 дописать уже в виде рефакторинга готового рабочего решения? Увеличивайте сложность постепенно, чтобы самим об неё не споткнуться.
Самоограничение по оптимизации. Имеет смысл, кроме случаев, когда максимальная производительность – основная цель решаемой задачи. Но даже и тогда стоит понимать, что на какие-то некритичные куски стоит забить до лучших времён. Так что расставляйте приоритеты, и не отвлекайтесь на мелочи, которые не приведут вас к цели. А «блокнотик» с заметками всегда к вашим услугам 😊
И далее мы приходим к заключительному этапу:
8. Финализация и полировка. Поздравляю, мы приехали! У вас на руках реализованное решение, которое даже работает. Да, там есть технический долг и всё такое, но это уже вопрос дальнейшей разработки и развития. С этого момента заканчивается борьба с неизведанным без гарантии результата – начинается скучная работа по конкретным и понятным таскам из вашего «блокнотика». Ну и реализация бизнес-задач – ради чего всё это и делалось.
Психологические моменты
Начали статью с нейросетей – предлагаю ими же её и закончить. Ниже будут мои личные лайфхаки по продуктивному расходованию мозговых ресурсов при таком «хакинге».
Меч – продолжение руки, ну а блокнотик – продолжение нашей скудной церебральной оперативки. Некоторые заметки я веду прямо в блокнотике, некоторые – записываю на планшете пером, но основная идея в том, чтобы работать с абстрактными не оформившимися идеями «от руки». Т.к. это самый «прямой» способ выгрузить в своп те данные, что в памяти уже не помещаются – вот практически «как есть», без необходимости прогонять через «сериализацию», чтобы загнать в какой-нибудь рисовщик диаграмм или набить в текстовый документ… Там вас начнут отвлекать какие-то особенности работы с конкретным приложением, детали форматирования и прочая шелуха, из-за которой есть риск «потерять нить». Нацарапать абы как пару каракуль на бумаге – гораздо быстрее, и ближайшие часы вы, скорее всего, всё ещё будете помнить, о чем они… а большего и не надо.
Да тут же всё понятно! Тому, кто это писал, конечно... но только на первый день. Кроме скорости есть ещё и второй момент. Пока вы делаете первые шаги по «хакингу» внешних библиотек, помнить, скорее всего, придётся многое. Наша память – она как альпинист: чтобы «забраться на вершину» ей нужно за что-то цепляться. Рукописные заметки и рисунки – идеальная гора с кучей выступов и выщерблин, за которые можно зацепиться. По крайней мере для меня даже зачеркнутый текст, помарка, опечатка – это отличный способ отпечатать текст / картинку в памяти, потому что у него уже появилась «особенность». Печатный же текст / диаграмма – как идеально гладкий склон, в котором всё настолько вылизано, что и зацепиться не за что. К этому я обычно перехожу на поздних этапах, когда нужно «запечатлеть в камне» финальную версию идеи, скорее всего уже реализованной.
Можно было, конечно, и в draw.io это сделать...Финальная версия, как правило, там как раз и делается. Порой код только отвлекает. Бывает, что сидишь, смотришь на него, смотришь бессмысленно, и возникает какой-то рефлекс начать что-то писать… а писать-то рано ещё, да и нечего, пока этап проектирования. В таких случаях я стараюсь, наоборот, уйти от кода, чтобы он не отвлекал, закрыть IDE и не смотреть в неё. Уйти гулять или рисовать в «блокнотике». Сущностями, алгоритмами взаимосвязями порой лучше оперировать в отрыве от конкретной реализации в коде, где слишком много отвлекающих деталей. Если вовремя не вырваться из этой ловушки, то ведь действительно начинаешь какую-то дичь писать, переставлять строки, именовать функции – в общем, делать бессмысленное перекладывания букв, вместо того чтобы утрясти более высокоуровневые сущности у себя в голове.
А ещё бывают моменты «просветления», когда «внезапно всё понял», и хочется кинуться скорее писать, пока понимание здесь, со мной. Тут тоже бывает по-разному. Если это какая-то мелочь, которую можно быстро проверить «работает – не работает» - тогда полный вперёд! А вот если озарение на счёт чего-то большого, чего за один день не напишешь – лучше держаться подальше от непосредственно кода, и зафиксировать идею «в блокнотике», там же начать её обращивать деталями, прежде чем приступить к программированию. Потому что если начать писать сразу код – можно не успеть сделать всё за один присест, а озарение уже уйдёт. Либо можно зарыться в деталях и опять потерять нить… Либо же, зарывшись в деталях, не заметить, что сама концепция ошибочна, написать кучу всего, потратить время на отладку, интерфейсы, взаимосвязи, документирование… и потом внезапно понять, что всё это – в утиль. В общем, в таких случаях лучше потратить образовавшуюся энергию на то, чтобы превратить творческий порыв в список конкретных рутинных шагов, которые вы будете равномерно и без лишних страстей выполнять до получения результата. Вот так вот, убил всю романтику творчества…
Самое простое – порой, идеям надо просто «отлежаться», чтобы черезо время вы вернулись в проект и посмотрели на то, что неделями не получалось, свежим взглядом. Хорошо, если сроки не горят и время на такой «творческий отпуск» есть…
А поговорить?..
Спасибо, если дочитали.
Конечно, статья довольно абстрактная, с почти нулевой технической составляющей. Поднятые мной вопросы, наверное, могут быть понятны далеко не всем – как минимум начинающим разработчикам в первые пару лет своей карьеры обычно не приходится с такими проблемами сталкиваться. Но и мой 10-летний опыт в разработке – тоже не ах какой «налёт часов», множество людей сталкивались с кейсами гораздо более сложными, чем приходилось решать мне. Поэтому приглашаю обменяться мнениями / опытом / своими лайфхаками в комментариях или в ответных статьях.
Всем добра и успешных проектов!