Как стать автором
Обновить

Что не так с валидацией данных и при чем тут принцип подстановки Лисков?

Время на прочтение 3 мин
Количество просмотров 14K


Если вы иногда задаете себе вопрос: «а всё ли хорошо мне в этот метод приходит?» и выбираете между «а вдруг пронесет» и «лучше на всякий случай проверить», то добро пожаловать под кат…

Поправка: Как заметили lorc и 0xd34df00d, то, о чем ниже идет речь, называется зависимыми типами. Почитать о них можно тут. Ну а ниже исходный текст с моими соображениями по этому поводу.

При разработке часто возникает потребность проверки валидности данных для некоторого алгоритма. Формально это можно описать следующим образом: пусть мы получаем некоторую структуру данных, проверяем ее значение на соответствие некоторой области допустимых значений (ОДЗ) и передаем ее дальше. Впоследствии эта же структура данных может быть подвергнута такой же проверке. В случае неизменяемости структуры, повторная проверка ее валидности – очевидно лишнее действие.

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

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

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

class Parent { ... }
class Child : Parent { ... }

...

void processValidObject(Parent parent) {
    if (parent is Child) {
        // process
    } else {
        // error
    }
}

Согласитесь, что теперь проблема гораздо яснее. Перед нами каноничное нарушение принципа подстановки Лисков. Почитать почему нарушать принцип подстановки плохо можно, например, тут.

Решить проблему передачи невалидных объектов можно с помощью создания подтипа для исходной структуры данных. Например, можно создавать объекты через фабрику, которая по исходной структуре возвращает либо валидный объект подтипа, либо null. Если мы изменим сигнатуру методов, ожидающих валидную структуру так, что они станут принимать только подтип, то проблема исчезнет. Так же помимо уверенности в том, что система точно работает, уменьшится количество валидаций на квадратный сантиметр кода. Еще одним плюсом является то, что такими действиями мы перекладываем ответственность валидации данных с разработчика на компилятор.

В Swift'е, на уровне синтаксиса, решается проблема проверки на null. Идея состоит в том, чтобы разделить типы на допускающие значение null и не допускающие. При этом сделано это в виде сахара таким образом, что программисту не требуется объявлять новый тип. При объявлении типа переменной ClassName гарантируется, что в переменной ненулевое значение, а при объявлении ClassName? переменная допускает значение null. При этом между типами существует коваринтность, то есть в методы, принимающие ClassName?, можно передать и объект типа ClassName.

Эту идею можно расширить до задаваемых пользователем ОДЗ. Снабжение объектов метаданными, содержащими ОДЗ, хранящимися в типе, устранит описанные выше проблемы. Хорошо бы получить поддержку такого средства в языке, но такое поведение реализуемо и в «обычных» ОО-языках, таких как Java или C# с помощью наследования и фабрики.

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

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

Так же в статье не хватает примера. Пусть на вход к нам поступают некоторые пути файлов. Наша система в некоторых случаях работает со всеми файлами, а в некоторых случаях только с файлами, к которым мы имеем доступ. Далее мы хотим передать их в разные подсистемы, которые так же работают как с доступными, так и с недоступными файлами. Далее эти подсистемы передают файлы еще дальше, где опять не понятно файл доступен или нет. Таким образом во всяком сомнительном месте появится проверка доступа или может напротив забудется. Из-за этого система усложнится в силу повсеместной неоднозначности и проверок. А проверки эти грузят диск и вообще тяжелые. Можно эту проверку кешировать в булевом поле, но это нас не избавит от самого факта необходимости проверки. Я предлагаю ответственность проверки переложить с разработчика на компилятор.
Теги:
Хабы:
+17
Комментарии 47
Комментарии Комментарии 47

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн