Неправильная абстракция: почему дублирование лучше, чем идеальный код
Один невинный рефакторинг, пять параметров и десять лет техдолга. Как неправильная абстракция пожирает ваш код заживо.

Есть момент в работе программиста, который не принято обсуждать вслух, но который знаком каждому, кто хоть раз рефакторил чужой код. Ты смотришь на метод, который живёт в кодовой базе три года. Когда-то он назывался просто, но теперь у него пять параметров, три из которых опциональные. Внутри каскад условий, каждое отвечает за отдельный случай. Код работает, тесты проходят, но ты чувствуешь, что что-то сломалось. Не в коде. В архитектуре.
И это грустно, потому что начиналось всё правильно. Кто-то заметил дублирование, два одинаковых куска кода в разных местах. Собрал их в один метод, дал имя. Чисто, коротко, понятно. Но потом пришло новое требование, почти идеально подходящее под эту абстракцию, но не совсем. И кто-то добавил параметр. Потом ещё один. Потом условие: если флаг True, делай так, если False, иначе. Абстракция перестала быть общей. Она стала процедурой, которая притворяется абстракцией.
Самое коварное не сам код. Самое коварное наше отношение к нему. Чем больше усилий вложено, тем труднее признать, что эти усилия были направлены не туда. В психологии это называется ошибкой невозвратных затрат. В программировании это звучит так: «этот метод такой сложный, над ним работали месяцами, нельзя его выкинуть». Можно и нужно. Потому что сложность кода не доказательство его ценности. Скорее наоборот. Если вам нужны три листа, чтобы объяснить, что делает один метод, значит, абстракция мертва и вы её хороните.
Единственный способ выбраться из этой ловушки признать, что время этой абстракции прошло. Не чинить её, не подпирать костылями, не добавлять ещё один параметр. Развернуть её обратно. Расправить код в каждый вызывающий метод, убрать лишние куски, оставить каждому конкретному случаю ровно то, что ему нужно. Это звучит как шаг назад. На самом деле это шаг вперёд, просто в другом направлении.
Когда проделываешь это впервые, обнаруживаешь удивительную вещь. Оказывается, изначально «одинаковые» куски кода были разными. Они просто казались похожими, потому что на них смотрели издалека. А когда расправляешь их обратно, видишь уникальную логику каждого случая. И вот тут становится ясно, какую абстракцию нужно было создать на самом деле.
Я замечал это много раз в реальных проектах. Команда бьётся над новой фичей неделями, потому что существующая абстракция не подходит, но её «жалко менять». Код становится всё более странным, в нём появляются флаги, fallback-логика, магические числа. А потом кто-то приходит и делает inline, и фича собирается за два дня. Не потому что этот кто-то гений. А потому что он не боится выкинуть то, что перестало работать. Ирония в том, что в программировании умение отказаться от своего кода часто ценится выше, чем умение его написать.
Дублирование не враг. Враг это преждевременная правильность. Код, который пытается быть универсальным до того, как проявятся все варианты использования. Лучше пусть будет три похожих куска, которые легко читать и менять по отдельности, чем одна абстракция, которая не подходит ни одному из них по-настоящему.
Хорошая архитектура это не та, где нет дублирования. Хорошая архитектура это та, где каждая абстракция честно отражает то, что мы знаем о задаче прямо сейчас. А не то, что мы предполагали год назад.