Параметры класса
Класс имеет два параметра - это пространство состояний и допустимые поведение.
Пространство состояний
Пространство состояний класса - это набор всех возможных допустимых значений его переменных.
Размерность пространства состояний - это координаты требуемые для определения состояния объекта. Например класс PRODUCT-LINE может иметь три переменные: weight, price, quantity-available, таким образом для отрисовки состояний класса потребуется трехмерное пространство.
Если Б является подклассом класса А, тогда пространство-состояний класса Б должно быть ограничено пространством-состояний класса А. Например если имеется класс ROAD-VEHICLE с ограничением массы от 0 до 10 тонн, то подкласс AUTOMOBILE не может иметь массу больше 10 тонн и меньше 0.
Если Б является подклассом класса А, тогда размерность пространства класса Б должна содержать размерность класса А или же превышать его. Если размерность пространства состояний класса Б превышает размерность состояний класса А, тогда говорят, что Б расширяет А.
Поведение подкласса
Поведение класса это допустимый набор переходов между состояниями объекта в рамках его пространства состояний.
Подкласс может расширять допустимый набор поведения, однако он так же ограничен набором поведения родительского класса, как и его пространством состояний.
Инварианты класса как ограничение пространства состояния
Инвариант класса это условие которое каждый объект класса всегда обязан соблюдать. Под всегда подразумевается, что до вызова метода объекта и после возвращения из него, инвариант соблюдается, но во время самого выполнения может не соблюдаться. (Тут от себя добавлю, что есть и более строгие правила, но уже на нулевом уровне инкапсуляции).
Предусловия и постусловия
Каждый метод класса имеет предусловие и постусловие. Предусловие это условие которое должно быть истинным перед выполнением метода, а постусловие естественно после. Бернард Мейер определяет это как контракт между клиентом объекта вызывающим метод и самим объектом обрабатывающим вызов. Метафора контракта подразумевает что:
- Клиент вызывающим метод гарантирует соблюдение предусловия, в таком случае вызываемый объект гарантирует постусловие.
- Если вызывающий не может гарантировать соблюдения предусловия, то вызываемый объект не может гарантировать выполнения своей работы.
NOTE: От себя добавлю, что выглядит странным, что гарантию инварианта внутреннего состояния объекта перекладывают на клиента, который вроде не должен этого знать. Однако скорее всего имеется ввиду, что хоть это и не корректно, но некоторые интерфейсы предоставляют прямую модификацию внутреннего состояния объекта и в таком случае гарантия перекладывается на клиента.
Принцип соответствия типов
Подтип и подкласс не одно и тоже!!!
Принцип соответствия типов гласит, что если S является истинным подтипом T, то S должен соответствовать T.
Тип S соответствует типу T, если объект типа S может быть передан везде где ожидается объект типа T с сохранением корректности при вызове любого из методов T.
Принцип соответствия типов в мире SOLID называется принципом подстановки Барбары Лисков.
Подклассы как подтипы
Подтип и подкласс не одно и тоже!!!
Говоря на языке мира ООД, иерархии наследования класса\подкласса следует соответствовать иерархии типа\подтипа. Другими словами следует проектировать таким образом, чтобы подкласс был подтипом родительского класса.
Чтобы это выполнялось, необходимо убедиться в следующем:
- Каждый метод родительского класса имеет соответствующие методы в подклассе, с теми же именами и сигнатурой.
- Каждое предусловие метода подкласса, не строже, чем предусловие этого метода у родительского класса.Это называется принципом контрвариантности (principle of contravariance).
- Каждое постусловие метода подкласса, как минимум имеет ту же строгость, что и постусловие этого метода у родительского класса. Это называется принципом ковариантности (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 значение шире (Менее строгое), значит мы нарушили условие
Если собирать все воедино, то получается следующее:
- Пространство состояния S иметь идентичную T размерность. Но S может ее расширять
- В размерностях общих для S и T, пространство состояний S должно быть как минимум эквивалентным или быть подмножеством пространства состояния T.
- S::m должен иметь идентичное имя с T::m
- S::m должен иметь идентичную формальную сигнатуру с T::m
- Предусловие S::m должно быть эквивалентно или мягче чем у T::m. Каждый тип передаваемого формального аргумента должен быть или тем же типом, что и аргумент у T::m или подтипом аргумента у T::m.
- Постусловие S::m должно быть эквивалентно или строже чем у T::m. Каждый тип передаваемого формального аргумента должен быть или тем же типом, что и аргумент у T::m или подтипом аргумента у T::m.
Принцип закрытости поведения
В иерархии основанной на типе\подтипе, поведение каждого класса, включая поведение родительского класса, должно соблюдать инварианты класса во время выполнения метода.
Например если имеется класс POLYGON с методом add-vertex и от этого класса наследуется класс TRIANGLE, то вызов TRIANGLE::add-vertex нарушит инварианты TRIANGLE, превратив его например в трапецию.
Чтобы избежать нарушения принципа, можно сделать вот что:
- Не наследоваться от этого класса
- Перегрузить метод TRIANGLE::add-vertex таким образом, чтобы возвращался этот же объект и не было никакого нарушения.
- Создать и вернуть новый объект POLYGON в TRIANGLE::add-vertex. Однако это может поломать приложение.