Рефакторинг в реальной жизни: в 3 раза меньше кода

Бэкэнды на Node.js

(сокращенная версия, полную версию на английском смотрите на https://dev.to/valentineshi-dev/real-life-refactoring-example-3x-less-code-to-read-3mdl)

Рефакторинг часто сводят к сокращению кода. Но главная проблема обычно не в количестве строк, а в неясном намерении главного публичного метода.

Так было в моем S3-адаптере для загрузки документов. Он передавал файл в S3, считал байты, вычислял контрольную сумму, обрабатывал ошибки и писал логи.

Всё это находилось прямо в методе `store()`. Метод стримит документ в S3 астинхронно внутри `try/catch`, создает объекты ошибок, удаляет невалидный объект, создает payload для логирования и возвращает желаемый результат или выбрасывает exception.

В исходном ыиде этот метод представлял из себя implementation details, вместо того, чтобы описывать историю выполнение в терминах интеграции, в декларативном, а не императивном виде. Метод четко подходил под Long Method code smell.

Первый шаг — обычный Extract Method. Более 40 строк обработки и стриминга документа в S3 стали одной:

```ts const response = await this.uploadToStorage(documentUUID, documentStorageKey, file, countedStream, startedAt); ```

Аналогичный рефакторинг я сдела для проверка ответа S3 и обработка пустого документа:

```ts this.throwIfS3Error(storedObjectKey, response, httpStatusCode, startedAt); await this.throwIfUploadedFileIsEmpty(storedObjectKey, file, countedStream, startedAt); ```

Позже сервису загрузки понадобилось удалять уже сохранённый объект при дубликате или неудачном сохранении в БД.

На тот момент уже существовал приватный метод `cleanupStoredObject(documentUUID, documentStorageKey, startedAt)` уже был. Сделать его публичным было бы ошибкой. `startedAt` нужен только для лога. Сам метод обрабатывал ошибку удаления документа на S3, что было актуально для использования внутри адаптера, но не для доменного сервиса загрузки документа, которые использовал адаптер S3.

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

```ts public async remove(storedObjectKey: string): Promise<void> ```

Он управляет S3-командой, отражает временя операции, нормализует ошибки и и логирует в structured JSON logging через PinoLoggingAdapter. Caller решает, что делать с ошибкой удаления. Адаптер обрабатывает ошибку пустого файла. Caller принимает доменное решение о дубликате. S3-клиент остаётся изолированным внутри адаптера.

Последняя проблема — логирование. Пять хеплер-меторов создавали почти одинаковый лог-объект: успех загрузки, успех удаления, ошибка S3, ошибка валидации, ошибка удаления. Разные события полезны. Пять реализаций создания на 70% похожего объекта — нет.

Их заменил `logStorageEvent(options)`: Parameterize Method + Introduce Parameter Object. Условные object spread выражения вынесены в ясные значения: `actualByteSize`, `httpStatusCode`, `providerRequestId`, `storageError`.

Результат: `store()` ясно отображает бизнес-сценарий загрузки документа в нескольких строках, S3-адаптер энкапсулирует S3, сервис загрузки документа — занимается бизнес-логикой, и логи строятся единообразно.

Около 90 строк implementation details стали несколькими вызовами с ясными именами на доменном и интеграционном языке (ubiquitous language). `store()` сократился со 121 строки до 39: на 68% или почти в 3 раза.

Заметьте: изначальная реализация была написана AI по строгим гайдлайнам и правилам.