Есть два непримиримых лагеря. Одни говорят: ты должен качественно и грамотно описывать свои коммиты. Каждый коммит — это законченный, осмысленный кусочек работы. Если ты не можешь сделать понятное и простое описания коммита, значит у тебя неправильные коммиты.
Другие считают — ты делаешь коммиты как хочешь, это часть твоего личного рабочего процесса. А вот пул реквесты описываются детально: что сделано, как сделано, зачем сделано. Как протестировано, какую проблему решает, на что стоит обратить внимание.
Я убежденный сторонник второго подхода — мне неудобно бить свою работу на микрокусочки. Я беру небольшую задачу и хаотично мечусь по кодовой базе, экспериментируя и внося изменения в том порядке, в котором получается. Если бы я мог нажать на кнопку, и моя работа переструктурировалась бы на хорошие коммиты — я бы на неё нажал. Но кнопки нет, а ломать себя мне не хочется. В то же время я достиг определенного мастерства в описании пул реквестов. Мне довелось фигачить код в майкрософт (через аутстаф, не считается), и там я почерпнул топовые стандарты оформления пул реквестов. С годами работы я только развивал эту практику. Обычно мне удавалось убеждать команду использовать именно такой подход.
Но на последней работе мне достался девлид — убежденный сторонник детальных коммитов. О, мы долго спорили. Моё подчиненное положение сыграло свою роль, совковскую привычку соглашаться с главным нелегко заткнуть. Я не был так категоричен, как обычно бываю, и был уложен на обе лопатки. Уложен, но не убежден.
Если вдруг разрабы объединятся в единое правительство, установят правила и начнут объявлять определенные подходы вне закона — гражданская война начнется на следующий же день. Все потому что в мире разработчиков существует вера в объективное и измеримое. В не зависящую от человеческого восприятия истину. И если все это существует, значит кто-то из спорящих точно объективно не прав.
Мой первый серьёзный рабочий спор произошёл довольно рано. Я тогда ещё был зелёным джуном, который думает что он аппер мидл, да и вообще очень крутой и умный разраб. Один настоящий разраб из моей команды закинул ПР. У нас была практика, что все члены команды делают код ревью. Я открыл код, и почти сразу увидел проблему. Человек написал тест к какой-то функции, которая принимала число. Так вот он скормил ей 0, 1000, и random(0, 1000).
Я в то время очень остро чувствовал, что другие тиммейты видят во мне глупого новичка. И выжидал момента, чтобы размазать их своим видением. Мне повезло — рандом в тестах!
Я не очень-то разбирался в теории юнит-тестирования, но я почитал пару книжек, и твёрдо запомнил — один и тот же юнит-тест на одной и той же кодовой базе должен давать один и тот же результат. Я потратил около часа на продумывание комментария, чтобы он не выглядел токсично, но явно давал понять — только мартышка, которую буквально вчера научили считать, сможет додуматься до такого решения. В итоге он конечно же смотрелся предельно глупо, как и всё, что из себя выдавливают вчерашние стажёры, возомнившие себя разработчиками.
На следующий день я понял, что открыл портал в ад. Под моим комментарием было ещё сто. Весь остаток дня вместо команды из шести разработчиков мы были командой из шести обезьян. Мы спорили на английском в самом ПРе, хотя все были русскими. Когда запаса слов не хватало — созванивались. Кидались друг в друга ссылками, цитатами из книг, исследованиями, переходили на личности. На один мой коммент коллега ответил скриншотом из английского словаря, чтобы высмеять мой английский. Но мы так и не пришли к компромиссу.
Вопрос был сложный. С одной стороны, мы все вообще не знали, за что отвечает этот параметр, наша функция передавала его в другую, из интёрнал либы, к которой не было доки. Поэтому, теоретически, функция могла свалится на любом значении, а значит тест мог это поймать, и тогда мы бы смогли предвосхитить ошибку на проде. С другой стороны, у нас и так была проблема рандомно валящихся билдов. Из-за этого, если проект билдился локально и валился на сиае, мы тупо заказывали новый билд, и не смотрели никакие логи. Ведь билд итак занимал четыре часа, а пулл реквест твой никто и не посмотрит, пока он не прошёл CI.
Кроме того тачки часто были заняты — гораздо прагматичнее было заказать билд, пока он ещё заказывается, чем искать потенциальную «проблему» (которая почти всегда оказывалась отлетевшим таймаутом в интеграционном тесте). Это означало, что даже если бы рандомный тест свалился — мы бы не обратили внимания.
Всё разрешилось на следующем дейлике. Мы созвонились всей командой, и полчаса продолжали спорить — на русском. Мы не заметили, как наш американский менеджер-разработчик присоединился к звонку, и продолжали на повышенных тонах громить друг друга аргументами. Пока не услышали «Hey guys, it is absolutely not important. Just merge it as is». Я не знаю, сколько времени он слушал нас до этого, и как понял, о чём мы говорим, но спорить мы резко перестали. Вмержили и забыли об этом. Разошлись с миром, но не переубежденными.
С тех пор я набрался опыта, чтобы понимать — да насрать вообще, рандом, фор, или граничные кейсы. Это чёртов юнит тест, будет мешать — перепишем. Всё лучше чем спорить весь день. Но если представить, что я в идеальном мире, где необходимо всегда принимать только более верные решения — я не знаю, что делать. Мы вмёржили тест с рандомом, и сейчас я это понимаю как выбор в пользу надёжности, но в ущерб удобству разработки.
На днях я столкнулся с похожей дилеммой. Я игрался с новым тайпскриптом, и написал штуку, которую можно использовать вот так
// LessThan<T> и MoreThan<T> - это и есть то, что я написал.
// Типы, с помощью которых можно ограничивать входной параметр функции
// вот тут я объявляю некую фн,
// которая принимает число, меньшее чем 100
const consumeNumberLessThan100 = (v: LessThan<100>) =>
doWork(v);
// а тут фн принимает число, большее чем 100
const consumeNumberMoreThan100 = (v: MoreThan<100>) => doWork(v);
const sample = (x: number) => {
// тут с помощью функции check я проверяю,
// меньше ли x чем 100
// кстати "<" и ">" чекаются компилятором,
// скормить вместо этих символов что-то другое не получится
const lessThan100 = check(x,'<', 100);
if (lessThan100) {
// здесь я точно знаю - да меньше
// потому что в этой ветке исполнения
// компилятор тайпскрипта знает что, у lessThan100 тип - LessThan<100>
// вот этот код работает
consumeNumberLessThan100(lessThan100);
// а здесь - ошибка компиляции, функция ждёт число,
//для которого доказано, что оно > 100.
consumeNumberMoreThan100(lessThan100);
}
const moreThan100 = check(x, '>', 100);
if (moreThan100) {
consumeNumberMoreThan100(moreThan100); // работает
consumeNumberLessThan100(moreThan100); // ошибка
}
consumeNumberLessThan100(x); // ошибка
consumeNumberMoreThan100(x); // ошибка
}
Я подумал, блин, это очень круто — так можно ловить большое количество ошибок на более ранней стадии. Сам дизайн моих инструментов ограничения значений ужасен, но пока это просто концепт. В дальнейшем его можно легко расширить, построить мощный DSL и формулировать действительно сложные условия соответствия параметра, гарантированные на стадии компиляции. Я прикинул, как оно может увеличить надёжность любой кодовой базы, воспрял духом, и разослал сниппет разным знакомым разрабам.
Мнения опять разделились, причём сильно не в мою сторону. Переусложнение, оверинжиниринг, неподдерживаемо, все будут глушить твой гард с помощью каста к эни. Не читается. Хорошо, как эксперимент — плохо в настоящем проекте. Примеры использования высоссаны из пальца. Займись уже делом, Фил.
Сторонники подхода говорили, да, так надёжней. Чем раньше ловишь ошибку, тем лучше. Кроме того, если ты пишешь функцию, которая работает с ограниченным числом, проверку всё равно придётся писать, но она будет работать только в рантайме, и увеличит количество кода в теле функции.
Сейчас я немного умнее, чем раньше, и научился слышать аргументы. Я представил себе, как в реальном проекте пишу функцию, защищённую моим типом. Как все, кто её использует спрашивают меня, что это за херня. Как, прошарив фишку, они начинают обмазывать кодовую базу кастомным, тухло выглядящим DSL-ем. Как мы по триста раз проверяем значения, которые на самом деле никогда не будут превышать допустимое. Подход действительно ужасен в использовании, какие-то проблемы можно решить или сгладить, но например вот такой кейс
consumeNumberLessThan100(90);
не сгладить никак. Мне придётся доказывать компилятору, что 90 меньше 100. Я снабжу либу асертами, и получится
consumeNumberLessThan100(assert(90, '<', 100));
Выглядит не очень-то круто. Все аргументы «против» — в кассу, но аргументам «за» они не противоречат. Получается дилемма — удобство разработки, или надёжность. Вот тут мы попадаем в ловушку, мы начинаем думать, что надо посчитать, что там за удобство и что там за надёжность. Но удобство и надёжность в разработке, очень, очень сложные вещи. Они состоят из тысяч параметров.
Например удобство, это когда IDE комплитит за тебя код по паре введённых символов, когда код легко читается, когда не заглядывая в доку ты можешь поменять функциональность метода. Когда компилятор не нагружается статическим анализом, билд проходит быстро, текстовый редактор мгновенно рендерит символы. Но когда компилятор обнаруживает и подсвечивает тебе ошибку — это тоже удобство. А ещё это надёжность. Которая в свою очередь собирается из огромного числа совершенно разных вещей.
Ты должен сесть, и посчитать, как долго просуществует проект, в какую сторону он пойдёт, сколько раз в истории человечества кто-то вызовет тот или иной метод твоей кодовой базы, какие разработчики будут здесь работать. Это бесконечный список вопросов, на добрую половину которых невозможно просчитать правильный ответ. Только угадать.
Однажды друг попросил меня подготовить технические вопросы для интервью с Андреем Бреславом. Я пошёл в доку котлина, чтобы найти спорные моменты в дизайне. Их там, как и везде, тьма. Но больше всего меня заинтересовал подход к обработке исключений. Обработчик исключений в Котлине — это выражение, вполне себе функциональный и надёжный подход. Вот только обрабатывать ошибки не обязательно. Что роняет всю надёжность на ноль. А интересно это потому, что прямо в доке разработчики не поленились объяснить свой выбор: «есть исследования, что обязательная обработка ошибок сильно снижает производительность разработчиков при незначительном снижении ошибок».
Я охренел, как ты можешь писать код, если понятия не имеешь, что делать в случае, когда он сработает неправильно!? Если ты не знаешь, как обработать исключение, то не обрабатывать — не решение проблемы, а откладывание. В какой-то момент код свалится, и проблема, которая существует всегда, нанесёт ущерб, который можно было предвосхитить.
Но логичный аргумент против не нужен, можно обойтись просто статистикой. Для разработчиков котлина, исследования бьют логику, потому что у них есть философия. Их философия — прагматизм. Железный, непрошибаемый прагматизм, последовательно встроенный во все особенности этого языка программирования. Точно так же поступают идеалисты, которые пилят хаскели/идрисы, и говнокодеры, которые пишут голанг. Философия разумного компромисса царит в кодовой базе F#. И нет ощущения, что кто-то из них прав, а остальные — дураки.
Все эти люди значительно умнее и опытнее таких как я, и они, похоже давно поняли — ты вырабатываешь себе философию, а потом просто следуешь ей. Потому что киллер фича любой философии — резолвить дилеммы.
И так философия появилась во всем в ИТ, а философия — это полная противоположность идее единой и истинной объективности, потому что философия предлагает выбрать для себя истину субъективную.
У каждого из нас есть своя философия — она не описывается одним словом, это сложный набор паттернов для принятия решений. И мы часто их меняем или расширяем. На практике получается, что у тебя есть проект со своей философией, который пишется на языке программирования со своей философией, с помощью фреймворков и инструментов, в каждом из которых есть своя философия. И пишут этот проект разработчики, каждый со своей философией. И каждый раз, когда надо принять какое то решение, только стечение обстоятельств влияет на то, какое именно будет принято. Не управление сложностью, не опыт, не подход, не знания, а просто случайность.
И все эти проекты работают. Львиная доля всех задач, которые решают разработчики — это ликвидация последствий ошибок других разработчиков, но проекты работают, и решают проблемы людей. Умные люди придумывают практики и архитектурные паттерны. Языки программирования с мощным контролем типов и контрактами — всё, лишь бы разработчики поменьше ошибались. А я каждый день открываю эджайл борд на работе, и вижу там больше багов, чем задач. Где каждый баг, это очередной, управляющий сложностью, обосравшийся разраб.
Философия разработки — это просто выбор способа, которым ты в финале обосрёшься. Но если не будет философии, а только чистая объективная логика, то любой спор сведется к проблеме всемогущего творца и неподнимаемого камня.
Я уже давно в разработке, и хорошо научился делать как говорят, даже когда в корне не согласен, и стал писать сверхдетальные коммиты. Мой девлид — настоящий вампир. Он требует не просто описание — он хочет, чтобы я написал, что делает каждый файл, зачем делает, как делает. Какую проблему решает коммит.
Я нафигачил восемь экстраподробных коммитов, такое же подробное описание пул реквеста — и мне это капец как понравилось. С тех пор мы почти не вели работу именно над этим проектом, но я время от времени захожу туда полюбоваться на эти коммиты. Серьезно, очень, очень круто. И во всех остальных проектах, кроме своих личных, я теперь применяю этот подход.
Мне было очень трудно перестроить рабочий процесс. Прошло полтора месяца, и мне всё ещё тяжело писать код так. Но, похоже, оно того стоит.
Или нет. Я, если честно, не знаю. И я не знаю, что если оно того точно стоит, делает ли это меня двухмесячной давности круглым идиотом. В мире точно где-то ходит человек десять, которых я отучил делать такие коммиты. А они идиоты? Я так не думаю.
Мой подкаст
Автор: Philipp Ranzhin