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. Далее все
примеры даны для выполнения на этом языке программирования.