С начала 2018 года я занимаю должность лида/начальника/ведущего разработчика в команде — называйте это как хотите, но суть в том, что я целиком отвечаю за один из модулей и за всех разработчиков, которые над ним работают. Эта позиция открывает мне новый взгляд на процесс разработки, так как я задействован в большем количестве проектов и активнее участвую в принятии решений. Недавно, благодаря этим двум обстоятельствам, я неожиданно осознал, как сильно мера понимания влияет на код и на приложение.
Мысль, которую я хочу выразить, сводится к тому, что качество кода (и конечного продукта) тесно связано с тем, насколько люди, которые занимаются проектированием и пишут код, осознают, что именно они делают.
Вы, возможно, сейчас думаете: «Спасибо, кэп. Конечно, неплохо бы понимать, что вообще пишешь. Иначе с тем же успехом можно нанять группу обезьян, чтоб они молотили по произвольным клавишам, и на этом успокоиться». И вы совершенно правы. Соответственно, я принимаю как данность: вы осознаете, что иметь общее представление о том, что делаешь, необходимо. Это можно назвать нулевым уровнем понимания, и его мы не будем разбирать подробно. Подробно мы будем разбирать, что именно нужно понимать и как это сказывается на решениях, которые вы принимаете каждый день. Если бы я знал эти вещи заранее, это избавило бы меня от массы потраченного впустую времени и сомнительного кода.
Хотя ниже вы ни одной строчки кода не увидите, я все-таки считаю, что все здесь сказанное имеет большое значение для написания качественного, выразительного кода.
Первый уровень понимания: Почему оно не работает?
К этому уровню разработчики обычно приходят на самых ранних этапах своей карьеры, иногда даже без какой-либо помощи со стороны окружающих — по крайней мере, по моим наблюдениям. Представьте, что вы получили багрепорт: какая-то функция в приложении не работает, ее нужно починить. Как вы будете действовать?
Стандартная схема выглядит так:
- Найти фрагмент кода, который вызывает проблему (как это делается — отдельная тема, ее я раскрываю в своей книге об устаревшем коде)
- Внести в этот фрагмент изменения
- Убедиться, что баг исправлен и регрессивных ошибок не возникло
Теперь сосредоточимся на втором пункте — внесении изменений в код. Есть два подхода к этому процессу. Первый: вникнуть в то, что именно происходит в текущем коде, выявить ошибку и исправить ее. Второй: продвигаться на ощупь — добавить, допустим, +1 в условный оператор или цикл, посмотреть, не заработала ли эта функция в нужном сценарии, потом еще что-нибудь попробовать и так до бесконечности.
Правильным является первый подход. Как объясняет в своей книге Code Complete Стив МакКоннелл (кстати, очень ее рекомендую), каждый раз, когда мы что-то меняем в коде, мы должны быть в состоянии с уверенностью предсказать, как это повлияет на приложение. Привожу цитату по памяти, но если багфикс срабатывает не так, как вы ожидали, вас должно это сильно насторожить, вы должны поставить под вопрос весь свой план действий.
Обобщая сказанное, чтобы выполнить добротный багфикс, который не ухудшит качество кода, нужно понимать и всю структуру кода, и источник конкретной проблемы.
Второй уровень понимания: Почему оно работает?
Этот уровень постигается гораздо менее интуитивно, чем предыдущий. Я, будучи еще начинающим разработчиком, усвоил его благодаря начальнику, а впоследствии неоднократно сам объяснял суть дела новичкам.
На этот раз давайте представим, что вам поступило сразу два багрепорта: в первом речь идет о сценарии A, во втором — о сценарии B. В обоих сценариях происходит что-то не то. Соответственно, вы принимаетесь сначала за первый баг. Руководствуясь принципами, которые мы вывели для первого уровня понимания, вы как следует вникаете в код, имеющий отношение к проблеме, выясняете, почему он заставляет приложение вести себя именно так в сценарии А, и вносите разумные коррективы, которые дают именно тот результат, которого вы ожидали. Все идет отлично.
Затем вы переходите к сценарию B. Вы повторяете сценарий в попытке спровоцировать ошибку, но — сюрприз! — теперь все работает как надо. Чтобы подтвердить свою догадку, вы отменяете изменения, внесенные в процессе работы над ошибкой А, и баг B снова возвращается. Ваш багфикс решил обе проблемы. Повезло!
Вы на это совсем не рассчитывали. Вы придумали способ исправить ошибку в сценарии А и понятия не имеете, почему он сработал и для сценария B. На этом этапе очень велик соблазн решить, что обе задачи благополучно выполнены. Это вполне логично: смысл ведь был в том, чтобы устранить ошибки, разве нет? Но работа еще не окончена: вам еще предстоит разобраться, почему ваши действия исправили ошибку в сценарии B. Зачем? Затем, что он, возможно, работает на неверных принципах, и тогда вам нужно будет искать другой выход. Вот пара примеров таких случаев:
- так как решение не подбиралось прицельно под ошибку B с учетом всех факторов, вы, возможно, сами того не подозревая сломали функцию C.
- не исключено, что где-то притаился еще и третий баг, связанный с той же функцией, и ваш багфикс завязывает корректную работу системы в сценарии B на нем. Сейчас все выглядит хорошо, но в один прекрасный день этот третий баг заметят и исправят. Тогда в сценарии B опять возникнет ошибка, и хорошо, если только там.
Все это вносит в код хаотичность и когда-нибудь свалится вам на голову — скорее всего, в самый неподходящий момент. Придется собрать волю в кулак, чтобы заставить себя тратить время на понимание того, почему все с виду работает, но оно того стоит.
Третий уровень понимания: Зачем оно работает?
Мое недавнее озарение связано как раз с этим уровнем, и, наверное, именно он дал бы мне больше всего преимуществ, если бы я пришел к этой мысли раньше.
Чтобы было понятнее, разберем на примере: ваш модуль нужно сделать совместимым с функцией X. Вы не особенно близко знакомы с функцией X, но вам сказали, что для совместимости с ней нужно использовать фреймворк F. Другие модули, которые интегрируются с X, работают именно с ним.
Ваш код с первого дня своей жизни вообще не соприкасался с фреймворком F, поэтому внедрить его будет не так-то просто. Это повлечет серьезные последствия для некоторых составляющих модуля. Тем не менее, вы с головой уходите в разработку: неделями пишете код, тестируете, выкатываете пилотные версии, получаете фидбек, исправляете регрессионные ошибки, обнаруживаете непредвиденные осложнения, не укладываетесь в изначально оговоренные сроки, пишете еще сколько-то кода, тестируете, получаете обратную связь, исправляете регрессионные ошибки — все это ради того, чтобы внедрить фреймворк F.
И в какой-то момент вы вдруг осознаете — или, может быть, слышите от кого-то — что, может быть, фреймворк F вовсе и не даст вам совместимости с функцией X. Может быть, все это время и силы были приложены совершенно не к тому.
Нечто подобное однажды произошло в ходе работы над проектом, за который я отвечал. Почему так получилось? Потому что я плохо понимал, в чем суть функции X и как она связана с фреймворком F. Как мне следовало поступить? Попросить человека, который ставит задачу на разработку, доходчиво объяснить, как намеченный план действий приводит к желаемому исходу, вместо того чтобы просто повторять то, что делалось для других модулей, или верить на слово, что так нужно для работы функции X.
Опыт этого проекта научил меня отказываться начинать процесс разработки, пока у нас нет ясного понимания, почему нас просят выполнить те или иные действия. Отказываться прямым текстом. Когда получаешь задачу, первый импульс — немедленно за нее взяться, чтобы не терять зря времени. Но политика «замораживаем проект, пока не войдем во все частности» может сократить растраченное впустую время на целые порядки.
Даже если на вас пытаются надавить, заставить начать работу, хотя вы не понимаете, чем это обосновано — сопротивляйтесь. Сначала разберитесь, с какой целью вам ставят такую задачу, и решите, верный ли это путь к цели. Мне пришлось все это узнать на горьком опыте — надеюсь, тем, кто это читает, мой пример облегчит жизнь.
Четвертый уровень понимания: ???
В программировании всегда есть чему поучиться, и полагаю, я только затронул самые верхние слои темы понимания. Какие еще уровни понимания вы обнаружили за годы работы с кодом? Какие принимали решения, которые хорошо сказались на качестве кода и приложения? Какие решения оказались ошибочными и преподали вам ценный урок? Делитесь опытом в комментариях.