Объектно-ориентированное программирование
1. Введение
Концепция объектно-ориентированного программирования подразумевает,
что основой управления процессом реализации программы является передача
сообщений объектам. Поэтому объекты должны определяться совместно с
сообщениями, на которые они должны реагировать при выполнении программы. В
этом состоит главное отличие ООП от процедурного программирования, где
отдельно определённые структуры данных передаются в процедуры (функции) в
качестве параметров. Таким образом, объектно-ориентированная программа
состоит из объектов – отдельных фрагментов кода, обрабатывающего данные,
которые взаимодействуют друг с другом через определённые интерфейсы.
Объектно-ориентированный язык программирования должен обладать
следующими свойствами:
1. абстракции – формальное о качествах или свойствах предмета путем
мысленного удаления некоторых частностей или материальных объектов;
2. инкапсуляции – механизма, связывающего вмести код и данные, которыми
он манипулирует, и защищающего их от внешних помех и некорректного
использования;
3. наследования – процесса, с помощью которого один объект приобретает
свойства другого, т.е. поддерживается иерархической классификации;
4. полиморфизма – свойства, позволяющего использовать один и тот же
интерфейс для общего класса действий.
Разработка объектно-ориентированных программ состоит из следующих
последовательных работ:
- определение основных объектов, необходимых для решения данной
задачи;
- определение закрытых данных (данных состояния) для выбранных
объектов;
- определение второстепенных объектов и их закрытых данных;
- определение иерархической системы классов, представляющих
выбранные объекты;
- определение ключевых сообщений, которые должны обрабатывать
объекты каждого класса;
- разработка последовательности выражений, которые позволяют
решить поставленную задачу;
- разработка методов, обрабатывающих каждое сообщение;
- очистка проекта, то есть устранение всех вспомогательных
промежуточных материалов, использовавшихся при проектировании;
- кодирование, отладка, компоновка и тестирование.
Объектно-ориентированное программирование позволяет программисту
моделировать объекты определённой предметной области путем программирования
их содержания и поведения в пределах класса. Конструкция «класс»
обеспечивает механизм инкапсуляции для реализации абстрактных типов данных.
Инкапсуляция как бы скрывает и подробности внутренней реализации типов, и
внешние операции и функции, допустимые для выполнения над объектами этого
типа.
2. Что такое объектно-ориентированное программирование
Элементы объектно-ориентированного программирования (ООП) появились в
начале 70-х годов в языке моделирования Симула, затем получили свое
развитие, и в настоящее время ООП принадлежит к числу ведущих технологий
программирования.
Основная цель ООП, как и большинства других подходов к
программированию – повышение эффективности разработки программ. Идеи ООП
оказались плодотворными и нашли применение не только в языках
программирования, но и в других областях Computer Science, например, в
области разработки операционных систем.
Появление ООП было связано с тем наблюдением, что компьютерные
программы представляют собой описание действий, выполняемых над различными
объектами. В роли последних могут выступать, например, графические объекты,
записи в базах данных или совокупности числовых значений. В традиционных
методах программирования изменение данных или правил и методов обработки
часто приводило к необходимости значительного изменения программы. Всякое
существенное изменения программы – это большая неприятность для
программиста, так как при этом увеличивается вероятность ошибок, вследствие
чего возрастает время, необходимое для «доводки» программы. Использование
ООП позволяет выйти из такой ситуации с минимальными потерями, сводя
необходимую модификацию программы к её расширению и дополнению. Необходимо
заметить, что ООП не является панацеей от всех программистских бед, но его
ценность как передовой технологии программирования несомненна. Изучение
идей и методов ООП может существенно упростить разработку и отладку сложных
программ.
Мы уже привыкли использовать в своих программах процедуры и функции
для программирования тех сложных действий по обработке данных, которые
приходится выполнять многократно. Использование подпрограмм в своё время
было важным шагом на пути к увеличению эффективности программирования.
Подпрограмма может иметь формальные предметы, которые при обращении к ней
заменяются фактическими предметами. В этом случае есть опасность вызова
подпрограммы с неправильными данными, что может привести к сбою программы и
её аварийному завершению при выполнении. Поэтому естественным обобщением
традиционного подхода к программированию является объединение данных и
подпрограмм (процедур и функций), предназначенных для их обработки.
3. Объекты
Базовым в объектно-ориентированном программировании является понятие
объекта. Объект имеет определённые свойства. Состояние объекта задаётся
значениями его признаков. Объект «знает», как решать определённые задачи,
то есть располагает методами решения. Программа, написанная с
использованием ООП, состоит из объектов, которые могут взаимодействовать
между собой.
Ранее отмечалось, что программная реализация объекта представляет
собой объединение данных и процедур их обработки. Переменные объектного
типа называют экземплярами объекта. Здесь требуется уточнение – экземпляр
можно лишь формально назвать переменной. Его описание даётся в предложение
описания переменных, но в действительности экземпляр – нечто большее, чем
обычная переменная.
В отличие от типа «запись», объектный тип содержит не только поля,
описывающие данные, но также процедуры и функции, описания которых
содержится в описании объекта. Эти процедуры и функции называют методами.
Методам объекта доступны его поля. Следует отметить, что методы и их
параметры определяются в описании объекта, а их реализация даётся вне этого
описания, в том мест программы, которое предшествует вызову данного метода.
В описании объекта фактически содержаться лишь шаблоны обращения к методам,
которые необходимы компилятору для проверки соответствия количества
параметров и их типов при обращении к методам. Вот пример описания
объекта[1]:
Type
Location = object
X,Y: Integer;
Procedure Init(InitX, InitY: Integer);
Function GetX: Integer;
Function GetY: Integer;
End;
Здесь описывается объект, который может использоваться в дальнейшем,
скажем, в графическом режиме и который предназначен для определения
положения на экране произвольного графического элемента. Объект описывается
с помощью зарезервированных слов
object…end, между которыми находиться описание полей и методов. В нашем
примере объект содержит два поля для хранения значений графических
координат, а так же для описания процедуры и двух функций - это методы
данного объекта. Процедура предназначена для задания первоначального
положения объекта, а функция – для считывания его координат.
4. Инкапсуляция
Инкапсуляция является важнейшим свойством объектов, на котором
строится объектно-ориентированное программирование. Инкапсуляция
заключается в том, что объект скрывает в себе детали, которые несущественны
для использования объекта. В традиционном подходе к программированию с
использованием глобальных переменных программист не был застрахован от
ошибок, связанных с использованием процедур, не предназначенных для
обработки данных, связанных с этими переменными. Предположим, например, что
имеется «не-ООП» программа, предназначенная для начисления заработной платы
сотрудникам некой организации, а в программе имеются два массива. Один
массив хранит величину заработной платы, а другой – телефонные номера
сотрудников (для составления отчёта для налоговой инспекции). Что
произойдёт, если программист случайно перепутает эти массивы? Очевидно, для
бухгалтерии начнутся тяжёлые времена. «Жёсткое» связание данных и процедур
их обработки в одном объекте позволит избежать неприятностей такого рода.
Инкапсуляция и является средством организации доступа к данным только через
соответствующие методы.
В нашем примере описание объекта процедура инициализации Init и
функции GetX и GetY уже не существуют как отдельные самостоятельные
объекты. Это неотъемлемые части объектного типа Location. Если в программе
имеется описание нескольких переменных указанного типа, то для каждой
переменной резервируется своя собственная область памяти для хранения
данных, а указатели на точки входа в процедуру и функции – общие. Вызов
каждого метода возможен только с помощью составного имени, явно
указывающего, для обработки каких данных предназначен данный метод.
5. Наследование
Наследование – это ещё одно базовое понятие объектно-ориентированного
программирования. Наследование позволяет определять новые объекты,
используя свойства прежних, дополняя или изменяя их. Объект-наследник
получает все поля и методы «родителя», к которым он может добавить свои
собственные поля и методы или заменить («перекрыть») их своими методами.
Пример описания объекта-наследника даётся ниже:
Tipe
Point = object(Location)
Visible: Boolean;
Procedure Int(IntX, IntY: Integer);
Procedure Show;
Procedure Hide;
Function IsVisible: Boolean;
Procedure MoveTo(NewX, NewY: Integer);
End;
Наследником здесь является объект Point, описывающий графическую
точку, а родителем – объект Location. Наследник не содержит описание полей
и методов родителя. Имя последнего указывается в круглых скобках после
слова object. Из методов наследника можно вызывать методы родителя. Для
создания наследника не требуется иметь исходный текст объекта родителя.
Объект-родитель может быть уже в составе оттранслированного модуля.
В чём привлекательность наследования? Если некий объект был уже
определён и отлажен, он может быть использован и в других программах. При
этом может оказаться, что новая задача отличается от предыдущей, и
возникает необходимость некоторой модификации как данных, так и методов их
обработки. Программисту приходится решать дилемму – создания объектов
заново или использовать результаты предыдущей работы, применяя механизм
наследования. Первый путь менее эффективен, так как требует дополнительных
затрат времени на отладку и тестирование. Во втором случае часть этой
работы оказывается выполненной, что сокращает время на разработку новой
программы. Программист при этом может и не знать деталей реализации объекта-
родителя.
В нашем примере к объекту, связанному с определением положения
графического элемента, просто добавилось новое поле, описывающее признак
видимости графической точки, и несколько новых методов, связанных с режимом
отображения точки и её преобразованиями.
6. Виртуальные методы
Наследование позволяет создавать иерархические, связанные отношениями
подчинения, структуры данных. Следует, однако, заметить, что при
использовании этой возможности могут возникнуть проблемы. Предположим, что
в нашей графической программе необходимо определить объект Circle, который
является потомком другого объекта Point:
Type
Circle = object (point)
Radius: Integer;
Procedure Show;
Procedure Hide;
Procedure Expand(ExpandBy: Integer);
Procedure Contact(ContactBy: Integer);
End;
Новый объект Circle соответствует окружности. Поскольку свойства
окружности отличаются от свойств точки, в объекте-наследнике придется
изменять процедуры Show и Hide, которые отображают окружность и удаляют её
изображение с экрана. Может оказаться, что метод Init (см. предыдущий
пример) объекта Circle, унаследованный от объекта Point, также использует
методы Show и Hide, впредь во время трансляции объекта Point использует
ссылки на старые методы. Очевидно в объекте Circle они работать не будут.
Можно, конечно, попытаться «перекрыть» метод Init. Чтобы это сделать, нам
придётся полностью воспроизвести текст метода. Это усложни работу, да и не
всегда возможно, поскольку исходного текста программы может не оказаться
под рукой (если объект-родитель уже находиться в оттранслированном модуле).
Для решения этой проблемы используется виртуальный метод. Связь между
виртуальным методом и вызывающими их процедурами устанавливается не во
время трансляции (это называется ранним связанием), а во время выполнения
программы (позднее связание.
Чтобы использовать виртуальный метод, необходимо в описании объекта
после заголовка метода добавить ключевое слово virtual. Заголовки
виртуальных методов родителя и наследника должны в точности совпадать.
Инициализация экземпляра объекта, имеющего виртуальные методы, должна
выполняться с помощью специального метода – конструктора. Конструктор
обычно присваивает полям объекта начальные значения и выполняет другие
действия по инициализации объекта. В заголовке метода-конструктора слово
procedure заменяется словом constructor. Действия обратные действиям
конструктора, выполняет ещё один специальный метод – деструктор. Он
описывается словом destructor.
Конструктор выполняет действия по подготовке позднего связывания. Эти
действия заключаются в создании указателя на таблицу виртуальных методов,
которая в дальнейшем используется для поиска методов. Таблица содержит
адреса всех виртуальных методов. При вызове виртуального метода по его
имени определяется адрес, а затем по этому адресу передается управление.
У каждого объектного типа имеется своя собственная таблица виртуальных
методов, что позволяет одному и тому же оператору вызывать разные
процедуры. Если имеется несколько экземпляров объектов одного типа, то
недостаточно вызвать конструктор для одного из них, а затем просто
скопировать этот экземпляр во все остальные. Каждый объект должен иметь
свой собственный конструктор, который вызывается для каждого экземпляра. В
противном случае возможен сбой в работе программы.
Заметим, что конструктор или деструктор, могут быть «пустыми», то есть
не содержать операторов. Весь необходимый код в этом случае создается при
трансляции ключевых слов construct и destruct.
7. Динамическое создание объектов
Переменные объектного типа могут быть динамическими, то есть
размещаться в памяти только во время их использования. Для работы с
динамическими объектами используются расширенный синтаксис процедур New и
Dispose. Обе процедуры в этом случае содержат в качестве второго параметра
вызов конструктора или деструктора для выделения или освобождения памяти
переменной объектного типа:
New(P, Construct)
или
Dispose(P, Destruct)
Где P – указатель на переменную объектного типа, а Construct или
Destruct – конструктор и деструктор этого типа.
Действие процедуры New в случае расширенного синтаксиса равносильно
действию следующей пары операторов:
New(P);
P^.Construct;
Эквивалентом Dispose является следующее:
P^Dispose;
Dispose(P)
Применение расширенного синтаксиса не только улучшает читаемость
исходного кода, но и генерирует более короткий и эффективный исполняемый
код.
8. Полиморфизм
Полиморфизм заключается в том, что одно и то же имя может
соответствовать различным действиям в зависимости от типа объекта. В тех
примерах, которые рассматривались ранее, полиморфизм проявлялся в том, что
метод Init действовал по-разному в зависимости от того, является объект
точкой или окружностью. Полиморфизм напрямую связан с механизмом позднего
связывания. Решение о том, какая операция должна быть выполнена в
конкретной ситуации, принимается во время выполнения программы.
Следующий вопрос, связанный с использованием объектов, заключается в
совместимости объектных типов. Полезно знать следующее. Наследник сохраняет
свойства совместимости с другими объектами своего родителя. В правой части
оператора присваивания вместо типов родителя можно использовать типы
наследника, но не наоборот. Таким образом, в нашем примере допустимы
присваивания:
Var
Alocation : Location;
Apoin : Point;
Acircle : Circle;
Alocation :=Apoint
Apoint := Acrcle;
Alocation := Acircle;
Дело в том, что наследник может быть более сложным объектом,
содержащим поля и методы, поэтому присваиваемые значения экземпляра объекта-
родителя экземпляру объекта-наследника может оставить некоторые поля
неопределёнными и, следовательно, представляет потенциальную опасность. При
выполнении оператора присвоения копируются только те поля данных, которые
являются общими для обоих типов.
-----------------------
[1] Выполняется на языке Turbo Pascal, начиная с версии 5.0. Далее все
примеры даны для выполнения на этом языке программирования.