Найти в Дзене
ZDG

ООП: Полиморфизм как обход фейс-контроля

Эта статья продолжает цикл материалов об объектно-ориентированном программировании. Если вы ещё не ознакомились с введением в ООП, следует это сделать.

Я откладывал написание материала про полиморфизм, потому что он напрямую связан с типами данных. Мне пришлось сначала написать про типы, но не знаю, насколько хорошо получилось. Ладно, продолжим.

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

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

Как функция определяет, правильно ли вы передали документы? Если в языке отсутствует строгая типизация, то никак. Вы можете передать туда что угодно, а функция пытается осуществить операции с тем, что дано. Если функция ожидает объект типа "квитанция", она обратится к известным ей свойствам этого объекта, например "ФИО". Если такое свойство найдется в объекте, то наверно это квитанция (или какой-то другой объект с такими же свойствами), а если не найдется – тогда возникнет ошибка.

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

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

Допустим, у вас есть два класса: Player и Monster. Вы создали две переменные (объекта) разных типов (классов):

player = new Player();
monster = new Monster();

У этих объектов есть одинаковые свойства, например уровень, здоровье и броня. Вы могли бы передавать объекты player и monster в некую функцию, которая вычисляет урон на основе этих свойств. Например,

damage = getDamage(player);
...
damage = getDamage(monster);

Собственно, так бы вы и сделали в языке с отсутствием типизации. Функция бы работала нормально. В переданных ей объектах она найдет свойства уровня, здоровья и брони и сделает с ними то, что нужно. Ей всё равно, что эти объекты разных классов.

Но в языке со строгой типизацией так не прокатит. Уже в самом объявлении функции нужно обязательно указать тип параметра:

function getDamage(Player p) ...

На входе в функцию появился фейс-контроль! В данном случае вход разрешен только объекту класса Player. И при вызове getDamage(monster) мы получим ошибку. Класс Monster это не класс Player. Даже если объекты ПОЛНОСТЬЮ совпадают по всем своим свойствам, но созданы из разных классов, они будут считаться разного типа.

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

function getDamagePlayer(Player p) ...
function getDamageMonster(Monster m) ...

Они будут делать одно и то же. Но это получается какая-то неудобная дичь. Ленивый программист никогда на такое не пойдет.

Тут и приходит на помощь полиморфизм. Поли – значит "много", морф – значит "форма". Много форм. Полиморфизм сочетает несколько разных аспектов. Посмотрим на них.

1. Общий предок

Классы Player и Monster могут быть отнаследованы от одного общего класса, например Unit (подробнее здесь). Если Unit обладает всеми нужными свойствами (уровень, здоровье, броня), то в качестве параметра функции можно назначить его:

function getDamage(Unit u) ...

Как теперь будет работать фейс-контроль? Unit, Player, Monster – это с одной стороны разные классы, но с другой стороны Unit является общим для всех. Мы можем теперь передать в функцию тип Player или Monster, и функция будет считать, что работает с Unit. Полиморфизм – это в данном случае много форм класса Unit.

2. Принудительный тип

Вот здесь не уверен, имеет ли это прямое отношение к полиморфизму. Но раз меняется форма, значит это тоже в какой-то степени полиморфизм. Объект может "притвориться" другим объектом во время передачи в функцию. Допустим, функция ожидает увидеть на входе объект класса Player. Но можно передать туда класс Monster под личиной класса Player:

damage = getDamage((Player) monster);

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

3. Сервис одного окна (Перегрузка методов)

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

Например, перед вами несколько окошек. В каждое окошко можно передать разное количество документов разного типа. В одно – квитанцию и деньги, в другое – только паспорт, в третье – четыре справки.

Всё это разные функции. Каждая знает свой порядок и типы параметров и занята своим делом.

Теоретически можно сделать одно окошко, в которое можно передавать любую комбинацию параметров, и чтобы оно само там разбиралось, что нужно сделать.

Но эти внутренние разборки – лишние сложности для ленивого программиста. Лучше всего было бы оставить каждую функцию отдельно, чтобы она по-прежнему занималась только своим делом, но дать им всем одно и то же имя, чтобы получилось одно окно. Но в языках программирования ни разные переменные, ни разные функции не могут иметь одинаковое имя. Что ж, есть исключение для этого случая:

function getDamage(Player p);
function getDamage(Monster m, Player p);
function getDamage(Player p1, Player p2);
function getDamage(int level, int hp, int armor);

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

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

getDamage(player);

Здесь он видит, что присутствует только один параметр класса Player, это совпадает с сигнатурой getDamage(Player p);

Следовательно, именно эта функция и вызывается. Если же написать так:

getDamage(10, 100, 5);

То транслятор видит, что это совпадает с сигнатурой getDamage(int level, int hp, int armor), и значит, будет вызвана именно эта функция.

Функция getDamage в данном случае называется "перегруженной" (overloaded), то есть имя у неё одно, а воплощения разные.

Полиморфизм в данном случае – перегрузка функции. Вот, наверно, и всё. Если что-то забыл – общую информацию вы всё равно уже знаете.

К списку ООП-тем

Наука
7 млн интересуются