10 шокирующих способов гарантировать, что ваша пицца в коде никогда не будет недействительной
20 апреля 2026 г.Вступление
В мире программирования часто встречается ситуация, когда объект, созданный в коде, оказывается «недействительным» – то есть нарушает бизнес‑правила, которые должны соблюдаться в реальном мире. На первый взгляд это кажется мелочью, но в крупных проектах такие ошибки могут приводить к падениям сервисов, неверным расчётам и даже к юридическим проблемам. Одним из ярких примеров служит простая «пицца»: в реальном меню нельзя одновременно заказать кремовую основу и ананас, однако обычные структуры данных позволяют создать такой «невкусный» объект без каких‑либо предупреждений.
В Reddit‑сообществе, посвящённом Rust, пользователи обсуждали, как типовая система может стать надёжным «охранником» бизнес‑правил. Мы разберём их аргументы, сравним их с другими подходами и предложим практические решения, которые работают не только в Rust, но и в более популярных языках, таких как Python.
«Пустая чаша для супа, что не знаешь, как наполнить»
— японское хокку, отражающее проблему неопределённости в типах.
Пересказ Reddit‑поста своими словами
Автор оригинального поста, пользователь NoLemurs, привёл пример кода на Rust, где пицца описана простой структурой:
pub struct Pizza {
crust: Crust,
base: Base,
ham: bool,
olives: bool,
potatoes: bool,
pineapple: bool,
}
В такой модели любой набор булевых флагов может быть записан, даже если он противоречит правилам (например, кремовая основа + ананас). NoLemurs предложил более «строгую» модель, где типы явно разделяют кремовые и томатные начинки, а набор топпингов хранится в HashSet внутри перечисления Toppings. Это делает невозможным создание «нелепой» пиццы без дополнительных проверок.
Другие комментаторы, такие как godofpumpkins, подчеркнули важность sum types (суммирующих типов, то есть перечислений) и сравнили их с математическим сложением, в то время как умножение соответствует структурам с несколькими полями. По их мнению, отсутствие таких типов в языке делает код «тяжёлым» и склонным к ошибкам.
В ответ на гипотезу о том, что только статическая типизация может обеспечить такие гарантии, godofpumpkins отметил, что существует целая область исследований – типовые системы и их проверка на этапе компиляции – и что вопрос гораздо глубже, чем простое сравнение «статический vs динамический».
Наконец, moreVCAs задал вопрос‑тривиум: «это тавтология?», а Ignisami в шутливой форме добавил: «Тогда магазин говорит: «нет пиццы для вас»», подчеркивая, что бизнес‑правила могут быть вынесены в отдельный слой проверки.
Суть проблемы, хакерский подход и основные тенденции
- Суть проблемы: отсутствие строгих типовых ограничений позволяет создавать объекты, нарушающие бизнес‑правила.
- Хакерский подход: использовать «обманные» конструкции (например, скрытые флаги, проверка в рантайме) для обхода ограничений, что часто приводит к уязвимостям.
- Тенденции:
- Рост популярности языков с продвинутой типовой системой (Rust, Kotlin, TypeScript).
- Появление библиотек, имитирующих sum types в динамических языках (enum‑like в Python, sealed classes в Java).
- Увеличение интереса к dependent types и refinement types, позволяющих задавать условия прямо в типах.
Детальный разбор проблемы с разных сторон
1. Традиционный подход (структуры + проверки в рантайме)
Самый простой способ – хранить все свойства в одной структуре и проверять их в момент создания объекта. Преимущества: простота и гибкость. Недостатки: проверка происходит только во время выполнения, ошибки могут проявиться в продакшене.
2. Builder‑паттерн с валидацией
Создаём объект пошагово, а в конце вызываем build(), который проверяет согласованность полей. Это уже ближе к «компиляторной» проверке, но всё равно остаётся рантайм‑валидация.
3. Суммирующие типы (enum) и типовые ограничения
Как показал NoLemurs, разделение топпингов на отдельные перечисления и хранение их в enum делает невозможным создание противоречивого состояния. В Rust это достигается без накладных расходов, а в Python можно имитировать с помощью Enum и typing.Protocol.
4. Зависимые типы и рефайнмент‑типы
В языках вроде Idris или F* можно задать тип Pizza[valid], где valid – предикат, проверяемый компилятором. На практике такие возможности пока недоступны в популярных языках, но появляются библиотеки‑надстройки (например, pydantic с валидаторами).
5. Декларативные схемы (JSON‑Schema, OpenAPI)
Для сервисов, работающих с JSON, часто используют схемы, которые проверяют данные на этапе десериализации. Это хороший компромисс, если бизнес‑логика хранится в микросервисах.
Практические примеры и кейсы
Пример на Rust (коротко)
pub enum Crust { Thin, Thick }
pub enum CreamTopping { Potatoes, Ham, Olives }
pub enum TomatoTopping { Pineapple, Ham, Olives }
pub enum Toppings {
Cream(HashSet<CreamTopping>),
Tomato(HashSet<TomatoTopping>),
}
pub struct Pizza {
crust: Crust,
toppings: Toppings,
}
Здесь тип Toppings гарантирует, что в наборе будет только один «вид» топпингов, а значит, «крем + ананас» невозможно.
Пример на Python (полный, 78 строк)
# -*- coding: utf-8 -*-
"""
Модуль, демонстрирующий типобезопасную модель пиццы.
Используем Enum и наборы (set) для имитации sum types.
"""
from __future__ import annotations
from enum import Enum, auto
from typing import Set, Union
class Crust(Enum):
"""Варианты теста."""
THIN = auto()
THICK = auto()
class CreamTopping(Enum):
"""Топпинги, совместимые с кремовой основой."""
POTATOES = auto()
HAM = auto()
OLIVES = auto()
class TomatoTopping(Enum):
"""Топпинги, совместимые с томатной основой."""
PINEAPPLE = auto()
HAM = auto()
OLIVES = auto()
class Toppings:
"""
Объект, хранящий набор топпингов.
Позволяет хранить только один тип топпингов (кремовый или томатный).
"""
def __init__(self, items: Set[Enum]) -> None:
if not items:
raise ValueError("Набор топпингов не может быть пустым")
# Определяем тип первого элемента
first = next(iter(items))
# Проверяем, что все элементы одного типа
if not all(isinstance(i, type(first)) for i in items):
raise TypeError("Все топпинги должны быть одного типа")
self.items: Set[Enum] = items
self.kind: type = type(first) # запоминаем, какой тип топпингов
def __repr__(self) -> str:
names = ', '.join(i.name for i in self.items)
return f""
class Pizza:
"""
Пицца, состоящая из теста и топпингов.
Конструктор проверяет согласованность: кремовая основа ↔ кремовые топпинги,
томатная основа ↔ томатные топпинги.
"""
def __init__(self, crust: Crust, toppings: Toppings) -> None:
self.crust = crust
self.toppings = toppings
self._validate()
def _validate(self) -> None:
"""Внутренняя проверка бизнес‑правил."""
# Если топпинги кремовые, то основа должна быть «кремовая».
# В нашем упрощённом примере считаем, что Thin = крем, Thick = томат.
if self.toppings.kind is CreamTopping and self.crust is Crust.THICK:
raise ValueError("Кремовые топпинги несовместимы с толстым тестом")
if self.toppings.kind is TomatoTopping and self.crust is Crust.THIN:
raise ValueError("Томатные топпинги несовместимы с тонким тестом")
def __repr__(self) -> str:
return f""
# ----------------------------------------------------------------------
# Примеры использования
# ----------------------------------------------------------------------
def demo() -> None:
"""Запуск демонстрации корректных и некорректных вариантов."""
# Корректная пицца: тонкое тесто + кремовые топпинги
cream_set = {CreamTopping.HAM, CreamTopping.OLIVES}
pizza1 = Pizza(Crust.THIN, Toppings(cream_set))
print(pizza1)
# Корректная пицца: толстое тесто + томатные топпинги
tomato_set = {TomatoTopping.PINEAPPLE, TomatoTopping.HAM}
pizza2 = Pizza(Crust.THICK, Toppings(tomato_set))
print(pizza2)
# Попытка создать нелогичную пиццу (исключение)
try:
bad_set = {CreamTopping.POTATOES, TomatoTopping.PINEAPPLE}
Pizza(Crust.THIN, Toppings(bad_set))
except Exception as e:
print(f"Ошибка при создании пиццы: {e}")
# Попытка смешать типы топпингов (исключение)
try:
mixed_set = {CreamTopping.HAM, TomatoTopping.HAM}
Pizza(Crust.THICK, Toppings(mixed_set))
except Exception as e:
print(f"Ошибка при создании пиццы: {e}")
if __name__ == "__main__":
demo()
В этом примере:
- Классы
Enumзаменяют суммирующие типы Rust. - Класс
Toppingsпроверяет, что все элементы одного типа, тем самым имитируяenum‑вариант. - Метод
_validateвPizzaреализует бизнес‑правило «крем ↔ тонкое тесто, томат ↔ толстое тесто», но уже на этапе инициализации объекта. - Демонстрация в функции
demo()показывает как корректные, так и ошибочные варианты, выводя сообщения об ошибках.
Экспертные мнения из комментариев
«Как только вы попробуете sum types (так называемые «enum с чем‑то») любой язык без них кажется громоздким. Действительно, они называются sum types, потому что они являются прямым математическим аналогом элементарного сложения, где структуры, содержащие несколько полей, эквивалентны умножению. Представьте, что пытаетесь делать арифметику без сложения и пытаетесь её аппроксимировать умножением».
— godofpumpkins
«Моя гипотеза состоит в том, что это немного больше, чем ваша гипотеза. Есть целая область исследований, посвящённая этой теме».
— godofpumpkins
«Тогда магазин говорит: «нет пиццы для вас»».
— Ignisami (шутка, но в ней скрыт смысл: бизнес‑правила могут быть вынесены в отдельный слой проверки.)
Возможные решения и рекомендации
- Выбирайте язык с продвинутой типовой системой. Если проект позволяет, Rust, Kotlin или TypeScript помогут задать ограничения на этапе компиляции.
- Имплементируйте sum types в динамических языках. В Python используйте
Enum+ кастомные классы‑обёртки, как в примере выше. - Разделяйте бизнес‑правила и модель данных. Храните правила в отдельном слое (валидация, сервис‑слой) и проверяйте их при каждом изменении состояния.
- Применяйте builder‑паттерн с проверкой. Он удобен, когда объект имеет много опциональных полей, но всё равно требует рантайм‑валидацию.
- Исследуйте библиотеки с рефайнмент‑типа́ми. Для Python это
pydantic(модели + валидация) иtypeguard(проверка типов в рантайме). - Автоматизируйте тесты. Пишите юнит‑тесты, покрывающие все комбинации полей, чтобы гарантировать отсутствие «нелепых» объектов.
Заключение и прогноз развития
Проблема создания недействительных объектов в коде – это не просто академический вопрос, а реальная угроза надёжности систем. Тенденция к использованию более выразительных типовых систем уже меняет подход к проектированию: в ближайшие годы мы увидим рост популярности dependent types в основных языках, а также появление новых библиотек, позволяющих задавать бизнес‑правила прямо в типах.
Для большинства проектов, где переход на Rust пока невозможен, достаточно внедрить «имитацию» суммирующих типов и строгую валидацию на уровне моделей. Это даст почти те же гарантии, но без необходимости переписывать код на новый язык.
Итоговый совет: начинайте с простых проверок, постепенно переходите к более строгим типовым ограничениям, а в долгосрочной перспективе планируйте миграцию на язык с нативной поддержкой sum types. Тогда ваш код будет «как хорошая пицца» – всегда вкусным и без лишних «ананасов» на кремовой основе.
Оригинал