ADR-0069: Явный результат функции, без скрытого вывода через мутацию входной структуры

Status: accepted Date: 2026-07-08 Deciders: Maxim Belkanov

Контекст

Опасный, трудно-читаемый паттерн: функция получает структуру на вход и пишет РЕЗУЛЬТАТ обратно в её поля (особенно reference-поля — map/slice/pointer, которые делят память даже при передаче по значению), вместо того чтобы вернуть результат явно. Тогда по сигнатуре не видно, что функция что-то «наполняет»; вызывающий не понимает, где сформировался результат, а мутация чужих данных приводит к неожиданным связям и гонкам.

Важно различать два случая, которые внешне похожи:

  • Локальный билдер — функция строит СВОЙ, только что созданный результат (result := FetchResult{}; fill(&result); return result). Это нормально.
  • Side-effecting команда — функция делает побочный эффект (запись в БД, публикация события) и возвращает СТАТУС (enum/ошибку), а не данные. Данные легитимно живут в побочном эффекте. Тоже нормально (пример: processOne пишет в БД и возвращает outcome; переданный fetch — read-only вход).

Анти-паттерн — это третий случай: мутировать переданную ВЫЗЫВАЮЩИМ структуру как скрытый выходной канал.

Решение

  1. Функция принимает вход, возвращает результат явно (значением/типизированным результатом/ошибкой). Результат виден в сигнатуре.
  2. Запрещено писать результат в поля структуры, переданной вызывающим, как скрытый выход — в т.ч. через reference-поля (map/slice/pointer). Входной аргумент трактуется как read-only, если контракт явно не говорит иначе.
  3. Разрешено: строить свой локальный результат мутацией (builder); возвращать статус-enum у команды с легитимным побочным эффектом; мутировать получателя, когда это ЯВНЫЙ контракт метода (func fill(dst *T) с очевидным именем и документированным назначением — но и тогда предпочтителен возврат).
  4. Проверка на ревью: если поле аргумента после вызова несёт результат, а по сигнатуре это неочевидно — переписать на явный возврат.

Последствия

  • Поток данных читается по сигнатурам; тесты не должны «доставать» результат из мутированного входа.
  • Текущее состояние ingestion-пайплайна соответствует: Pipeline.Normalize возвращает (NormalizedOffer, Observation, error); processOne — read-only по fetch + outcome; коннекторы (markKindError, markInterimIfOnlyPendingStock) заполняют СВОЙ локальный result (builder), а не чужой вход. Новый код обязан держать эту границу; helper’ы вида fill(dst *T) вводить только с явным именем/доком.

Связано

  • ADR-0066 (не кодировать условия строками), ADR-0067 (читаемость оркестраторов), ADR-0068 (бизнес-параметры явно, не через context) — общий принцип: контракт выражается сигнатурой и возвращаемым значением, а не спрятанным состоянием.