Comments 22
Более существенно количество паразитных трудозатрат (отношение объема нового кода к объему переписанного кода, объем регрессионного тестирования) при добавлении фич. В продукте с большим техническим долгом внедрение фич вызывает непропорционально большие трудозатраты и чем дольше копится этот долг, тем выше сложность/цена внедрения нового функционала.
При взшляде со стороны продукта правильными метриками могут служить:
- какая фича меняется чаще всего (напр., авторизация пользователей, дашборд, чат)
- где чаще всего возникают ошибки
— добавил код, который меняет данные, ошибка вылезла совсем в другом месте, где раньше все нормально работало (ужас еще в том, что поскольку это место совсем другое, то проблем там не ждешь и это место не тестируется — тут спасет только полный регресс тест, а это время и деньги).
Согласен. Но все же уточню.
- Я не совсем точно выразился. Под фичей я понимал, скорее, подсистему.
- Ошибка в совсем другом месте. Тут можно возразить, что возможно, в таком случае надо рефакторить обе подсистемы — раз они так связаны, что ошибка в одной проявляется в другой. Но в моей практике обычно все же это сигнализировало о проблеме именно в той подсистеме, в которой ошибка и появлялась (хотя и необязательно именно та самая фича).
При такой трактовке, правда, область проблемного кода определяется более широко. Может служить, скорее, как руководство для долгосрочного планирования, нежели решение по тактическому локальному рефакторингу.
В качестве наблюдения: Пробовал анализировать по частоте изменения и цикломатической сложности. Выявилось, что чаще изменяются не куски с большой сложностью, а их клиенты, причём с нарастанием сложности. Проще говоря, сложные непонятные, но точечные куски кода начинают обрастать не менее сложными обёртками, основное назначение которых обычно сводится к обработке особых случаев или новых кейсов. Причём есть тенденция к многослойности таких обёрток, зачастую с повторением во внешних слоях логики, которая уже есть на внутреннем слое, но к которой не пробиться через уже существующие обёртки.
function someComplex(a, b, c) {
// some very complex code
}
function someComplexWithSimpleCase (a, b, c) {
if (a === 0 ) {
// some simple code
} else {
someComplex(a, b, c);
}
}
function adminComplex(user, a, b, c) {
if (user !=== 'admin' ) {
if (a === 0) {
// another simple code
} else {
someComplexWithSimpleCase(a, b, c);
}
} else {
// the same very complex code
}
}
Пример: у нас есть огромный монстр на несколько десятков тысяч строк кода, где внутри ant вызывает maven через bash, а тот в свою очередь обратно ant с помощью десятка захардкоженных паролей (и это, на минуточку, банк). Этот код творил сам Сатана.
Он не меняется, но является стандартом. Все, что он делает — «автоматизирует» работу maven, curl и прочих простых инструментов. Он никому совершенно не нужен.
Все, чем мы заняты круглые сутки — мы привинчиваем свой maven так, чтобы он делал свою работу, но передавал этому монстру пустышку и якобы тот работал тоже.
При этом монстра никто не меняет. Он по всем метрикам идеальный код…
Я вчера уволился к черту со словами: «я не могу больше делать непрофессиональную работу». Ничего не изменилось. Идеальный монстр все равно не был признан техническим долгом. Он будет там, даже когда человечество сбежит из-за него на Марс.
Можно даже предположить, что там где часто изменяют одни и те же файлы, фактически происходит рефакторинг, а там, где старый код долго остается неизменным, есть какая то проблема)
Потребность в рефакторинге очевидна
А вот неочевидна, кстати. Потребность эта появится тогда, когда при дальнейшей поддержке придется вносить изменения в несколько мест сразу. И когда пойдут ошибки из-за того, что частенько забывают изменить везде.
А вот если этот дублирующий код изменяться в дальнейшем будет редко (или вообще никогда), то и рефакторить его по большому счету незачем. Ну, разве что из чувства прекрасного.
- Повторное написание кода — лишние трудозатраты
- Дублирование кода — усложнение продукта, ухудшение его понятности
- Дублирование кода — путь к труднообнаружимым ошибкам, которые сложно устранять (в разных ситуациях работает разный код с одними и теми же данными)
Не-не-не :)
- Повторный код УЖЕ написан. Оставить его как есть — ноль трудозатрат. Рефакторинг — лишние трудозатраты.
- Усложнение продукта. Ну и ладно, работает — не трогай :)
- Путь к ошибкам. Будут ошибки — рассмотрим необходимость рефакторинга. Рефакторинг тоже может быть источником ошибок (я уверен, что нездоровая ситуация, описанная выше, также подразумевает недостаточное покрытие тестами).
С большой вероятностью, если повторный код уже написан второй раз, то он будет писаться и третий, и четвёртый. Причём с бОльшей чем второй: кроме факторов "вроде этого ещё нет" и "что-то похожее есть, но не разобрался, поэтому напишу своё", появляется фактор "кажется тут принято каждый раз писать заново вместо изменения"
Кстати, на третий раз выделить повторяющийся код в абстракцию проще, чем во второй.
Точнее говоря, выделить, может и не проще. Но сама абстракция, скорее всего, будет более правильная.
При втором написании того же кода совсем не всегда понятно, что же все-таки является общим случаем решаемой проблемы.
Я в ряде случаев предпочитаю на второй раз скопировать, а на третий уже отрефакторить.
Это не так очевидно.
- Иногда скопировать кусок кода и внести небольшую правку проще, чем добавить новую функциональность к существующему коду.
- Очень часто добавление новой функциональности к существующему коду усложняет и ухудшает его понятность.
В целом мне кажется, что дублирование допустимо, если продублированный код опирается на общий код нижележащего уровня.
Есть мнение, что неправильная абстракция хуже чем дублирование кода. Проблема в том, что заметить правильную абстракцию может быть сложно пока дублирования не достаточно много.
Не только для изменений нужен рефакторинг. Вовремя проведенный и удачный рефакторинг предотвратит появление дублирующегося кода. Проще говоря, не будет дважды, трижды и более раз тратиться время разработчиков на разработку одного и того же, лишь потому что они не поняли, что это уже реализовано или догадывались, что реализовано, но не нашли.
Есть такой метод добавления и даже изменения фич, при котором изменения практически не вносятся — внедряемся в интересующий флоу на самом нижнем месте, которое мы понимаем и пишем там новый с нуля, обходя старый при некоторых условиях. Фича доставлена, изменено несколько строк, добавлен несколько тысяч из которых большинство дублируют имеющийся код. И хорошо если дублируют так, что этот дубль вылавливается методами автоматизированного статического анализа. А если дублируется только функциональность, а реализована она совсем разными способами и обнаружить дублирование можно только путём полного анализа кодовой базы, то, скорее всего, дублирование будет идти дальше.
Вовремя проведенный и удачный рефакторинг предотвратит появление дублирующегося кода
Вопрос только в том, был ли данном сценарии технический долг вообще? Написали код без оглядки на повторное использование — и все было хорошо, повторно использовать не надо. Поменялись требования, надо использовать повторно — рефакторим, переиспользуем. В какой момент у нас имел место технический долг?
не поняли, что это уже реализовано или догадывались, что реализовано, но не нашли
А это даже не всегда определяется качеством кода. Это может быть результатом документации / процессов (или их отсутствия).
Ну и наконец можно дискутировать о вреде дублирующегося кода в общем случае :)
В какой момент у нас имел место технический долг?
Самое позднее — в момент появления новых требований. Если следовать принципу "разработчик должен предвидеть изменения требований", то в момент когда написали код без оглядки на повторное использование.
А это даже не всегда определяется качеством кода.
Одна из основных целей рефакторинга — увеличение самодокументируемости кода.
- «Повторный код уже написан» — плохо, но еще хуже если его еще раз продублировать (т е написать то же самое в третий, четвертый раз)
- «работает — не трогай» — каждая новая фича дается все тяжелей, устранение простых, на первый взгляд, багов требует много времени
- Отсутствие тестов или их низкое качество, также можно отнести к техническому долгу. Автотесты (на прктике) не могут выявить все ошибки. Естественно, что найденную ошибку придется устранить, но никто не гарантирует, что устраняя ошибку в одном месте, тем самым не создаешь ее в другом.
Обо всем этом много уже написано: Роберт Мартин «Чистый код», Мартин Фаулер «Рефакторинг» и т д. Думаю, что в этих книгах можно найти гораздо больше аргументов на тему «почему» надо писать чистый код. Хотя, конечно, все зависит от продукта и установок в команде — если команда слышит что продукт никому не нужен, а самих программистов завтра выгонят, ну и конечно незабываемое — «это надо было сделать вчера», то все это не стимулирует разработчиков писать качественный код. Часто менеджер однодневка своим общением с командой закладывает мину в продукт.
Количество изменений в системе контроля версий не всегда является корректным показателем.
Я бы скорее смотрел на:
— Количество строк в файле/классе (если больше 2000, то скорее всего нужен рефакторинг)
— Количество условий и циклов (чем больше, тем больше вероятность, что нужен рефакторинг)
— Количество дублирующихся блоков кода (часто идёт вместе с предыдущим пунктом)
— Для добавления нового функционала необходимо исправлять код во многих местах (не всегда очевидных)
— Количество обращений от поддержки на исправление ошибок в одной фиче.
Технический долг и места его обитания