Фундаментальные принципы объектно ориентированного проектирования (Часть 3): Полиморфизм

Вольный перевод статьи Fundamental Object Oriented Design principles (Part 3): Polymorphism.

Рассмотрим Полиморфизм предоставляемый объектно ориентированными языками программирования.

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

Рассматриваемые принципы проектирования применены для не объектно ориентированных языков, но с большими сложностями.

Что такое Полиморфизм?

Когда мы ссылаемся на объект мы хотим чтобы его поведение определялось типом объекта, а не типом ссылки которую мы используем. Обращение может идти даже через абстрактный тип (например, class abstract в Delphi), всё равно должны вызываться методы типа конкретного экземпляра объекта на который указывает ссылка. Это суть полиморфизма. Как вы видели в разделе про абстракцию мы можем взаимодействовать с объектами через переменную с типом класса-предка, хотя объект, на который ссылается переменная, на самом деле является экземпляром класса-потомка.

Наиболее общее понимание полиморфизма в том что метод может быть виртуальным. Это означает что класс-потомок может переопределить метод и заменить реализацию предка на свою собственную.

Переопределение виртуального метода

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

Давайте используем похожую структуру, но сделаем замену более явной. Наш метод заменит метод определённый в предке.

Если мы создали объекты классов TPoodle, TToyPoodle или TDog и ссылаемся на них через переменную типа TDog то мы можем вызвать метод Bark(), причём будет вызван метод именно того класса к которому относится реальный объект, мы даже можем не знать что именно это за класс.

Перезагрузка функций и перегрузка операторов

Мы обычно не думаем о перегрузке (overload) как о полиморфизме, но она также позволяет добавить в класс больше функциональности оставляя интерфейс более сжатым. Часто этот тип полиморфизма применяется и без ООП. При вызове перегруженной функции объекта корректный метод выбирается на основе названия функции и списка параметров функции, которые уникальны для каждой перегруженного метода.

Ниже общий пример использования полиморфизма через перегрузку функций. Объект потока (TMyStreamReader1) содержит методы для чтения данных: ReadBoolean, ReadDouble, ReadInteger. При использовании объекта придётся в каждом случае проверять тип переменной и использовать соответствующую функцию. Гораздо проще вызвать метод Read и получить вызов корректной версии метода в зависимости от сигнатуры (TMyStreamReader2).

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

Наследование

Мы уже видели что можем унаследовать одни класс от другого и переопределить виртуальные (virtual) методы родительского класса. Если не переопределять виртуальные методы, то они унаследуются из родительского класса. Например, если мы объявим публичный не виртуальный метод TDog.Pant(), то мы увидим его когда будем ссылаться на наш объект через более специфичный класс (класс-потомок).

Субтипирование

Субтипирование (Subtyping) применяется к наследованию интерфейсов. Думаю, что название "субтипирование" неудачное потому что классы связываются через отношение "является" со своими предками и каждый класс в сущности более специализированный тип своего родителя. "Расширение интерфейса" может быть технически более корректным термином. В этом случае интерфейс который наследуется от другого действительно расширяет контракт, наследуемый интерфейс может добавлять больше требований к определению, но не может убирать их. В надуманном примере ниже любой объект который реализует IEquatable должен также полностью удовлетворять IComparable.

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

Существуют случаи когда наследование интерфейса действительно представляет субтипирование. Это случай когда интерфейс определяется похожим на чистый абстрактный класс. В этом случае мы можем иметь псевдо-отношение "является" между интерфейсам.

IMyEnumerableABCList действительно выглядит как подтип IMyABCList. Можно применить эту иерархию интерфейсов к классам независимо от их структуры наследования. Сигнатуры определения методов и свойств наследуются от супертипа к подтипу, но поведения реализованных методов не определено в подтипах (или вообще в интерфейсах).

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

Резюме

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *