В идеальном мире программист сразу понимает задачу полностью во всех деталях, затем продумывает наилучший метод её решения, держа в голове все нюансы, а затем садится и пишет идеальный код, после чего уезжает в закат по радуге на единороге. Но на практике, без итераций при написании кода не обходится. Но их гранулированность должна быть адекватной. Иными словами, если вы пишете, потом переписываете, рефакторите, а снова переписываете – вы что-то делаете не так. Другая крайность – пытаться всё сделать идеально с первого раза, а так как это невозможно для сколь-нибудь нетривиальной задачи, то можно войти в ступор. Описанные ниже идеи могут помочь в этих случаях, дав некоторый критерий того, что уже можно писать код, так как понимания достаточно. Или того, что уже нужно писать код, так как понимания достаточно и дальнейшее промедление — это прокрастинация в чистом виде или саботаж.
Данная статья имеет необычную структуру: вступление, потом – перевод двух статей, которые я не буду комментировать – в них и так всё достаточно понятно – но ниже дополняю своими идеями по теме.
Вступление
Когда-то я вёл практические занятия по программированию, то раз за разом наблюдал одну и ту же картину: после объяснения студентам задания, они тут же принимались писать программу, точнее - стучать по клавишам. Через полчаса – час я интересовался, как у них дела и обнаруживал, что у них ничего не получилось. Часто причиной этому были глупые ошибки, но ещё чаще у студентов не было понимания алгоритма решения задачи и иногда даже понимания самой сути задачи. Напомню, при этом они полчаса – час что-то уже делали, какую-то программу писали.
Размышляя над этой проблематикой, я придумал новый подход – разработка через документирование. Но, как это чаще всего и бывает – оказалось, что всё уже придумано до меня. Правда, поиск в Google выдал не очень много результатов и то, только на английском, и я решил перевести их. Более того, некоторые идеи, которые возникли у меня, в этих статьях не изложены, и будут приведены после перевода в виде дополнений.
Сначала приведу документ за авторством Zach Supalla, который похож на манифест. Так сказать, для разминки. Потом будет перевод подробной статьи от Corbin Crutchley.
Разработка через документирование
Автор: Zach Supalla
Философия разработки, основанной на документации, проста: с точки зрения пользователя, если функция не задокументирована, значит, она не существует, а если функция задокументирована неправильно, то она сломана.
Сначала задокументируйте функцию. Подумайте, как вы собираетесь описать эту функцию пользователям; если это не задокументировано, то оно не существует. Документация — лучший способ определить функцию с точки зрения пользователя.
По возможности документация должна быть проверена пользователями (сообществом или Spark Elite) до начала какой-либо разработки.
После написания документации следует начать разработку, причем разработка через тестирование является предпочтительной.
Должны быть написаны модульные тесты, которые проверяют функции, как описано в документации. Если функциональность когда-либо выйдет за рамки документации, тесты должны провалиться.
Когда функция изменяется, ее следует сначала изменить в документации.
Когда изменяется документация, то же самое следует делать и с тестами.
И документация, и программное обеспечение должны иметь версии, а версии должны совпадать, чтобы тот, кто работает со старыми версиями программного обеспечения, мог найти подходящую документацию.
Итак, предпочтительный порядок операций для новых функций:
Написание документации
Получить отзыв о документации
Разработка через тестирование (когда тесты соответствуют документации)
Перенесите функции в промежуточную версию
Функциональное тестирование на стадии разработки, при необходимости
Функция доставки
Публикация документации
Дополнительные версии
Лучший способ программирования: Разработка через документирование
Автор: Corbin Crutchley
Если вы давно в разработке, то, несомненно, слышали выражение «разработка через тестирование» или сокращенно «TDD».
Идея TDD заключается в том, чтобы писать тесты перед тем, как приступать к реализации чего-либо. Предположим, что вы хотите реализовать функцию calculateUserScore на основе K/D пользователя в разрабатываемой вами игре. Согласно TDD, вам следует начать с написания модульных или интеграционных тестов.
Написанные тесты могут оказаться отличными помощниками для того, чтобы ваша программа работала так, как задумано. Однако есть один недостаток: тесты по-прежнему являются формой программирования. Даже тогда, когда вы следуете лучшим практикам тестирования, это по-прежнему разработка программного обеспечения, и в конце концов ваши тесты все равно должны быть пройдены. А быть уверенным в том, что тесты могут быть пройдены, может оказаться непросто, поскольку часто неизвестны детали реализации. Например, если вы ожидаете, что функция parseInt работает одним образом, а по факту окажется что она работает другим, вам, скорее всего, придется переписать тесты, которые были написаны на основе неправильного предположения.
В результате многие предпочитают начать реализовывать функцию для проверки концепции, а затем постепенно добавлять тесты по мере реализации: так сказать, TDD-lite.
Проблема в том, что, поступая так, вы теряете одно из наиболее важных преимуществ разработке через тестирование: её способности заставить вас столкнуться с вашим API как можно раньше.
API — это сложно
Предположим, вы работаете в инди-компании и разрабатываете небольшой шутер с видом сверху, реализуемом на JavaScript с помощью Phaser. Ваш начальник попросил вас реализовать систему подсчёта очков.
«Нет проблем, реализовать функцию calculateUserScore будет очень просто — нет нужды много думать — думаете вы, печатая базовую реализацию:
function calculateUserScore({kills, deaths}) {
return parseInt(kills / deaths, 10)
}
Но стоп! А что насчет ассистирований? Они считаются? Конечно! Давайте приравняем их к половине убийства:
function calculateUserScore({kills, deaths, assists}) {
const totalKills = kills + (assists / 2);
return parseInt(totalKills / deaths, 10)
}
Да, но за некоторые убийства должны даваться бонусные очки. Если раньше параметр kills был просто числом, то с учётом новых вводных его нужно заменить на массив объектов, например:
const killsArr = [{additionalPoints: 3 } ]
Теперь функция подсчета очков должна выглядеть так:
function calculateUserScore({killsArr, deaths, assists}) {
const kills = killsArr.length;
const additionalPoints = killsArr.reduce((prev, k) => k.additionalPoints, 0);
const totalKills = kills + (assists / 2);
return parseInt((totalKills / deaths) + additionalPoints, 10);
}
В то время, когда мы рассматриваем изменения в данной функции, нужно помнить, что она может использоваться во многих местах кодовой базы. Хуже того, есть вероятность что ваш API всё ещё не идеален. Что, если вы хотите отобразить убийства с дополнительными очками после матча?
Эти радикальные изменения означают, что каждая итерация требует дополнительной работы по рефакторингу, что, вероятно, приведёт к задержке выполнения задачи.
Давайте сделаем шаг назад. Почему так случилось?
Эти проблемы, как правило, возникают из-за неправильного понимания масштаба задачи. Это недопонимание может возникать между командами, отдельными разработчиками и даже просто в рамках вашего внутреннего монолога.
Тестирование — это сложно
Один из способов, с помощью которого многие предлагают обойти эту проблему — следовать TDD. TDD может помочь заранее обратиться к вашему API, добавив цикл обратной связи.
Например, прежде чем реализовывать функцию calculateUserScore, вы можете протестировать первую реализацию, добавить test.todo как вспомогательное средство и понять, как следует обновить свой API, прежде чем двигаться дальше.
Однако, хотя TDD и заставляет вас обращаться к своему API, этот подход не позволяет преодолеть ограниченность вашего понимания того, как и где разрабатываемая функция будет использоваться в рамках всего проекта.
Позволь объяснить:
Допустим, отображение особых убийств на экране результатов в конце матча невозможно до более поздних стадий разработки. Вы это знаете и решили остановиться на второй реализации, где параметр kills просто число. Однако, поскольку функция часто используется в кодовой базе, позже вам потребуется провести масштабный рефакторинг.
Если бы вы поговорили с другими инженерами, вы бы узнали, что разработка экрана результатов была завершена раньше, чем ожидалось. К сожалению, это обнаружится только после код ревью, что приведёт к немедленному рефакторингу.
Переходим к работе
Хорошо, хорошо, я перейду к делу: есть лучший способ решить эту проблему «изменения API», чем TDD. Этот «лучший путь» — «разработка через документирование».
Предварительное написание документации может помочь вам заранее уточнить детали реализации, прежде чем принимать окончательные решения по поводу реализации. Даже референсные API могут помочь продвинуться в дизайне проекта.
Давайте вернемся к более старому примеру calculateUserScore. Как и раньше, вы созываете короткое собрание, чтобы собрать требования от команды. Однако на этот раз вы начинаете с написания документации, а не с кода.
Вы включаете упоминание о том, как должен выглядеть API, исходя из этих требований:
/**
* This function should calculate the user score based on the K/D of the
* player.
*
* Assists should count as half of a kill
*
* TODO: Add specialty kills with bonus points
*/
function calculateUserScore(props: {kills: number, deaths: number, assists: number}): number;
Вы также задокументировали некоторые примеры использования:
caluculateUserScore({kills: 12, deaths: 9, assists: 3});
Работая над этой документацией, вы решаете быстро набросать, как может выглядеть будущий API с добавлением бонусных баллов.
/*
* TODO: In the future, it might look something like this to accommodate
* bonus points
*/
calculateUserScore({kills: [{killedUser: 'user1', bonusPoints: 1}], deaths: 0, assists: 0});
Написав это, вы поймёте, что вам следует использовать массив для свойства kills с самого начала. Вам не обязательно передавать в функцию бонусные очки, вместо этого вы можете просто отслеживать 'unknown' пользователя и запросить bonusPoints для него когда это понадобиться
calculateUserScore({kills: [{killedUser: 'unknown'}], deaths: 0, assists: 0});
Хотя сейчас это кажется очевидным, изначально это было не ясно. В этом и преимущество подхода в разработке через документирование: он заставляет пройти цикл обратной связи по определению API и объема работы.
Совершенствование процесса
Хорошо, я понимаю. Документирование обычно рассматривается как рутинная работа. Хотя можно было бы продолжать в стиле «спорт полезен для здоровья», у меня есть для вас хорошие новости: документация не означает только то, что вы думаете.
Документацию можно найти во многих формах: дизайн макеты, справочная информация по API, хорошо оформленные задачи в jira, описание планов на будущее и многое другое.
По сути, все, что можно использовать чтобы поделиться мыслями по теме, является документацией.
Фактически, это включает в себя и тесты. Тесты — хороший способ предоставить примеры API и самого по себе TDD может быть достаточно, чтобы передать эту информацию вам в будущем. В иных случаях TDD подход может быть отличным дополнением к другим формам документации.
В частности, если вы хороши в написании интеграционных тестов, вы фактически пишете документацию по использованию API во время написания этих тестов.
Это особенно актуально при создании инструментов или библиотек для разработчиков. Примеры их использования, чрезвычайно полезны.
------------------------------
Еще одна вещь, которую не предписывает «разработка на основе документации», — это «пиши правильно с первого раза». Эта идея — миф и может принести много проблем.
Как было показано на примере calculateUserScore, может потребоваться изменить дизайн API, прежде чем переходить к окончательной версии: это нормально. Документация влияет на код, код влияет на документы. То же самое справедливо и для TDD.
------------------------------
Подход DDD полезен не только при разработке. На собеседованиях, для информирования о процессе разработки, будет полезно сначала писать комментарии к коду, а затем писать само решение. Это позволяет допустить ошибки на этапе документирования (написания комментариев), а не на этапе написания кода, что потребует меньших затрат времени на исправления, чем если бы вы допустили ошибку при реализации.
Делая так, вы сможете показать интервьюеру, что умеете работать в команде и достигать чётко поставленных целей. Понимаете, как получить реализацию, избавленную от edgecases - нетипичных или непредвиденных ситуаций, когда что-то может работать неправильно или не так, как ожидалось.
Отмотаем всё назад
Я понимаю, что в этой статье уже больше неожиданных поворотов, чем в фильме Найта Шьямалана, но вот еще один: разработка через документирование, как мы видим, является устоявшейся концепцией. Просто её называют другими именами:
Каждая из этих концепций относится к форме проверки функциональности кода с учетом поведения пользователя. Каждая из концепций поощряет коммуникации, которые часто включают в себя документацию. «DDD» — это просто еще одна форма логики такого типа.
Заключение
Я использовал концепцию разработки через документирование в некоторых проектах. Среди них был проект CLI Testing Library, который позволил мне написать мириады страниц документации, а также подробные ишью на GitHub.
И то, и другое заставляло меня лучше формулировать свои цели и задачи. Я считаю, что конечный продукт от этого становился лучше.
Что вы думаете? «DDD» — хорошая идея? Будете ли вы использовать его для своего следующего проекта? Дайте нам знать, что вы думаете, и присоединяйтесь к нашему Discord, чтобы поговорить с нами об этом!
Дополнение 1. Domain-driven design
В программной индустрии есть ещё такой подход из трёх букв D - domain-driven design или предметно-ориентированное проектирование – это подход, который основан на создании программных абстракций, называемыми моделями предметных областей. В эти модели входит бизнес-логика, устанавливающая связь между реальными условиями области применения продукта и кодом. Иными словами, подход учит разработчиков разговаривать с бизнесом на одном языке. Не на языке программирования, а на языке бизнеса.
А это всё значит на практике: перед тем как писать код мы должны подумать, разработать набор абстракций, на основе которых соорудить модели. И естественно, так как с эти имеют дело одновременно программисты и представители бизнеса, в какой-то форме, хоть даже в устной, всё это не может быть не задокументировано.
Дополнение 2. Документация как ТЗ
Наверное, никто из разработчиков не любит писать ТЗ, отчасти потому что считают это хоть и необходимой, но бестолковой работой. И здесь нет противоречия: без ТЗ практически невозможно конструктивное взаимодействие заказчика с исполнителем, поэтому оно необходимо. Но ТЗ не является частью конечного продукта, и после выполнения и оплаты работ будет пылиться где-то в архивах.
А что если вначале написать документацию и использовать её в качестве ТЗ. Или, формально, первый, вариант документации, возможно, без деталей реализации. Тогда потраченные на это усилия не пропадут даром. И само техническое задание может представлять собой одно предложение: “Программа должна соответствовать документации, представленной в Приложении 1.”, где приложение 1 - и есть наша документация.
Дополнение 3. Документация это не только и не столько описание API
В приведенных выше статьях документация представляла собой приведение сигнатуры функции с описанием того, какие значения она принимает, что делает и что возвращает. Можно сравнить эти описания с деревьями, за которыми сложно увидеть лес – логику и особенности работы модуля, подсистемы или всей программы целиком. Так, правовые акты часто содержат преамбулу – вводная или вступительная часть, в которой в концентрированной форме излагаются цели и задачи данного акта, условия, обстоятельства и мотивы.
В документации самая важная и содержательная часть, на мой взгляд, это не та, что описывает код, а та, что описывает используемые концепции, методологии, паттерны, особенности, причины принятых архитектурных решений и возможные альтернативные варианты. Тем более, что сейчас почти все код пишут самодокументированный, в большей или меньшей степени. И не надо себя обманывать, документация типа:
/*
* Open the file specified by fileName in the given mode.
*/
Handle fileOpen(String fileName, Mode mode);
по факту не несёт никакой информации, помимо той, что уже и так есть в названиях идентификаторов. И как раз написанием общей части зачастую все пренебрегают, потому что это намного сложней, чем просто задокументировать классы и методы.
Дополнение 4. Задокументировать чтобы понять
Можно посмотреть на документирование под другим углом, а именно, как на некий критерий того, что есть понимание происходящего. Но понимание - это слишком общий термин, поэтому можно вспомнить методом выдающегося физика Ричарда Фейнмана. Одна из его интерпретаций гласит: если ты можешь объяснить что-то достаточно простым языком с использованием только базовых понятий и аналогий из других областей, то можно считать что ты это понимаешь. Попытка простым языком описать, как работает программа или её подсистема и какая логика лежит за ней может быстро окончиться болезненной неудачей. Поздравляю: вы разрушили иллюзию понимания.
Лично я не раз сталкивался с ситуацией, когда возникали значительные трудности при попытке изложить на бумаге мысли, которые до этого казались простыми, понятными и структурированными. Как, например, произошло при написании этой статьи.
Можно, конечно, всегда объяснять что-то коллегам, но не всегда в распоряжении есть свободные уши, если не считать резинового утёнка, конечно. Тем более это объяснение скорей всего будет полезным не только вам, но даже и тебе самому, но в будущем. Поэтому практичней будет всё это записать, то есть задокументировать.
Дополнение 5. Документация как промт для GPT
Эта статья была написана давно, когда ChatGPT только появился и вокруг него только разгорался хайп, а переведённые статьи и того раньше. Но появление больших языковых моделей, которые могут написать работающий код, пусть и с большими оговорками, сделало эту статью ещё более актуальной. При вайб-кодинге код перестаёт иметь первостепенную ценность, а тем более тесты и низкоуровневая документация API, с написанием которых нейронка справляется вполне сносно, и на первый план выходят идеи, архитектурные решения, правильно поставленные задачи.
И при таком подходе документация, как пользовательская, так и для разработчиков может стать фундаментом запросов для генерации или исправления кода.
Заключение
В заключение хотелось бы подытожить: важно задокументировать не только API, но и вводные, архитектуру, и всё, что позволяет понять, как работает программа не заглядывая в код. И это нужно не только для того, чтобы не рассказывать одно и то же разным коллегам, но и как критерий того, что вообще есть понимание происходящего.
А порядок действий может выглядеть так:
Разобраться в задаче и продумать метод её решения
Написать черновик документации: либо высокоуровневой — описывающей задачу и подход к её решению, либо низкоуровневой — детализирующей API, либо и ту и другую.
Показать документацию кому-нибудь ещё, и если тебя не понимают, не искать кого-нибудь “поумней”, не пояснять устно, а возвращаться к п.1.
Если используется TDD подход, написать тесты
Написать код
Применение этого подхода может быть, и скорей всего будет болезненным, потому что начнут рушиться иллюзии - иллюзии понимания. А это всегда болезненно. Поэтому не удивляйтесь голосу у себя в голове, который скажет Вам что это пустая трата времени, никому не нужно, да и дедлайн на носу, и вообще я сейчас сделаю задачу по-быстрому и забуду. Не верьте ему.
И напоследок хочу привести одну цитату: “в любой непонятной ситуации – думайте”. И хочется её дополнить: “думайте, и записывай”
P. S. Предвижу вопрос в комментариях: “А сам–то ты пользуешься этим подходом?”. На данный момент нет, но планирую со временем внедрить в свою работу. И эта статья является пунктами 1 и 2 плана выше, то есть я подумал и написал верхнеуровневую документацию, оформив её в виде этого текста.
Gromilo
Я пишу апи и создаю документацию для бэкендеров и других в команде заставляю. Потому что коду не хватает выразительности и люди долго реверсят из него, что хотел сказать автор. А потом не понимают замысла и делают всё поперёк. В итоге, знания хранятся в людях.
В этой доке находятся заложенные идеи, чтобы тот кто хочет погрузиться в проект или в какую-то незнакомую часть мог быстро разобраться что к чему, не реверсируя это из кода.
Я кучу раз сталкивался со странным кодом и не было никакой подсказки - это так задумано, или автор ошибся? Да, тестов тоже не было, как не трудно догадаться. И тут ещё один источник истины с намерениями помог бы.
А если нужно написать какой-то сложный процесс, то коду не хватает выразительности и хорошо бы иметь общую схему, которая будет гораздо короче и содержать всё в одном месте.
Да, код работает в соответствии с кодом, а не документацией и есть проблема с тем, что дока и код разъезжаются. Я решаю эту проблему с помощь дисциплины и ревью. Дока хранится в репе, и если в коде поменялось что-то значимое, а дока не поменялась, значит нужно поменять или дописать.
Сейчас возлагаю надежды на LLM, чтобы они сравнивали написанное в доке с написанным в коде. Или отвечала на вопросы вида: я хочу сделать ххх, что мне нужно знать?