Безопасное объектно-ориентированное программирование

Появившись в первый раз в языке Simula в 1960-х, объектно-ориентированное программирование (ООП) распространилось затем в исследовательских и академических кругах с такими языками как Smalltalk в конце 1980-х и заняло существующее прочное положение с появлением C++ и Java в начале 1990-х. Выдающимся свойством ООП считается его гибкость. Но гибкость в чем-то похожа на свободу, которую мы обсуждали во введении — злоупотребляя ею, можно допустить появление опасных ошибок.

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

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

Язык Ада достигает поразительного баланса. Мы с уверенностью можем назвать его методологически нейтральным в сравнении с языком Java, например, который является «чистым» ООП. В самом деле, идеи ООП проникли в язык Ада еще при его создании, в 1980-х, проявившись в концепции пакетов и приватных типов, как механизма скрытия деталей реализации и задач, как механизма абстракции активных сущностей. Стандарт Ада 95 включил главные свойства ООП, такие как наследование, полиморфизм, динамическое связывание и понятие «класса» как набора типов, связанных отношением наследования.

ООП вместо структурного программирования

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

Сначала рассмотрим геометрические объекты. Для простоты остановимся на объектах на плоскости. У каждого объекта есть позиция. Мы можем определить базовый объект со свойствами, общими для всех видов объектов:

type Object is tagged record
   X_Coord : Float;
   Y_Coord : Float;
end record;

Здесь ключевое слово tagged (тегированный, то есть каждому типу соответствует уникальный тег) отличает этот тип от обычных записей (таких как Date из главы 3) и означает, что тип можно в будущем расширить. Более того, объекты такого типа хранят в себе специальное поле-тег, и этот тег определяет тип объекта во время исполнения. Объявив типы для специфических геометрических объектов, таких как окружность, треугольник, квадрат и прочие, мы получим различные значения тегов для каждого из типов. Компоненты X_Coord, Y_Coord задают центр объекта.

Мы можем объявить различные свойства объекта, такие как площадь и момент инерции. Каждый объект имеет эти свойства, но они зависят от формы объекта. Такие свойства можно определить при помощи функций и объявить их в том же пакете, что и тип. Таким образом, мы начнем с пакета:

package Geometry is
   type Object is abstract tagged record
      X_Coord, Y_Coord : Float;
   end record;

   function Area(Obj: Object) return Float is abstract;
   function Moment(Obj: Object) return Float is abstract;
end Geometry;

Здесь мы объявили тип и его операции, как абстрактные. На самом деле нам не нужны объекты типа Object. Объявив тип абстрактным, мы предотвратим создания таких объектов по ошибке. Нам нужны реальные объекты, такие как окружности, у которых есть свойства, такие как площадь. Если нам потребуется объект точка, мы объявим отдельный тип Point для этого. Объявив функции Area и Moment абстрактными, мы гарантируем, что каждый конкретный тип, такой как окружность, предоставит свой код для вычисления этих свойств.

Теперь мы готовы определить тип окружность. Лучше всего сделать это в дочернем пакете:

package Geometry.Circles is
   type Circle is new Object with record
      Radius : Float;
   end record;

   function Area(C: Circle) return Float;
   function Moment(C: Circle) return Float;
end Geometry.Circles;

with Ada.Numerics; use Ada.Numerics;  -- для доступа к π

package body Geometry.Circles is

   function Area(C: Circle) return Float is
   begin
      return π * C.Radius ** 2;
   end Area;

   function Moment(C: Circle) return Float is
   begin
      return 0.5 * C.Area * C.Radius ** 2;
   end Moment;
end Geometry.Circles;

В этом примере мы порождаем тип Circle от Object. Написав new Object, мы неявно наследуем все видимые операции типа Object пакета Geometry, если мы не переопределим их. Поскольку наши операции абстрактны, а сам тип Circle — нет, мы обязаны переопределить их явно. Определение типа расширяет тип Object новым компонентом Radius, добавляя его к определенным в Object компонентам (X_Coord и Y_Coord).

Заметим, что код вычисления площади и момента располагается в теле пакета. Как было объяснено в главе «Безопасная архитектура», это значит, что код можно изменить и перекомпилировать без необходимости перекомпиляции описания самого типа и всех модулей, использующих его.

Затем мы могли бы объявить тип для квадрата Square (с дополнительным компонентом длинны стороны квадрата), треугольника Triangle (три компоненты соответствующие его сторонам) и так далее. При этом код для Object и Circle не нарушается.

Множество всевозможных типов из иерархии наследования от Object обозначаются как Object'Class и называются в терминологии Ады классом. Язык тщательно различает отдельные типы, такие как Circle от классов типа, таких как Object'Class. Это различие помогает избежать путаницы, которая может возникнуть в других языках. Если мы, в свою очередь, унаследуем тип от Circle, то будем иметь возможность говорить о классе Circle'Class.

В теле функции Moment продемонстрировано использование точечной нотации. Мы можем воспользоваться одной из следующих форм записи:

  • C.Area – точечная нотация

  • Area (C) – функциональная нотация

Точечная нотация появилась в стандарте Ада 2005 и смещает акцент в сторону ООП, указывая на то, что объект C доминирует над функцией Area. Теперь объявим несколько объектов, например:

A_Circle: Circle := (1.0, 2.0, Radius => 4.5);
My_Square: Square := (0.0, 0.0, Side => 3.7);
The_Triangle: Triangle := (1.0, 0.5, A=>3.0, B=>4.0, C=>5.0);

Процедура для печати свойств объекта может выглядеть так:

procedure Print(Obj: Object'Class) is  -- Obj is polymorfic
begin
   Put("Area is "); Put(Obj.Area);  -- dispatcing call of Area
   ...
end Print;

Print (A_Circle);
Print (My_Square);

Формальный параметр Obj — полиморфный, т. е. он может ссылаться на объекты различного типа (но только из иерархии растущей из Object) в различные моменты времени.

Процедура Print может принимать любой объект из класса Object'Class. Внутри процедуры вызов Area динамически диспетчеризируется, чтобы вызвать функцию Area, соответствующую конкретному типу параметра Obj. Это всегда безопасно, поскольку правила языка таковы, что любой объект в классе Object'Class будет иметь функцию Area. В тоже время сам тип Object объявлен абстрактным, следовательно нет способа создать объекты этого типа, поэтому не важно, что для этого типа нет функции Area, ведь ее вызов невозможен.

Аналогично мы могли бы объявить типы для людей. Например:

package People is

   type Person is abstract record
      Birthday: Date;
      Height: Inches;
      Weight: Pounds;
   end record;

   type Man is new Person with record
      Bearded: Boolean;  -- Имеет бороду
   end record;

   type Woman is new Person with record
      Births: Integer;  -- Кол-во рожденных детей
   end record;

   ...  -- Различные операции
end People;

Поскольку все альтернативы заранее известны и новых типов не будет добавляться, мы могли бы использовать здесь запись с вариантами. Это будет в духе структурного программирования.

type Gender is (Male, Female);

type Person (Sex: Gender) is record
   Birthday: Date;
   Height: Inches;
   Weight: Pounds;
   case Sex is
      when Male =>
         Bearded: Boolean;
      when Female =>
         Births: Integer;
   end case;
end record;

Затем мы объявим различные необходимые операции для типа Person. Каждая операция может иметь в теле инструкцию case чтобы принять во внимание пол человека.

Это может выглядеть довольно старомодно и не так элегантно, тем не менее этот подход имеет свои значительные преимущества.

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

В случае со структурным программированием все выглядит с точностью до наоборот.

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

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

Ада поддерживает оба подхода и они оба безопасны в Аде.

Индикатор overriding

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

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

function Aera(C: Circle) return Float;

либо в типе аргумента или результата:

function Area(C: Circle) return Integer;

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

(Хотя Ада предлагает механизм абстрактных операций, как например Area у Object, это лишь дополнительная мера предосторожности. И этого недостаточно, если мы порождаем тип от не абстрактного типа, либо мы не слишком осторожны, чтобы объявить функцию Area абстрактной.)

Чтобы предотвратить подобные ошибки, мы можем воспользоваться синтаксической конструкцией, появившейся в стандарте Ада 2005:

overriding function Area(C: Circle) return Float;

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

not overriding function Aera(C: Circle) return Float;

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

Возможно, синтаксис not overriding индикатора неоправдано тяжеловесен, учитывая необходимость частого его использования. Идеальным было бы требовать использования overriding для всех переопределенных операций и только для них. Другими словами, не использовать not overriding вообще. Это позволило бы находить оба типа ошибок:

  • Ошибки в написании и другие ошибки, приводящие к отсутствию переопределения операций, фиксируются благодаря наличию overriding;

  • Случайное переопределение операции (например, в случае появления новой операции в родительском типе) обнаруживается ввиду отсутствия overriding.

Требование совместимости делает такой подход невозможным в общем случае. Однако такого эффекта можно достичь, используя опции компилятора GNAT фирмы AdaCore:

  • -gnatyO включает появление предупреждений о переопределении операций без использования overriding;

  • -gnatwe заставляет компилятор трактовать все предупреждения как ошибки.

Другие языки, типа C++ и Java, предоставляют меньше содействия в этом аспекте, оставляя подобные ошибки не обнаруженными.

Запрет диспетчеризации вызова подпрограмм

В условиях, когда надежность критически важна, динамическое связывание часто запрещено. Надежность повышается, если мы можем доказать, что поток управления соответствует заданному шаблону, например, отсутствует недостижимые участки кода. Традиционно это означает, что нам приходится следовать методикам структурного программирования, вводя в явном виде такие инструкции, как if и case.

Хотя диспетчеризация вызовов подпрограмм является ключевым свойством ООП, другие возможности (например повторное использование кода благодаря наследованию) также являются привлекательными. Таким образом, возможность расширять типы, но без использования диспетчеризации, все еще имеет значительную ценность. Использование точечной нотации вызова также имеет свои преимущества.

Существует механизм, который позволяет отключать некоторые возможности языка Ада в текущей программе. Речь идет о директиве компилятору Restrictions. В данном случае мы напишем:

pragma Restrictions(No_Dispatch);

Это гарантирует (в момент компиляции), что в программе отсутствует конструкции типа X'Class, а значит и диспетчеризация вызова невозможна.

Заметим, что это ограничение соответствует требованиям языка SPARK, о котором мы упоминали во введении, часто используемого в критических областях. Язык SPARK позволяет использовать расширения типов, но запрещает надклассовые операции и типы.

Когда используется ограничение No_Dispatch, реализация языка получает возможность избежать накладных расходов, связанных с ООП. Нет необходимости создания в каждом типе «виртуальных таблиц» для диспетчеризации вызовов. (Такие таблицы содержат адреса всех операций данного типа). Также нет необходимости в специальном поле тега в каждом объекте.

Здесь существуют также менее очевидные преимущества. При полном ООП подходе некоторые предопределенные операции (например операция сравнения) также имеет возможность диспетчеризации, что приводит к дополнительным расходам. Итоговый результат применения ограничения минимизирует документирование неактивного кода (это код, который присутствует в программе и соответствует требованиям к ПО, но никогда не исполняется) в целях сертификации по стандартам DO-178B и DO-178C.

Интерфейсы и множественное наследование

Иногда множественное наследование расценивается как Святой Грааль в индустрии ПО и как критерий оценки языков программирования. Здесь мы не будем отвлекаться на историю развития этого вопроса. Вместо этого мы остановимся на основных проблемах, связанных с этим механизмом.

Допустим, у нас есть возможность унаследовать тип от двух произвольных родительских типов. Вспомним знаменитый роман «Флатландия» Эдвина Эбботта (вышедший в 1884г.). Это сатира на социальную иерархию, в которой люди - это плоские геометрические фигуры. Рабочий класс - это треугольники, средний класс - другие полигоны, аристократы - окружности. Удивительно, но женщины там - двуугольники, т. е. просто отрезки прямой линии.

Используя объявленные ранее типы Object и Person, мы могли бы попытаться определить обитателей Флатландии как тип, унаследованный от обоих типов:

type Flatlander is
  new Geometry.Object and People.Person; -- illegal

Вопрос который теперь возникает: какие свойства унаследует новый тип? Мы ожидаем, что Flatlander унаследует компоненты X_Coord и Y_Coord от Object и Birthday от Person, хотя Heigh и Weight выглядит сомнительно для плоских персонажей. Конечно Area должен быть унаследован, потому, что Flatlander имеет площать, как и момент инерции.

Теперь должно быть ясно, какие проблемы могут возникнуть. Допустим, оба родительских типа имеют операцию с одним именем. Это весьма вероятно для широко распространенных имен типа Print, Make, Copy и т. д. Какую из них нужно наследовать? Что делать, если оба родителя имеют компоненты с одинаковыми именами? Подобные вопросы объязательно возникнут, если оба родителя имеют общий родительский тип.

Некоторые языки реализуют множественное наследование в такой форме, вводя сложные правила, чтобы урегулировать подобные вопросы. Примером могут служить C++ и Eiffel. Возможные решения включают переименование, явное указание имя родителя в неоднозначных случаях, либо выбор родительского типа согласно позиции в списке. Некоторые из предлагаемых решений имеют субъективный оттенок/характер, для кого-то они очевидны, хотя других приводят в замешательство. Правила C++ дают широкую свободу программисту наделать ошибок.

Трудности возникают в основном двух видов: наследование компонент и наследование реализаций операций от более чем одного родителя. Но фактически проблем с наследованием спецификации (другими словами интерфейса) операции не бывает. Этим воспользовались в языке Java и этот путь оказался довольно успешным. Он также реализован и в языке Ада.

Таким образом, в Аде, начиная со стандарта Ada 2005, мы можем наследовать от более чем одного типа:

type T is new A and B and C with record
   ... -- дополнительные компоненты
end record;

но только первый тип в списке (A) может иметь компоненты и реальные операции. Остальные типы должны быть так называемыми интерфейсами (термин позаимствован у Java умышленно), т. е. абстрактными типами без компонент, у которых все операции либо абстрактные, либо null-процедуры. (Первый тип может также быть интерфейсом.)

Мы можем описать Object, как интерфейс:

package Geometry is

   type Object is interface;

   procedure Move(   Obj: in out Object;
            New_X, New_Y: Float) is abstract;
   function X_Coord(Obj: Object) return Float is abstract;
   function Y_Coord(Obj: Object) return Float is abstract;
   function Area(Obj: Object) return Float is abstract;
   function Moment(Obj: Object) return Float is abstract;
end Geometry;

Обратите внимание, что компоненты были удалены и заменены дополнительными операциями. Процедура Move позволяет передвигать объект, т. е. устанавливать новые координаты x и y. Функции X_Coord, Y_Coord возвращают текущую позицию.

Также следует заметить, что точечная нотация позволяет по прежнему обращаться к координатам, написав A_Circle.X_Coord и The_Triangle.Y_Coord, как если бы это были компоненты.

Теперь при определении конкретного типа мы должны предоставить реализацию всем этим операциям. Предположим:

package Geometry.Circles is

   type Circle is new Object with private; -- partial view
   procedure Move(C: in out Circle; New_X, New_Y: Float);
   function X_Coord(C: Circle) return Float;
   function Y_Coord(C: Circle) return Float;
   function Area(C: Circle) return Float;
   function Moment(C: Circle) return Float;

   function Radius(C: Circle) return Float;
   function Make_Circle(X, Y, R: Float) return Circle;
private

   type Circle is new Object with record
      X_Coord, Y_Coord: Float;
      Radius : Float;
   end record;
end Geometry.Circles;

package body Geometry.Circles is

   procedure Move(C: in out Circle; New_X, New_Y: Float) is
   begin
      C.X_Coord := New_X;
      C.Y_Coord := New_Y;
   end Move;

   function X_Coord(C: Circle) return Float is
   begin
      return C.X_Coord;
   end X_Coord;

   -- Аналогично Y_Coord, а Area и Moment — как ранее
end Geometry.Circles;

Мы определили тип Circle как приватный, чтобы спрятать все его компоненты. Тем не менее, поскольку приватный тип унаследован от Object, он наследует все свойства Object. Обратите внимание, что мы добавили функции для создания окружности и получения его радиуса.

В основе программирования с использованием интерфейсов лежит идея, что мы обязаны предоставить реализацию операций, требуемых интерфейсом. Здесь множественное наследование не касается наследования существующих свойств, скорее это наследование контрактов, которым необходимо следовать. Таким образом, Ада позволяет множественное наследование интерфейсов, но только единичное наследование для реализаций.

Возвращаясь к Флатландии, мы можем определить:

package Flatland is

   type Flatlander is abstract new Person and Object
      with private;

   procedure Move(F: in out Flatlander; New_X,New_Y: Float);
   function X_Coord(F: Flatlander) return Float;
   function Y_Coord(F: Flatlander) return Float;
private

   type Flatlander is abstract new Person and Object
      with record
          X_Coord,  Y_Coord : Float := 0.0;
         ... -- остальные необходимые компоненты
      end record;
end Flatland;

Теперь тип Flatlander будет наследовать компоненту Birthday и прочие от типа Person и все реализации операций типа Person (мы не показываем их тут) и абстрактные операции типа Object. Удобно определить координаты, как компоненты типа Flatlander, чтобы легко реализовать такие операции, как Move, X_Coord, Y_Coord. Обратите внимание, что мы задали начальное значение этих компонент, как ноль, чтобы задать положение Flatlander по умолчанию.

Тело пакета будет следующим:

package body Flatland is

   procedure Move(F: in out Flatlander; New_X,New_Y: Float)
   is
   begin
      F.X_Coord := New_X;
      F.Y_Coord := New_Y;
   end Move;

   function X_Coord(F: Flatlander) return Float is
   begin
      return F.X_Coord;
   end X_Coord;
   -- аналогично Y_Coord
end Flatland;

Сделав тип Flatlander абстрактным, мы избегаем необходимости немедленно предоставить реализацию всем операциям, например Area. Теперь мы можем объявить тип Square пригодным для Флатландии (когда роман вышел в печать, автор подписался псевдонимом A Square):

package Flatland.Squares is

   type Square is new Flatlander with record
      Size: Float;
   end record;

   function Area (S: Square) return Float;
   function Moment (S: Square) return Float;
end Flatland.Square;

package body Flatland.Squares is

   function Area (S: Square) return Float is
   begin
      return S.Size ** 2;
   end Area;

   function Moment (S: Square) return Float is
   begin
      return S.Area * S.Side ** 2 / 6.0;
   end Moment;

end Flatland.Square;

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

A_Square : Square := (Flatland with Side => 3.0);

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

Существуют другие важные свойства интерфейсов, которых мы коснемся лишь вскользь. Интерфейс может иметь в качестве операции null-процедуру. Такая процедура ничего не делает при вызове. Если два родителя имеют одну и туже операцию, то null-процедура переопределяет абстрактную. Если два родителя имеют одну и ту же операцию (с совпадающими параметрами и результатом), они сливаются в одну операцию, для которой требуется реализация. Если параметры и/или результат отличаются, то необходимо реализовать обе операции, т. к. они перегружены. Таким образом, правила сформулированы так, чтобы минимизировать сюрпризы и максимизировать выигрыш от множественного наследования.

Взаимозаменяемость

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

Наследование можно рассматривать с двух перспектив. С точки зрения возможностей языка, это способность породить тип от родительского и унаследовать состояние (компоненты) и операции, сохранив при этом возможность добавлять новые компоненты и операции и переопределять унаследованные операции. С точки зрения моделирования либо теории типов, наследование это отношение («это есть») между подклассом и супер-классом: если класс S - это подкласс T, то любой объект класса S также является объектом класса T. Это свойство — основа полиморфизма. В терминах языка Ада, для любого тегового типа T, переменная типа T'Class может ссылаться на объект типа T или любого типа, унаследованного (прямо или косвенно) от T. Это значит, что любая операция, возможная для T, будет работать (как унаследованная либо переопределенная) и для объекта любого подкласса T.

Более формальная формулировка этого требования известна, как принцип подстановки Барбары Лисков, который выражен в терминах теории типов:

Пусть q(x) свойство объектов x типа T истинно. Тогда q(y) истинно для объектов типа S, где S является подтипом T. (Здесь «подтип» означает «подкласс».)

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

Хотя сам принцип Лисков может показаться очевидным, его связь с контрактным программированием таковой не является. Напомним, что в методе вы можете дополнить спецификацию подпрограмм пред- и/или пост-условиями. Встает вопрос, если вы переопределяете операцию, налагает ли принцип Лисков дополнительные ограничения на пред- и пост-условия для новой версии подпрограммы? Ответ - «да»: пред-условия не могут быть усилены (например, вы не можете сформулировать пред-условие, добавив условие к родительскому через and оператор). Аналогично, пост-условие не может быть ослаблено.

На первый взгляд это противоречит тому, чего вы можете ожидать. Подкласс обычно ограничивает множество значений своего супер-класса. Поэтому накладывать более сильные пред-условия операции подкласса может показаться имеющим смысл. Но при ближайшем рассмотрении оказывается, что это нарушает принцип Лисков. С точки зрения вызывающего, для выполнения операции X.Op(...) полиморфной переменной X, имеющей тип T, необходимо обеспечить выполнение предусловия операции Op типа T. У автора этого кода нет возможности знать все возможные подклассы T. Если случится так, что X ссылается на объект типа T1, у которого предусловие Op сильнее, чем у T, то вызов не сможет состояться, поскольку проверка пред-условия не пройдет. Аналогичные рассуждения докажут, что пост-условие в подклассе не может быть слабее. Вызывающий ожидает выполнение пост-условия после вызова операции, если подкласс этого не гарантирует, это приведет к ошибке.

Дополнение в области ООП и связанных технологий к стандарту DO-178C (DO-332) останавливается на этом вопросе. Оно не требует подчинения принципу Лисков, вместо этого предлагает проверять «локальную согласованность типов». «Согласованность типов» означает выполнение принципа Лисков: операции подкласса не могут иметь более сильные пред-условия и более слабые пост-условия. «Локальная» означает, что необходимо анализировать только реально встречающийся в программе контекст. Например, если существует операция, у которой пред-условие более сильное, но она никогда не вызывается при диспетчеризации вызова, то это не вредит.

Стандарт DO-332 предлагает три подхода доказательства локальной согласованности типов, один на основе формальных методов и два на основе тестирования:

  • формально проверить взаимозаменяемость;

  • удостовериться, что каждый тип проходит все тесты всех своих родителей, которых он может замещать;

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

Первый подход предполагает непосредственную проверку принципа Лисков. Ада 2012 поддерживает явным образом пред- и пост-условия, что помогает провести автоматический формальный анализ, используя специальный инструментарий. Это рекомендуемый подход, если возможно использовать формальные методы, например при помощи инструмента SPARK Pro, поскольку он обеспечивает наивысший уровень доверия.

Второй подход применим при использовании модульного тестирования. В этом контексте, каждая операция класса обладает набором тестов для проверки требований. Переопределенная операция обычно имеет расширенные требования по сравнению с изначальной, поэтому будет иметь больше тестов. Каждый класс тестируется отдельно при помощи всех тестов, принадлежащих ему методов. Идея в том, чтобы проверить принцип взаимозаменяемости для некоторого тегового типа, выполнив все тесты всех его родительских типов, используя объект данного тегового типа. Gnattest, инструмент для модульного тестирования из состава GNAT Pro, предоставляет необходимую поддержку для автоматизации процесса тестирования, в том числе и принципа Лисков.

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

Более подробную информацию по этой теме можно найти в документации «ООП для надежных систем на языке Ада» от AdaCore.