---
course: ПиТПМ
lecture: 5
title: "Лекция №5. Современные концепции поддержки ПО: Рефакторинг, регрессионное тестирование и управление техническим долгом"
---

# ◆ Лекция №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 |
| **Совершенствующая** | Проактивная | По запросу бизнеса | Улучшение производительности |
| **Превентивная** | Проактивная | Для предотвращения проблем | Рефакторинг дублирующегося кода |

### Главная проблема поддержки

Главная проблема поддержки заключается в вопросе: **«Как менять код, не ломая то, что работает?»**

Представьте себе башню из кубиков. Вы построили её, она стоит. Теперь представьте, что вам нужно заменить один кубик в середине башни, не разрушив её. Как это сделать? Если просто выдернуть кубик и вставить новый, башня, скорее всего, упадёт. Нужна особая техника — например, сначала установить временные опоры, затем заменить кубик, а потом убрать опоры.

То же самое происходит и с программным обеспечением. Любое изменение, даже самое маленькое, несёт риск сломать существующую функциональность. Разработчик добавляет одну строчку кода — и из-за неё перестаёт работать модуль, который казался совершенно не связанным. Это явление называют **регрессией**.

**Регрессия**

— это когда новое изменение приводит к ошибке в уже работавшем функционале.

**Откуда берётся регрессия?**

- **Скрытые зависимости.** Модули программы связаны между собой, и вы можете не знать обо всех этих связях. Изменили один метод — и сломали другой, который его использовал.
- **Непредвиденные побочные эффекты.** Каждое изменение может повлиять на производительность, потребление памяти или поведение в граничных условиях.
- **Человеческий фактор.** Разработчик может просто забыть, что его изменение затронет какой-то другой модуль.

### Как бороться с регрессией?

Ответ дают две ключевые концепции:

| Концепция | Определение | Цель |
| --- | --- | --- |
| **Рефакторинг** | Изменение внутренней структуры кода без изменения его внешнего поведения | Сделать код чище, проще и понятнее, снизить сложность |
| **Регрессионное тестирование** | Проверка того, что после внесённых изменений существующая функциональность продолжает работать корректно | Ловить регрессии до того, как они попадут к пользователю |

Связь концепций

Эти две концепции тесно связаны. **Рефакторинг без регрессионных тестов** — это рискованное занятие, потому что вы не знаете, что сломали. А **регрессионные тесты без рефакторинга** — это просто консервация текущего состояния, которое может быть далеко от идеального.

Именно эти две темы мы и будем изучать в данной лекции. Сначала мы разберёмся, что такое рефакторинг и как его правильно делать, затем познакомимся с понятием технического долга и запахов кода, а потом перейдём к регрессионному тестированию — инструменту, который делает рефакторинг безопасным.

### Жизненный цикл ПО в целом

**Классическая модель включает следующие фазы:**

1. **Анализ требований** — что должна делать система?
2. **Проектирование** — как мы это построим?
3. **Разработка** — написание кода.
4. **Тестирование** — проверка корректности.
5. **Внедрение (релиз)** — передача пользователям.
6. **Поддержка** — эксплуатация, исправление ошибок, адаптация, улучшение.

Часто начинающие специалисты думают, что их работа заканчивается на 5-м этапе. Однако именно **6-й этап — поддержка** — является самым длительным и затратным. Более того, именно на этом этапе проявляется истинное качество кода.

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

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

## §5.2 Рефакторинг: что это и зачем он нужен

В предыдущем параграфе мы выяснили, что поддержка программного обеспечения — это самая длительная и затратная фаза жизненного цикла. Мы также обозначили главную проблему: любое изменение в коде несёт риск сломать существующую функциональность. Возникает естественный вопрос: **как же тогда поддерживать и улучшать программу, если каждое изменение опасно?** Ответ на этот вопрос даёт понятие **рефакторинга**.

Определение Мартина Фаулера

**Рефакторинг**

— это изменение внутренней структуры программного кода **без изменения его внешнего поведения**.

Разберём это определение по частям:

- **«Изменение внутренней структуры»** — мы переписываем код внутри: меняем названия переменных, разбиваем длинные методы на короткие, выделяем общие части в отдельные функции, перестраиваем иерархии классов. Код становится другим, но только с точки зрения разработчика.
- **«Без изменения внешнего поведения»** — для пользователя ничего не меняется. Программа делает ровно то же самое, что и раньше. Те же кнопки, те же расчёты, те же ответы на запросы. Если пользователь не смотрит в исходный код, он вообще не заметит, что что-то изменилось.

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

### Почему код нуждается в рефакторинге?

Начинающие разработчики часто задают вопрос: **«Зачем трогать код, если он работает? Ведь работает — и хорошо»**. Это распространённое заблуждение, и оно приводит к серьёзным проблемам в долгосрочной перспективе.

Дело в том, что код имеет свойство **«гнить»** (это явление называют **software rot**). В отличие от физического гниения, здесь речь идёт не о времени, а о накоплении изменений. Каждое новое изменение, каждое исправление бага, каждое добавление фичи делает код чуть сложнее, чуть запутаннее, чуть менее понятным.

Процесс гниения кода

**Процесс гниения кода выглядит так:**

1. Разработчик пишет код быстро, чтобы уложиться в дедлайн. Он не задумывается о красоте и читаемости — главное, чтобы работало.
2. Через месяц появляется новый баг. Разработчик находит место проблемы и добавляет быстрое исправление (костыль). Код становится чуть сложнее.
3. Ещё через месяц приходит задача добавить новую функцию. Разработчик видит, что существующий код уже не очень понятен, но копаться в нём некогда, поэтому он просто копирует кусок старого кода, вставляет в новое место и немного правит. Появляется дублирование.
4. Так продолжается месяцы и годы. Код превращается в «спагетти»: длинные методы, дублирование, странные названия переменных, непонятные условия, вложенные друг в друга на 5-7 уровней.

В результате простейшая задача, которая должна занимать час, занимает два дня. Разработчики боятся что-то менять, потому что непонятно, к чему это приведёт. Новые сотрудники неделями разбираются в коде, вместо того чтобы писать полезный функционал. Именно в этот момент и нужен рефакторинг.

### Как понять, что код нуждается в рефакторинге?

Есть несколько явных признаков, которые говорят о том, что код пора улучшать. Обратите внимание: это не баги — программа может работать идеально. Это симптомы того, что код стал сложным для понимания и изменения.

### Рефакторинг ≠ переписывание

Это один из самых важных пунктов, который нужно понять. Многие разработчики, особенно начинающие, путают рефакторинг с полной переделкой кода. Это принципиально разные вещи.

| Характеристика | Рефакторинг | Переписывание |
| --- | --- | --- |
| **Масштаб** | Небольшие, эволюционные изменения | Крупные, революционные изменения |
| **Внешнее поведение** | Не меняется (строго) | Может меняться (новая архитектура, новый подход) |
| **Риск** | Низкий (изменения малы и контролируемы) | Высокий (меняется всё, сложно проверить) |
| **Время** | Минуты или часы | Дни, недели, месяцы |
| **Когда применять** | Регулярно, как часть повседневной работы | Когда код настолько плох, что рефакторинг невозможен (крайний случай) |

**Переписывание**

— это когда вы говорите: «Всё плохо, я удаляю старый код и пишу новый с нуля». Такой подход крайне рискован. Пока вы пишете новую версию, старая продолжает работать, но в ней не появляются новые фичи. Команда тратит месяцы на переписывание, а бизнес не получает нового функционала. К тому же, новая версия может содержать ровно те же ошибки, что и старая, плюс новые.

**Рефакторинг**

— это когда вы улучшаете код маленькими шагами. Убрали дублирование → запустили тесты → убедились, что всё работает. Переименовали переменную → запустили тесты. Разбили длинный метод на три маленьких → запустили тесты. Каждый шаг — крошечный, безопасный, контролируемый.

### Пример рефакторинга

Вот как выглядит рефакторинг на примере того самого метода `Calc`:

```csharp
// Шаг №1. Переименовываем параметры, чтобы стало понятно, что они означают:
public decimal Calc(decimal basePrice, decimal quantity, bool isPremium, int customerYears)
{
   // ... тело метода
}
```

Код стал понятным. Теперь любой разработчик сразу видит: сначала считаем базовую цену, потом применяем скидку, потом — дополнительную скидку. Никакой магии.

Важно

На каждом шаге мы запускали тесты и убеждались, что внешнее поведение не изменилось. Если тесты были написаны правильно, мы могли быть спокойны.

### Рефакторинг — это инвестиция

Рефакторинг часто воспринимают как трату времени. Менеджеры говорят: «Зачем вы тратите день на переписывание кода, который и так работает? Давайте лучше сделаем новую функцию!»

Но это **ложная экономия**. Рефакторинг — это **инвестиция в будущее**. Вы тратите время сегодня, чтобы сэкономить в десятки раз больше времени завтра.

Представьте два проекта:

- **Проект А.** Код никогда не рефакторится. В нём накапливается технический долг. С каждым годом добавлять новые фичи становится всё сложнее и дольше. Через 5 лет любое изменение требует недель работы и сопровождается пачкой багов.
- **Проект Б.** Команда регулярно выделяет 10-20% времени на рефакторинг. Код остаётся чистым и понятным. Новые фичи добавляются быстро, баги редки. Через 5 лет проект всё ещё легко поддерживать, что очень хорошо для самого проекта.

Какой проект вы бы хотели поддерживать? Ответ очевиден.

### Когда начинать рефакторинг?

Ответ простой: **начинать нужно прямо сейчас, но маленькими шагами.**

Не нужно планировать «большой рефакторинг» на месяц. Это путь к провалу. Вместо этого:

1. **Каждый раз, когда вы касаетесь какого-то куска кода** (исправляете баг или добавляете функцию), оставляйте его немного чище, чем нашли. Убрали дублирование, переименовали переменную, разбили метод — это уже рефакторинг.
2. **Используйте принцип «Бойскаута»:** «Оставляй код чище, чем ты его нашёл».
3. **Всегда имейте регрессионные тесты.** Без них рефакторинг — это игра в русскую рулетку.

## §5.3 Запахи кода (Code Smells): как распознать проблему до того, как она станет критической

В предыдущем параграфе мы говорили о рефакторинге и о том, что код имеет свойство «гнить». Но как понять, что код уже начал портиться? Как распознать проблему до того, как она превратится в критический баг или причину многодневных страданий команды?

Для этого в инженерии ПО существует понятие **запахов кода (Code Smells)**.

> **Code Smell (запах кода)**
>
> — это поверхностный признак того, что в структуре или логике кода что-то не так.

Это **не баг** и не ошибка — программа может работать идеально. Это **симптом**, который говорит: «Здесь потенциально может возникнуть проблема в будущем».

Термин придумал **Кент Бек**, один из создателей методологии экстремального программирования (XP). Он использовал аналогию с настоящим запахом:

Когда вы входите на кухню и чувствуете запах газа, вы не ждёте, пока произойдёт взрыв. Вы сразу проверяете плиту. Точно так же, когда вы видите Code Smell, вы не ждёте, пока он превратится в баг. Вы сразу анализируете код и, если нужно, рефакторите его.

Важно понимать

Code Smell — это не приговор. Иногда код пахнет, но это оправдано спецификой задачи или архитектурными ограничениями. Однако в большинстве случаев Code Smell — это сигнал к действию.

Теперь давайте рассмотрим самые распространённые запахи кода. У каждого из них есть свои симптомы, причины и способы исправления.

### 1. Дублирование кода (Duplicated Code)

**Симптом:** Один и тот же или очень похожий фрагмент кода встречается в нескольких местах.

**Пример:**

csharp

```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

```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

```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

```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

```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

```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

```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

```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

```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

```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

```csharp
public void ProcessData()
{
    try
    {
        var data = _repository.GetData();
        // ... обработка данных
    }
    catch (Exception ex)
    {
        // Пустой блок catch — ничего не делаем!
    }
}
```

**Почему это плохо:**

- Ошибка скрывается. Программа продолжает работать, но делает это некорректно.
- Разработчик не знает, что произошла ошибка, и не может её исправить.
- Ошибка может накапливаться и привести к катастрофическому сбою позже.

Это один из самых опасных Code Smells, потому что он маскирует реальные проблемы.

**Для исправления** необходимо никогда не оставлять блок `catch` пустым.

csharp

```csharp
public void ProcessData()
{
    try
    {
        var data = _repository.GetData();
        // ... обработка данных
    }
    catch (Exception ex)
    {
        // Логируем ошибку, чтобы знать, что произошло
        _logger.LogError(ex, "Ошибка при получении данных");
        
        // И либо пробрасываем дальше, либо возвращаем fallback
        throw; // или return default;
    }
}
```

Исключение из правила

Иногда вы действительно хотите игнорировать ошибку (например, при попытке удалить файл, который уже удалён). Но даже в этом случае нужно логировать факт ошибки и указывать, почему вы её игнорируете.

csharp

```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 году программист **Уорд Каннингем** предложил простую, но гениальную аналогию. Он сравнил разработку программного обеспечения с финансовой системой:

**Технический долг**

— это как финансовый долг. Вы берёте кредит, чтобы быстрее достичь цели, но потом вам придётся его возвращать с процентами.

**Представьте себе, что вы строите дом.** Вы можете построить его быстро, используя дешёвые материалы и упрощённые технологии. Дом будет готов через месяц, и вы сможете в него заехать. Но через год стены начнут трещать, крыша протекать, а проводка искрить. Вам придётся тратить деньги и время на постоянный ремонт. В итоге вы заплатите больше, чем если бы построили качественно с самого начала.

То же самое происходит с программным кодом:

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

### Почему возникает технический долг?

В идеальном мире мы всегда писали бы идеальный код. Но в реальном мире существуют жёсткие ограничения:

Важный момент

Технический долг — это не всегда зло. Иногда это осознанное решение. Вопрос в том, осознанно ли вы берёте долг и знаете ли, когда и как его отдавать.

### Два вида технического долга

Технический долг бывает двух видов, и их важно различать:

```csharp
// Осознанное решение: делаем быстро, но знаем, что надо переделать
public class QuickOrderProcessor
{
   // Это временное решение для MVP (Minimum Viable Product)
   // TODO: Переписать после релиза, использовать паттерн Стратегия
   public void Process(Order order)
   {
       // Простейшая логика без учёта всех типов заказов
       SaveToDatabase(order);
       SendEmail(order);
   }
}
```

### Проценты по техническому долгу

Как и в финансовом мире, у технического долга есть проценты. Чем дольше вы не отдаёте долг, тем больше процентов накапливается.

**Как выглядят проценты в реальном проекте:**

1. **Время на добавление новых фич растёт.** То, что раньше занимало день, теперь требует недели. Код стал сложным и запутанным, разработчики тратят время на понимание существующей логики.
2. **Количество багов растёт.** Каждое изменение в одной части системы вызывает ошибки в других, потому что код плохо структурирован и зависимости не очевидны.
3. **Сложность найма.** Новые разработчики не могут быстро войти в проект. Им нужны месяцы, чтобы разобраться в коде.
4. **Моральный дух команды падает.** Разработчики устают от постоянного «пожара» и теряют мотивацию. Лучшие сотрудники уходят.
5. **Бизнес теряет гибкость.** Вы не можете быстро реагировать на изменения рынка, потому что любое изменение требует огромных усилий.

### Как измерить технический долг?

Технический долг сложно измерить в деньгах, но есть несколько косвенных показателей:

### Стратегии работы с техническим долгом

Как и с финансовым долгом, с техническим долгом нужно работать системно:

> **Стратегия №1. Плановое погашение**
>
> Выделите **10-20% времени каждого спринта** на рефакторинг. Это как регулярные платежи по кредиту. Вы не гасите весь долг сразу, но не даёте ему расти.
>
> **Стратегия №2. Рефакторинг по касанию**
>
> Каждый раз, когда вы трогаете какой-то участок кода (исправляете баг или добавляете функцию), вы улучшаете его. Это правило **«Бойскаута»**: «Оставляй код чище, чем ты его нашёл».
>
> **Стратегия №3. Приоритизация**
>
> Не все долги одинаково важны. Сначала погашайте те, которые мешают разработке больше всего:
>
> 1. Код, который сложно тестировать.
> 2. Код, который часто меняется.
> 3. Код, в котором чаще всего возникают баги.
>
> **Стратегия №4. Автоматизация проверок**
>
> Включите статические анализаторы в CI/CD-пайплайн. Если покрытие тестами падает ниже 80% или появляются новые Code Smells — сборка падает. Это предотвращает накопление неосознанного долга.
> [/TABS]

Главное правило

**Берите долг осознанно, платите регулярно и не позволяйте процентам накапливаться.**

Технический долг не обязательно плох. В стартапе на ранней стадии быстрый выход на рынок может быть важнее идеального кода. Но если вы не планируете возвращать долг, он убьёт ваш проект.

Мартин Фаулер

«Если вы не инвестируете в качество кода, вы инвестируете в его ухудшение. А это гораздо более дорогое вложение.»

## §5.5 Регрессионное тестирование: защита от «починили одно — сломали другое»

В предыдущих параграфах мы говорили о рефакторинге и техническом долге. Мы выяснили, что код нужно постоянно улучшать, чтобы он оставался понятным и поддерживаемым. Но здесь возникает фундаментальная проблема: **любое изменение в коде несёт риск сломать то, что уже работало.**

Представьте себе, что вы чините кран на кухне. Вы перекрываете воду, откручиваете старый кран, ставите новый. Всё работает, вода течёт. Вы довольны. Но через час соседи снизу звонят: «У вас потёк стояк, и у нас залило потолок». Оказывается, пока вы меняли кран, вы случайно задели трубу, которая идёт к соседям. Вы починили одно, но сломали другое, что, понятное дело, не есть хорошо.

В программном обеспечении это явление называется **регрессией**.

**Регрессия** (или регрессионный дефект) — это ошибка, которая возникает в уже работавшей функциональности после внесения изменений в код.

**Простой пример:**

csharp

```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 получит скидку, хотя раньше не получал. Это **регрессия**: работавшая функция сломалась из-за изменения в другой части кода.

### Почему возникают регрессии?

Ответ кроется в нескольких причинах:

### Основной способ борьбы с регрессиями

Основной способ борьбы с регрессиями — это **регрессионное тестирование**.

**Регрессионное тестирование**

— это проверка того, что после внесённых изменений существующая функциональность продолжает работать корректно.

**Вручную выполнять регрессионное тестирование невозможно** по нескольким причинам:

1. **Объём.** В большой системе могут быть тысячи сценариев. Проверять их вручную после каждого изменения — это недели работы.
2. **Частота.** В современных проектах изменения вносятся несколько раз в день (иногда десятки раз). Ручное тестирование просто не успевает.
3. **Утомляемость.** Человек устаёт и начинает пропускать ошибки. Особенно если одни и те же сценарии проверяются сотый раз.

Поэтому индустрия пришла к стандартному решению: **регрессионное тестирование должно быть автоматизированным**.

### Какие тесты включать в регрессионный набор?

Автоматизированное регрессионное тестирование — это не просто «запустили все тесты». Это продуманный набор проверок, который включает три уровня:

```csharp
[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%).

**Почему такое распределение?** Потому что:

1. Модульные тесты дёшевы и быстры. Их можно запускать после каждого изменения. Они дают быструю обратную связь разработчику.
2. Интеграционные тесты дороже, но проверяют важные аспекты. Их запускают реже (например, при сборке в ветку разработки).
3. E2E-тесты дорогие и медленные. Их запускают только перед релизом или ночью.

### Как часто нужно запускать регрессионные тесты?

Золотое правило

**«Регрессионные тесты должны запускаться при каждом изменении кода».**

В мире **CI/CD (Continuous Integration/Continuous Delivery)** это реализуется так:

1. Разработчик делает коммит в репозиторий.
2. CI-сервер (например, GitHub Actions, GitLab CI, Jenkins) автоматически:

   - Собирает проект.
   - Запускает модульные тесты (быстро, за 1-2 минуты).
   - Если всё прошло — запускает интеграционные тесты.
   - Если и они прошли — собирает артефакты для релиза.
3. E2E-тесты могут запускаться отдельно (например, ночью) или перед выкаткой в продакшен.

**Вот как это выглядит в конфигурации GitHub Actions:**

yaml

```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

```csharp
// Было: длинный, но работающий метод
public void ProcessOrder(Order order)
{
    // 50 строк кода с валидацией, расчётами,
    // сохранением и отправкой писем
    // ... (код работал, но был сложным для понимания)
}

// Стало: красиво разбитый на методы
public void ProcessOrder(Order order)
{
    ValidateOrder(order);
    CalculateTotal(order);
    SaveOrder(order);
    SendConfirmation(order);
}
```

Вы сделали рефакторинг. Код стал красивым и понятным. Но вы не запустили тесты (их просто не было). Вы выкатываете изменения в продакшен. И... система падает.

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

Вывод

**Рефакторинг без тестов — это не улучшение кода, это игра в русскую рулетку.**

### Как тесты делают рефакторинг безопасным?

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

**Как это выглядит на практике:**

**Пример с тестами, который ловит регрессию:**

Предположим, у нас есть тест, проверяющий порядок действий при обработке заказа:

csharp

```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

```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-кодом** (старым кодом), у которого нет тестов? Это распространённая ситуация в реальных проектах.

В этом случае алгоритм меняется:

### Практические выводы для команды

Вот как выглядит полная картина связей между рефакторингом и регрессионным тестированием:

Практические выводы

1. **Не рефакторьте код без тестов.** Если тестов нет — сначала напишите их (хотя бы страховочные).
2. **Запускайте регрессию после каждого изменения.** Даже маленького. Это даёт мгновенную обратную связь.
3. **Делайте изменения маленькими.** Одно изменение → запуск тестов → следующее изменение. Так вы всегда знаете, что сломали.
4. **Поддерживайте тесты в актуальном состоянии.** Если изменилось поведение системы, обновите тесты. Устаревшие тесты хуже, чем их отсутствие.
5. **Автоматизируйте запуск регрессии.** В идеале тесты должны запускаться автоматически при каждом коммите (CI/CD). Тогда разработчик не может «забыть» их запустить.

## §5.7 Антипаттерны рефакторинга: чего делать НЕ надо

В предыдущих параграфах мы подробно разобрали, что такое рефакторинг, зачем он нужен и как правильно его выполнять с опорой на регрессионные тесты. Мы говорили о том, что рефакторинг — это дисциплинированный процесс улучшения кода маленькими шагами.

Однако у многих разработчиков, особенно начинающих, есть соблазн «улучшить» код так, что становится только хуже. Это происходит, когда рефакторинг превращается из инструмента в самоцель, а разработчик начинает «украшать» код там, где это не нужно, или применяет неподходящие паттерны.

В инженерии ПО такие ошибки называют **антипаттернами рефакторинга**. Это не технические ошибки (код компилируется и даже работает), а стратегические просчёты, которые делают код сложнее, а не проще.

### Антипаттерн №1: Золотой молоток (Golden Hammer)

**Симптом:** Разработчик использует один и тот же паттерн или технологию для всех задач, даже когда он явно не подходит.

Название происходит из поговорки: **«Если у тебя есть молоток, всё вокруг кажется гвоздём»**. Когда разработчик выучил какой-то модный паттерн (например, Dependency Injection, фабрики или даже микросервисы), ему кажется, что это решение подходит для всех задач без исключения.

**Пример «Золотого молотка»:**

csharp

```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

```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

```csharp
public class OrderProcessor { ... }
public class EmailService { ... }
public class ReportGenerator { ... }
public class PaymentService { ... }
public class UserService { ... }
public class CacheService { ... }
```

Теперь каждый класс имеет свою чёткую зону ответственности. Их легко тестировать, легко изменять, легко переиспользовать.

Признак

Признак того, что вы создаёте «Божественный объект»: вы пишете класс, и в его названии есть слово **Manager**, **System** или **Application**.

### Антипаттерн №3: Преждевременная оптимизация

**Симптом:** Разработчик «улучшает» код, чтобы он работал быстрее, хотя никто не жаловался на скорость. Или усложняет код ради микроскопического выигрыша в производительности.

**Пример преждевременной оптимизации:**

csharp

```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 раз сложнее, чем был.
- Пока никто не жаловался на скорость загрузки продуктов.
- Возможно, реальное узкое место — не загрузка продуктов, а что-то другое (например, сеть или база данных).
- Сложный код сложно поддерживать и труднее находить баги.

**Как исправить:**

1. **Сначала измерьте производительность** (профилирование). Узнайте, где реальное узкое место.
2. **Оптимизируйте только то, что реально медленное.**
3. **Оптимизируйте простыми способами,** которые не усложняют код.
4. **Документируйте,** почему вы сделали такую оптимизацию.

Дональд Кнут

«Преждевременная оптимизация — корень всех зол».

Сначала напишите простой, понятный код. Если он работает достаточно быстро — отлично. Если нет — измерьте и оптимизируйте только проблемное место.

Признак

Признак преждевременной оптимизации: вы усложняете код, потому что «может быть, когда-нибудь это станет узким местом», а не потому, что вы измерили и доказали, что это проблема.

### Антипаттерн №4: Бойлерплейт (Boilerplate)

**Симптом:** Вы постоянно копируете один и тот же код из одного места в другое, вместо того чтобы создать общую абстракцию, что всегда приводит к ненужным последствиям.

В отличие от дублирования кода (которое мы обсуждали в §5.3 как Code Smell), бойлерплейт — это шаблонный код, который почти одинаков в разных местах, но с небольшими вариациями. Важно понимать, что это не значит, что вы не дублируете код.

**Пример бойлерплейта:**

csharp

```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

```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

```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);
}
```

**Почему это плохо:**

- Первая версия была простой и понятной. Вторая — сложная и требует понимания фабрик и стратегий.
- Никто не жаловался на первую версию.
- Если таких «рефакторингов» много, код становится переусложнённым.
- Другие разработчики тратят время на понимание «улучшений», которые ничего не улучшили.

**Как исправить:**

Задайте себе **три вопроса** перед любым рефакторингом:

1. **Есть ли реальная проблема?** (читаемость, сложность, баги, производительность)
2. **Улучшит ли это код для команды?** (не только для вас)
3. **Стоит ли игра свеч?** (окупится ли затраченное время)

Если хотя бы на один вопрос ответ «нет» — возможно, рефакторинг не нужен.

### Сравнительная таблица антипаттернов

| Антипаттерн | Симптом | Чем грозит | Как исправить |
| --- | --- | --- | --- |
| **Золотой молоток** | Один паттерн везде | Переусложнение кода | Использовать паттерны осознанно |
| **Божественный объект** | Огромный класс | Сложно тестировать, менять, переиспользовать | Разделить на маленькие классы |
| **Преждевременная оптимизация** | Сложный код ради скорости | Трата времени, сложность поддержки | Измерить → оптимизировать только узкое место |
| **Бойлерплейт** | Повторяющиеся шаблоны | Много кода, сложно менять | Вынести общую логику |
| **Рефакторинг ради рефакторинга** | Изменения без причины | Переусложнение, трата времени | Задать 3 вопроса перед началом |

## §5.8 Инструменты для рефакторинга и статического анализа в .NET

Мы уже знаем, что такое запахи кода и почему с ними нужно бороться. Но вот незадача: в реальном проекте может быть сотни классов и тысячи строк кода. Просматривать их все вручную в поисках длинных методов или дублирования — это всё равно что искать иголку в стоге сена. Это долго, утомительно, и всё равно вы что-то пропустите.

К счастью, эту рутину можно и нужно автоматизировать. Здесь на помощь приходят **инструменты статического анализа**.

**Статический анализ**

— это проверка исходного кода без его фактического выполнения. Инструмент анализирует текст программы, её структуру и синтаксис, выявляя потенциальные проблемы ещё до того, как код будет запущен.

Представьте себе автоматическую проверку орфографии в текстовом редакторе. Вы печатаете текст, и редактор сразу подчёркивает ошибки красным. Вы ещё не отправили письмо, но уже видите, где допустили опечатку. Статический анализ делает то же самое, но с программным кодом.

### Что может найти статический анализ?

Важно понимать

Статический анализ **не запускает ваш код**. Он просто читает его и ищет паттерны, которые в прошлом часто приводили к проблемам.

В экосистеме .NET существует несколько инструментов, каждый из которых решает свою задачу. Давайте рассмотрим основные.

### 1. Roslyn Analyzers (встроенные анализаторы Microsoft)

**Roslyn**

— это компиляторная платформа Microsoft, которая лежит в основе C#. Она предоставляет API для анализа кода прямо во время компиляции. **Roslyn Analyzers** — это набор правил, которые проверяют код на соответствие рекомендациям Microsoft.

**Что проверяют:**

- **Производительность** (например, использование `StringBuilder` вместо конкатенации строк в циклах).
- **Безопасность** (проверка на потенциальные уязвимости).
- **Корректность** (например, проверка, что методы не выбрасывают непредусмотренные исключения).
- **Использование новых возможностей языка**.

**Как выглядят в IDE:**

csharp

```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

```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

```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 Dashboard](/images/lectures/pitpm/05/image-01.svg)

Почему это круто

SonarQube показывает **динамику** — растёт ли качество со временем или падает. Это помогает команде принимать решения: «Нам нужно срочно заняться покрытием тестами, оно упало на 5%».

**Как подключить в .NET:**

bash

```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

```csharp
// Легаси-код: мы не знаем, правильно ли он работает, но он работает
public class DiscountCalculator
{
    public decimal CalculateDiscount(decimal amount, bool isPremium)
    {
        // Сложная логика, которая выросла за 5 лет
        // ... много кода, который никто уже не помнит
    }
}
```

Вы пишете страховочный тест:

csharp

```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

```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

```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

```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)

Когда вы работаете с легаси-кодом, возникает соблазн переписать всё сразу. «Давайте перепишем этот модуль с нуля, он же такой старый и страшный!»

Это почти всегда **плохая идея**. Переписывание с нуля:

1. **Дорого** — занимает месяцы, а то и годы, что крайне невыгодно для разработки.
2. **Рискованно** — новая версия может содержать те же баги, что и старая, плюс новые.
3. **Тормозит бизнес** — пока вы переписываете, новые функции не разрабатываются.
4. **Непредсказуемо** — вы не знаете всех нюансов старой логики, что может привести к непредвиденным последствиям.

Вместо этого используйте **стратегию «Стеклянная банка» (Glass Box strategy)**:

> Меняйте код по одному модулю за раз, как будто вы аккуратно перекладываете содержимое стеклянной банки в новую, не высыпая ни одной детали.

**Как это выглядит на практике:**

Стратегия Glass Box

**Преимущества этой стратегии:**

- **Безопасность** — вы всегда знаете, что остальная система работает.
- **Прогресс** — вы двигаетесь вперёд, а не застреваете на годы.
- **Быстрая обратная связь** — каждый модуль становится лучше, и это видно.
- **Возможность остановиться** — если приоритеты меняются, вы можете переключиться на другую задачу, не потеряв уже сделанное.

### Что делать, если тестов совсем нет?

Это самый сложный случай. Бывает, что код настолько старый и запутанный, что даже написать страховочный тест сложно — метод делает слишком много и зависит от внешних ресурсов.

В этом случае алгоритм немного меняется:

### Главные правила работы с легаси-кодом

Главные правила

1. **Не паникуйте.** Легаси-код есть у всех. Это нормально.
2. **Не пытайтесь переписать всё сразу.** Это путь к провалу.
3. **Начните с тестов.** Даже если они будут интеграционными и медленными — это лучше, чем ничего.
4. **Двигайтесь маленькими шагами.** Одно изменение → запуск тестов → следующее изменение.
5. **Изолируйте зависимости.** Внедряйте интерфейсы постепенно, один за другим.
6. **Фиксируйте прогресс.** Ведите список того, что уже сделано. Это мотивирует команду.
7. **Помните: вы не один.** Обсуждайте проблемы с командой, проводите код-ревью, учитесь друг у друга.

## §5.10 Как рефакторинг, регрессия и управление долгом формируют культуру качества

Мы прошли долгий путь. В этой лекции мы разобрали, что происходит с программным обеспечением после релиза, почему код имеет свойство «гнить» и как с этим бороться. Давайте соберём всё вместе и ответим на главный вопрос: **что делает команду по-настоящему качественной?**

Многие начинающие специалисты думают, что качество ПО — это когда в системе мало багов. Или, когда тесты проходят зелёным. Или, когда пользователи не жалуются. **Это не так.**

**Качество**

— это процесс, культура и дисциплина. Это то, как вы пишете код каждый день. Это то, как вы принимаете решения. Это то, как вы относитесь к своему инструменту и к своей команде.

Если вы просто исправляете баги, но не меняете подход к разработке — баги будут возвращаться. Если вы просто добавляете тесты, но не рефакторите код — тесты будут ломаться от каждого изменения. Если вы просто пишете код, но не думаете о его поддержке — через год никто не сможет в нём разобраться.

**Качество начинается с осознанного подхода.** С понимания того, что код пишется не для компьютера, а для людей. И что его будут читать, менять и поддерживать годы.

### Три ключевых элемента поддержки ПО

В этой лекции мы выделили три ключевых элемента, которые формируют основу поддержки программного обеспечения:

**1. Рефакторинг — улучшаем внутреннюю структуру.**

Код должен быть понятным, простым и гибким. Рефакторинг — это способ постоянно поддерживать его в таком состоянии. Это не «украшательство» и не трата времени. Это **инвестиция в будущее**. Чем чище код, тем легче его тестировать, изменять и развивать.

**2. Регрессионные тесты — проверяем, что ничего не сломали.**

Без тестов рефакторинг — это риск. С тестами — это контролируемый процесс. Регрессионные тесты — это **страховка**, которая позволяет менять код смело, зная, что если вы что-то сломаете, тесты скажут вам об этом.

**3. Управление техническим долгом — принимаем осознанные решения.**

Технический долг неизбежен. Дедлайны, нехватка знаний, меняющиеся требования — всё это приводит к тому, что мы пишем код неидеально. Но важно **брать долг осознанно**, понимая, как и когда вы будете его возвращать. И не позволять процентам расти.

### Роль QA-инженера

В первой лекции мы говорили, что QA — это не про «нажать кнопки и найти баг». Теперь, когда мы прошли все пять лекций, эта мысль становится ещё понятнее.

**Настоящий QA-инженер не просто находит дефекты. Он:**

1. **Понимает архитектуру.** Знает, как устроен код, и может оценить риски изменений.
2. **Владеет инструментами тестирования.** Пишет модульные, интеграционные и E2E-тесты, понимает, что и когда использовать.
3. **Анализирует качество кода.** Использует статический анализ, читает отчёты SonarQube, выявляет Code Smells.
4. **Участвует в процессе разработки.** Помогает команде принимать решения, которые делают код более тестопригодным и поддерживаемым.
5. **Думает о будущем.** Не просто проверяет текущую версию, а помогает строить систему, которую легко будет развивать через год и через пять лет.

### Заключительная мысль

Давайте завершим лекцию одной важной мыслью.

Цепочка качества

**«Код, который нельзя протестировать, нельзя и рефакторить.»**

Если у вас нет тестов, вы не знаете, что сломаете, и не можете безопасно менять код.

**«Код, который нельзя рефакторить, нельзя поддерживать.»**

Если код слишком сложный и запутанный, каждое изменение превращается в муку. Команда тратит всё время на борьбу с кодом, а не на решение бизнес-задач.

**«Код, который нельзя поддерживать, мёртв.»**

Если вы не можете менять код, он не может развиваться. А код, который не развивается, умирает — его заменяют другие системы, он теряет актуальность, он превращается в обузу.

**Но есть и обратная сторона:**

Обратная цепочка

**«Код, который можно тестировать — можно рефакторить.»**

**«Код, который можно рефакторить — можно поддерживать.»**

**«Код, который можно поддерживать — жив.»**

Мы не просто пишем код, который работает сегодня. Мы пишем код, который будет работать и через год, и через пять лет. Код, который можно развивать. Код, который не боится менять. **Код, который живёт.**
