Назад Домой! Дальше Лекция 14.(окончание):
3.5.5 Виртуальные методы, полиморфизм, абстрактные классы


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

Все это мы видели на примерах в предыдущем разделе. Но представим себе такую задачу. Вы объявили в приложении массив объектов типа TPerson:
var PersArray: array[1..10] of TPerson;

Далее заполняете этот массив вперемешку объектами классов TStudent и TEmpl, т.е. создаете, например, общий список учащихся и преподавателей. В разд. 3.5.4 вы видели, что это возможно, так как переменная базового класса может принимать объекты производных классов. А затем хотите пройти в цикле элементы этого массива и отобразить в окне Memo информацию о них:

for i:=l to 10 do Memol.Lines.Add(PersArray[i].PersonToStr);

Или аналогичная задача с использованием списка TList:
объявлена переменная var List: TList; в нее заносятся указатели на объекты классов TStudent и TEmpl, а затем вы хотите пройти в цикле элементы этого списка и отобразить их в окне Memo:

for i:=0 to List.Count - 1 do Memol.Lines.Add(TPerson(List[i]).PersonToStr);

Реализуйте один из этих вариантов (не забудьте при завершении приложения удалять объекты из памяти), и посмотрите, что получится. А получится очевидный результат: во всех случаях сработает метод PersonToStr родительского класса TPerson, так как соответствующие объекты объявлены объектами этого класса. И программа, естественно, не знает, объекты какого класса хранятся в массиве или в списке.

Конечно, можно усложнить код, проверять каждый раз операцией is истинный класс объекта, и указывать операцией as этот класс (см. об этих операциях в разд. 3.5.4). Для массива это будет выглядеть так:

if PersArrayfi] is TStudent then 
 Memol.Lines.Add((PersArray[i] as TStudent).PersonToStr)
  else 
if PersArray[i] is TEmpl then 
 Memol.Lines.Add((PersArray[i] as TEmpl).PersonToStr);

А для списка аналогичный код имеет вид:


if  TPerson(List[i])   is  TStudent then    Memol.Lines.Add(TStudent(List[i]).PersonToStr) 
 else  
 if  TPerson(List[i]) is TEmpl then    Memol.Lines.Add(TEmpl(List[i]).PersonToStr);

Задача будет решена, но некрасиво. Во-первых, код усложняется. А во-вторых, если вы впоследствии решите создать какие-то новые классы, производные от TPerson, вам придется вводить соответствующие дополнительные проверки в эти коды.

Красивое решение подобных задач обеспечивается очень легко с помощью виртуальных методов. Виртуальные методы не связаны с другими методами с тем же именем в классах-наследниках. Если в классах-наследниках эти методы перегружены, то при обращении к такому методу во время выполнения будет вызываться тот из методов с одинаковыми именами, который соответствует истинному классу объекта. В приведенных примерах, если сделать методы PersonToStr виртуальными, то операторы

Memol.Lines.Add(PersArray[i].PersonToStr); Memol.Lines.Add(TPerson(List[i]).PersonToStr);

будут автоматически вызывать методы PersonToStr того класса (TStudent, TEmpl или других производных от TPerson), к которому относится каждый объект. Такой подход, облегчающий работу с множеством родственных объектов, называется полиморфизмом.

Сделать метод родительского класса виртуальным очень просто. При объявлении в классе виртуальных методов после точки с занятой, завершающей объявление метода, добавляется ключевое слово virtual. Например, чтобы объявить в базовом классе TPerson метод PersonToStr виртуальным, надо в его объявление добавить слово virtual:

function PersonToStr: string; virtual;

Чтобы перегрузить в классе-наследнике виртуальный метод, надо после его объявления поставит ключевое слово override. В наших примерах объявления метода в классах и должны выглядеть так:

function PersonToStr: string; override;
И это все! Методы стали виртуальными и приведенные в начале данного раздела операторы, будут работать безо всяких проверок if.

Если в каком-то базовом классе метод был объявлен как виртуальный, то он остается виртуальным во всех классах-наследниках (в частности, и в наследниках классов наследников). Однако обычно для облегчения понимания кодов, перегруженные методы принято повторно объявлять виртуальными, чтобы была ясна их суть для тех, кто будет строить наследников данного класса. Например:

function PersonToStr: string; override; virtual;

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

Объявляется абстрактный метод с помощью ключевого слова abstract после слова virtual. Например, вы можете в классе TPerson объявить метод PersonToStr следующим образом:

function PersonToStr: string; virtual; abstract;

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

Некоторые итоги

В данной главе (учебника [3]) мы практически завершили рассмотрение языка Object Pascal. Вы уже можете считать себя грамотным программистом на этом языке (если, конечно, освоили изложенный материал и можете применять его на практике). Не страшно, если вы не помните наизусть какие-то синтаксические конструкции, имена свойств, методов, функций. Важнее, чтобы вы знали, какие задачи можно решать и как подойти к их решению. А имена функций, их параметры и т.п. вы всегда сможете посмотреть во встроенных справках Delphi, или с помощью оперативных подсказок в окне Редактора Кода.

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

Но, все-таки, работа с массивами, строками, файлами — это чисто технические задачи, которыми надо просто достаточно свободно владеть. А основное в объектно-ориентированном проектировании и программировании — это объекты. Если говорить об элементах языка, то это записи и классы. И тут мало овладеть техникой. Надо постараться приучить себя формулировать в терминах объектов те задачи, которые вам приходится решать.

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

Для каждого объекта надо составить список свойств, которыми он может обладать. Далее надо представить себе, какие операции могут потребоваться над этими свойствами. Только после того, как вся эта предварительная работа проделана и зафиксирована на бумаге (или в виде текстового и графического документа в памяти компьютера), можно начинать продумывать реализацию проекта. Некоторые детали реализации зависят от того, будет ли у вас фиксированное число объектов нижних уровней, или это число заранее неизвестно. Например, рассмотренные в разд. 3.3.2, 3.3.3, 3.3.4 примеры списков записей о сотрудниках некоторой организации — типичные примеры, когда число объектов заранее неизвестно. То же относится к сведениям о продажах каких-то изделий или услуг, модели какого-то склада, производства и т.д. Если число объектов не фиксировано, значит для реализации потребуются или динамические массивы, или списки.

Реализация большинства объектов возможна или с помощью записей, или с помощью классов. В классах не представляет труда реализовать любые свойства (данные) и методы работы с ними. Реализация в виде записей позволяет хранить в них любые данные, и может быть дополнена набором функций, оперирующих с данными в записях. Пожалуй, наиболее принципиальный момент, различающий эти два подхода — доступ к данным. Работая с записями, программист и создаваемые им функции имеют прямой доступ к данным. Хорошо, если при этом в принципе невозможно занести в объект какие-то недопустимые значения, которые нарушат в нем целостность данных и функционирование. Если же такая опасность есть, предпочтение следует отдавать объектам, реализованным классами. В этом случае в полной мере можно реализовать принцип инкапсуляции — скрытия данных. Запись и чтение данных может осуществляться соответствующими функциями свойств. И если вы в дальнейшем надумаете изменить способы хранения данных и их обработки, то вы можете это сделать, не меняя открытые разделы класса. Тогда все потребители вашего класса даже не заметят изменения, никакого перепрограммирования приложений не потребуется. Просто эти приложения станут работать более эффективно, благодаря вашей модернизации класса.

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

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

Назад Дальше

Rambler's Top100
Hosted by uCoz