Краеугольные камни ООП
Что такое объект?
Объект — это самостоятельный фрагмент кода, который знает о себе и может рассказать об этом другим объектам, если они зададут вопрос, который он понимает.
Объект имеет свойства и методы, являющиеся вопросами, на которые он может ответить (даже если они не выглядят вопросами). Набор методов, на которые объект знает как реагировать, является его интерфейсом. Некоторые методы являются общедоступными (public), это означает, что другой объект может вызвать (или активизировать) их. Этот набор методов известен под названием public-интерфейс.
Когда один объект вызывает метод другого объекта, это называется передачей сообщения. Эта фраза соответствует ОО-терминологии, но чаще всего люди говорят Вызвать этот метод, а не Передать это сообщение. В следующем разделе мы рассмотрим концептуальный пример, который должен прояснить все это.
Концептуальный пример объекта
Предположим, что мы имеем объект Человек. Каждый объект Человек имеет имя, возраст, национальность и пол. Каждый объект Человек знает, как говорить и ходить. Один объект может спросить у другого о его возрасте, или может cказать, чтобы другой объект начал (или закончил) перемещение. В терминах программирования вы можете создать объект Person и назначить ему некоторые переменные (например, имя и возраст). Если вы создали второй объект Person, он может спросить у первого его возраст или сказать ему начать перемещение. Он может сделать это путем вызова методов первого объекта Person.
Обычно концепция объекта остается неизменной и в языке Deplhi и в других объектно-ориентированных языках программирования, хотя реализуют они ее по-разному. Эта концепция универсальна. По этой причине объектно-ориентированные программисты, независимо от применяемого ими языка, общаются не так, как процедурные программисты. Процедурные программисты часто говорят о функциях и модулях. Объектно-ориентированные программисты говорят об объектах и часто говорят о них, используя личные местоимения. Вы часто можете услышать, как один ОО-программист говорит другому: Этот объект Supervisor говорит здесь объекту Employee Дай мне свой ID, поскольку он должен назначить задания для Employee.
Процедурные программисты могут считать такой способ мышления странным, но он является естественным для ОО-программистов. В их программном мире все является объектом (с некоторыми исключениями в языке Delphi), а программы представляют собой взаимодействие (или разговор) объектов между собой.
Объект = Данные + Операции
На основании этой формулы была разработана методология объектно-ориентированного программирования (ООП).
Природа объекта
В общем случае каждый объект помнит необходимую информацию, умеет выполнять некоторый набор действий и характеризуется набором свойств. То, что объект помнит, хранится в его полях. То, что объект умеет делать, реализуется в виде его внутренних процедур и функций, называемых методами. Свойстваобъектов аналогичны свойствам, которые мы наблюдаем у обычных предметов. Значения свойств можно устанавливать и читать. Программно свойства реализуются через поля и методы.
***Рисунок-представление объекта
Например, объект кнопка имеет свойство цвет. Значение цвета кнопка запоминает в одном из своих полей. При изменении значения свойства цвет вызывается метод, который перерисовывает кнопку.
Кстати, этот пример позволяет сделать важный вывод: свойства имеют первостепенное значение для программиста, использующего объект. Чтобы понять суть и назначение объекта вы обязательно должны знать его свойства, иногда — методы, очень редко — поля (объект и сам знает, что с ними делать).
Понятие класса объектов
В объектных системах объекты одного типа принято объединять в классы объектов. Каждый объект всегда принадлежит некоторому классу объектов. Класс объектов — это обобщенное (абстрактное) описание множества однотипных объектов.
Объекты являются конкретными представителями своего класса, их принято называть экземплярами класса. Например, с точки зрения объектной технологии, каждый человеческий индивидуум – это экземпляр класса «Человек».
Класс объектов определяет свойства и методы, то есть тот интерфейс, который является единым для всех его экземпляров. Это определение устанавливает своего рода рамки, которые каждый экземпляр заполняет своими индивидуальными значениями. Стало быть, экземпляры имеют тип соответствующего класса.
Экземпляры одного класса могут отличаться лишь значениями своих свойств, но не своими методами. Методы устанавливаются для всех экземпляров при определении класса. Отсюда следует, что все члены одного класса обнаруживают не только идентичный интерфейс, но и идентичное «поведение».
В качестве примера представим себе класс «Треугольник», обладающий такими методами, как «Нарисовать», «Переместить», «Удалить», а также свойствами «Позиция», «Размер», «Цвет». Для этого класса определены методы и написан соответствующий код, с помощью которого треугольник рисуется, удаляется или перемещается с одной позиции на другую. Отдельные экземпляры обладают значениями свойств: определенной позицией, размером, цветом. Если требуется удалить какой-нибудь треугольник, пользуются его методом «Удалить». Хотя этот метод и определен в классе объектов, он является методом экземпляров, действующим для каждого экземпляра в отдельности, и относится непосредственно к тому треугольнику, для которого вызывается.
***Рисунок-класс и его экземпляры
Три кита ООП
Весь мир ООП держится на трех китах: инкапсуляции, наследовании и полиморфизме.
Объединение данных и операций в одну сущность — объект — тесно связано с понятием инкапсуляции. Инкапсуляция означает, что для внутренней реализации справедлив принцип сокрытия информации, согласно которому ни программный код, ни поля не выставляются на всеобщее обозрение и не могут использоваться за пределами объекта.
В противоположность этому объекты имеют открытый интерфейс, описываемый в виде набора свойств и методов. Методы определяют поведение объекта, а свойства – его «знания», иными словами, данные.
Второй кит ООП — наследование. Этот простой принцип означает, что если вы хотите создать новый класс объектов, который расширяет возможности уже существующего класса, то нет необходимости в переписывании заново всех полей, методов и свойств. Вы объявляете, что новый класс является потомком (или дочерним классом) имеющегося класса объектов, называемого предком (или родительским классом), и добавляете к нему новые поля, методы и свойства. Процесс порождения новых классов на основе других классов называется наследованием. Новые классы объектов имеют все унаследованные признаки, так и, возможно, новые.
Третий кит — это полиморфизм. Он означает, что в производных классах вы можете изменять работу уже существующих в базовом классе методов. При этом весь программный код, управляющий объектами родительского класса, пригоден для управления объектами дочернего класса без всякой модификации. Например, вы можете породить новый класс кнопок с рельефной надписью, переопределив метод рисования кнопки.
Объекты и компоненты
Когда прикладные программы были консольно-ориентированными, а пользовательский интерфейс был простым, объекты казались пределом развития программирования, поскольку были идеальным средством разбиения сложных задач на простые подзадачи. Однако с появлением графических систем программирование пользовательского интерфейса резко усложнилось. Программист в какой-то мере стал дизайнером, а визуальная компоновка и увязка элементов пользовательского интерфейса (кнопок, меток, строк редактора) начали отнимать основную часть времени. И тогда программистам пришла в голову идея визуализировать объекты, объединив программную часть объекта с его видимым представлением на экране дисплея в одно целое. То, что получилось в результате, было названо компонентом.
Компоненты в среде Delphi — это особые объекты, которые являются строительными кирпичиками визуальной среды разработки и приспособлены к визуальной установке свойств. Чтобы превратить объект в компонент, первый разрабатывается по определенным правилам, а затем помещается в палитру компонентов. Конструируя приложение, вы берете компоненты из Палитры Компонентов, располагаете на форме и устанавливаете их свойства в окне Инспектора Объектов. Внешне все выглядит просто, но чтобы достичь такой простоты, потребовалось создать механизмы, обеспечивающие функционирование объектов-компонентов уже на этапе проектирования приложения! Все это было придумано и блестяще реализовано в среде Delphi. Таким образом, компонентный подход значительно упростил создание приложений с графическим пользовательским интерфейсом и дал толчок развитию новой индустрии компонентов.
Cсейчас мы рассмотрим лишь вопросы создания и использования объектов, а потом научимся превращать объекты в компоненты.
Классы
Понятие класса
Для поддержки ООП в язык Delphi введены объектные типы данных, с помощью которых одновременно описываются данные и операции над ними. Объектные типы данных называют классами, а их экземпляры — объектами.
Классы объектов определяются в секции type глобального блока.
Описание класса начинается с ключевого слова class и заканчивается ключевым словом end. По форме объявления классы похожи на обычные записи, но помимо полей данных могут содержать объявления пользовательских процедур и функций. Такие процедуры и функции обобщенно называют методами, они предназначены для выполнения над объектами различных операций.
Общий синтаксис объявления класса:
type className = class (ancestorClass) memberListend; |
MemberList служит для описания полей, методов и свойств.
Или:
Type TClassName = class(TParentClass) private … { private declarations here} protected … { protected declarations here } public … { public declarations here } published … { published declarations here } end; |
Замечания:
1. Если TparentClass не указан, то подразумевается Tobject, включая базовые constructor и destructor.
2. Методы описываются своим заголовком без тела подпрограммы.
3. При описании полей, свойств и методов можно разграничить доступ к этим атрибутам класса специальных ключевых слов: private, protected, public, published.
- Private. Все, что объявлено в секции private недоступно за пределами модуля. Секция private позволяет скрыть те поля и методы, которые относятся к так называемым особеностям реализации.
- Public. Поля, методы и свойства, объявленные в секцииpublic не имеют никаких ограничений на использование, т.е. всегда видны за пределами модуля. Все, что помещается в секцию public, служит для манипуляций с объектами и составляет программный интерфейс класса.
- Protected. Поля, методы и свойства, объявленные в секцииprotected, видны за пределами модуля только потомкам данного класса; остальным частям программы они не видны. Так же как и private, директива protected позволяет скрыть особенности реализации класса, но в отличие от нее разрешает другим программистам порождать новые классы и обращаться к полям, методам и свойствам, которые составляют так называемый интерфейс разработчика. В эту секцию обычно помещаются виртуальные методы.
- Published. Устанавливает правила видимости те же, что и директиваpublic. Особенность состоит в том, что для элементов, помещенных в секцию published, компилятор генерирует информацию о типах этих элементов. Эта информация доступна во время выполнения программы, что позволяет превращать объекты в компоненты визуальной среды разработки. Секцию published разрешено использовать только тогда, когда для самого класса или его предка включена директива компилятора $TYPEINFO.
4. Перечисленные секции могут чередоваться в объявлении класса в произвольном порядке, однако в пределах секции сначала следует описание полей, а потом методов и свойств.
5. Если в определении класса нет ключевых слов private, protected, public и published, то для обычных классов всем полям, методам и свойствам приписывается атрибут видимости public, а для тех классов, которые порождены от классов библиотеки VCL, — атрибут видимостиpublished.
6. Внутри модуля никакие ограничения на доступ к атрибутам классов, реализованных в этом же модуле, не действуют.
Приведем пример объявления класса:
TYPE TCoordinates=record X,Y:Integer; end; Tfigure = classprivateFcolor:TColor;Coords:TCoordinates; ProtectedProcedure setColor(c:TColor); virtual;Procedure Draw; virtual; abstract;Procedure Hide; virtual; abstract;Procedure Move(NewX,NewY:Integer);Property Color: TColor read Fcolor write setcolor;End; |
Класс содержит поля (Fcolor, Coords) и методы (Draw, Hide, Move). Заголовки методов, (всегда) следующие за списком полей, играют роль упреждающих (forward) описаний. Программный код методов пишется отдельно от определения класса.
В некоторых случаях требуется, чтобы объекты разных классов содержали ссылки друг на друга. Возникает проблема: объявление первого класса будет содержать ссылку на еще не определенный класс. Она решается с помощью упреждающего объявления:
type TReadersList = class; // упреждающее объявление класса TReadersList TDelimitedReader = class Owner: TReadersList; … end; TReadersList = class Readers: array of TDelimitedReader; … end; |
Первое объявление класса TDelimitedReader называется упреждающим (от англ. forward). Оно необходимо для того, чтобы компилятор нормально воспринял объявление поля Owner в классе TDelimitedReader.
Классы в программных модулях
Классы очень удобно собирать в модули. При этом их описание помещается в секцию interface, а код методов — в секцию implementation. Создавая модули классов, нужно придерживаться следующих правил:
- все классы, предназначенные для использования за пределами модуля, следует определять в секции interface;
- описание классов, предназначенных для употребления внутри модуля, следует располагать в секции implementation;
- если модуль B использует модуль A, то в модуле B можно определять классы, порожденные от классов модуля A.
Объекты
Чтобы от описания класса перейти к объекту, следует выполнить соответствующее объявление в секции var:
var Figure: TFigure; |
При работе с обычными типами данных этого объявления было бы достаточно для получения экземпляра типа. Однако объекты в среде Delphi являются динамическими данными, т.е. распределяются в динамической памяти. Поэтому переменная Figure — это просто ссылка на экземпляр (объект в памяти), которого физически еще не существует.
Чтобы сконструировать объект (выделить память для экземпляра) класса TFigure и связать с ним переменную Figure, нужно в тексте программы поместить следующий оператор:
Figure := TFigure.Create; |
Create — это так называемый конструктор объекта; он всегда присутствует в классе и служит для создания и инициализации экземпляров. При создании объекта в памяти выделяется место только для его полей. Методы, как и обычные процедуры и функции, помещаются в область кода программы; они умеют работать с любыми экземплярами своего класса и не дублируются в памяти.
После создания объект можно использовать в программе: получать и устанавливать значения его полей, вызывать его методы. Доступ к полям и методам объекта происходит с помощью уточненных имен, например:
Figure.Move; |
Кроме того, как и при работе с записями, допустимо использование оператора with, например:
with Figure do Move; |
Если объект становится ненужным, он должен быть удален вызовом специального метода Destroy, например:
Figure.Destroy; // Освобождение памяти, занимаемой объектом |
Destroy — это так называемый деструктор объекта; он присутствует в классе наряду с конструктором и служит для удаления объекта из динамической памяти. После вызова деструктора переменная Figure становится несвязанной и не должна использоваться для доступа к полям и методам уже несуществующего объекта. Чтобы отличать в программе связанные объектные переменные от несвязанных, последние следует инициализировать значением nil. Например, в следующем фрагменте обращение к деструктору Destroy выполняется только в том случае, если объект реально существует:
Figure := nil;…if Figure nil then Figure.Destroy; |
Вызов деструктора для несуществующих объектов недопустим и при выполнении программы приведет к ошибке.
Чтобы избавить программистов от лишних ошибок, в объекты ввели предопределенный метод Free, который следует вызывать вместо деструктора. Метод Free сам вызывает деструктор Destroy, но только в том случае, если значение объектной переменной не равно nil. Поэтому последнюю строчку в приведенном выше примере можно переписать следующим образом.
Figure.Free; |
После уничтожения объекта переменная Figure сохраняет свое значение, продолжая ссылаться на место в памяти, где объекта уже нет. Если эту переменную предполагается еще использовать, то желательно присвоить ей значение nil, чтобы программа могла проверить, существует объект или нет. Таким образом, наиболее правильная последовательность действий при уничтожении объекта должна быть следующая:
Figure.Free;Figure := nil; |
С помощью стандартной процедуры FreeAndNilэто можно сделать проще и элегантнее:
FreeAndNil(Figure); // Эквивалентно: Figure.Free; Figure := nil; |
Значение одной объектной переменной можно присвоить другой. При этом объект не копируется в памяти, а вторая переменная просто связывается с тем же объектом, что и первая:
var R1, R2: TFigure; // Переменные R1 и R2 не связаны с объектомbegin R1 := TFigure.Create; // Связывание переменной R1 с новым объектом // Переменная R2 пока еще не связана ни с каким объектом R2 := R1; // Связывание переменной R2 с тем же объектом, что и R1 // Теперь обе переменные связаны с одним объектом R2.Free; // Уничтожение объекта // Теперь R1 и R2 не связаны ни с каким объектом end; |
Объекты могут выступать в программе не только в качестве переменных, но также элементов массивов, полей записей, параметров процедур и функций. Кроме того, они могут служить полями других объектов. Во всех этих случаях программист фактически оперирует указателями на экземпляры объектов в динамической памяти. Следовательно, объекты изначально приспособлены для создания сложных динамических структур данных, таких как списки и деревья. Указатели на объекты для этого не нужны.
Итак, вы уже имеете некоторое представление об объектах, перейдем теперь к вопросу реализации их методов.
Методы
Понятие метода
Процедуры и функции, предназначенные для выполнения над объектами действий, называются методами. Предварительное объявление методов выполняется при описании класса в секции interface модуля, а их программный код записывается в секции implementation.
Однако в отличие от обычных процедур и функций заголовки методов должны иметь уточненные имена, т.е. содержать наименование класса. Приведем пример реализацию одного из методов в классе TFigure:
Procedure TFigure.Move (NewX,NewY:Integer);BeginHide;Coords.X:=NewX;Coords.Y:=NewY;Draw;End; |
Обратите внимание, что внутри методов обращения к полям и другим методам выполняются как к обычным переменным и подпрограммам без уточнения экземпляра объекта. Такое упрощение достигается путем использования в пределах метода псевдопеременной Self(стандартный идентификатор). Физически Self представляет собой дополнительный неявный параметр, передаваемый в метод при вызове. Этот параметр и указывает экземпляр объекта, к которому данный метод (Move) применяется.
Практика показывает, что псевдопеременная Self редко используется в явном виде. Ее необходимо применять только тогда, когда при написании метода может возникнуть какая-либо двусмысленность для компилятора, например при использовании одинаковых имен и для локальных переменных, и для полей объекта.