Статьи

Построение твердых баз данных: странность подстановки Лискова

 Если принцип Open / Closed начинает выглядеть несколько странно применительно к объектно-реляционному проектированию, то принцип подстановки Лискова применяется в базе данных почти в точности так же, как и в приложениях. Причина становится понятной только после изучения этого принципа, определения его ограничений и изучения реалий разработки баз данных. Канонические примеры нарушений LSP в прикладных программах вообще не нарушают принцип в базе данных. Как всегда, причина в том, что базы данных моделируют разные вещи, а не приложения, и поэтому их ограничения разные.

Формальное определение принципа подстановки Лискова


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

Принцип подстановки Лискова в прикладном программировании

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

Как правило, для удовлетворения LSP предварительные ограничения не могут быть ослаблены, пост-ограничения не могут быть усилены, а инварианты должны поддерживаться не только в унаследованных методах, но и в не наследуемых (это достаточно широко, чтобы включить ограничение истории). Типичная проблема, к которой это относится, — это проблема прямоугольника, которая является хорошо понятой проблемой объектно-ориентированного программирования и которая ставит под вопрос определенные обещания ООП в целом.

Проблема прямоугольника и прямоугольника ставит проблему неприводимой сложности в объектно-ориентированном программировании. Типичные примеры: «Квадрат — это прямоугольник» или «Круг — это эллипс». Математически оба эти утверждения верны, но реализация их в объектно-ориентированной среде значительно усложняет задачу. Причина в том, что математическая иерархия не имеет понятия об изменении состояния, которое сохраняет инварианты, и поэтому иерархия объектов в конечном итоге усложняется из-за необходимости переходить к тому, чего в геометрической иерархии явно не хватает. Это приводит либо к взрыву абстрактных классов, взрыву метаданных объекта (можно ли растянуть этот прямоугольник — всегда «нет» для квадрата)? Можно ли его масштабировать?), Или потеря функциональности, которую мы обычно хотим получить от наследования.

На уровне базы данных эти проблемы в основном не существуют в реляционных проектах, но они существуют в объектных реляционных проектах, только по-другому (подробнее об этом ниже).

Принцип подстановки Лискова в прикладном программировании в основном работает, чтобы сохранить предположения об изменениях состояния объекта при вызове интерфейса. Если квадрат (определенный как прямоугольник, где измерения X и Y одинаковы) можно растянуть, он больше не является квадратом. С другой стороны, если мы не можем растянуть его, он может не заполнить контракт прямоугольника.

Когда квадрат — это подтип прямоугольника

Проблема прямоугольного прямоугольника может быть в некоторой степени перевернута с ног на голову вопросом о том, когда это отношение является допустимым. На самом деле отношение is-a является действительным для неизменяемых объектов. Вполне допустимо, чтобы класс ImmutableSquare был подклассом ImmutableRectangle.

Во-вторых, подтип может быть действительным, если к классу прикреплена достаточная информация для указания инвариантов. Например, у нас может быть класс с атрибутами «can_stretch» ​​и «can_scale», где установка can_stretch в выключенном состоянии (как это всегда будет в квадрате) гарантирует, что пропорции длины к ширине всегда сохраняются. Проблема здесь в том, что инварианты подкласса требуют поддержки в суперклассе, и это приводит к большой сложности в реализации суперкласса,
а также фундаментальные пределы того, что затем может быть разделено на подклассы.

Взгляд на ограничения базы данных

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

В системах баз данных, где в таблицах не создаются экземпляры типов и не допускается подстановка (Oracle, DB2), LSP применяется так же, как в прикладном программировании. В случаях наследования таблиц (Informix, PostgreSQL) все начинает становиться немного странным.

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

Проблема квадрата-прямоугольника в объектно-реляционных базах данных

Проблема Square-Rectangle — это даже потенциальная проблема в объектно-реляционных базах данных. Чисто реляционные проекты никогда не могут столкнуться с этой проблемой. В объектно-реляционных проектах может возникнуть несколько очень специфических проблем, полностью зависящих от того, как объектно-реляционная функциональность реализована на уровне базы данных. В базах данных, таких как Oracle и DB2, LSP применяется практически как есть. В Informix и PostgreSQL возникающие проблемы на самом деле несколько отличаются и приводят к появлению новых классов аномалий, которые необходимо учитывать. К ним относятся наиболее заметные обновления аномалий при обновлении родительских таблиц. Они не обязательно имеют простые обходные пути.

Рассмотрим следующую структуру таблицы:

CREATE TABLE my_rectangle (
    id serial primary key,
    height numeric,
    width numeric
 );

 CREATE TABLE my_square ( 
    check (height = width)
 ) INHERITS (my_rectangle);

Теперь мы можем вставить группу квадратов и прямоугольников, и у нас могут быть проверки. Более того, мы могли бы иметь собственные триггеры для проверки ссылочной целостности для строк, ссылающихся на my_rectangle и my_square.

Итак, предположим, что мы используем update для my_rectangle, чтобы удвоить высоту каждого прямоугольника в системе:

UPDATE my_rectangle SET height = height * 2;

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

  1. Запретить обновления, разрешать только вставки и удаления. Это затрудняет управление ссылочной целостностью, когда строка должна быть удалена и вставлена ​​заново.
  2. Используйте триггер, чтобы переписать обновление, чтобы оно было удалено и вставлено в текущую или родительскую таблицу в зависимости от ограничений. Это усложняет управление RI, так как процедура ДОЛЖНА отключить пользовательские триггеры RI перед этим. Это потребует, чтобы процедура знала обо всех пользовательских триггерах RI, чтобы сделать это.

Выводы

Принцип подстановки Лискова во многом зависит от специфики того, как система объектно-реляционных баз данных пересекает таблицы и объекты, как она применяется. В целом, в чисто реляционном дизайне вам никогда не придется беспокоиться об этом, но в объектно-реляционном дизайне могут возникнуть некоторые неприятные угловые случаи, особенно когда запрос может работать с гетерогенными подтипами в виде набора. Это, пожалуй, единственный из принципов SOLID, который специфичен для ИЛИ и затрагивает БД не так, как приложение, потому что БД работает по другим принципам.