BiVANT   Книги   К началу главы

В.А. Биллиг
VBA в Office 2000
Офисное программирование
"Русская редакция", 1999 г.

Глава 5(3)

Абстрактные классы

Виртуальный метод называется чистым, если в классе не определена его реализация. Класс называется абстрактным, если в нем объявлен один или более чистых методов. Из этого определения следует, что абстрактный класс не полностью определен, и потому работать с объектами абстрактного класса невозможно. Это, конечно, не означает, что нельзя объявлять объекты такого класса, это делать можно и нужно. Вся прелесть в том и состоит, что, перед тем как начать работать с таким родительским объектом, он связывается с одним из своих потомков ¾ объектом, класс которого определен полностью. Абстрактные классы - важный механизм создания семейства классов. Как правило, абстрактные классы выступают в роли прародителей семейства классов, в котором многие классы имеют подобные свойства и поведение, отличаясь рядом деталей. Абстрактный класс позволяет задать общий интерфейс семейства, предопределяя, что все потомки должны уметь реагировать на вызов общих методов, хотя эта реакция может быть и разной. Благодаря свойствам, определенных абстрактным классом, все потомки позволяют задавать значения некоторых общих свойств. Итак, назначение абстрактного класса в том, что он задает общие свойства и общие виртуальные методы, которыми должны обладать его потомки. Именно потомки будут определять реализацию чистых виртуальных методов. Но эти методы предусмотрены и запроектированы на самом верхнем уровне.

Наследование и полиморфизм в Office 2000

В двух предыдущих пунктах мы рассмотрели довольно бегло общие понятия наследования и полиморфизма. Теперь же попробуем понять, как эти понятия реализованы в Office 2000. Прежде всего, заметим, что полиморфизм семейства классов реализован в полном объеме, а наследование, как ни странно, лишь частично. Начнем наше рассмотрение с наследования интерфейсов.

Наследование интерфейсов

В Office 2000 на классах введено отношение «наследование интерфейса». Здесь под интерфейсом понимается совокупность всех открытых (public) свойств и методов класса. Пусть уже создан класс A, который будем называть родительским или базовым. Тогда при создании нового класса B, который будем называть классом-потомком, можно объявить, что потомок наследует интерфейс родительского класса. Заметьте, что это отношение в отличие от «классического» наследования не транзитивно, ¾ потомок класса B не наследует интерфейс класса A - родителя класса B. Однако допускается множественное наследование интерфейсов, потомок может иметь несколько родителей, ¾ наследовать интерфейсы нескольких классов. Говоря о родительском классе, следует отметить три возможности:

·   Родительский класс может быть полностью абстрактным классом, то есть все его открытые методы будут чистыми, не имеющими реализации.

·    Родительский класс может быть полностью определенным классом с заданной реализацией методов.

·    Зачастую, родительский класс является абстрактным классом, где часть методов, реализация которых предполагается одинаковой для большинства классов - потомков, задана, а некоторые методы являются чистыми.

Синтаксически, объявить о том, что класс наследует интерфейс другого класса, достаточно просто. Для этого достаточно в объявление класса поместить одну строчку:

Implements имя_родительского_класса

При появлении такой строки класс- потомок наследует интерфейс родительского класса. Это означает, что будут наследоваться все открытые методы, а для каждого открытого свойства, будет наследоваться пара процедур - свойств Property Get и Property Let. Сами свойства наследоваться не будут. Как только в раздел объявлений класса вставлена строка “Implements”, в окне кода этого класса появится список методов родительского класса, реализацию которых предстоит написать. Для того чтобы задать реализацию этих методов, используется привычная техника первоначального создания заготовок методов с последующим наполнением их содержательным текстом.

Важно понимать, что наследование интерфейсов не означает наследование реализации методов. Это просто некоторый контракт между родителем и потомком. Потомок обязуется реализовать все открытые методы своего родителя, а для открытых свойств соответствующие процедуры - свойства. Заметьте, реализованы должны быть все методы, хотя реализация может быть фиктивной и свестись к заданию некоторого комментария.

Если соответствующий метод родительского класса является чистым, то понятно, что потомок обязан дать собственную реализацию метода. Собственная реализация необходима и в том случае, если потомок хочет переопределить существующий метод родителя. Во многих случаях хотелось бы просто наследовать метод родителя, но, заметьте, автоматически это не делается. Здесь есть две возможности:

·   Использовать обычную схему Copy - Paste, копируя реализацию родителя.

·   Реализовать наследование типичным для Office 2000 способом путем встраивания родительского объекта в класс - потомок. Тогда можно вызывать в нужном месте нужный метод родителя. Чуть позже мы рассмотрим проект «Люди и машины», где используем этот прием.

Еще одна важная особенность наследования интерфейса состоит в том, что интерфейс родителя не становится интерфейсом потомка. Как следствие этого факта, отсутствие транзитивности отношения «наследование интерфейсов». Синтаксически, это определяется двумя обстоятельствами:

·   Заготовки методов, строящиеся автоматически, используют спецификатор Private. Это означает, что наследуемые методы не являются открытыми методами потомка и, следовательно, не входят в его интерфейс и не передаются далее внукам родителя.

·   Еще одна особенность состоит в том, что имена наследуемых методов содержат имя родителя. Это также означает, что наследуемый метод не становится собственным методом, который можно передать далее своим потомкам

Возникает естественный вопрос, если наследуемые методы не входят в интерфейс класса, а следовательно не могут быть вызваны объектами данного класса, то какой в них толк? Толк есть, поскольку эти методы могут быть все-таки вызваны объектами, правда, принадлежащими родительскому классу. Объект родительского класса, ссылающийся на своего потомка, может вызывать методы родителя, наследуемые потомком. Вот небольшой пример, где действую объекты трех классов Father, Son и GrandSon, связанные отношением наследования интерфейсов. Пусть определен абстрактный класс Father, интерфейс которого состоит из одного свойства, одного чистого метода и метода с заданной реализацией:

 

'Class Father
'
Свойства класса
Public MyProperty As String

'
Методы класса

Public Sub MyPureMethod()
'
Чистый метод
End Sub

Public Sub MyRealMethod()
     MsgBox ("It's the Father")
End Sub

Класс Son, наследующий интерфейс класса Father, по контракту должен реализовать две процедуры - свойства и два его метода. Мы дали только формальную реализацию, ¾ в созданные автоматически заготовки добавили комментарии. Лишь в реализацию метода Father_MyPureMethod класса Son вставлена строка текста. Кроме того, в класс добавлен новый метод, определяющий собственный интерфейс этого класса. Вот описание этого класса:

'Класс Son - Наследник класса Father
Implements Father

Private Property Let Father_MyProperty(ByVal RHS As String)
'
Реализация отложена

End Property

Private Property Get Father_MyProperty() As String
'
Реализация отложена

End Property

Private Sub Father_MyPureMethod()
     MsgBox ("It's the Son")

End Sub

Private Sub Father_MyRealMethod()
'Реализация отложена

End Sub
  

Public Sub SonNewMethod()
     MsgBox ("It's the Son of his Father")
End Sub

Заметьте, что, конечно, можно в наследуемых методах изменить спецификатор Private на Public, и тогда эти методы войдут в интерфейс класса - потомка. Но лучше этого не делать, хотя бы потому, что наследование таких методов приведет к ошибке, ¾ наследование наследуемых методов не допускается, как мы уже говорили. Правильный путь состоит в создании собственных Public методов, в которых при необходимости вызываются Private методы.

Приведем теперь описание класса GrandSon ¾ потомка классов Son и Father. Заметьте, класс явно определяет обоих своих родителей. Опять-таки мы ограничились формальной реализацией, определив лишь реализацию метода Father_MyRealMethod:

Option Explicit

Implements Father
Implements Son

Private Property Let Father_MyProperty(ByVal RHS As String)
'
Реализация отложена

End Property

Private Property Get Father_MyProperty() As String
'
Реализация отложена

End Property

Private Sub Father_MyPureMethod()
'
Реализация отложена

End Sub

Private Sub Father_MyRealMethod()
'
Реализация отложена
MsgBox ("It's the GrandSon")
End Sub

Private Sub Son_SonNewMethod()
'
Реализация отложена

End Sub

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

Public Sub Family()

Dim F As New Father, S As New Son, GS As New GrandSon
Dim Grand As Father, GrandS As Son

Set Grand = F
Grand.MyProperty = "Flat"
Grand.MyRealMethod
Grand.MyPureMethod
Debug.Print Grand.MyProperty
Set Grand = S
Grand.MyProperty = "Flat"
Grand.MyRealMethod
Grand.MyPureMethod
If TypeOf Grand Is Son Then
     Set GrandS = Grand: GrandS.SonNewMethod
End If
Debug.Print Grand.MyProperty

Set Grand = GS
If TypeOf Grand Is GrandSon Then
     Set GrandS = Grand: GrandS.SonNewMethod
End If
    Grand.MyProperty = "Flat"
    Grand.MyRealMethod
    Grand.MyPureMethod
Debug.Print Grand.MyProperty

End Sub

В этом примере объект Grand класса Father связывается поочередно с объектами класса Father, Son, GrandSon и всякий раз вызываются методы родительского класса, унаследованные потомками. Обратите внимание на конструкцию TypeOfIs, позволяющую определить текущий тип объекта. В процессе работы этой процедуры будут открываться диалоговые окна, уведомляющие, что мы встретились с объектами Father, Son, Son of his Father, GrandSon. При печати свойств лишь однажды будет напечатано значение Flat.

Уже этот пример показывает, что наследование интерфейсов мало что дает с точки зрения наследования свойств и методов, поскольку ни свойства напрямую не наследуются, ни реализация не наследуется. Наследование интерфейсов представляет несомненный интерес и весьма полезно по другой причине, ¾ оно позволяет реализовать в полной мере полиморфизм семейства классов VBA. К подробному рассмотрению этого вопроса мы сейчас и переходим.

Полиморфизм семейства классов

Два фактора обеспечивают полиморфизм:

1.     Присваивание снизу - вверх.

2.     Механизм позднего связывания.

Присваивание снизу - вверх от потомков к родителям обеспечивает возможность связать указатель родительского класса с объектом, принадлежащим любому из классов потомков. И в процедуру, формальный параметр которой является объектом родительского класса, можно передать в качестве фактического параметра объект - потомок. Позднее связывание гарантирует, что будет вызван виртуальный метод нужного класса, поскольку решение о вызове метода принимается динамически в тот момент, когда ясно, объект какого класса вызвал метод. Если в тексте процедуры стоит вызов X.VirtMethod, то по этому тексту ничего нельзя сказать о результате этого вызова, поскольку по первому свойству полиморфизма с X может быть связан объект любого из классов семейства, а благодаря второму свойству будет найден виртуальный метод именно этого класса.

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

Наследование интерфейсов позволяет организовать присваивание снизу - вверх и, как следствие, позволяет работать с группой элементов, каждый из которых может принадлежать различным классам семейства. Поскольку позднее связывание и ранее было характерно для VBA, то полиморфизм в Office 2000 реализован в полном объеме.

Итак, подводя некоторый итог, еще раз отметим, что полиморфизм обеспечивает два важных достоинства при работе с семейством классов:

·    Возможность работать с группой объектов, принадлежащих разным классам.

·   Равенство прав по вызову, означающее, что потомки могут вызывать методы родителей, но и родители могут вызывать методы потомков.

Одним из классических приемов построения сложных систем, широко применяемое еще в недавнем прошлом, было построение их методом «раскрутки». Суть его состоит в том, что вначале строится ядро системы, содержащее базовые методы. Затем строится следующий слой системы, методы которого могут вызывать методы ядра. Этот процесс продолжается, строятся новые слои, пока система живет и развивается. Недостаток раскрутки состоял в том, что в нем не было равенства по вызову, внешние слои могли вызывать методы внутренних слоев, а обратное не допускалось. Позже для преодоления возникающих трудностей был создан специальный механизм обратного вызова, ¾callback “функции.

В языках программирования, где есть классы и наследование, сложная система может проектироваться как семейство классов. При этом крайне полезно, если семейство обладает таким гибким механизмом как полиморфизм, обеспечивающим, в частности, равенство по вызову между предками и потомками. Для реализации сложных систем может быть очень полезным и такое свойство языка, как множественное наследование, позволяющее организовать куда более сложные связи между объектами, чем в случае одиночного наследования. Мы уже говорили, что в Office 2000 наследование интерфейсов является множественным. Это позволяет одному и тому же объекту быть участником разных групп. Так бывает и в жизни, ¾ один и тот же человек может быть членом Союза рыболовов и охотников, но он же входит и в Союз кинематографистов, кроме того, как личность он входит и в общую группу «homo sapiens».

Давайте перейдем от слов к делу и посмотрим, как это все работает на более или менее содержательном примере.

  Назад                  Вперед    

Hosted by uCoz