Лекция №5. Современные концепции поддержки ПО: Рефакторинг, регрессионное тестирование и управление техническим долгом
Введение
В предыдущих лекциях мы сосредоточились на том, как создавать программное обеспечение: писать код, тестировать его, исправлять дефекты и готовить релиз. Однако у начинающих разработчиков существует устойчивый миф: «Программа написана, релиз состоялся — можно выдохнуть и переходить к следующему проекту». Этот миф опасен, потому что он формирует искажённое представление о реальном жизненном цикле программного обеспечения.
§5.1 Жизненный цикл ПО после релиза: поддержка как основная фаза
Статистика
От 60 до 80% всех затрат на программное обеспечение приходится на фазу его поддержки (maintenance). Это означает, что создание продукта — лишь начало долгого пути. Релиз — это не финишная черта, а стартовая точка для основного этапа жизни программы.
Поддержка программного обеспечения
— совокупность действий, направленных на сохранение и улучшение работоспособности системы после её передачи в эксплуатацию.
Поддержка ПО
— это как обслуживание автомобиля: вы можете купить новую машину, но вам придётся её обслуживать — менять масло, шины, проходить техосмотр, устранять неисправности. Точно так же программа требует постоянного внимания.
Почему поддержка стоит так дорого?
Почему поддержка стоит так дорого?
Четыре типа поддержки программного обеспечения
В международной практике, закреплённой в стандарте ISO/IEC 14764, выделяют четыре типа поддержки программного обеспечения. Каждый из них решает свои задачи и требует разных подходов.
1. Корректирующая поддержка (Corrective Maintenance)
Это самый очевидный и знакомый всем вид поддержки — исправление дефектов, которые были обнаружены после релиза. Несмотря на все усилия тестировщиков, часть ошибок неизбежно проникает в эксплуатацию: они могут проявляться только при определённых комбинациях данных, в редких сценариях или под высокой нагрузкой.
Пример: Интернет-магазин начал работать, и вдруг пользователи сообщают, что при оформлении заказа с определённым типом кредитной карты происходит ошибка. Это корректирующая поддержка: разработчики находят причину, пишут исправление и выпускают патч.
Важно
Корректирующая поддержка — это реактивная деятельность. Она не предотвращает проблемы, а устраняет их последствия.
2. Адаптивная поддержка (Adaptive Maintenance)
Мир не стоит на месте. Выходят новые версии операционных систем, обновляются библиотеки, меняются протоколы передачи данных. Адаптивная поддержка — это изменение программного обеспечения под новое окружение.
Пример: Ваше приложение работает на .NET 6. Вышла новая версия .NET 8, и через год поддержка .NET 6 будет прекращена. Чтобы программа продолжала работать безопасно и получать обновления безопасности, её нужно перенести на новую версию. Или, например, браузер Chrome изменил политику обработки cookies — и ваше веб-приложение перестало корректно хранить сессии пользователей. Приходится адаптировать код.
Проактивность
Адаптивная поддержка — это проактивная деятельность. Хорошая команда не ждёт, пока что-то сломается, а планомерно обновляет зависимости и следит за изменениями во внешнем мире.
3. Совершенствующая поддержка (Perfective Maintenance)
Это улучшение программного обеспечения без изменения его основного назначения. Сюда относятся: повышение производительности, улучшение пользовательского интерфейса, оптимизация работы с базой данных, рефакторинг кода и добавление новых функций, которые не были запланированы в первоначальной версии.
Пример: Пользователи жалуются, что страница оформления заказа грузится слишком долго. Вы переписываете запрос к базе данных, добавляете кеширование, и страница начинает грузиться в два раза быстрее. Функциональность не изменилась, но пользовательский опыт стал лучше. Или вы добавляете возможность восстановления забытого пароля — функция была не в первом релизе, но теперь бизнес этого требует.
Значимость
Совершенствующая поддержка — это самая большая и важная часть работы команды после релиза. Именно благодаря ей продукт остаётся конкурентоспособным.
4. Превентивная поддержка (Preventive Maintenance)
Это профилактика проблем до того, как они произошли. Сюда относится рефакторинг кода, улучшение документации, обновление тестов, внедрение новых инструментов анализа качества.
Пример: Разработчик видит, что в коде есть повторяющийся фрагмент — один и тот же алгоритм используется в пяти разных местах. Вместо того чтобы ждать, пока этот фрагмент приведёт к рассинхронизации (например, в одном месте его поправят, а в остальных четырёх забудут), он выделяет его в отдельный метод. Это превентивная поддержка: проблема ещё не возникла, но её предотвратили.
Инвестиция
Превентивная поддержка — это инвестиция в будущее. Она не даёт видимого эффекта сегодня, но спасает от огромных затрат завтра.
Сравнительная таблица типов поддержки
| Тип поддержки | Характер деятельности | Когда применяется | Пример |
|---|---|---|---|
| Корректирующая | Реактивная | После обнаружения дефектов | Исправление ошибки при оплате |
| Адаптивная | Проактивная | При изменении окружения | Перенос на новую версию .NET |
| Совершенствующая | Проактивная | По запросу бизнеса | Улучшение производительности |
| Превентивная | Проактивная | Для предотвращения проблем | Рефакторинг дублирующегося кода |
Главная проблема поддержки
Главная проблема поддержки заключается в вопросе: «Как менять код, не ломая то, что работает?»
Представьте себе башню из кубиков. Вы построили её, она стоит. Теперь представьте, что вам нужно заменить один кубик в середине башни, не разрушив её. Как это сделать? Если просто выдернуть кубик и вставить новый, башня, скорее всего, упадёт. Нужна особая техника — например, сначала установить временные опоры, затем заменить кубик, а потом убрать опоры.
То же самое происходит и с программным обеспечением. Любое изменение, даже самое маленькое, несёт риск сломать существующую функциональность. Разработчик добавляет одну строчку кода — и из-за неё перестаёт работать модуль, который казался совершенно не связанным. Это явление называют регрессией.
Регрессия
— это когда новое изменение приводит к ошибке в уже работавшем функционале.
Откуда берётся регрессия?
- Скрытые зависимости. Модули программы связаны между собой, и вы можете не знать обо всех этих связях. Изменили один метод — и сломали другой, который его использовал.
- Непредвиденные побочные эффекты. Каждое изменение может повлиять на производительность, потребление памяти или поведение в граничных условиях.
- Человеческий фактор. Разработчик может просто забыть, что его изменение затронет какой-то другой модуль.
Как бороться с регрессией?
Ответ дают две ключевые концепции:
| Концепция | Определение | Цель |
|---|---|---|
| Рефакторинг | Изменение внутренней структуры кода без изменения его внешнего поведения | Сделать код чище, проще и понятнее, снизить сложность |
| Регрессионное тестирование | Проверка того, что после внесённых изменений существующая функциональность продолжает работать корректно | Ловить регрессии до того, как они попадут к пользователю |
Связь концепций
Эти две концепции тесно связаны. Рефакторинг без регрессионных тестов — это рискованное занятие, потому что вы не знаете, что сломали. А регрессионные тесты без рефакторинга — это просто консервация текущего состояния, которое может быть далеко от идеального.
Именно эти две темы мы и будем изучать в данной лекции. Сначала мы разберёмся, что такое рефакторинг и как его правильно делать, затем познакомимся с понятием технического долга и запахов кода, а потом перейдём к регрессионному тестированию — инструменту, который делает рефакторинг безопасным.
Жизненный цикл ПО в целом
Классическая модель включает следующие фазы:
- Анализ требований — что должна делать система?
- Проектирование — как мы это построим?
- Разработка — написание кода.
- Тестирование — проверка корректности.
- Внедрение (релиз) — передача пользователям.
- Поддержка — эксплуатация, исправление ошибок, адаптация, улучшение.
Часто начинающие специалисты думают, что их работа заканчивается на 5-м этапе. Однако именно 6-й этап — поддержка — является самым длительным и затратным. Более того, именно на этом этапе проявляется истинное качество кода.
Если код написан плохо, без учёта тестопригодности и сопровождения, то каждый акт поддержки превращается в мучение. Одно небольшое изменение требует недель, потому что разработчики боятся что-то сломать. Каждый баг фиксится через костыли, потому что переписать правильно слишком долго и дорого.
Если же код написан качественно, покрыт тестами и регулярно рефакторится, то поддержка становится рутинным, но спокойным процессом. Новые фичи добавляются быстро, адаптация к новым окружениям проходит безболезненно, а технический долг не накапливается.
§5.2 Рефакторинг: что это и зачем он нужен
В предыдущем параграфе мы выяснили, что поддержка программного обеспечения — это самая длительная и затратная фаза жизненного цикла. Мы также обозначили главную проблему: любое изменение в коде несёт риск сломать существующую функциональность. Возникает естественный вопрос: как же тогда поддерживать и улучшать программу, если каждое изменение опасно? Ответ на этот вопрос даёт понятие рефакторинга.
Определение Мартина Фаулера
Рефакторинг
— это изменение внутренней структуры программного кода без изменения его внешнего поведения.
Разберём это определение по частям:
- «Изменение внутренней структуры» — мы переписываем код внутри: меняем названия переменных, разбиваем длинные методы на короткие, выделяем общие части в отдельные функции, перестраиваем иерархии классов. Код становится другим, но только с точки зрения разработчика.
- «Без изменения внешнего поведения» — для пользователя ничего не меняется. Программа делает ровно то же самое, что и раньше. Те же кнопки, те же расчёты, те же ответы на запросы. Если пользователь не смотрит в исходный код, он вообще не заметит, что что-то изменилось.
Простая аналогия: представьте, что вы ремонтируете кухню. Вы не меняете планировку, не переносите стены, не заменяете сантехнику. Вы просто наводите порядок: раскладываете посуду по полкам, сортируете специи, вешаете новые светильники, чтобы было светлее. Кухня осталась той же самой, но работать на ней стало удобнее. Это и есть рефакторинг.
Почему код нуждается в рефакторинге?
Начинающие разработчики часто задают вопрос: «Зачем трогать код, если он работает? Ведь работает — и хорошо». Это распространённое заблуждение, и оно приводит к серьёзным проблемам в долгосрочной перспективе.
Дело в том, что код имеет свойство «гнить» (это явление называют software rot). В отличие от физического гниения, здесь речь идёт не о времени, а о накоплении изменений. Каждое новое изменение, каждое исправление бага, каждое добавление фичи делает код чуть сложнее, чуть запутаннее, чуть менее понятным.
Процесс гниения кода
Процесс гниения кода выглядит так:
- Разработчик пишет код быстро, чтобы уложиться в дедлайн. Он не задумывается о красоте и читаемости — главное, чтобы работало.
- Через месяц появляется новый баг. Разработчик находит место проблемы и добавляет быстрое исправление (костыль). Код становится чуть сложнее.
- Ещё через месяц приходит задача добавить новую функцию. Разработчик видит, что существующий код уже не очень понятен, но копаться в нём некогда, поэтому он просто копирует кусок старого кода, вставляет в новое место и немного правит. Появляется дублирование.
- Так продолжается месяцы и годы. Код превращается в «спагетти»: длинные методы, дублирование, странные названия переменных, непонятные условия, вложенные друг в друга на 5-7 уровней.
В результате простейшая задача, которая должна занимать час, занимает два дня. Разработчики боятся что-то менять, потому что непонятно, к чему это приведёт. Новые сотрудники неделями разбираются в коде, вместо того чтобы писать полезный функционал. Именно в этот момент и нужен рефакторинг.
Как понять, что код нуждается в рефакторинге?
Есть несколько явных признаков, которые говорят о том, что код пора улучшать. Обратите внимание: это не баги — программа может работать идеально. Это симптомы того, что код стал сложным для понимания и изменения.
Рефакторинг ≠ переписывание
Это один из самых важных пунктов, который нужно понять. Многие разработчики, особенно начинающие, путают рефакторинг с полной переделкой кода. Это принципиально разные вещи.
| Характеристика | Рефакторинг | Переписывание |
|---|---|---|
| Масштаб | Небольшие, эволюционные изменения | Крупные, революционные изменения |
| Внешнее поведение | Не меняется (строго) | Может меняться (новая архитектура, новый подход) |
| Риск | Низкий (изменения малы и контролируемы) | Высокий (меняется всё, сложно проверить) |
| Время | Минуты или часы | Дни, недели, месяцы |
| Когда применять | Регулярно, как часть повседневной работы | Когда код настолько плох, что рефакторинг невозможен (крайний случай) |
Переписывание
— это когда вы говорите: «Всё плохо, я удаляю старый код и пишу новый с нуля». Такой подход крайне рискован. Пока вы пишете новую версию, старая продолжает работать, но в ней не появляются новые фичи. Команда тратит месяцы на переписывание, а бизнес не получает нового функционала. К тому же, новая версия может содержать ровно те же ошибки, что и старая, плюс новые.
Рефакторинг
— это когда вы улучшаете код маленькими шагами. Убрали дублирование → запустили тесты → убедились, что всё работает. Переименовали переменную → запустили тесты. Разбили длинный метод на три маленьких → запустили тесты. Каждый шаг — крошечный, безопасный, контролируемый.
Пример рефакторинга
Вот как выглядит рефакторинг на примере того самого метода Calc:
// Шаг №1. Переименовываем параметры, чтобы стало понятно, что они означают:
public decimal Calc(decimal basePrice, decimal quantity, bool isPremium, int customerYears)
{
// ... тело метода
}
Код стал понятным. Теперь любой разработчик сразу видит: сначала считаем базовую цену, потом применяем скидку, потом — дополнительную скидку. Никакой магии.
Важно
На каждом шаге мы запускали тесты и убеждались, что внешнее поведение не изменилось. Если тесты были написаны правильно, мы могли быть спокойны.
Рефакторинг — это инвестиция
Рефакторинг часто воспринимают как трату времени. Менеджеры говорят: «Зачем вы тратите день на переписывание кода, который и так работает? Давайте лучше сделаем новую функцию!»
Но это ложная экономия. Рефакторинг — это инвестиция в будущее. Вы тратите время сегодня, чтобы сэкономить в десятки раз больше времени завтра.
Представьте два проекта:
- Проект А. Код никогда не рефакторится. В нём накапливается технический долг. С каждым годом добавлять новые фичи становится всё сложнее и дольше. Через 5 лет любое изменение требует недель работы и сопровождается пачкой багов.
- Проект Б. Команда регулярно выделяет 10-20% времени на рефакторинг. Код остаётся чистым и понятным. Новые фичи добавляются быстро, баги редки. Через 5 лет проект всё ещё легко поддерживать, что очень хорошо для самого проекта.
Какой проект вы бы хотели поддерживать? Ответ очевиден.
Когда начинать рефакторинг?
Ответ простой: начинать нужно прямо сейчас, но маленькими шагами.
Не нужно планировать «большой рефакторинг» на месяц. Это путь к провалу. Вместо этого:
- Каждый раз, когда вы касаетесь какого-то куска кода (исправляете баг или добавляете функцию), оставляйте его немного чище, чем нашли. Убрали дублирование, переименовали переменную, разбили метод — это уже рефакторинг.
- Используйте принцип «Бойскаута»: «Оставляй код чище, чем ты его нашёл».
- Всегда имейте регрессионные тесты. Без них рефакторинг — это игра в русскую рулетку.
§5.3 Запахи кода (Code Smells): как распознать проблему до того, как она станет критической
В предыдущем параграфе мы говорили о рефакторинге и о том, что код имеет свойство «гнить». Но как понять, что код уже начал портиться? Как распознать проблему до того, как она превратится в критический баг или причину многодневных страданий команды?
Для этого в инженерии ПО существует понятие запахов кода (Code Smells).
Code Smell (запах кода)
— это поверхностный признак того, что в структуре или логике кода что-то не так.
Это не баг и не ошибка — программа может работать идеально. Это симптом, который говорит: «Здесь потенциально может возникнуть проблема в будущем».
Термин придумал Кент Бек, один из создателей методологии экстремального программирования (XP). Он использовал аналогию с настоящим запахом:
Когда вы входите на кухню и чувствуете запах газа, вы не ждёте, пока произойдёт взрыв. Вы сразу проверяете плиту. Точно так же, когда вы видите Code Smell, вы не ждёте, пока он превратится в баг. Вы сразу анализируете код и, если нужно, рефакторите его.
Важно понимать
Code Smell — это не приговор. Иногда код пахнет, но это оправдано спецификой задачи или архитектурными ограничениями. Однако в большинстве случаев Code Smell — это сигнал к действию.
Теперь давайте рассмотрим самые распространённые запахи кода. У каждого из них есть свои симптомы, причины и способы исправления.
1. Дублирование кода (Duplicated Code)
Симптом: Один и тот же или очень похожий фрагмент кода встречается в нескольких местах.
Пример:
csharp
// В методе расчёта скидки для обычных клиентов
decimal discount = 0;
if (order.Total > 1000)
{
discount = order.Total * 0.05m;
}
if (order.IsWeekend)
{
discount += order.Total * 0.02m;
}
// ... ещё логика
// В методе расчёта скидки для VIP-клиентов (почти то же самое!)
decimal discount = 0;
if (order.Total > 1000)
{
discount = order.Total * 0.10m; // Отличие: 10% вместо 5%
}
if (order.IsWeekend)
{
discount += order.Total * 0.02m;
}
// ... ещё логика
Почему это плохо:
- Если логика изменится (например, изменится размер скидки или добавится новое условие), вам придётся править код в нескольких местах, что очень времязатратно.
- Велика вероятность, что вы забудете поправить одно из мест, и система станет работать несогласованно.
- Код становится длиннее и сложнее для понимания.
Для исправления необходимо выделить общий код в отдельный метод. Отличия передавать через параметры.
csharp
private decimal CalculateBaseDiscount(Order order, decimal vipMultiplier)
{
decimal discount = 0;
if (order.Total > 1000)
{
discount = order.Total * 0.05m * vipMultiplier;
}
if (order.IsWeekend)
{
discount += order.Total * 0.02m;
}
return discount;
}
// Использование для обычных клиентов (учитывается это в виде 1.0m)
var discount = CalculateBaseDiscount(order, 1.0m);
// Использование для VIP-клиентов (VIP-скидка в 2 раза больше,
// поэтому мы это учитываем в виде 2.0m)
var discount = CalculateBaseDiscount(order, 2.0m);
Статистика
Дублирование кода — самый распространённый Code Smell. Исследования показывают, что в среднем в проектах дублируется от 10 до 20% кода. Это значит, что каждая пятая строка кода — лишняя.
2. Длинный метод (Long Method)
Симптом: Метод занимает больше 20–30 строк (хотя границы условны). Он делает слишком много разных вещей.
Пример:
csharp
public void ProcessOrder(Order order)
{
// Проверка валидности
if (order.CustomerId <= 0)
throw new ArgumentException("Invalid customer");
if (order.Items.Count == 0)
throw new ArgumentException("Empty order");
// Расчёт итоговой суммы
decimal total = 0;
foreach (var item in order.Items)
{
total += item.Price * item.Quantity;
}
// Применение скидки
if (total > 1000)
{
total *= 0.9m;
}
if (order.IsPremiumCustomer)
{
total *= 0.95m;
}
// Сохранение в базу
using var connection = new SqlConnection(_connectionString);
// ... SQL-запрос на сохранение
// Отправка письма
_emailSender.Send(order.CustomerEmail, "Order confirmed", $"Total: {total}");
}
Почему это плохо:
- Метод делает пять разных вещей: валидация, расчёт, применение скидки, сохранение, отправка письма.
- Его сложно читать — нужно вникать в каждую часть.
- Его сложно тестировать — нужно проверять все пять аспектов одновременно.
- Если одна часть изменится (например, логика расчёта скидки), есть риск задеть остальные.
Для исправления необходимо разбить длинный метод на несколько маленьких, каждый из которых делает одну конкретную вещь.
csharp
public void ProcessOrder(Order order)
{
ValidateOrder(order);
var total = CalculateTotal(order);
total = ApplyDiscounts(order, total);
SaveToDatabase(order, total);
SendConfirmation(order, total);
}
private void ValidateOrder(Order order) { ... }
private decimal CalculateTotal(Order order) { ... }
private decimal ApplyDiscounts(Order order, decimal total) { ... }
private void SaveToDatabase(Order order, decimal total) { ... }
private void SendConfirmation(Order order, decimal total) { ... }
Теперь каждый метод делает ровно одно действие, имеет понятное название, и его легко тестировать отдельно.
Золотое правило
Метод должен делать одну вещь и делать её хорошо (Single Responsibility Principle). Если название метода содержит союз «и» — это признак того, что он слишком длинный.
3. Большой класс (Large Class)
Симптом: Класс содержит слишком много полей, свойств и методов. Он пытается делать слишком много.
Пример:
csharp
public class OrderProcessor
{
// 15 полей
private SqlConnection _dbConnection;
private IEmailSender _emailSender;
private ILogger _logger;
private DiscountCalculator _discountCalc;
private TaxCalculator _taxCalc;
private InventoryService _inventory;
private PaymentGateway _payment;
// ... и ещё 8 полей
// 20 методов
public void ProcessOrder(Order order) { ... }
public void CancelOrder(int orderId) { ... }
public decimal CalculateTotal(Order order) { ... }
public void ApplyDiscount(Order order) { ... }
public decimal CalculateTax(Order order) { ... }
public void CheckInventory(Order order) { ... }
public void ProcessPayment(PaymentInfo info) { ... }
public void SendReceipt(Order order) { ... }
// ... и ещё 12 методов
}
Почему это плохо:
- Класс нарушает принцип единственной ответственности (SRP). Он отвечает за слишком много задач: заказы, платежи, инвентаризацию, налоги, логирование.
- Класс сложно тестировать — нужно поднимать все 15 зависимостей.
- Класс сложно изменять — изменение в одной области может задеть другие.
Для исправления необходимо выделить отдельные классы для каждой ответственности.
csharp
public class OrderProcessor
{
private readonly IOrderRepository _repository;
private readonly IEmailSender _emailSender;
private readonly IPaymentGateway _paymentGateway;
// ... методы, которые связывают все компоненты
}
public class DiscountCalculator { ... }
public class TaxCalculator { ... }
public class InventoryService { ... }
public class PaymentService { ... }
Каждый класс делает одну вещь. Их легко тестировать, легко изменять, легко переиспользовать.
4. Длинный список параметров (Long Parameter List)
Симптом: Метод принимает больше 3–4 параметров. Чем больше параметров, тем сложнее метод вызывать и понимать.
Пример:
csharp
public void CreateOrder(
int customerId,
string customerName,
string customerEmail,
string customerAddress,
List<OrderItem> items,
DateTime orderDate,
string paymentMethod,
string shippingMethod,
decimal discountPercent)
{
// ... логика создания заказа
}
Почему это плохо:
- Метод трудно вызывать — нужно запоминать порядок всех параметров.
- Метод трудно читать — непонятно, какие параметры связаны между собой.
- При изменении сигнатуры (добавлении нового параметра) нужно править все вызовы.
- Часто это указывает на то, что метод делает слишком много, чего в проектах быть не должно!
Для исправления необходимо группировать связанные параметры в отдельные объекты (DTO, классы-контейнеры).
csharp
public class CustomerInfo
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Address { get; set; }
}
public class OrderOptions
{
public DateTime OrderDate { get; set; }
public string PaymentMethod { get; set; }
public string ShippingMethod { get; set; }
public bool IsExpress { get; set; }
public decimal DiscountPercent { get; set; }
}
public void CreateOrder(CustomerInfo customer, List<OrderItem> items, OrderOptions options)
{
// ... логика создания заказа
}
Теперь метод принимает всего три параметра, и все они логически сгруппированы.
5. Класс-завистник (Feature Envy)
Симптом: Метод одного класса активно использует данные и методы другого класса. Он «завидует» другому классу и хочет быть его частью, что нарушает принципы SOLID.
Пример:
csharp
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public decimal LoyaltyPoints { get; set; }
}
public class OrderService
{
public string GenerateCustomerGreeting(Customer customer)
{
// Метод OrderService активно использует поля Customer
return $"Уважаемый {customer.Name}! " +
$"Ваш email: {customer.Email}, " +
$"телефон: {customer.Phone}. " +
$"Ваши бонусы: {customer.LoyaltyPoints} баллов.";
}
}
Почему это плохо:
- Если у Customer появятся новые поля, скорее всего, придётся менять
GenerateCustomerGreeting. - Логика, работающая с данными Customer, логически должна принадлежать классу Customer.
- Код становится рассредоточенным: непонятно, где искать логику, связанную с клиентом.
Для исправления необходимо переместить метод в тот класс, чьи данные он использует.
csharp
public class Customer
{
public string Name { get; set; }
public string Email { get; set; }
public string Phone { get; set; }
public string Address { get; set; }
public decimal LoyaltyPoints { get; set; }
public string GenerateGreeting()
{
return $"Уважаемый {Name}! " +
$"Ваш email: {Email}, телефон: {Phone}. " +
$"Ваши бонусы: {LoyaltyPoints} баллов.";
}
}
Теперь логика лежит там, где ей и положено — в классе Customer.
6. Игнорируемые исключения (Empty Catch Block)
Симптом: В коде есть блок catch, который ловит исключение, но ничего с ним не делает. Исключение просто проглатывается.
Пример:
csharp
public void ProcessData()
{
try
{
var data = _repository.GetData();
// ... обработка данных
}
catch (Exception ex)
{
// Пустой блок catch — ничего не делаем!
}
}
Почему это плохо:
- Ошибка скрывается. Программа продолжает работать, но делает это некорректно.
- Разработчик не знает, что произошла ошибка, и не может её исправить.
- Ошибка может накапливаться и привести к катастрофическому сбою позже.
Это один из самых опасных Code Smells, потому что он маскирует реальные проблемы.
Для исправления необходимо никогда не оставлять блок catch пустым.
csharp
public void ProcessData()
{
try
{
var data = _repository.GetData();
// ... обработка данных
}
catch (Exception ex)
{
// Логируем ошибку, чтобы знать, что произошло
_logger.LogError(ex, "Ошибка при получении данных");
// И либо пробрасываем дальше, либо возвращаем fallback
throw; // или return default;
}
}
Исключение из правила
Иногда вы действительно хотите игнорировать ошибку (например, при попытке удалить файл, который уже удалён). Но даже в этом случае нужно логировать факт ошибки и указывать, почему вы её игнорируете.
csharp
try
{
File.Delete(tempFile);
}
catch (FileNotFoundException)
{
// Файл уже удалён — это нормально, ничего не делаем
// Но логируем для истории
_logger.LogInformation("Файл уже отсутствовал при попытке удаления");
}
Code Smell ≠ Баг
Важно запомнить главное отличие Code Smell от бага:
| Характеристика | Code Smell | Баг |
|---|---|---|
| Программа работает? | Да, работает корректно | Нет, выдаёт неверный результат |
| Что это? | Симптом потенциальной проблемы | Актуальная проблема |
| Когда исправлять? | При первой возможности (рефакторинг) | Немедленно |
| Пример | Длинный метод, дублирование кода | Приложение падает при запуске |
Важно
Если в вашем проекте много Code Smells — это не значит, что проект плохой. Это значит, что в нём накопился технический долг, и его пора погашать, пока он не превратился в критическую проблему.
Как бороться с Code Smells на практике
§5.4 Технический долг: метафора, которая объясняет всё
В предыдущих параграфах мы говорили о запахах кода и рефакторинге. У вас мог возникнуть закономерный вопрос: «Если код имеет свойство портиться, и его нужно постоянно улучшать, почему разработчики вообще допускают появление плохого кода? Почему они не пишут идеально с самого начала?»
Ответ на этот вопрос даёт одна из самых известных метафор в инженерии ПО — технический долг (Technical Debt).
Определение Уорда Каннингема (1992)
В 1992 году программист Уорд Каннингем предложил простую, но гениальную аналогию. Он сравнил разработку программного обеспечения с финансовой системой:
Технический долг
— это как финансовый долг. Вы берёте кредит, чтобы быстрее достичь цели, но потом вам придётся его возвращать с процентами.
Представьте себе, что вы строите дом. Вы можете построить его быстро, используя дешёвые материалы и упрощённые технологии. Дом будет готов через месяц, и вы сможете в него заехать. Но через год стены начнут трещать, крыша протекать, а проводка искрить. Вам придётся тратить деньги и время на постоянный ремонт. В итоге вы заплатите больше, чем если бы построили качественно с самого начала.
То же самое происходит с программным кодом:
- Вы берёте технический кредит, когда пишете код быстро, но неидеально: нарушаете принципы проектирования, откладываете рефакторинг, копируете код вместо создания общих абстракций.
- Вы платите проценты, когда каждая новая функция требует всё больше времени, когда баги возникают в неожиданных местах, когда новые сотрудники не могут разобраться в коде неделями.
- Вы не можете взять новый кредит, если старый не отдан. Когда технический долг становится слишком большим, любое изменение превращается в кошмар. Вы не можете двигаться вперёд, потому что всё время тратите на исправление старого.
Почему возникает технический долг?
В идеальном мире мы всегда писали бы идеальный код. Но в реальном мире существуют жёсткие ограничения:
Важный момент
Технический долг — это не всегда зло. Иногда это осознанное решение. Вопрос в том, осознанно ли вы берёте долг и знаете ли, когда и как его отдавать.
Два вида технического долга
Технический долг бывает двух видов, и их важно различать:
// Осознанное решение: делаем быстро, но знаем, что надо переделать
public class QuickOrderProcessor
{
// Это временное решение для MVP (Minimum Viable Product)
// TODO: Переписать после релиза, использовать паттерн Стратегия
public void Process(Order order)
{
// Простейшая логика без учёта всех типов заказов
SaveToDatabase(order);
SendEmail(order);
}
}
Проценты по техническому долгу
Как и в финансовом мире, у технического долга есть проценты. Чем дольше вы не отдаёте долг, тем больше процентов накапливается.
Как выглядят проценты в реальном проекте:
- Время на добавление новых фич растёт. То, что раньше занимало день, теперь требует недели. Код стал сложным и запутанным, разработчики тратят время на понимание существующей логики.
- Количество багов растёт. Каждое изменение в одной части системы вызывает ошибки в других, потому что код плохо структурирован и зависимости не очевидны.
- Сложность найма. Новые разработчики не могут быстро войти в проект. Им нужны месяцы, чтобы разобраться в коде.
- Моральный дух команды падает. Разработчики устают от постоянного «пожара» и теряют мотивацию. Лучшие сотрудники уходят.
- Бизнес теряет гибкость. Вы не можете быстро реагировать на изменения рынка, потому что любое изменение требует огромных усилий.
Как измерить технический долг?
Технический долг сложно измерить в деньгах, но есть несколько косвенных показателей:
Стратегии работы с техническим долгом
Как и с финансовым долгом, с техническим долгом нужно работать системно:
Стратегия №1. Плановое погашение
Выделите 10-20% времени каждого спринта на рефакторинг. Это как регулярные платежи по кредиту. Вы не гасите весь долг сразу, но не даёте ему расти.
Стратегия №2. Рефакторинг по касанию
Каждый раз, когда вы трогаете какой-то участок кода (исправляете баг или добавляете функцию), вы улучшаете его. Это правило «Бойскаута»: «Оставляй код чище, чем ты его нашёл».
Стратегия №3. Приоритизация
Не все долги одинаково важны. Сначала погашайте те, которые мешают разработке больше всего:
- Код, который сложно тестировать.
- Код, который часто меняется.
- Код, в котором чаще всего возникают баги.
Стратегия №4. Автоматизация проверок
Включите статические анализаторы в CI/CD-пайплайн. Если покрытие тестами падает ниже 80% или появляются новые Code Smells — сборка падает. Это предотвращает накопление неосознанного долга. [/TABS]
Главное правило
Берите долг осознанно, платите регулярно и не позволяйте процентам накапливаться.
Технический долг не обязательно плох. В стартапе на ранней стадии быстрый выход на рынок может быть важнее идеального кода. Но если вы не планируете возвращать долг, он убьёт ваш проект.
Мартин Фаулер
«Если вы не инвестируете в качество кода, вы инвестируете в его ухудшение. А это гораздо более дорогое вложение.»
§5.5 Регрессионное тестирование: защита от «починили одно — сломали другое»
В предыдущих параграфах мы говорили о рефакторинге и техническом долге. Мы выяснили, что код нужно постоянно улучшать, чтобы он оставался понятным и поддерживаемым. Но здесь возникает фундаментальная проблема: любое изменение в коде несёт риск сломать то, что уже работало.
Представьте себе, что вы чините кран на кухне. Вы перекрываете воду, откручиваете старый кран, ставите новый. Всё работает, вода течёт. Вы довольны. Но через час соседи снизу звонят: «У вас потёк стояк, и у нас залило потолок». Оказывается, пока вы меняли кран, вы случайно задели трубу, которая идёт к соседям. Вы починили одно, но сломали другое, что, понятное дело, не есть хорошо.
В программном обеспечении это явление называется регрессией.
Регрессия (или регрессионный дефект) — это ошибка, которая возникает в уже работавшей функциональности после внесения изменений в код.
Простой пример:
csharp
// Было: метод работал корректно
public decimal CalculateDiscount(decimal amount)
{
if (amount > 1000)
return amount * 0.1m;
return 0;
}
// Разработчик добавляет новую функцию (скидка для премиум-клиентов)
public decimal CalculateDiscount(decimal amount, bool isPremium)
{
// Новая логика
if (isPremium)
return amount * 0.15m;
// Старая логика — но разработчик случайно изменил условие!
if (amount >= 1000) // Было: > 1000, стало: >= 1000
return amount * 0.1m;
return 0;
}
В этом примере разработчик добавил новый параметр isPremium и новую логику для премиум-клиентов. Но в процессе он случайно изменил условие в старой логике: > на >=. Теперь клиент с суммой заказа ровно 1000 получит скидку, хотя раньше не получал. Это регрессия: работавшая функция сломалась из-за изменения в другой части кода.
Почему возникают регрессии?
Ответ кроется в нескольких причинах:
Основной способ борьбы с регрессиями
Основной способ борьбы с регрессиями — это регрессионное тестирование.
Регрессионное тестирование
— это проверка того, что после внесённых изменений существующая функциональность продолжает работать корректно.
Вручную выполнять регрессионное тестирование невозможно по нескольким причинам:
- Объём. В большой системе могут быть тысячи сценариев. Проверять их вручную после каждого изменения — это недели работы.
- Частота. В современных проектах изменения вносятся несколько раз в день (иногда десятки раз). Ручное тестирование просто не успевает.
- Утомляемость. Человек устаёт и начинает пропускать ошибки. Особенно если одни и те же сценарии проверяются сотый раз.
Поэтому индустрия пришла к стандартному решению: регрессионное тестирование должно быть автоматизированным.
Какие тесты включать в регрессионный набор?
Автоматизированное регрессионное тестирование — это не просто «запустили все тесты». Это продуманный набор проверок, который включает три уровня:
[Fact]
public void CalculateDiscount_AmountGreaterThan1000_Returns10Percent()
{
var service = new DiscountService();
var result = service.CalculateDiscount(1500m);
Assert.Equal(150m, result);
}
[Fact]
public void CalculateDiscount_AmountLessThan1000_ReturnsZero()
{
var service = new DiscountService();
var result = service.CalculateDiscount(500m);
Assert.Equal(0m, result);
}
Пирамида тестирования для регрессии
Соотношение количества тестов разных уровней описывается пирамидой тестирования, которую мы уже обсуждали в лекции №4.
Пирамида тестирования для регрессии
Правило для регрессии:
- Много модульных тестов (70-80% всех тестов).
- Умеренно интеграционных тестов (15-20%).
- Мало E2E-тестов (не более 5-10%).
Почему такое распределение? Потому что:
- Модульные тесты дёшевы и быстры. Их можно запускать после каждого изменения. Они дают быструю обратную связь разработчику.
- Интеграционные тесты дороже, но проверяют важные аспекты. Их запускают реже (например, при сборке в ветку разработки).
- E2E-тесты дорогие и медленные. Их запускают только перед релизом или ночью.
Как часто нужно запускать регрессионные тесты?
Золотое правило
«Регрессионные тесты должны запускаться при каждом изменении кода».
В мире CI/CD (Continuous Integration/Continuous Delivery) это реализуется так:
-
Разработчик делает коммит в репозиторий.
-
CI-сервер (например, GitHub Actions, GitLab CI, Jenkins) автоматически:
- Собирает проект.
- Запускает модульные тесты (быстро, за 1-2 минуты).
- Если всё прошло — запускает интеграционные тесты.
- Если и они прошли — собирает артефакты для релиза.
-
E2E-тесты могут запускаться отдельно (например, ночью) или перед выкаткой в продакшен.
Вот как это выглядит в конфигурации GitHub Actions:
yaml
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0'
- name: Run Unit Tests
run: dotnet test --filter "Category=Unit"
- name: Run Integration Tests
run: dotnet test --filter "Category=Integration"
- name: Run E2E Tests (critical only)
run: dotnet test --filter "Category=E2ECritical"
Что делать, если регрессионный тест упал?
Если тест упал — это не катастрофа. Это информация. Вот что делать дальше:
§5.6 Как рефакторинг и регрессионное тестирование связаны
В предыдущих параграфах мы рассмотрели рефакторинг как способ улучшения внутренней структуры кода и регрессионное тестирование как способ проверки того, что ничего не сломалось. Настало время ответить на главный вопрос: как эти две концепции работают вместе на практике?
Краткий ответ
Они неразрывно связаны. Одно без другого теряет смысл.
Представьте себе хирурга, который делает операцию. У него есть скальпель (рефакторинг) и есть монитор, показывающий жизненные показатели пациента (регрессионные тесты). Хирург делает разрез (меняет код) и смотрит на монитор: давление не упало, пульс в норме (тесты зелёные). Он делает следующий разрез и опять проверяет. Без монитора хирург работал бы вслепую — это опасно для жизни пациента.
Точно так же разработчик, который рефакторит код без тестов, работает вслепую. Он меняет код и не знает, не убил ли он систему.
Почему рефакторинг без тестов опасен?
Вспомним определение рефакторинга: это изменение внутренней структуры кода без изменения его внешнего поведения. Но как узнать, что внешнее поведение не изменилось?
Без тестов — никак. Вы просто надеетесь, что ничего не сломали. А надежда — плохая стратегия в инженерии.
Пример опасного рефакторинга без тестов:
Представьте, что у вас есть метод, который обрабатывает заказы. Он работает уже год, и все им довольны. Вы решаете, что его пора улучшить: разбить на несколько маленьких методов, убрать дублирование, переименовать переменные.
csharp
// Было: длинный, но работающий метод
public void ProcessOrder(Order order)
{
// 50 строк кода с валидацией, расчётами,
// сохранением и отправкой писем
// ... (код работал, но был сложным для понимания)
}
// Стало: красиво разбитый на методы
public void ProcessOrder(Order order)
{
ValidateOrder(order);
CalculateTotal(order);
SaveOrder(order);
SendConfirmation(order);
}
Вы сделали рефакторинг. Код стал красивым и понятным. Но вы не запустили тесты (их просто не было). Вы выкатываете изменения в продакшен. И... система падает.
Оказывается, в старом методе была тонкая логика: если сумма заказа больше 10000, то письмо отправлялось до сохранения, а не после. При рефакторинге вы случайно поменяли порядок вызовов. Тесты обнаружили бы эту проблему. Но их не было.
Вывод
Рефакторинг без тестов — это не улучшение кода, это игра в русскую рулетку.
Как тесты делают рефакторинг безопасным?
Когда у вас есть надёжный набор регрессионных тестов, рефакторинг превращается из рискованного предприятия в контролируемый процесс.
Как это выглядит на практике:
Пример с тестами, который ловит регрессию:
Предположим, у нас есть тест, проверяющий порядок действий при обработке заказа:
csharp
[Fact]
public void ProcessOrder_WhenOrderIsLarge_SendsEmailAfterSaving()
{
var mockEmail = new Mock<IEmailSender>();
var mockRepo = new Mock<IOrderRepository>();
var service = new OrderService(mockRepo.Object, mockEmail.Object);
// Отслеживаем порядок вызовов
var callOrder = new List<string>();
mockRepo.Setup(r => r.Save(It.IsAny<Order>()))
.Callback(() => callOrder.Add("Save"));
mockEmail.Setup(e => e.Send(It.IsAny<string>(), It.IsAny<string>()))
.Callback(() => callOrder.Add("Send"));
var order = new Order { Total = 15000m };
service.ProcessOrder(order);
// Проверяем, что сначала было сохранение, потом письмо
Assert.Equal(new[] { "Save", "Send" }, callOrder);
}
Теперь, когда вы будете рефакторить ProcessOrder и случайно поменяете порядок вызовов, тест упадёт и скажет вам: «Эй, ты что-то сломал! Порядок должен быть сначала Save, потом Send, а у тебя наоборот!»
Золотое правило рефакторинга
В профессиональной разработке есть золотое правило, которое стоит запомнить раз и навсегда:
Золотое правило
Делайте рефакторинг маленькими шагами, прогоняя тесты после каждого шага.
Почему это важно? Рассмотрим два подхода:
| Подход А (плохой) | Подход Б (хороший) |
|---|---|
| 1. Вы делаете 10 изменений в коде за один раз | 1. Вы делаете ОДНО маленькое изменение |
| 2. Запускаете тесты | 2. Запускаете тесты |
| 3. Тесты падают | 3. Тесты зелёные → отлично, идём дальше |
| 4. Вы не знаете, какое из 10 изменений вызвало проблему | 4. Делаете следующее маленькое изменение |
| 5. Вы тратите часы на поиск причины | 5. Запускаете тесты |
| 6. Если тест упал, вы точно знаете, какое изменение вызвало проблему (потому что оно было только одно) |
Маленькие шаги дают вам мгновенную обратную связь. Вы всегда знаете, что пошло не так, и можете быстро исправить.
Пример пошагового рефакторинга:
csharp
Шаг 1: Переименовываем переменную для ясности
Было: var d = GetDiscount(order);
Стало: var discount = GetDiscount(order);
Запускаем тесты → зелёные ✓
Шаг 2: Выделяем расчёт скидки в отдельный метод
Было: var discount = 0;
if (order.Total > 1000) discount = order.Total * 0.1m;
Стало: var discount = CalculateDiscount(order);
Запускаем тесты → зелёные ✓
Шаг 3: Упрощаем логику внутри CalculateDiscount
... маленькое изменение
Запускаем тесты → зелёные ✓
Шаг 4: Ещё одно маленькое изменение
Запускаем тесты → зелёные ✓
После 10 маленьких шагов код стал гораздо лучше, и все тесты зелёные. Если на каком-то шаге тест упал, вы сразу знаете, что именно пошло не так.
Что делать с legacy-кодом без тестов?
А что, если вы работаете с legacy-кодом (старым кодом), у которого нет тестов? Это распространённая ситуация в реальных проектах.
В этом случае алгоритм меняется:
Практические выводы для команды
Вот как выглядит полная картина связей между рефакторингом и регрессионным тестированием:
Практические выводы
- Не рефакторьте код без тестов. Если тестов нет — сначала напишите их (хотя бы страховочные).
- Запускайте регрессию после каждого изменения. Даже маленького. Это даёт мгновенную обратную связь.
- Делайте изменения маленькими. Одно изменение → запуск тестов → следующее изменение. Так вы всегда знаете, что сломали.
- Поддерживайте тесты в актуальном состоянии. Если изменилось поведение системы, обновите тесты. Устаревшие тесты хуже, чем их отсутствие.
- Автоматизируйте запуск регрессии. В идеале тесты должны запускаться автоматически при каждом коммите (CI/CD). Тогда разработчик не может «забыть» их запустить.
§5.7 Антипаттерны рефакторинга: чего делать НЕ надо
В предыдущих параграфах мы подробно разобрали, что такое рефакторинг, зачем он нужен и как правильно его выполнять с опорой на регрессионные тесты. Мы говорили о том, что рефакторинг — это дисциплинированный процесс улучшения кода маленькими шагами.
Однако у многих разработчиков, особенно начинающих, есть соблазн «улучшить» код так, что становится только хуже. Это происходит, когда рефакторинг превращается из инструмента в самоцель, а разработчик начинает «украшать» код там, где это не нужно, или применяет неподходящие паттерны.
В инженерии ПО такие ошибки называют антипаттернами рефакторинга. Это не технические ошибки (код компилируется и даже работает), а стратегические просчёты, которые делают код сложнее, а не проще.
Антипаттерн №1: Золотой молоток (Golden Hammer)
Симптом: Разработчик использует один и тот же паттерн или технологию для всех задач, даже когда он явно не подходит.
Название происходит из поговорки: «Если у тебя есть молоток, всё вокруг кажется гвоздём». Когда разработчик выучил какой-то модный паттерн (например, Dependency Injection, фабрики или даже микросервисы), ему кажется, что это решение подходит для всех задач без исключения.
Пример «Золотого молотка»:
csharp
// Разработчик выучил паттерн "Фабрика" и теперь использует его везде
public interface ICalculatorFactory
{
ICalculator CreateCalculator(CalculatorType type);
}
public class SimpleAddition
{
private readonly ICalculatorFactory _factory;
public SimpleAddition(ICalculatorFactory factory)
{
_factory = factory;
}
public int Add(int a, int b)
{
// Зачем тут фабрика? Нам просто нужно сложить два числа!
var calculator = _factory.CreateCalculator(CalculatorType.Addition);
return calculator.Calculate(a, b);
}
}
Почему это плохо:
- Код становится избыточным и сложным там, где можно было обойтись простым методом.
- Читаемость падает: чтобы понять простую операцию сложения, нужно разобраться с интерфейсами, фабриками и типами калькуляторов.
- Производительность страдает от лишних вызовов и создания объектов.
- Поддержка усложняется: любое изменение требует правки в нескольких местах.
Как исправить:
Используйте паттерны только тогда, когда они действительно решают проблему. Для простого сложения двух чисел достаточно обычного метода. Паттерн «Фабрика» имеет смысл, когда у вас есть множество вариаций создания объектов и эта логика меняется часто.
Признак
Признак того, что вы используете «Золотой молоток»: вы можете объяснить, почему этот паттерн подходит именно здесь, а не просто «это модно» или «я так умею».
Антипаттерн №2: Божественный объект (God Object)
Симптом: Один класс знает и делает всё. Он содержит десятки полей, сотни методов и отвечает за разные, порой не связанные между собой области. Этот антипаттерн — противоположность нормальному рефакторингу. Вместо того чтобы разделить ответственность между несколькими маленькими классами, разработчик собирает всё в одном месте.
Пример «Божественного объекта»:
csharp
public class ApplicationManager
{
// 20+ полей для разных задач
private SqlConnection _dbConnection;
private EmailSender _emailSender;
private Logger _logger;
private CacheManager _cache;
private PaymentProcessor _paymentProcessor;
private ReportGenerator _reportGenerator;
private UserManager _userManager;
// ... и так далее
// 50+ методов для разных ответственностей
public void ProcessOrder(Order order) { ... }
public void SendEmail(string to, string message) { ... }
public void GenerateReport(DateTime from, DateTime to) { ... }
public void ProcessPayment(PaymentInfo info) { ... }
public void ManageUsers() { ... }
public void ClearCache() { ... }
// ... и так далее
}
Почему это плохо:
- Нарушается принцип единственной ответственности (SRP). Класс пытается делать слишком много.
- Класс сложно тестировать — нужно поднимать все 20 зависимостей.
- Класс сложно изменять — изменение в одной области может задеть другие.
- Класс сложно переиспользовать — вы не можете использовать только часть его функциональности.
Как исправить:
Выделите отдельные классы для каждой ответственности. Каждый класс должен делать одну вещь и делать её хорошо.
csharp
public class OrderProcessor { ... }
public class EmailService { ... }
public class ReportGenerator { ... }
public class PaymentService { ... }
public class UserService { ... }
public class CacheService { ... }
Теперь каждый класс имеет свою чёткую зону ответственности. Их легко тестировать, легко изменять, легко переиспользовать.
Признак
Признак того, что вы создаёте «Божественный объект»: вы пишете класс, и в его названии есть слово Manager, System или Application.
Антипаттерн №3: Преждевременная оптимизация
Симптом: Разработчик «улучшает» код, чтобы он работал быстрее, хотя никто не жаловался на скорость. Или усложняет код ради микроскопического выигрыша в производительности.
Пример преждевременной оптимизации:
csharp
// Разработчик решил, что стандартный List работает слишком медленно
// и заменил его на сложную структуру с кешированием
public class ProductCache
{
private readonly Dictionary<int, Product> _cache = new();
private readonly List<int> _accessOrder = new();
private readonly int _maxSize = 100;
public Product GetProduct(int id)
{
if (_cache.ContainsKey(id))
{
// Обновляем порядок доступа (LRU-логика)
_accessOrder.Remove(id);
_accessOrder.Add(id);
return _cache[id];
}
// Сложная логика загрузки из базы
var product = LoadFromDatabase(id);
if (_cache.Count >= _maxSize)
{
// Удаляем самый старый элемент
var oldest = _accessOrder[0];
_accessOrder.RemoveAt(0);
_cache.Remove(oldest);
}
_cache.Add(id, product);
_accessOrder.Add(id);
return product;
}
}
Почему это плохо:
- Код стал в 10 раз сложнее, чем был.
- Пока никто не жаловался на скорость загрузки продуктов.
- Возможно, реальное узкое место — не загрузка продуктов, а что-то другое (например, сеть или база данных).
- Сложный код сложно поддерживать и труднее находить баги.
Как исправить:
- Сначала измерьте производительность (профилирование). Узнайте, где реальное узкое место.
- Оптимизируйте только то, что реально медленное.
- Оптимизируйте простыми способами, которые не усложняют код.
- Документируйте, почему вы сделали такую оптимизацию.
Дональд Кнут
«Преждевременная оптимизация — корень всех зол».
Сначала напишите простой, понятный код. Если он работает достаточно быстро — отлично. Если нет — измерьте и оптимизируйте только проблемное место.
Признак
Признак преждевременной оптимизации: вы усложняете код, потому что «может быть, когда-нибудь это станет узким местом», а не потому, что вы измерили и доказали, что это проблема.
Антипаттерн №4: Бойлерплейт (Boilerplate)
Симптом: Вы постоянно копируете один и тот же код из одного места в другое, вместо того чтобы создать общую абстракцию, что всегда приводит к ненужным последствиям.
В отличие от дублирования кода (которое мы обсуждали в §5.3 как Code Smell), бойлерплейт — это шаблонный код, который почти одинаков в разных местах, но с небольшими вариациями. Важно понимать, что это не значит, что вы не дублируете код.
Пример бойлерплейта:
csharp
// Метод 1: Обработка заказа
public void ProcessOrder(Order order)
{
try
{
Logger.LogInfo($"Начинаем обработку заказа {order.Id}");
// ... бизнес-логика
Logger.LogInfo($"Заказ {order.Id} обработан");
}
catch (Exception ex)
{
Logger.LogError($"Ошибка при обработке заказа {order.Id}: {ex.Message}");
throw;
}
}
// Метод 2: Отправка письма
public void SendEmail(string to, string message)
{
try
{
Logger.LogInfo($"Отправляем письмо на {to}");
// ... логика отправки
Logger.LogInfo($"Письмо на {to} отправлено");
}
catch (Exception ex)
{
Logger.LogError($"Ошибка при отправке письма на {to}: {ex.Message}");
throw;
}
}
// Метод 3: Генерация отчёта (опять то же самое!)
public void GenerateReport(DateTime from, DateTime to)
{
try
{
Logger.LogInfo($"Генерируем отчёт за {from}-{to}");
// ... логика генерации
Logger.LogInfo($"Отчёт сгенерирован");
}
catch (Exception ex)
{
Logger.LogError($"Ошибка при генерации отчёта: {ex.Message}");
throw;
}
}
Почему это плохо:
- Огромное количество повторяющегося кода.
- Если логика логирования изменится (например, добавится отправка уведомлений в Slack), придётся править все методы.
- Код становится длиннее, его сложнее читать.
Как исправить:
Используйте AOP (Aspect-Oriented Programming) или просто вынесите повторяющуюся логику в общий метод.
csharp
// Общий метод для обработки с логированием
private T ExecuteWithLogging<T>(string operationName, Func<T> action)
{
try
{
Logger.LogInfo($"Начинаем {operationName}");
var result = action();
Logger.LogInfo($"{operationName} успешно выполнен");
return result;
}
catch (Exception ex)
{
Logger.LogError($"Ошибка при {operationName}: {ex.Message}");
throw;
}
}
// Теперь методы стали короткими и понятными
public void ProcessOrder(Order order)
{
ExecuteWithLogging($"обработку заказа {order.Id}", () =>
{
// только бизнес-логика, без логирования
});
}
public void SendEmail(string to, string message)
{
ExecuteWithLogging($"отправку письма на {to}", () =>
{
// только логика отправки
});
}
Признак
Признак бойлерплейта: вы видите, что в разных методах повторяется один и тот же шаблон из 5-10 строк, и это не просто дублирование, а структурный паттерн, который можно вынести.
Aspect-Oriented Programming (Аспектно-ориентированное программирование)
— это подход, который позволяет вынести сквозную функциональность (cross-cutting concerns) за пределы основного кода.
Сквозная функциональность
— это задачи, которые нужны во многих местах программы, но не являются основной бизнес-логикой:
- Логирование
- Проверка безопасности (авторизация)
- Обработка ошибок
- Измерение времени выполнения
- Кеширование
- Транзакции базы данных
Антипаттерн №5: Рефакторинг ради рефакторинга
Симптом: Разработчик меняет код не потому, что есть реальная проблема (читаемость, сложность, производительность), а потому что «так красивее» или «так принято в моём любимом проекте».
Пример:
csharp
// Код работал и был понятен
public decimal GetDiscountedPrice(decimal price, decimal discountPercent)
{
return price - price * discountPercent / 100;
}
// Разработчик решает "улучшить"
public decimal GetDiscountedPrice(decimal price, decimal discountPercent)
{
// Используем паттерн "Стратегия" для расчёта скидки!
var strategy = new DiscountStrategyFactory()
.GetStrategy(discountPercent);
return strategy.Apply(price);
}
Почему это плохо:
- Первая версия была простой и понятной. Вторая — сложная и требует понимания фабрик и стратегий.
- Никто не жаловался на первую версию.
- Если таких «рефакторингов» много, код становится переусложнённым.
- Другие разработчики тратят время на понимание «улучшений», которые ничего не улучшили.
Как исправить:
Задайте себе три вопроса перед любым рефакторингом:
- Есть ли реальная проблема? (читаемость, сложность, баги, производительность)
- Улучшит ли это код для команды? (не только для вас)
- Стоит ли игра свеч? (окупится ли затраченное время)
Если хотя бы на один вопрос ответ «нет» — возможно, рефакторинг не нужен.
Сравнительная таблица антипаттернов
| Антипаттерн | Симптом | Чем грозит | Как исправить |
|---|---|---|---|
| Золотой молоток | Один паттерн везде | Переусложнение кода | Использовать паттерны осознанно |
| Божественный объект | Огромный класс | Сложно тестировать, менять, переиспользовать | Разделить на маленькие классы |
| Преждевременная оптимизация | Сложный код ради скорости | Трата времени, сложность поддержки | Измерить → оптимизировать только узкое место |
| Бойлерплейт | Повторяющиеся шаблоны | Много кода, сложно менять | Вынести общую логику |
| Рефакторинг ради рефакторинга | Изменения без причины | Переусложнение, трата времени | Задать 3 вопроса перед началом |
§5.8 Инструменты для рефакторинга и статического анализа в .NET
Мы уже знаем, что такое запахи кода и почему с ними нужно бороться. Но вот незадача: в реальном проекте может быть сотни классов и тысячи строк кода. Просматривать их все вручную в поисках длинных методов или дублирования — это всё равно что искать иголку в стоге сена. Это долго, утомительно, и всё равно вы что-то пропустите.
К счастью, эту рутину можно и нужно автоматизировать. Здесь на помощь приходят инструменты статического анализа.
Статический анализ
— это проверка исходного кода без его фактического выполнения. Инструмент анализирует текст программы, её структуру и синтаксис, выявляя потенциальные проблемы ещё до того, как код будет запущен.
Представьте себе автоматическую проверку орфографии в текстовом редакторе. Вы печатаете текст, и редактор сразу подчёркивает ошибки красным. Вы ещё не отправили письмо, но уже видите, где допустили опечатку. Статический анализ делает то же самое, но с программным кодом.
Что может найти статический анализ?
Важно понимать
Статический анализ не запускает ваш код. Он просто читает его и ищет паттерны, которые в прошлом часто приводили к проблемам.
В экосистеме .NET существует несколько инструментов, каждый из которых решает свою задачу. Давайте рассмотрим основные.
1. Roslyn Analyzers (встроенные анализаторы Microsoft)
Roslyn
— это компиляторная платформа Microsoft, которая лежит в основе C#. Она предоставляет API для анализа кода прямо во время компиляции. Roslyn Analyzers — это набор правил, которые проверяют код на соответствие рекомендациям Microsoft.
Что проверяют:
- Производительность (например, использование
StringBuilderвместо конкатенации строк в циклах). - Безопасность (проверка на потенциальные уязвимости).
- Корректность (например, проверка, что методы не выбрасывают непредусмотренные исключения).
- Использование новых возможностей языка.
Как выглядят в IDE:
csharp
public void ProcessData()
{
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString(); // ⚠️ Зелёное подчёркивание!
// CA1846: Используйте StringBuilder
}
}
Visual Studio подчеркнёт эту строку зелёным и предложит исправление: «Заменить на StringBuilder для повышения производительности».
Почему это круто
Анализаторы работают прямо во время написания кода. Вы видите проблему мгновенно, а не через час, когда запустите тесты.
2. StyleCop — проверка стиля кода
StyleCop
— это инструмент, который проверяет, соответствует ли код корпоративному стилю. Он отвечает на вопрос: «Красиво ли написан код с точки зрения оформления?»
Что проверяет:
- Отступы (пробелы или табы).
- Названия (PascalCase для классов, camelCase для параметров).
- Расположение элементов (порядок
using, полей, свойств, методов). - Длина строк (не более 120 символов).
- Правила оформления документации (XML-комментарии).
Пример нарушения StyleCop:
csharp
// ✕ StyleCop найдёт нарушение
// Неправильное имя класса (должно быть OrderProcessor)
// Открывающая скобка должна быть на новой строке
public class orderProcessor
{
// Неправильное именование поля (должно быть _orderId)
private int _OrderId;
public void Process() // Отсутствует пробел после public
{
// ...
} // Нет пробела между методами
}
Как это помогает: Единый стиль кода делает проект читаемым для всех разработчиков. Если каждый пишет как хочет, код превращается в «кашу», и новому сотруднику сложно разобраться.
Настройка StyleCop
Правила StyleCop можно настраивать через файл .editorconfig. Например, вы можете разрешить длину строки 150 символов вместо 120, если ваша команда так решила.
3. FxCopAnalyzers (классические правила из FxCop)
FxCop
— это старый, но очень известный инструмент от Microsoft. Его правила были настолько полезны, что Microsoft перенесла их в Roslyn Analyzers под именем Microsoft.CodeAnalysis.FxCopAnalyzers.
По сути, это те же правила, что и у Roslyn Analyzers, но с акцентом на классические проверки качества и безопасности.
Что проверяет (отличия от Roslyn):
- Более строгие правила для библиотек (например, правильная реализация
IDisposable). - Проверки на совместимость между версиями .NET.
- Анализ использования исключений (не выбрасывать
Exception, только конкретные типы). - Проверка на правильное использование атрибутов.
Подключение:
xml
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers"
Version="3.3.2" />
После подключения вы получаете дополнительные предупреждения в IDE.
Важно
FxCop Analyzers больше не развивается как отдельный продукт — Microsoft рекомендует использовать Roslyn Analyzers. Но в старых проектах вы всё ещё можете встретить его.
4. SonarQube — комплексный анализ
SonarQube
— это не просто анализатор. Это целая платформа, которая даёт полную картину качества кода. Она работает не в IDE, а отдельно — на сервере.
Что проверяет SonarQube:
- Code Smells (запахи кода) — длинные методы, дублирование, сложные условия.
- Уязвимости — потенциальные проблемы безопасности.
- Покрытие тестами — сколько кода покрыто юнит-тестами.
- Дублирование — процент дублированного кода.
- Сложность — цикломатическая сложность методов.
Как выглядит отчёт SonarQube:
Почему это круто
SonarQube показывает динамику — растёт ли качество со временем или падает. Это помогает команде принимать решения: «Нам нужно срочно заняться покрытием тестами, оно упало на 5%».
Как подключить в .NET:
bash
# Установка SonarQube Scanner
dotnet tool install --global dotnet-sonarscanner
# Анализ проекта
dotnet sonarscanner begin /k:"MyProject" /d:sonar.host.url="http://localhost:9000"
dotnet build
dotnet sonarscanner end
SonarQube — это стандарт для крупных компаний, где важно не просто найти проблемы, а контролировать качество на уровне всего предприятия.
§5.9 Работа с легаси-кодом: стратегии без страха
В жизни каждого разработчика наступает момент, когда он открывает проект и видит код, который был написан давно, непонятно кем и непонятно как. В этом коде всё перемешано, нет никакой структуры, а главное — нет ни одного теста. И перед ним стоит задача: что-то изменить, что-то добавить или просто понять, как это работает.
Этот код называют легаси-кодом. И рано или поздно с ним сталкиваются все.
Что такое легаси-код?
У термина «легаси-код» есть два определения — формальное и неформальное:
- Формальное: Легаси-код — это код, который был написан для старых версий платформ, языков или библиотек и требует адаптации для работы в новых условиях.
- Неформальное (и более точное): Легаси-код — это код, у которого нет автоматических тестов.
Это определение принадлежит Майклу Фезерсу, автору книги «Working Effectively with Legacy Code». И оно очень точное. Потому что проблема легаси не в том, что код старый или написан на устаревшем языке. Проблема в том, что вы не знаете, что он делает.
У вас есть работающая система. Она принимает данные, что-то считает, выдаёт результаты. Пользователи ей довольны (или хотя бы привыкли). Но как она работает внутри? Какие есть нюансы? Где скрытые зависимости? Вы не знаете. И у вас нет тестов, которые могли бы это проверить.
Почему это проблема:
- Вы не можете безопасно рефакторить код — вы не знаете, что сломаете.
- Вы не можете легко добавлять новые функции — вы не знаете, как изменения повлияют на старую логику.
- Вы не можете быстро исправлять баги — каждое исправление может породить новые проблемы.
Это называется легаси-ловушка: чем дольше код живёт без тестов, тем сложнее его менять, тем больше он обрастает костылями, и тем сложнее становится написать тесты.
Но выход есть. И он не требует переписывания всего проекта с нуля.
Шаг №1. Написать страховочные тесты (Characterization Tests)
Первый и самый важный шаг — не пытаться сразу всё исправить. Вместо этого вы должны понять, как код работает в данный момент.
Characterization tests (тесты-характеристики)
— это тесты, которые фиксируют текущее поведение системы. Они не проверяют, правильно ли работает код. Они просто запоминают, как он работает сейчас.
Даже если в коде есть баги, страховочные тесты их зафиксируют. Зачем? Чтобы, когда вы начнёте что-то менять, вы знали: поведение изменилось только тогда, когда вы этого хотели.
Пример страховочного теста:
Представьте, что у вас есть метод, который считает скидку. Вы не знаете точно, правильный ли он, но он работает в продакшене уже год.
csharp
// Легаси-код: мы не знаем, правильно ли он работает, но он работает
public class DiscountCalculator
{
public decimal CalculateDiscount(decimal amount, bool isPremium)
{
// Сложная логика, которая выросла за 5 лет
// ... много кода, который никто уже не помнит
}
}
Вы пишете страховочный тест:
csharp
[Fact]
public void CalculateDiscount_CurrentBehavior_FixedByTest()
{
// ARRANGE
var calculator = new DiscountCalculator();
var amount = 1000m;
var isPremium = true;
// ACT
var result = calculator.CalculateDiscount(amount, isPremium);
// ASSERT: фиксируем текущее поведение
// Мы не знаем, правильно ли это, но мы это запоминаем
Assert.Equal(100m, result); // Сейчас возвращает 100
}
Теперь, когда вы будете рефакторить метод CalculateDiscount и случайно измените логику, тест упадёт и скажет: «Эй, раньше для 1000 и true возвращалось 100, а теперь 150. Ты уверен, что это правильное изменение?»
Если вы нашли баг в старой логике и хотите его исправить, вы сначала меняете тест на новое ожидаемое значение, а потом меняете код. Это называется TDD наоборот — сначала тест фиксирует старое поведение, потом вы его уточняете.
Шаг №2. Рефакторинг маленькими шагами
Теперь, когда у вас есть страховочные тесты, вы можете начать рефакторинг. Но есть одно важное правило:
Золотое правило
Делайте рефакторинг маленькими шагами, перезапуская тесты после каждого изменения.
Почему это так важно для легаси-кода? Потому что в легаси-коде всё переплетено. Одно изменение может вызвать цепную реакцию. Если вы сделаете 10 изменений за раз и тесты упадут, вы не будете знать, какое изменение вызвало проблему.
Пример пошагового рефакторинга легаси-кода:
csharp
// Было: огромный метод с кучей всего
public void ProcessOrder(Order order)
{
// 50 строк кода
// валидация, расчёты, сохранение, отправка писем
// всё в одном месте
}
// Шаг 1: Выделяем валидацию
private void ValidateOrder(Order order) { /* ... */ }
public void ProcessOrder(Order order)
{
ValidateOrder(order);
// остальной код
}
// Запускаем тесты → зелёные ✓
// Шаг 2: Выделяем расчёт суммы
private decimal CalculateTotal(Order order) { /* ... */ }
public void ProcessOrder(Order order)
{
ValidateOrder(order);
var total = CalculateTotal(order);
// остальной код
}
// Запускаем тесты → зелёные ✓
// Шаг 3: Выделяем сохранение
private void SaveOrder(Order order) { /* ... */ }
public void ProcessOrder(Order order)
{
ValidateOrder(order);
var total = CalculateTotal(order);
SaveOrder(order);
// остальной код
}
// Запускаем тесты → зелёные ✓
Каждый шаг — маленький, безопасный, контролируемый. Если на каком-то шаге тест упал, вы сразу знаете, что именно пошло не так.
Шаг №3. Постепенное внедрение Dependency Injection
После того как вы разбили большие методы на маленькие и покрыли их страховочными тестами, можно начинать внедрять Dependency Injection.
Почему DI так важен? Потому что он делает код тестопригодным. Вместо того чтобы внутри класса создавать зависимости через new, вы передаёте их через конструктор. И тогда в тестах вы можете подставить заглушки (Stub или Mock).
Пример внедрения DI в легаси-код:
csharp
// До: жёсткая зависимость от конкретной БД
public class OrderService
{
private readonly SqlConnection _connection =
new SqlConnection("Server=localhost;...");
public void ProcessOrder(Order order)
{
// ...
_connection.Execute("INSERT INTO Orders...");
}
}
// После внедрения интерфейса:
// Интерфейс для абстракции
public interface IOrderRepository
{
void Save(Order order);
}
// Реальная реализация для продакшена
public class OrderRepository : IOrderRepository
{
private readonly string _connectionString;
public OrderRepository(string connectionString) =>
_connectionString = connectionString;
public void Save(Order order) { /* ... */ }
}
// Класс с внедрённой зависимостью
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository) =>
_repository = repository;
public void ProcessOrder(Order order)
{
// ...
_repository.Save(order);
}
}
Теперь в тестах вы можете подставить заглушку:
csharp
[Fact]
public void ProcessOrder_ValidOrder_SavesToRepository()
{
// Arrange
var mockRepo = new Mock<IOrderRepository>();
var service = new OrderService(mockRepo.Object);
var order = new Order { ... };
// Act
service.ProcessOrder(order);
// Assert
mockRepo.Verify(r => r.Save(order), Times.Once);
}
Стратегия «Стеклянная банка» (Glass Box)
Когда вы работаете с легаси-кодом, возникает соблазн переписать всё сразу. «Давайте перепишем этот модуль с нуля, он же такой старый и страшный!»
Это почти всегда плохая идея. Переписывание с нуля:
- Дорого — занимает месяцы, а то и годы, что крайне невыгодно для разработки.
- Рискованно — новая версия может содержать те же баги, что и старая, плюс новые.
- Тормозит бизнес — пока вы переписываете, новые функции не разрабатываются.
- Непредсказуемо — вы не знаете всех нюансов старой логики, что может привести к непредвиденным последствиям.
Вместо этого используйте стратегию «Стеклянная банка» (Glass Box strategy):
Меняйте код по одному модулю за раз, как будто вы аккуратно перекладываете содержимое стеклянной банки в новую, не высыпая ни одной детали.
Как это выглядит на практике:
Стратегия Glass Box
Преимущества этой стратегии:
- Безопасность — вы всегда знаете, что остальная система работает.
- Прогресс — вы двигаетесь вперёд, а не застреваете на годы.
- Быстрая обратная связь — каждый модуль становится лучше, и это видно.
- Возможность остановиться — если приоритеты меняются, вы можете переключиться на другую задачу, не потеряв уже сделанное.
Что делать, если тестов совсем нет?
Это самый сложный случай. Бывает, что код настолько старый и запутанный, что даже написать страховочный тест сложно — метод делает слишком много и зависит от внешних ресурсов.
В этом случае алгоритм немного меняется:
Главные правила работы с легаси-кодом
Главные правила
- Не паникуйте. Легаси-код есть у всех. Это нормально.
- Не пытайтесь переписать всё сразу. Это путь к провалу.
- Начните с тестов. Даже если они будут интеграционными и медленными — это лучше, чем ничего.
- Двигайтесь маленькими шагами. Одно изменение → запуск тестов → следующее изменение.
- Изолируйте зависимости. Внедряйте интерфейсы постепенно, один за другим.
- Фиксируйте прогресс. Ведите список того, что уже сделано. Это мотивирует команду.
- Помните: вы не один. Обсуждайте проблемы с командой, проводите код-ревью, учитесь друг у друга.
§5.10 Как рефакторинг, регрессия и управление долгом формируют культуру качества
Мы прошли долгий путь. В этой лекции мы разобрали, что происходит с программным обеспечением после релиза, почему код имеет свойство «гнить» и как с этим бороться. Давайте соберём всё вместе и ответим на главный вопрос: что делает команду по-настоящему качественной?
Многие начинающие специалисты думают, что качество ПО — это когда в системе мало багов. Или, когда тесты проходят зелёным. Или, когда пользователи не жалуются. Это не так.
Качество
— это процесс, культура и дисциплина. Это то, как вы пишете код каждый день. Это то, как вы принимаете решения. Это то, как вы относитесь к своему инструменту и к своей команде.
Если вы просто исправляете баги, но не меняете подход к разработке — баги будут возвращаться. Если вы просто добавляете тесты, но не рефакторите код — тесты будут ломаться от каждого изменения. Если вы просто пишете код, но не думаете о его поддержке — через год никто не сможет в нём разобраться.
Качество начинается с осознанного подхода. С понимания того, что код пишется не для компьютера, а для людей. И что его будут читать, менять и поддерживать годы.
Три ключевых элемента поддержки ПО
В этой лекции мы выделили три ключевых элемента, которые формируют основу поддержки программного обеспечения:
1. Рефакторинг — улучшаем внутреннюю структуру.
Код должен быть понятным, простым и гибким. Рефакторинг — это способ постоянно поддерживать его в таком состоянии. Это не «украшательство» и не трата времени. Это инвестиция в будущее. Чем чище код, тем легче его тестировать, изменять и развивать.
2. Регрессионные тесты — проверяем, что ничего не сломали.
Без тестов рефакторинг — это риск. С тестами — это контролируемый процесс. Регрессионные тесты — это страховка, которая позволяет менять код смело, зная, что если вы что-то сломаете, тесты скажут вам об этом.
3. Управление техническим долгом — принимаем осознанные решения.
Технический долг неизбежен. Дедлайны, нехватка знаний, меняющиеся требования — всё это приводит к тому, что мы пишем код неидеально. Но важно брать долг осознанно, понимая, как и когда вы будете его возвращать. И не позволять процентам расти.
Роль QA-инженера
В первой лекции мы говорили, что QA — это не про «нажать кнопки и найти баг». Теперь, когда мы прошли все пять лекций, эта мысль становится ещё понятнее.
Настоящий QA-инженер не просто находит дефекты. Он:
- Понимает архитектуру. Знает, как устроен код, и может оценить риски изменений.
- Владеет инструментами тестирования. Пишет модульные, интеграционные и E2E-тесты, понимает, что и когда использовать.
- Анализирует качество кода. Использует статический анализ, читает отчёты SonarQube, выявляет Code Smells.
- Участвует в процессе разработки. Помогает команде принимать решения, которые делают код более тестопригодным и поддерживаемым.
- Думает о будущем. Не просто проверяет текущую версию, а помогает строить систему, которую легко будет развивать через год и через пять лет.
Заключительная мысль
Давайте завершим лекцию одной важной мыслью.
Цепочка качества
«Код, который нельзя протестировать, нельзя и рефакторить.»
Если у вас нет тестов, вы не знаете, что сломаете, и не можете безопасно менять код.
«Код, который нельзя рефакторить, нельзя поддерживать.»
Если код слишком сложный и запутанный, каждое изменение превращается в муку. Команда тратит всё время на борьбу с кодом, а не на решение бизнес-задач.
«Код, который нельзя поддерживать, мёртв.»
Если вы не можете менять код, он не может развиваться. А код, который не развивается, умирает — его заменяют другие системы, он теряет актуальность, он превращается в обузу.
Но есть и обратная сторона:
Обратная цепочка
«Код, который можно тестировать — можно рефакторить.»
«Код, который можно рефакторить — можно поддерживать.»
«Код, который можно поддерживать — жив.»
Мы не просто пишем код, который работает сегодня. Мы пишем код, который будет работать и через год, и через пять лет. Код, который можно развивать. Код, который не боится менять. Код, который живёт.