Принципы объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм
В основе классов лежат три фундаментальных принципа - инкапсуляция, наследование и полиморфизм.
Инкапсуляция. Проектирование программных и технических систем базируется на том условии, что никакая подсистема данного уровня не должна зависеть от устройства любой другой подсистемы этого уровня. Такая независимость внутреннего устройства одного объекта от внутреннего устройства другого называется инкапсуляцией.
Принцип инкапсуляции использовался в технологии модульного программирования. В модуле в явной форме введена инкапсуляция путем разделения его на секции интерфейса и реализации.
В объектно-ориентированном программировании принцип инкапсуляции используется для изоляции класса от остальных частей программы, чтобы сделать его самодостаточным для решения конкретной задачи. Например, класс TForm в среде Delphi содержит (инкапсулирует в себе) все необходимое для создания Windows-окна, класс ТМето представляет собой полнофункциональный текстовый редактор, класс TTimer обеспечивает работу программы с таймером и т. д.
Инкапсуляция достигается путем совмещения в одной записи языка программирования структур данных с процедурами и функциями, которые манипулируют полями данных этой записи, для получения нового типа данных ~ класса. Инкапсуляция позволяет защитить по интерфейсу доступ к полям и методам. Доступ разрешается лишь к открытым методам и полям. Полная совокупность методов и тонкости их реализаций являются скрытыми.
type
TMyClass = class
IntField: Integer;
function MyFunc(a: Integer): Integer;
procedure MyProc; end;
Путем использования принципа инкапсуляции появляется возможность осуществлять обмен готовыми к работе программными заготовками. Например, библиотека классов Delphi - это, фактически, набор кирпичиков для построения прикладных программ.
Наследование. Число абстракций в сложных программных системах намного превышает наши возможности их осознания. Инкапсуляция частично помогает устранить это препятствие, убирая из поля зрения внутреннее содержание абстракций.
Однако значительное упрощение понимания сложных задач достигается за счет усложнения иерархии. Под иерархией здесь понимается упорядочение абстракций, расположение их по уровням. Усложнение иерархии от уровня к уровню достигается за счет наследования.
Принцип наследования оперирует с понятиями «предок - потомок» и предусматривает расширение набора свойств наследника за счет принятия всех свойств предка.
Любой класс может быть порожден от другого класса. Для этого при его объявлении указывается имя класса-родителя:
TChildCIass = class (TParentClass )
Порожденный класс автоматически наследует поля, методы и свойства своего родителя и может добавлять их новыми. Таким образом, принцип наследования обеспечивает поэтапное создание сложных классов и разработку собственных библиотек классов.
Все классы в Object Pascal порождены от единственного родителя - класса TObject. Этот класс не имеет полей и свойств, но включает в себя методы самого общего назначения, обеспечивающие весь жизненный цикл любых объектов - от их создания до уничтожения. Поэтому программист не может создать класс, который не был бы дочерним классом TObject. Следующие два объявления идентичны.
TaClass = class(TObject) <==> TaClass = class
Принцип наследования приводит к созданию ветвящегося дерева классов. Каждый потомок дополняет возможности своего родителя новыми и передает их своим потомкам. Например, класс TPersistent обогащает возможности своего родителя TObject тем, что он умеет сохранять данные в файле и получать их из него, в результате это умеют делать и все его потомки. Класс TComponent, в свою очередь, умеет взаимодействовать со средой разработчика и передает это умение своим потомкам. TControl не только способен работать с файлами и средой разработчика, но он еще умеет создавать и обслуживать видимые на экране изображения, а его потомок TWinControl может создавать Windows-окна и т. д.
В Object Pascal возможно только так называемое одиночное наследование, но в реальном мире у потомка два родителя, поэтому в ряде языков (например, в C++) предусмотрен механизм множественного наследования. Множественное наследование более логично с точки зрения моделирования реального мира, однако, оно усложняет реализацию языков программирования.
Полиморфизм. Одним из базовых понятий технологии объектно-ориентированного программирования является полиморфизм. Этот термин имеет греческое происхождение и приблизительно означает «много форм» (poly — много, morphos — форма).
Полиморфизм - это средство для придания различных значений одному и тому же событию в зависимости от типа обрабатываемых данных. Этот принцип определяет различные формы реализации одноименного действия.
Целью полиморфизма является использование одного имени для задания общих для класса действий, причем каждый объект или класс иерархии имеет возможность по-своему реализовать это действие своим собственным, подходящим для него, кодом. Таким образом, полиморфизм является свойством классов решать схожие по смыслу проблемы разными способами.
В рамках Object Pascal поведенческие свойства класса определяются набором входящих в него методов. Этот принцип используется, когда требуется расширить свойства класса не путем добавления новых методов, а путем достраивания одного из методов или набора методов. Изменяя алгоритм того или иного метода в потомках класса, программист может придавать этим потомкам отсутствующие у родителя специфические свойства.
Для изменения метода необходимо перекрыть его в потомке, т. е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода, имеющие различную алгоритмическую основу и придающие объектам разные свойства. В этом сущность полиморфизма объектов.
Кроме этого, в Object Pascal полиморфизм достигается не только механизмом наследования и перекрытия методов родителя, но и их виртуализацией, позволяющей родительским методам обращаться к методам своих потомков.
ПОЛЯ
Класс представляет собой единство трех сущностей -полей, методов и свойств. Объединение этих сущностей в единое целое достигается за счет применения инкапсуляции.
Полями называются инкапсулированные в классе данные. По аналогии с описанием переменных, описание поля указывает идентификатор, именующий поле и тип данного этого поля. Поля могут быть любого типа, в том числе классами, например:
type
TMyClass = class
aIntField: Integer; aStrField: String; aObjField: TObject;
end;
Каждый объект получает уникальный набор полей, но общий для всех объектов данного класса набор методов и свойств. Фундаментальный принцип инкапсуляции требует обращаться к полям только с помощью методов и свойств класса. Однако в Object Pascal разрешается обращаться к полям и напрямую. Для этого используются составные имена полей, содержащие имя объекта в качестве префикса.
type
TMyClass = class
aIntField: Integer; aStrField: String;
end; var
aObject: TMyClass; begin
aObject.aIntField : = 0;
aObject.aStrField : = 'Строка символов';
end;
Класс-потомок получает все поля своих предков и может дополнять их своими, но он не может переопределять их или удалять. Таким образом, чем ниже в дереве иерархии располагается класс, тем больше данных получают в свое распоряжение его объекты.
МЕТОДЫ
Методы представляют собой процедуры и функции, принадлежащие заданному объекту. Поэтому методы определяют поведение объекта. Для класса можно самостоятельно создать произвольное количество любых методов, необходимых для решения конкретных задач. Создание метода осуществляется в два этапа. Сначала следует описать метод в объявлении типа, а затем создать текст его реализации. Вот пример описания и определения метода:
type
TMyClass = class Work: Boolean; procedure DoWork; end;
procedure TMyClass.DoWork; begin
Work := True; end;
При определении тела метода необходимо использовать его полное имя с указанием класса. Вторая важная деталь - к любому полю объекта его метод может обратиться непосредственно. Доступ к методам класса, как и к его полям, возможен с помощью составных имен.
var
aObject: TMyClass; begin
aObject.DoWork;
end;
Статические методы. Как уже говорилось, методы класса могут перекрываться в потомках. Например:
type
TParentClass = class
procedure DoWork; end;
TChildClass = class(TparentClass)
procedure DoWork; end;
Потомки обоих классов могут выполнять сходную по названию процедуру DoWork, но, в общем случае, будут это делать по-разному. Такое замещение методов называется статическим, так как реализуется компилятором.
Статический метод DoWork работает подобно обычной процедуре или функции. Этот тип методов принимается по умолчанию. Адрес такого метода известен уже на стадии компиляции, и компилятор в тексте программы оформляет все вызовы данного метода как статические. Такие методы работают быстрее других, однако не могут быть перегружены для поддержки полиморфизма объектов.
Динамические и виртуальные методы. В Object Pascal гораздо чаще используется динамическое замещение методов на этапе прогона программы. Для реализации этого, метод, замещаемый в родительском классе, должен объявляться как динамический (с директивой dynamic) или виртуальный (virtual). тив такое объявление, компилятор создаст две таблицы - DMT (Dynamic Method Table) и VMT (Virtual Method Table) и поместит в них адреса точек входа соответственно динамических и виртуальных методов.
При каждом обращении к замещаемому методу компилятор вставляет код, позволяющий извлечь адрес точки входа в подпрограмму из той или иной таблицы. В классе потомке замещающий метод объявляется с директивой override, которая вызывает замещение строки описания исходного метода в VMT строкой описания нового метода. Получив такое указание, компилятор создаст код, который на этапе прогона программы поместит в родительскую таблицу точку входа метода класса-потомка, что позволит родителю выполнить нужное действие с помощью нового метода.
Например, родительский класс с помощью методов Show и Hide соответственно показывает что-то на экране или прячет изображение. Для создания изображения он использует метод Draw с логическим параметром:
type
TVisualObject = class(TWinControl)
procedure Hide;
procedure Show;
procedure Draw(IsShow: Boolean); virtual; end; TVisualChildObject = class(TVisualObject)
procedure Draw(IsShow: Boolean); override; end;
Реализация методов Show и Hide выглядит следующим образом:
procedure TVisualObject.Show; begin
Draw(True); end;
procedure TVisualObject.Hide begin
Draw(False); end;
Методы Draw у родителя и потомка имеют разную реализацию и создают разные изображения. В результате родительские методы Show и Hide будут прятать или показывать те или иные изображения в зависимости от конкретной реализации метода Draw у любого из своих потомков. Таким образом, динамическое связывание в полной мере реализует полиморфизм классов.
Разница между динамическими и виртуальными методами состоит в том, что таблица динамических методов DMT содержит адреса только тех методов, которые объявлены как dynamic в данном классе, в то время как таблица VMT содержит адреса виртуальных методов не только данного класса, но и всех его родителей. Большая по размеру таблица VMT обеспечивает более быстрый поиск, в то время как при обращении к динамическому методу программа сначала просматривает таблицу DMT у объекта, затем - у его родительского класса и так далее, пока не будет найдена нужная точка входа.
Конструкторы и деструкторы. В Object Pascal объекты создаются с помощью вызова одного из конструкторов этого объекта. Конструктор отвечает за создание объекта, а также за выделение памяти и необходимую инициализацию полей. Он распределяет объект в динамической памяти и помещает адрес этой памяти в переменную Self, которая автоматически объявляется в классе. Конструктор не только создает объект, но и приводит его в состояние, необходимое для его дальнейшего использования.
У класса TObject конструктор называется Create(), так же он называются в подавляющем большинстве его потомков. Каждый объект содержит, по крайней мере, один такой конструктор, который может иметь различное число параметров разного типа — в зависимости от типа объекта.
Функцией деструктора является удаление объекта из памяти. По своей форме конструкторы и деструкторы являются процедурами, но объявляются с помощью зарезервированных слов Constructor и Destructor:
type
TMyClass = class
IntField: Integer;
Constructor Create(Value: Integer); Destructor Destroy; end;
В отличие от C++, в Object Pascal конструкторы автоматически не вызываются. Каждый объект создается с помощью вызова его конструктора. Обращение к конструктору должно предварять любое обращение к полям и некоторым методам объекта. Синтаксис вызова конструктора следующий:
Var
MyObject: TMyClass; begin
MyObject.IntField := 0; {Ошибка! Объект не создан
конструктором}
MyObject := TMyClass.Create; //Нужно создать объект MyObject.IntField := 0; //и обратиться к его полю
MyObject.Free; //Уничтожаем ненужный объект
end;
Особенность вызова конструктора заключается в том, что он вызывается с помощью ссылки на класс, а не на объект. В этом заложен глубокий смысл, так как экземпляр класса в момент вызова конструктора еще не создан. Однако код конструктора класса TMyClass находится в памяти и поэтому такой вызов вполне корректен.
При создании объекта с помощью конструктора компилятор гарантирует, что все поля объекта будут инициализированы. Все числовые поля будут обнулены, указатели примут значение Nill, а строки будут пусты.
С помощью метода Free() освобождается выделенная для объекта память после его использования. Этот метод проверяет, не равен ли объект значению Nill, и затем вызывает деструктор объекта - метод Destroy(), который освобождает всю выделенную память и выполняет другие действия по освобождению захваченных конструктором объекта ресурсов. В отличие от вызова конструктора, вызов метода Free() выполняется с помощью ссылки на экземпляр, а не на класс.
Большинство конструкторов реализует некоторые действия, необходимые для правильной работы объекта. Поэтому в конструкторе класса потомка следует сначала вызвать конструктор своего родителя, а затем уже осуществлять дополнительные действия. Вызов любого метода родительского класса достигается с помощью ключевого слова Inherited (унаследованный). Например:
Constructor TmyClass.Create(Value: Integer);
//Возможная реализация конструктора
begin
Inherited Create; {Вызываем унаследованный
конструктор}
IntField := Value; {Реализуем дополнительные действия} end;
В большинстве примеров, поставляемых вместе с Delphi, нет вызовов конструкторов и деструкторов. Это объясняется тем, что любой компонент, попавший при визуальном проектировании в приложение из палитры компонентов, включается в определенную иерархию, которая замыкается на форме TForm, а для всех ее составных частей конструкторы и деструкторы вызываются автоматически. Создает и уничтожает формы глобальный объект с именем Application. Для объектов, создаваемых динамически (во время выполнения приложения), обязательно нужен вызов конструктора.
СВОЙСТВА
Свойства объекта представляют собой специальный механизм классов, регулирующий доступ к полям объекта. По отношению к компонентам свойства являются теми элементами, сведения о которых отображаются в окне Object Inspector.
Свойства объявляются с помощью зарезервированных слов property, read и write. Слова read и write считаются зарезервированными только в контексте объявления свойства. Обычно свойство связано с некоторым полем и указывает те методы класса, которые должны использоваться при записи в это поле или при чтении из него. Например:
type
TaClass = class
IntField: Integer;
function GetField: Integer;
procedure SetField(Value: Integer);
property IntegerValue: Integer read GetField
write SetField; end;
В контексте программы свойство ведет себя как обычное поле. Поэтому возможен следующий оператор присваивания:
aClass.IntField := NewValue;
Разница между этим оператором и оператором aCIass.IntegerValue := NewValue;
заключается в том, что при обращении к свойству автоматически подключается метод SetField, в котором могут реализовываться специфические действия.
Когда нет необходимости в специальных действиях при чтении или записи свойства, вместо имени соответствующего метода можно указывать имя поля. Если нужно, чтобы поле было доступно только для чтения или только для записи, следует опустить соответственно часть write или read.
Вся эта технология имеет два основных преимущества. Во-первых, она позволяет представить конечному пользователю некий интерфейс, полностью скрывающий реализацию объекта и обеспечивающий контроль за доступом к объекту. Во-вторых, она дает программисту возможность замещать методы в классах-потомках с целью обеспечения полиморфного поведения объектов.
4.7. ОПРЕДЕЛЕНИЕ ОБЛАСТИ ВИДИМОСТИ КЛАССА
Object Pascal предоставляет дополнительный контроль над доступностью членов классов (полей и методов) с помощью директив private, protected, public, published. Синтаксис использования этих директив следующий:
type
TMyClass = class private
AprivateVariable: Integer; AnotherPrivateVariable: Boolean; protected
procedure AProtectedProcedure; function ProtectMe: Byte; public
constructor APublicContructor; destructor APublicKiller; published property AProperty
read AprivateVariable write APrivateVariable; end;
За каждой из директив может следовать любое необходимое количество объявлений полей или методов. Эти директивы имеют следующий смысл.
private - эта часть объекта доступна только для кода, находящегося в одном модуле с другим кодом данного объекта. Директива private скрывает особенности реализации объекта от пользователей и защищает члены этого объекта от непосредственного доступа и изменения извне.
protected - члены объекта, описанные в этой секции, доступны для производных объектов, что позволяет скрыть внутреннее устройство объекта от пользователя и в то же время обеспечить необходимую гибкость и эффективность доступа к полям и методам объекта для его потомков.
public - описанные подобным образом члены объекта доступны в любом месте данной программы. В этой секции всегда описываются конструкторы и деструкторы.
published - для этой части объекта при компиляции будет сгенерирована информация о типе времени исполнения. Это даст возможность другим частям приложения получать информацию о части объекта, описанной в этой секции. В частности, подобная информация используется утилитой Object Inspector при построении списков свойств объектов.
В Object Pascal разрешается многократно объявлять любую секцию, причем порядок следования секций не имеет значения. Любая секция может быть пустой.
ПРИНЦИПЫ РАБОТЫ