все посты

Теория ограничений

Так это назвал коллега, когда я в очередной раз рассказывал, как надо строить системы. Название, конечно, уже занято — у Голдратта оно про узкие места на производстве, — но ничего лучше мы не придумали, а речь тут про ограничения в другом смысле: про NOT NULL, про лимит на размер загрузки, про то, что у заказа ровно один получатель.

Мысль простая.

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

Две судьбы одной колонки

Проще всего показать это на колонке в базе.

Колонка, у которой NOT NULL стоит с первого дня, живёт скучно. Кто-то однажды попробует записать пустое значение, получит ошибку и в этот момент — прямо в этот момент, пока он ещё помнит, что делает и зачем, — решит, чем её заполнять. Решение принимается один раз, человеком, у которого в голове есть контекст.

Колонка, у которой ограничения нет, к третьему году содержит несколько тысяч пустых значений. И они разные:

  • часть — импорт из старой системы, где поля попросту не было;
  • часть — записи из админки, где форма не требовала заполнения;
  • часть — обычный пользовательский путь, и почему там пусто, уже никто не скажет.

Поставить NOT NULL теперь — это не миграция в одну строку. Это сначала разобрать три группы, для каждой понять, что пустота означала, и найти, чем её осмысленно заменить. Выясняется, что заменить нечем, и в базе появляется заглушка, которая переживёт всех.

Разница между двумя историями — не в аккуратности людей. Люди одни и те же. Разница в том, где стоит проверка: до записи или после.

Ослабить дёшево, ужесточить дорого

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

Посмотрите на любое ослабление:

  • поле принимало сто символов, стало принимать тысячу — ни одна существующая запись не сломалась;
  • параметр был обязательным, у него появилось значение по умолчанию — ни один клиент не заметил;
  • колонка была NOT NULL, стала необязательной — всё, что работало вчера, работает сегодня.

Ни одно из них не ломает того, что уже записано. А каждое из этих изменений, сделанное наоборот, — это вопрос «а что делать с тем, что уже есть?». И у него нет технического ответа: он всегда упирается в чьи-то давние решения, которые никто не записал.

Пустота становится контрактом

Есть и вторая причина, менее очевидная и, кажется, более важная.

Код, написанный там, где ограничения нет, не просто его не соблюдает. Он начинает зависеть от того, что его нет.

  • Если поле может быть пустым — рано или поздно кто-то на пустоте построит поведение: пустое значит «черновик», пустое значит «ещё не проверили», пустое значит «импортировано».
  • Если в базе могут быть два пользователя с одной почтой — появится скрипт, который на это рассчитывает.
  • Если у формы загрузки нет лимита — обязательно найдётся тот, кто будет заливать через неё что-нибудь на гигабайт, потому что однажды получилось и с тех пор так и делается.

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

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

«Но это же преждевременное усложнение»

Обычно в этом месте мне отвечают, что не надо усложнять раньше времени. YAGNI, сделаем просто, потом посмотрим.

Возражение хорошее, только оно про другое. Преждевременное усложнение — это когда пишут код под задачу, которой сегодня нет: абстракция под второго платёжного провайдера, локализация под вторую страну. Ограничение — прямо противоположная операция: это отказ писать код. Оно говорит «здесь бывает только один случай» и тем самым убирает ветки, а не добавляет.

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

С чего начинать

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

  • Самый узкий тип, который описывает данные. Не строка, если это перечисление. Не перечисление, если это булево.
  • Обязательность по умолчанию. Необязательность пусть требует объяснения, а не наоборот.
  • Список разрешённого, а не список запрещённого. Второй неполон уже в тот момент, когда его пишут.
  • Лимит на всё, у чего есть размер. Само число почти неважно, пусть будет щедрым — важно, что проверка есть. Добавить лимит потом — значит сломать тех, кто уже за него вышел.
  • Приватное по умолчанию. Публичность — это обещание, а обещание нельзя отозвать.

Снятое ограничение — это событие

Вторая половина всего этого — про то, что ограничения надо снимать. Не терпеть, не обходить, а именно снимать, открыто, когда появилась причина.

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

Отсутствие ограничения событием не является. Оно ничем не датировано и ничего не объясняет. На вопрос «почему тут можно NULL?» ответа не найдётся никогда — искать негде.

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

Где правило ломается

Не хочу, чтобы это звучало как универсальный закон. У него есть как минимум три слабых места.

Ограничение должно выражать свойство предметной области, а не то, что пока не додумали. «У заказа ровно один получатель» — утверждение о бизнесе: оно либо верно, либо нет. «Мы ещё не решили, что делать с несколькими получателями» — это отложенное решение, и зашивать его в схему нечестно: выглядит как знание, а знанием не является. Отличить одно от другого просто — попробуйте назвать причину вслух.

Есть вещи, которые ослабить так же дорого, как ужесточить. Формат хранения, схема протокола, структура URL, идентификаторы, которые уже уехали в чужие системы. Тут асимметрия не работает, и выбирать надо не «поуже», а «с местом для роста» — версия в протоколе, поле для расширений, префикс в идентификаторе.

Жёсткость — не ограничение. Захардкоженное значение, которое должно было быть настройкой, снаружи выглядит похоже, но не даёт ничего из перечисленного: оно не выражает инвариант, его никто не проверяет, оно только мешает. Ограничение говорит «так нельзя». Жёсткость говорит «я не подумал». Разница огромная, но замечают её обычно поздно — когда уже упёрлись.

Итог

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

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

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

Свободу легко выдать. Забрать — почти никогда не получается.

Комментарии 0

Комментариев пока нет.