Вы должны подтвердить еще раз? Более разумный способ обработки данных данных

Вы должны подтвердить еще раз? Более разумный способ обработки данных данных

31 июля 2025 г.

Введение: когда вы не знаете, следует ли вам подтвердить

В повседневной разработке программного обеспечения многие инженеры задают тот же вопрос: «Нужно ли снова проверить эти данные, или я могу предположить, что они уже действительны?»

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

В этой статье я хочу изучить эту общую проблему с точки зрения дизайна и показать, как она связана с более глубокой архитектурной проблемой: нарушение принципа замещения Лискова (LSP), одного из основных принципов объектно-ориентированного дизайна.

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

Проблема с проверкой данных

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

Хотя валидация может повлиять на производительность, более значительной проблемой является неоднозначность: кто отвечает за обеспечение того, чтобы данные были действительны в любой момент в программе?

Эта неопределенность создает трение в обоих направлениях:

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

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

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

Нарушение Лискова

Эта двусмысленность в отношении ответственности за проверку - это больше, чем просто проблема гигиены кода - это недостаток дизайна. Точнее, это часто приводит к нарушению принципа замены Лискова (LSP), одной из основополагающих идей в объектно-ориентированном программировании.

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

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

class Parent { ... }
class Child : Parent { ... }
...
// somewhere
void processValidObject(Parent parent) {
    if (parent is Child) {
        // process
    } else {
        // error
    }
}

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

С точки зрения проверки, такая же ошибка часто совершается неявно: метод заявляет, что он принимает общую структуру (например, пользователь, InputData, запрос), но молча предполагает, что эта структура уже прошла некоторый процесс проверки - без применения ее соблюдения и не выражая ее в системе типа.

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

Лучший способ: договоренности контрактов и подтипов

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

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

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

Это создает договор достоверности:

  • VALITEDINPUT Гарантирует инварианты, такие как неверные поля, правильные форматы и логическая последовательность.
  • Любая функция, которая принимает ValidatedInput, может доверять, что эти предварительные условия уже выполнены.
  • Если данные не являются действительными, абонент не может построить объект - компилятор предотвратит неправильное использование.

Этот подход меняет ответственность:

  • Из «Каждый метод должен подтвердить»
  • На «только конструктор (или завод) может проверить - и как только это сделает, значение безопасно навсегда».

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

Таким образом, мы устраняем как избыточную проверку, так и небезопасные предположения. Мы больше не спрашиваем: «Должен ли я подтвердить это снова?» - Потому что типовая система обеспечивает соблюдение ответа.

Примеры реального мира: файлы, доступ и опасность предположений

Чтобы проиллюстрировать, как работает этот принцип на практике, давайте рассмотрим общий пример: доступ к файлу.

Представьте себе функцию, которая принимает объект файла и должен читать из нее. Достаточно просто - но что на самом деле представляет объект файла? Это:

  • Путь к диску, независимо от того, существует ли он?
  • Файл, который был проверен на существование?
  • Файл, который гарантированно будет читаемым?

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

fun processFile(file: File) {
    if (file.exists() && file.canRead()) {
        // read file
    } else {
        // log or throw
    }
}

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

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

fun tryMakeReadable(file: File): ReadableFile?

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

Этот подход естественным образом масштабируется в более сложных системах:

  • Пользователь, который уже аутентифицирован.
  • Документ, который прошел проверку схемы.
  • Запрос, который был ограничен ставкой и продезинфицирован.

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

Уроки инженерии и выводы

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

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

  • Функциям больше не нужно выполнять защитные проверки.
  • Неверные данные отклоняются на границе, а не передаются внутри.
  • Компилятор обеспечивает правильность построением.

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

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

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

Если вам понравилась эта статья, вам может понравиться моя книгаSafe By Design: Исследования в области архитектуры и выразительности программного обеспеченияПолем Он углубляется в такие темы, как этот - контракты, безопасность типа, архитектурная ясность и философия, стоящую за лучшим кодом.

👉 Проверьте это наGitHub


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE