Параметры класса

#OOP #OOD

Класс имеет два параметра - это пространство состояний и допустимые поведение.

Пространство состояний

Пространство состояний класса - это набор всех возможных допустимых значений его переменных.

Размерность пространства состояний - это координаты требуемые для определения состояния объекта. Например класс PRODUCT-LINE может иметь три переменные: weight, price, quantity-available, таким образом для отрисовки состояний класса потребуется трехмерное пространство.

Если Б является подклассом класса А, тогда пространство-состояний класса Б должно быть ограничено пространством-состояний класса А. Например если имеется класс ROAD-VEHICLE с ограничением массы от 0 до 10 тонн, то подкласс AUTOMOBILE не может иметь массу больше 10 тонн и меньше 0.

Если Б является подклассом класса А, тогда размерность пространства класса Б должна содержать размерность класса А или же превышать его. Если размерность пространства состояний класса Б превышает размерность состояний класса А, тогда говорят, что Б расширяет А.

Поведение подкласса

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

Инварианты класса как ограничение пространства состояния

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

Предусловия и постусловия

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

  1. Клиент вызывающим метод гарантирует соблюдение предусловия, в таком случае вызываемый объект гарантирует постусловие.
  2. Если вызывающий не может гарантировать соблюдения предусловия, то вызываемый объект не может гарантировать выполнения своей работы.
    NOTE: От себя добавлю, что выглядит странным, что гарантию инварианта внутреннего состояния объекта перекладывают на клиента, который вроде не должен этого знать. Однако скорее всего имеется ввиду, что хоть это и не корректно, но некоторые интерфейсы предоставляют прямую модификацию внутреннего состояния объекта и в таком случае гарантия перекладывается на клиента.

Принцип соответствия типов

Подтип и подкласс не одно и тоже!!!

Принцип соответствия типов гласит, что если S является истинным подтипом T, то S должен соответствовать T.
Тип S соответствует типу T, если объект типа S может быть передан везде где ожидается объект типа T с сохранением корректности при вызове любого из методов T.

Принцип соответствия типов в мире SOLID называется принципом подстановки Барбары Лисков.

Подклассы как подтипы

Подтип и подкласс не одно и тоже!!!

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

Чтобы это выполнялось, необходимо убедиться в следующем:

  1. Каждый метод родительского класса имеет соответствующие методы в подклассе, с теми же именами и сигнатурой.
  2. Каждое предусловие метода подкласса, не строже, чем предусловие этого метода у родительского класса.Это называется принципом контрвариантности (principle of contravariance).
  3. Каждое постусловие метода подкласса, как минимум имеет ту же строгость, что и постусловие этого метода у родительского класса. Это называется принципом ковариантности (principle of covariance).

Под строгостью подразумевается не степень качества, а рамки. Например если имеется набор допустимых значений (1, 2, 3, 4, 5, 6), то более строгим набором будет например (1, 2, 3), то есть подмножество, а более слабым наоборот, например (0, 1, 2, 3, 4, 5, 6, 7), то есть надмножество.

Например возьмем два класса. Класс Employee и его подкласс Manager (Менеджер это разновидность сотрудника). Employee имеет инвариант grade-level > 0, а Manager инвариант grade-level > 20. У Manager инвариант более строгий, значит все хорошо.
Далее возьмем метод calc-bonus(int perf-eval): bonus-pct который берет оценку производительности и вычисляет бонус, как процент к регулярной зарплате. Для простоты предположим, что perf-eval от 0 до +5, а bonus-pct между 0% и 10%.
Алгоритмы вычисления бонуса для Employee и Manager могут быть разными, по этому предположим, что метод был переопределен для Manager.

Теперь давайте посмотрим, что получается для предусловия значения perf-eval для Manager::calc-bonus
0 до 5 // Эквивалентно Employee, значит все хорошо
0 до 8 // У Manager значение шире (Менее строгое), значит все хорошо
-1до 9 // У Manager значение шире (Менее строгое), значит все хорошо (Предполагается, что негативное значение допускается)
1 до 5 // У Manager значение уже (Строже), значит мы нарушили условие
2 до 4 // У Manager значение уже (Строже), значит мы нарушили условие

Теперь давайте посмотрим, что получается для постусловия значения bonus-pct для Manager::calc-bonus

0% - 10% // Эквивалентно Employee, значит все хорошо
0% - 6% У Manager значение уже (Строже), значит все хорошо
2% - 4% У Manager значение уже (Строже), значит все хорошо

0% - 12% У Manager значение шире (Менее строгое), значит мы нарушили условие
-1% - 13% У Manager значение шире (Менее строгое), значит мы нарушили условие

Если собирать все воедино, то получается следующее:

  1. Пространство состояния S иметь идентичную T размерность. Но S может ее расширять
  2. В размерностях общих для S и T, пространство состояний S должно быть как минимум эквивалентным или быть подмножеством пространства состояния T.
  3. S::m должен иметь идентичное имя с T::m
  4. S::m должен иметь идентичную формальную сигнатуру с T::m
  5. Предусловие S::m должно быть эквивалентно или мягче чем у T::m. Каждый тип передаваемого формального аргумента должен быть или тем же типом, что и аргумент у T::m или подтипом аргумента у T::m.
  6. Постусловие S::m должно быть эквивалентно или строже чем у T::m. Каждый тип передаваемого формального аргумента должен быть или тем же типом, что и аргумент у T::m или подтипом аргумента у T::m.

Принцип закрытости поведения

В иерархии основанной на типе\подтипе, поведение каждого класса, включая поведение родительского класса, должно соблюдать инварианты класса во время выполнения метода.

Например если имеется класс POLYGON с методом add-vertex и от этого класса наследуется класс TRIANGLE, то вызов TRIANGLE::add-vertex нарушит инварианты TRIANGLE, превратив его например в трапецию.

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

  1. Не наследоваться от этого класса
  2. Перегрузить метод TRIANGLE::add-vertex таким образом, чтобы возвращался этот же объект и не было никакого нарушения.
  3. Создать и вернуть новый объект POLYGON в TRIANGLE::add-vertex. Однако это может поломать приложение.