Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[18.217.73.187] |
|
Сообщ.
#1
,
|
|
|
Часто в коде начинающих программистов можно встретить громоздкие конструкции, которые с легкостью могли бы быть упрощены, используй автор всю мощь ООП. Особенно часто на начальном этапе возникают проблемы с полиморфизмом. Слово это, уверен, знакомо каждому, а что за ним кроется, понимают не все. Возможно причина в том, что во многих источниках при попытке объяснить этот термин, большой упор делается на раскрытии принципов позднего связывания, его отличиях от раннего связывания, и при этом практически забывается суть – для чего это нужно. В этой небольшой статье я попытаюсь раскрыть смысл понятия “полиморфизм” для тех, кто понимает его не достаточно хорошо.
Итак, прежде чем начать, введем несколько понятий, которые будут использоваться далее: • Базовый класс. Допустим, у нас есть несколько родственных классов с общим предком. Например, это могут быть классы TFileStream, TMemoryStream, TStringStream с общим предком – TStream. Так вот, класс TStream и будем называть базовым для классов, являющихся его потомками. • Клиентским кодом будем называть код, использующий тот или иной класс, т.е. вызывающий его методы, обращающийся к свойствам. Другими словами, сам класс как сервер предоставляет клиентам набор определенных функций, клиенты могут их использовать. • Интерфейсом класса будем называть совокупность его членов (методов, свойств), объявленных в классе и доступных для клиентского кода. Понятие интерфейса неразрывно связано с понятием инкапсуляции. Воздействовать на объект клиент может только используя объявленный в классе интерфейс. При наследовании классы получают интерфейс своего предка и могут дополнить его собственными членами. Примечание: в Delphi, да и во многих других языках программирования, словом “интерфейс” так же называется тип данных, представляющий собой совокупность методов и свойств без их реализации, который может быть реализован в классе. В Delphi такой тип данных объявляется с помощью ключевого слова interface. Дабы не возникало путаницы, везде ниже под словом “интерфейс” мы будем подразумевать именно контракт класса – набор объявленных им членов, доступный клиенту, а не одноименный тип данных. Теперь вернемся к полиморфизму и попробуем понять, что это такое и какую функцию он выполняет. Если попытаться дать понятию определение, то можно сказать, что полиморфизм – это возможность одного и того же клиентского кода вести себя по-разному в зависимости от того, с экземпляром какого класса он имеет дело. Для этого клиентский код оперирует только лишь интерфейсом базового класса, а его потомки, по-разному реализуя методы этого интерфейса, обеспечивают и различное поведение. Расшифруем на примере: procedure SaveToStream(Stream: TStream); var I: Integer; begin Stream.WriteBuffer(Count, SizeOf(Count)); for I := 0 to Count – 1 do Stream.WriteBuffer(Data[i], SizeOf(Data[i])); end; Процедура SaveToStream – это полиморфный клиентский код. Он принимает ссылку на поток в качестве параметра и вызывает его методы для записи каких-либо данных. Ссылка на поток передается под видом базового класса TStream, хотя реально при вызове SaveToStream мы можем подставить любого его потомка. Если мы подставим экземпляр TMemoryStream, данные будут записаны в память, если TFileStream – в файл. Наш код может вести себя по-разному в зависимости от того, с каким классом реально имеет дело. В то же время, он нигде не проверяет классы, а просто использует общий для всех классов данной иерархии интерфейс. Из этого примера должно быть видно основное назначение полиморфизма – возможность писать обобщенный клиентский код, а всю конкретику переложить на сами классы. Благодаря этому мы избавлены от необходимости писать сложные длинные проверки, построенные на условных операторах, код становится более коротким, понятным и, что не менее важно – расширяемым (в будущем мы можем написать новые потомки от TStream, клиентский код переделывать не придется, т.е. наш код работает даже с классами которых еще нет, но которые обязуются поддерживать заложенный в базовом классе интерфейс). Если с практической точки зрения все понятно, то вернемся к теории. Как это работает? Полиморфизм, прежде всего, основан на двух вещах: • Наследование интерфейсов. Выше мы говорили, что при наследовании класс получает целиком интерфейс своего предка. Без этого полиморфизм невозможен. Чтобы писать обобщенный код, нужно обращаться к объектам разных типов через их общий интерфейс. • Методы позднего связывания. Под ними чаще всего подразумеваются виртуальные методы, объявляемые в Delphi с помощью ключевого слова virtual. Их особенность в том, что наследуя интерфейс с этими методами, класс-потомок может подменить их реализацию. В итоге у нас и получается одинаковый интерфейс при разной реализации, что и означает полиморфное поведение. Примечание: для того, чтобы переопределить реализацию виртуального метода, нужно в потомке переобъявить этот метод с ключевым словом “override”. Если забыть указать “override”, то мы не переопределим реализацию существующего в интерфейсе предка метода, а введем в интерфейсе потомка новый метод с тем же именем. В результате, мы не сможем вызвать этот метод через общий интерфейс, любая попытка будет вызывать непереопределенный метод базового класса. Новый же метод будет доступен в итоге только через интерфейс нашего потомка, где мы этот метод ввели, а для полиморфизма обязательным условием является возможность вызова именно через базовый интерфейс. Вот почему важно не забывать указывать “override”, хотя если вы забудете это сделать – среда предупредит вас соответствующим warning-ом. Примерно то же самое будет, если не указать ни “virtual”, ни “override”. Без директивы “virtual“ мы вводим в классе статический (не виртуальный) метод, чью реализацию подменить в потомках невозможно. Единственное что можно сделать – объявить в потомке одноименный метод, но это будет именно другой метод (а не тот же метод с другой реализацией) другого интерфейса со всеми вытекающими последствиями. Эту вещь важно понимать. Примечание: под наследованием в ООП обычно подразумевают две вещи: наследование интерфейса и наследование реализации. Первое означает, что потомок получает интерфейс своего предка и может дополнить его своими членами. Второе – что реализация методов от предка также переходит потомку, если ничего не предпринять для того, чтобы это изменить. Для осуществления полиморфного поведения достаточно только чтобы потомок унаследовал интерфейс, а реализация в базовом классе иногда не нужна, главное – это возможность ее подменить своей. Для того чтобы не писать реализацию в базовом классе, можно использовать абстрактные методы, объявляемые с помощью ключевого слова “abstract”. Последний вопрос, на который стоит ответить – как найти то место в клиентском коде, в котором целесообразно использовать полиморфизм? Выше уже частично был дан ответ на этот вопрос: в тех местах, где вам хочется применить условную логику, заключающуюся в проверке типа класса, и в зависимости от этого реализовать по-разному одно и то же действие. Встретив такое место в коде, подумайте, а не лучше ли в данном случае перейти от условной логики к полиморфизму? Хорошим признаком является наличие оператора “is”. Если ваш код построен на проверках типа с помощью “is” (особенно если они многочисленны), то почти наверняка он нуждается в том, чтобы переделать его с использованием полиморфизма. Хотя не обязательно в условии должен быть именно “is”, вы можете и по другим косвенным признакам определять тип объекта. Примеры использования полиморфизма не составляет труда найти почти в любом учебнике по ООП, здесь их приводить нет смысла. Надеюсь, что если после прочтения учебных материалов и разбора примеров из них у вас остались вопросы, эта статья помогла на них ответить. |