Теория ограничений
Так это назвал коллега, когда я в очередной раз рассказывал, как надо строить системы. Название, конечно, уже занято — у Голдратта оно про узкие места на производстве, — но ничего лучше мы не придумали, а речь тут про ограничения в другом смысле: про NOT NULL, про лимит на размер загрузки, про то, что у заказа ровно один получатель.
Мысль простая.
Систему надо начинать с ограничений и дальше их снимать — по одному, когда появится причина. Не наоборот: не строить свободную систему, а потом ходить по ней с заборами.
Две судьбы одной колонки
Проще всего показать это на колонке в базе.
Колонка, у которой NOT NULL стоит с первого дня, живёт скучно. Кто-то однажды попробует записать пустое значение, получит ошибку и в этот момент — прямо в этот момент, пока он ещё помнит, что делает и зачем, — решит, чем её заполнять. Решение принимается один раз, человеком, у которого в голове есть контекст.
Колонка, у которой ограничения нет, к третьему году содержит несколько тысяч пустых значений. И они разные:
- часть — импорт из старой системы, где поля попросту не было;
- часть — записи из админки, где форма не требовала заполнения;
- часть — обычный пользовательский путь, и почему там пусто, уже никто не скажет.
Поставить NOT NULL теперь — это не миграция в одну строку. Это сначала разобрать три группы, для каждой понять, что пустота означала, и найти, чем её осмысленно заменить. Выясняется, что заменить нечем, и в базе появляется заглушка, которая переживёт всех.
Разница между двумя историями — не в аккуратности людей. Люди одни и те же. Разница в том, где стоит проверка: до записи или после.
Ослабить дёшево, ужесточить дорого
Если обобщить, получается асимметрия: снять ограничение почти ничего не стоит, а поставить — дорого. И дорого не в человеко-часах на реализацию, а в том, что перед этим нужно провести расследование, и заранее неизвестно, что оно найдёт.
Посмотрите на любое ослабление:
- поле принимало сто символов, стало принимать тысячу — ни одна существующая запись не сломалась;
- параметр был обязательным, у него появилось значение по умолчанию — ни один клиент не заметил;
- колонка была
NOT NULL, стала необязательной — всё, что работало вчера, работает сегодня.
Ни одно из них не ломает того, что уже записано. А каждое из этих изменений, сделанное наоборот, — это вопрос «а что делать с тем, что уже есть?». И у него нет технического ответа: он всегда упирается в чьи-то давние решения, которые никто не записал.
Пустота становится контрактом
Есть и вторая причина, менее очевидная и, кажется, более важная.
Код, написанный там, где ограничения нет, не просто его не соблюдает. Он начинает зависеть от того, что его нет.
- Если поле может быть пустым — рано или поздно кто-то на пустоте построит поведение: пустое значит «черновик», пустое значит «ещё не проверили», пустое значит «импортировано».
- Если в базе могут быть два пользователя с одной почтой — появится скрипт, который на это рассчитывает.
- Если у формы загрузки нет лимита — обязательно найдётся тот, кто будет заливать через неё что-нибудь на гигабайт, потому что однажды получилось и с тех пор так и делается.
Закон Хайрама я знал давно: при достаточном числе пользователей любое наблюдаемое поведение системы становится чьей-то зависимостью. Мне всегда казалось, что это про публичные API. Но это и про отсутствие проверки: его точно так же видно, и на него точно так же опираются.
Поэтому ограничение, добавленное поздно, — это не «добавили валидацию». Это изменение контракта, которого никто не писал, но на который все успели положиться.
«Но это же преждевременное усложнение»
Обычно в этом месте мне отвечают, что не надо усложнять раньше времени. YAGNI, сделаем просто, потом посмотрим.
Возражение хорошее, только оно про другое. Преждевременное усложнение — это когда пишут код под задачу, которой сегодня нет: абстракция под второго платёжного провайдера, локализация под вторую страну. Ограничение — прямо противоположная операция: это отказ писать код. Оно говорит «здесь бывает только один случай» и тем самым убирает ветки, а не добавляет.
Свободная система не проще ограниченной — у неё сложность просто не записана. Она размазана по всем местам, где кто-то потом будет разбираться, что делать с состоянием, которого не должно было быть. Ограничение эту сложность собирает в одну точку и даёт ей имя.
С чего начинать
Никакого предвидения для этого не требуется. Нужно честно ответить, что возможно прямо сейчас, — не что может понадобиться через год.
- Самый узкий тип, который описывает данные. Не строка, если это перечисление. Не перечисление, если это булево.
- Обязательность по умолчанию. Необязательность пусть требует объяснения, а не наоборот.
- Список разрешённого, а не список запрещённого. Второй неполон уже в тот момент, когда его пишут.
- Лимит на всё, у чего есть размер. Само число почти неважно, пусть будет щедрым — важно, что проверка есть. Добавить лимит потом — значит сломать тех, кто уже за него вышел.
- Приватное по умолчанию. Публичность — это обещание, а обещание нельзя отозвать.
Снятое ограничение — это событие
Вторая половина всего этого — про то, что ограничения надо снимать. Не терпеть, не обходить, а именно снимать, открыто, когда появилась причина.
У снятия есть свойство, которое я ценю едва ли не больше, чем сам эффект: это событие. У него есть дата, автор, коммит и, если повезло, внятное сообщение. Когда через два года кто-то спросит, почему в системе бывает пользователь без почты, ответ находится за минуту: вот миграция, вот тикет про вход через GitHub, где почта скрыта настройками профиля.
Отсутствие ограничения событием не является. Оно ничем не датировано и ничего не объясняет. На вопрос «почему тут можно NULL?» ответа не найдётся никогда — искать негде.
Ограничения оказываются способом хранить знание о предметной области в исполняемом виде, а каждое снятое ограничение отмечает момент, когда это знание изменилось. У системы, построенной от свободы, такого журнала нет вовсе.
Где правило ломается
Не хочу, чтобы это звучало как универсальный закон. У него есть как минимум три слабых места.
Ограничение должно выражать свойство предметной области, а не то, что пока не додумали. «У заказа ровно один получатель» — утверждение о бизнесе: оно либо верно, либо нет. «Мы ещё не решили, что делать с несколькими получателями» — это отложенное решение, и зашивать его в схему нечестно: выглядит как знание, а знанием не является. Отличить одно от другого просто — попробуйте назвать причину вслух.
Есть вещи, которые ослабить так же дорого, как ужесточить. Формат хранения, схема протокола, структура URL, идентификаторы, которые уже уехали в чужие системы. Тут асимметрия не работает, и выбирать надо не «поуже», а «с местом для роста» — версия в протоколе, поле для расширений, префикс в идентификаторе.
Жёсткость — не ограничение. Захардкоженное значение, которое должно было быть настройкой, снаружи выглядит похоже, но не даёт ничего из перечисленного: оно не выражает инвариант, его никто не проверяет, оно только мешает. Ограничение говорит «так нельзя». Жёсткость говорит «я не подумал». Разница огромная, но замечают её обычно поздно — когда уже упёрлись.
Итог
Всё это выводится из одного свойства систем: данные накапливаются, а прошлое не переписывается. Ограничение стоит дёшево ровно до тех пор, пока нет записей, которые его нарушают, — дальше цена растёт вместе с базой и обратно уже не падает.
Отсюда правило в две половины, и обе обязательны. Начинать с самого узкого, что описывает сегодняшнюю задачу. И снимать по требованию — открыто, с причиной, потому что ограничение, которое некому снять, со временем перестаёт быть ограничением и становится жёсткостью.
Оно не универсально: всё держится на том, что ослабить дешевле, чем ужесточить, и там, где это не так, правило не работает. Но решений, которые принимаются в первый месяц жизни проекта, это касается почти всех.
Свободу легко выдать. Забрать — почти никогда не получается.
Комментарии 0
Комментариев пока нет.