Рефакторинг плохого кода 1С

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

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

Найти дефекты (прошлый урок) — половина дела. Дальше интервьюер часто говорит: «Хорошо, а теперь перепишите». И смотрит, как вы двигаетесь: по шагам, объясняя каждый, или сносите всё и лепите заново. Хороший рефакторинг — это серия маленьких понятных шагов, где после каждого код всё ещё работает. Именно так его и стоит показывать.

Вопрос-крючок

«Вот процедура заполнения цен в документе. Перепишите её так, как сделали бы в проде, и по ходу объясняйте, что и зачем меняете».

Было: код новичка

&НаКлиенте
Процедура ЗаполнитьЦены(Команда)

    Для Каждой Стр Из Объект.Товары Цикл
        Запрос = Новый Запрос;
        Запрос.Текст = "ВЫБРАТЬ Цена ИЗ РегистрСведений.Цены.СрезПоследних(,Номенклатура = &Н) КАК Ц";
        Запрос.УстановитьПараметр("Н", Стр.Номенклатура);
        Выборка = Запрос.Выполнить().Выбрать();
        Выборка.Следующий();
        Стр.Цена = Выборка.Цена;
    КонецЦикла;

КонецПроцедуры

Здесь всё сразу: запрос в цикле, работа с базой под &НаКлиенте, и вдобавок строка Стр.Цена = Выборка.Цена упадёт, если цены на номенклатуру нет, — Следующий() вернёт Ложь, а мы всё равно читаем поле. Идём по шагам.

Шаг 1. Выносим запрос из цикла

Главная беда — обращение к базе на каждой строке. Соберём номенклатуру в массив и получим все цены одним запросом. Условие В (&Массив) позволяет отфильтровать срез последних сразу по списку:

// Собираем номенклатуру из табличной части в массив
МассивНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");

Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |   Цены.Номенклатура КАК Номенклатура,
    |   Цены.Цена         КАК Цена
    |ИЗ
    |   РегистрСведений.Цены.СрезПоследних(&Дата, Номенклатура В (&Массив)) КАК Цены";
Запрос.УстановитьПараметр("Дата", Объект.Дата);
Запрос.УстановитьПараметр("Массив", МассивНоменклатуры);

Метод ВыгрузитьКолонку() одним вызовом достаёт все значения колонки в массив. Заодно мы заменили ТекущаяДата() на Объект.Дата — цены теперь берутся на дату документа. Один запрос вместо двухсот — это главный прирост.

Шаг 2. Раскладываем результат в соответствие

Чтобы в цикле по строкам быстро находить цену по номенклатуре, сложим результат запроса в Соответствие — структуру «ключ → значение» с мгновенным доступом:

ЦеныПоНоменклатуре = Новый Соответствие;

Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
    ЦеныПоНоменклатуре.Вставить(Выборка.Номенклатура, Выборка.Цена);
КонецЦикла;

Для Каждой Стр Из Объект.Товары Цикл
    Цена = ЦеныПоНоменклатуре.Получить(Стр.Номенклатура);
    Стр.Цена = ?(Цена = Неопределено, 0, Цена);
КонецЦикла;

Метод Получить() у соответствия вернёт Неопределено, если ключа нет, — и тернарный оператор ?(условие, тогда, иначе) аккуратно подставит ноль. Так мы заодно закрыли ошибку из исходника: строки без цены больше не роняют код, а получают ноль.

Шаг 3. Разделяем клиент и сервер

Вся работа с базой должна уехать на сервер. Обернём её в функцию &НаСервереБезКонтекста — она не тащит форму целиком, принимает массив и возвращает соответствие. Клиентская процедура только собирает данные и раскладывает результат:

&НаСервереБезКонтекста
Функция ПолучитьЦены(МассивНоменклатуры, ДатаДокумента)

    ЦеныПоНоменклатуре = Новый Соответствие;

    Запрос = Новый Запрос;
    Запрос.Текст =
        "ВЫБРАТЬ Цены.Номенклатура КАК Номенклатура, Цены.Цена КАК Цена
        |ИЗ РегистрСведений.Цены.СрезПоследних(&Дата, Номенклатура В (&Массив)) КАК Цены";
    Запрос.УстановитьПараметр("Дата", ДатаДокумента);
    Запрос.УстановитьПараметр("Массив", МассивНоменклатуры);

    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        ЦеныПоНоменклатуре.Вставить(Выборка.Номенклатура, Выборка.Цена);
    КонецЦикла;

    Возврат ЦеныПоНоменклатуре;

КонецФункции

&НаКлиенте
Процедура ЗаполнитьЦены(Команда)

    Массив = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
    Цены   = ПолучитьЦены(Массив, Объект.Дата);

    Для Каждой Стр Из Объект.Товары Цикл
        Цена = Цены.Получить(Стр.Номенклатура);
        Стр.Цена = ?(Цена = Неопределено, 0, Цена);
    КонецЦикла;

КонецПроцедуры

Теперь на сервер уходит один вызов вместо двухсот, и он несёт только массив ссылок, а не всю форму.

Шаг 4. Добавляем обработку ошибок

Запрос к базе может упасть (нет прав, блокировка). Оборачивать серверную работу в Попытка ... Исключение ... КонецПопытки стоит там, где сбой ожидаем, а его нужно показать пользователю понятно, а не «страшным» стеком. Важно: не «глотать» исключение молча — если ловим, то логируем и сообщаем:

&НаСервереБезКонтекста
Функция ПолучитьЦены(МассивНоменклатуры, ДатаДокумента)

    ЦеныПоНоменклатуре = Новый Соответствие;

    Попытка
        Запрос = Новый Запрос;
        Запрос.Текст =
            "ВЫБРАТЬ Цены.Номенклатура КАК Номенклатура, Цены.Цена КАК Цена
            |ИЗ РегистрСведений.Цены.СрезПоследних(&Дата, Номенклатура В (&Массив)) КАК Цены";
        Запрос.УстановитьПараметр("Дата", ДатаДокумента);
        Запрос.УстановитьПараметр("Массив", МассивНоменклатуры);

        Выборка = Запрос.Выполнить().Выбрать();
        Пока Выборка.Следующий() Цикл
            ЦеныПоНоменклатуре.Вставить(Выборка.Номенклатура, Выборка.Цена);
        КонецЦикла;
    Исключение
        ЗаписьЖурналаРегистрации("Заполнение цен", УровеньЖурналаРегистрации.Ошибка,
            , , ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
        ВызватьИсключение "Не удалось получить цены. Обратитесь к администратору.";
    КонецПопытки;

    Возврат ЦеныПоНоменклатуре;

КонецФункции

Здесь ИнформацияОбОшибке() отдаёт объект с деталями сбоя, а ПодробноеПредставлениеОшибки() разворачивает его в текст для журнала. Пользователь видит понятное сообщение, а разработчик — полный стек в журнале регистрации.

Стало: итоговый результат

Мы прошли путь из четырёх шагов, и после каждого код оставался рабочим. Итог: один запрос вместо цикла, чистое разделение клиент/сервер, безопасное чтение цены и внятная обработка ошибок. Тот же результат — но такой код не стыдно отдать в прод.

Как отвечать

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

Как это работает: почему шаги, а не «всё сразу»

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

Частые ошибки на собеседовании

  • Сносят всё и пишут заново. Теряется связь с исходником, легко потерять важную деталь. Рефакторьте шагами.
  • «Глотают» исключение. Пишут Попытка ... Исключение КонецПопытки с пустым блоком — ошибка исчезает молча, и данные тихо портятся. Если ловите — логируйте и сообщайте.
  • Оборачивают в Попытку вообще всё. Обработка ошибок не бесплатна и маскирует баги, если натыкана везде. Ставьте её там, где сбой ожидаем и его надо показать пользователю.
  • Забывают про дату документа. Выносят запрос из цикла, но оставляют ТекущаяДата() — производительность починили, корректность нет.
  • Перекладывают весь цикл на сервер вместе с формой. Используют &НаСервере вместо &НаСервереБезКонтекста и тащат на сервер всю форму без нужды.

Итоги-шпаргалка

  • Рефакторинг — улучшение кода без изменения результата; показывайте его маленькими шагами.
  • Порядок: 1) запрос из цикла → 2) результат в Соответствие3) клиент/сервер через &НаСервереБезКонтекста4) обработка ошибок.
  • ВыгрузитьКолонку() собирает ключи в массив; Соответствие даёт быстрый доступ «ключ → значение».
  • Получить() возвращает Неопределено для отсутствующего ключа — обрабатывайте это, чтобы код не падал.
  • Не «глотайте» исключения: логируйте через ЗаписьЖурналаРегистрации и показывайте пользователю понятный текст.
Проверьте себя
1. С какого шага логичнее всего начинать рефакторинг такого кода и почему?
AС обработки ошибок — она защищает от падений
BС переименования переменных для читаемости
CС выноса запроса из цикла — это даёт наибольший прирост производительности
DС добавления комментариев к каждой строке
2. Почему пустой блок «Попытка ... Исключение КонецПопытки» без логирования считают ошибкой?
AТакой синтаксис не компилируется в 1С
BИсключение исчезает молча — сбой не виден, а данные могут тихо испортиться
CПопытка замедляет выполнение в сотни раз
DВнутри Попытки нельзя выполнять запросы