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 вход).
Анти-паттерн — это третий случай: мутировать переданную ВЫЗЫВАЮЩИМ структуру как скрытый выходной канал.
Решение
- Функция принимает вход, возвращает результат явно (значением/типизированным результатом/ошибкой). Результат виден в сигнатуре.
- Запрещено писать результат в поля структуры, переданной вызывающим, как
скрытый выход — в т.ч. через reference-поля (
map/slice/pointer). Входной аргумент трактуется как read-only, если контракт явно не говорит иначе. - Разрешено: строить свой локальный результат мутацией (builder); возвращать
статус-enum у команды с легитимным побочным эффектом; мутировать получателя,
когда это ЯВНЫЙ контракт метода (
func fill(dst *T)с очевидным именем и документированным назначением — но и тогда предпочтителен возврат). - Проверка на ревью: если поле аргумента после вызова несёт результат, а по сигнатуре это неочевидно — переписать на явный возврат.
Последствия
- Поток данных читается по сигнатурам; тесты не должны «доставать» результат из мутированного входа.
- Текущее состояние 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) — общий принцип: контракт выражается сигнатурой и возвращаемым значением, а не спрятанным состоянием.