Принципы объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм

В основе классов лежат три фундаментальных принци­па - инкапсуляция, наследование и полиморфизм.

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

Принцип инкапсуляции использовался в технологии модульно­го программирования. В модуле в явной форме введена инкапсуля­ция путем разделения его на секции интерфейса и реализации.

В объектно-ориентированном программировании принцип ин­капсуляции используется для изоляции класса от остальных частей программы, чтобы сделать его самодостаточным для решения кон­кретной задачи. Например, класс 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 разрешается многократно объявлять любую секцию, причем порядок следования секций не имеет значения. Любая секция может быть пустой.

ПРИНЦИПЫ РАБОТЫ

Наши рекомендации