В.А. Биллиг
VBA в Office 2000
Офисное программирование
"Русская редакция", 1999 г.
Глава 5(4)
Проектирование семейства классов, как правило, начинают с проектирования схемы, задающей иерархию и связи в семействе, определения прародителей, определяющих базовые свойства и поведение. Приведем в качестве примера семейство классов, объектами которого будут люди и машины. Вот одна из возможных схем, определяющая классы этого семейства и отношения между ними.
Рис. 5_3 Схема семейства классов «Люди и Машины»
Мы не станем давать полную реализацию этой схемы. Для наших целей достаточно ограничиться ее частичной реализацией, где будут два родителя, имеющие общего потомка. У нас уже построен класс Личность, ¾ пусть он и будет одним из родительских классов. Другим базовым родительским классом пусть будет класс Машина, задающий машины. Потомком этих двух родительских классов будет класс ВладелецМашины. Он наследует интерфейсы класса Личность и класса Машина. Поэтому объекты этого класса смогут входить и обрабатываться в разных группах, ¾ в группе личностей и в группе машин. В нашем примере родительские классы не будут абстрактными. Напомним, что определенный в предыдущей главе класс Личность является классом с событиями, так что его объекты могут реагировать на некоторые события, происходящие с ними. Мы не будем повторять здесь его описание. Класс Машина будет достаточно простым. Вот его определение:
Option
Explicit
'Класс Машина
'Свойства класса
Private Марка As String
Private ДатаВыпуска As Date
Private Цвет As String
'Конструкторы класса
Private Sub Class_Initialize()
Марка = "Форд"
ДатаВыпуска =
"20.07.1925"
Цвет = "Вишневый"
End Sub
Public Sub НоваяМашина(M As String, D As Date, C As String)
Марка = M
ДатаВыпуска =
D
Цвет = C
End Sub
'Методы класса
Public Sub PrintDataCar()
Debug.Print "Марка
= ", Марка
Debug.Print "ДатаВыпуска
= ", ДатаВыпуска
Debug.Print "Цвет
= ", Цвет
End Sub
Public Property Get МаркаМашины() As String
МаркаМашины =
Марка
End Property
Public Property Get ЦветМашины() As String
ЦветМашины =
Цвет
End Property
Public Property Get ДатаВыпускаМашины() As Date
ДатаВыпускаМашины
= ДатаВыпуска
End Property
У класса Машина имеется:
· три закрытых свойства, обладающие статусом Read only,
· два конструктора, ¾ закрытый конструктор по умолчанию и открытый конструктор НоваяМашина,
· один типичный для классов открытый метод, задающий печать свойств ¾ PrintDataCar
Интерфейс класса составляют два открытых метода, они и будут наследоваться классом - потомком. Класс - потомок ВладелецМашины значительно больше унаследует от своего другого родителя - класса Личность. Вот определение класса ВладелецМашины:
Option Explicit
'Класс ВладелецМашины
'Наследует интерфейсы классов Личность и
Машина
Implements Машина
Implements Личность
'Свойства класса
Private Сам As Личность
Private ЕгоМашина As Машина
Private Sub Class_Initialize()
Set Сам = New Личность
Set ЕгоМашина = New
Машина
End Sub
'Реализация интерфейсов класса Личность
Private Sub Личность_CopyPerson(You As Личность)
Сам.CopyPerson (You)
End Sub
Private Sub Личность_InitPerson(ByVal FN As String, ByVal LN As String,
ByVal DoB As Date)
'Инициализация
личности
Сам.InitPerson FN, LN,
DoB
End Sub
Private Sub Личность_PrintPerson()
'Печать в
отладочном окне Immediate
Сам.PrintPerson
End Sub
Private Sub Личность_SayWhoIs()
' Вывод сообщения о поле
и возрасте владельца машины
Dim StrMsg As
String
StrMsg = "Думаю,
Владелец машины марки: " & _
ЕгоМашина.МаркаМашины
& " это - "
If Сам.WhoIs
Then
If
Year(Сам.ВашаДатаРождения) > 1967 Then
StrMsg = StrMsg & "молодая девушка!"
Else: StrMsg = StrMsg &
"женщина!"
End If
Else
If
Year(Сам.ВашаДатаРождения)
> 1967 Then
StrMsg = StrMsg & "молодой
человек!"
Else: StrMsg = StrMsg & "мужчина!"
End If
End If
MsgBox (StrMsg)
End Sub
Private Function Личность_WhoIs() As Boolean
Сам.WhoIs
End Function
Private Property Let Личность_ВашаДатаРождения(ByVal
NewValue As Date)
Сам.ВашаДатаРождения
= NewValue
End Property
Private Property Get Личность_ВашаДатаРождения()
As Date
'Зажигает
событие
ДеньРождения
'в
зависимости
от
значения
текущей
даты
Личность_ВашаДатаРождения
= Сам.ВашаДатаРождения
End Property
Private Property Let Личность_ВашаФамилия(ByVal
NewValue As String)
'Зажигает
событие
ИзменениеФамилии
Сам.ВашаФамилия
= NewValue
End Property
Private Property Get Личность_ВашаФамилия()
As String
Личность_ВашаФамилия
= Сам.ВашаФамилия
End Property
Private Property Let Личность_ВашеИмя(ByVal
NewValue As String)
Сам.ВашеИмя
= NewValue
End Property
Private Property Get Личность_ВашеИмя()
As String
Личность_ВашеИмя
= Сам.ВашеИмя
End Property
Private Property Let Личность_ВашеОтчество(ByVal
NewValue As String)
Сам.ВашеОтчество
= NewValue
End Property
Private Property Get Личность_ВашеОтчество()
As String
Личность_ВашеОтчество
= Сам.ВашеОтчество
End Property
'Реализация интерфейсов класса Машина
Private Property Get Машина_ДатаВыпускаМашины() As Date
Машина_ДатаВыпускаМашины
= ЕгоМашина.ДатаВыпускаМашины
End Property
Private Property Get Машина_МаркаМашины() As String
Машина_МаркаМашины
= ЕгоМашина.МаркаМашины
End Property
Private Property Get Машина_ЦветМашины() As String
Машина_ЦветМашины
= ЕгоМашина.ЦветМашины
End Property
Private Sub Машина_PrintDataCar()
ЕгоМашина.PrintDataCar
End Sub
Private Sub Машина_НоваяМашина(M As String, D As Date, C As String)
ЕгоМашина.НоваяМашина
M, D, C
End Sub
'Собственный интерфейс класса ВладелецМашины
'Public методы
- интерфейс Владельца машины
Public Sub InitCarOwner(FN As String, LN As String, DoB As Date, _
Marka As String,
DB As Date, Color As String)
'Инициализация
данных
о
хозяине
и
его
машине
Личность_InitPerson
FN, LN, DoB
Машина_НоваяМашина
Marka, DB, Color
End Sub
Public Sub ConnectOwnerAndCar(pers
As Личность,
car As Машина)
'соединяет
данные
о
хозяине
и
его
новой
машине
Сам.CopyPerson
pers
Машина_НоваяМашина
car.МаркаМашины,
car.ДатаВыпускаМашины,
_
car.ЦветМашины
End Sub
Public Sub PrintOwnerData()
Личность_PrintPerson
Debug.Print " владеет машиной:
"
Машина_PrintDataCar
End Sub
Прокомментируем этот довольно длинный текст. Вот на какие моменты следует обратить внимание:
1. Для того, чтобы реализовать полноценное наследование свойств и поведения родителей, а не только наследование интерфейсов, используется встраивание, ¾ в классе определены свойства Сам класса Личность и свойство ЕгоМашина класса Машина.
2. В конструкторе по умолчанию Class_Initialize, вызываемом при создании объекта класса ВладелецМашины, объекты Сам и ЕгоМашина инициализируются, ¾ будут вызваны их конструкторы по умолчанию класса Личность и класса Машина.
3. При наследовании интерфейсов класса Личность почти для всех методов наследовалось поведение родительского класса. Реализуется это достаточно просто ¾ вызывается соответствующий метод объекта Сам.
4. Для метода SayWhoIs поведение переопределено. В данном случае при вызове метод учитывает специфику класса и сообщает некоторые данные, как о владельце, так и о его машине. Так что при вызове этого метода у просто личностей и у личностей, являющихся владельцами машин, результат будет различным.
5. Поскольку родительский класс допускает события, то и у потомка при вызове методов зажигаются соответствующие события.
6. Интерфейс класса Машина реализуется подобным образом. Полностью наследуется поведение родительского класса.
7. Собственный интерфейс класса довольно прост. У объектов этого класса нет новых дополнительных свойств, - все спрятано в свойствах встроенных объектов Сам и ЕгоМашина. Интерфейс составляют два конструктора и метод, осуществляющий печать данных. Обратите внимание, один конструктор InitCarOwner позволяет сконструировать новый объект класса по терминальным данным, характеризующим личность и его машину. Второй конструктор ConnectOwnerAndCar в качестве параметров использует ранее созданные объекты классов Личность и Машина, соединяя эти два объекта в один объект класса ВладелецМашины.
На рисунке F5_2 отражен один из моментов проектирования класса. Можно видеть, что в окне кода список объектов содержит объекты Class, Личность и Машина. При выборе объекта Class правый раскрывающийся список покажет список стандартных событий класса, а при выборе объектов Личность и Машина будет раскрываться список наследуемых методов.
Рис. 5_4 Проектирование класса ВладелецМашины
Определение семейства классов дано и нам осталось продемонстрировать работу с различными объектами этих классов. Главное, что хотелось показать, это совместную работу с объектами разных классов. Начнем с программного текста:
Option Explicit
'Модуль Примеры
Public FriendOne As New Личность
Public FriendTwo As New Личность
Public FriendThree As New Личность
Public carOne As New Машина
Public carTwo As New Машина
Public carThree As New Машина
Public OwnerOne As New ВладелецМашины
Public OwnerTwo As New ВладелецМашины
Public OwnerThree As New ВладелецМашины
Public FOne As New Личности
Public Sub Люди()
'Вызывается конструктор с параметрами
'и происходит знакомство с объектами
FriendOne.InitPerson FN:="Станислав", LN:="Федотов",
_
DoB:="21.05.39"
FriendTwo.InitPerson FN:="Катя", LN:="Павлова", _
DoB:="22.03.79"
FriendThree.InitPerson FN:="Остап", LN:="Бендер",
DoB:="23.07.1910"
FriendOne.PrintPerson
FriendTwo.PrintPerson
FriendOne.SayWhoIs
FriendTwo.SayWhoIs
'Связывание с двойниками.
'Теперь объекты могут реагировать на
события!
FOne.Connect
End Sub
Public Sub Cars()
'Вызывается конструктор с параметрами
carOne.НоваяМашина "Антилопа",
"12.12.12", "Неопределенный"
carTwo.НоваяМашина "Москвич", "12.11.98",
"Морская волна"
carThree.НоваяМашина "Jeep", "23.05.97",
"Orange"
End Sub
Public Sub CarOwners()
OwnerOne.ConnectOwnerAndCar
FriendOne, carTwo
OwnerTwo.ConnectOwnerAndCar
FriendThree, carOne
OwnerThree.InitCarOwner
FN:="Юрий", LN:="Вегера", _
DoB:="21.08.34",
Marka:="Газ69", DB:="20.01.76", Color:="Зеленый"
OwnerOne.PrintOwnerData
OwnerTwo.PrintOwnerData
OwnerThree.PrintOwnerData
End Sub
Public Sub CallEvents()
Dim DoB As Date
'Вызов
методов приведет к возникновению событий!
'При
замене фамилии возникнет событие
ИзменениеФамилии
'Заметьте, не
всегда фамилия будет изменена!
FriendOne.ВашаФамилия
= "Фидотов"
FriendTwo.ВашаФамилия
= "Волконская"
'При
попытке узнать дату рождения
'может
быть вызван обработчик события
ДеньРождения.
DoB = FriendOne.ВашаДатаРождения
Debug.Print DoB
DoB = FriendTwo.ВашаДатаРождения
Debug.Print DoB
FriendOne.PrintPerson
FriendTwo.PrintPerson
'События
не
наследуются
Set FriendOne =
OwnerTwo
'Нельзя
связать
теперь
объект
FriendOne с
двойником
'FOne.Connect
FriendOne.ВашаФамилия
= "Воробьянинов"
FriendOne.PrintPerson
End Sub
Public Sub Группа()
Const SizeGroup =
6
Const SizeGarage =
6
Dim i As Byte
Dim Group(1 To
SizeGroup) As Личность
Dim Гараж(1
To SizeGarage) As Машина
Set Group(1) =
FriendOne
Set Group(2) =
FriendTwo
Set Group(3) =
FriendThree
Set Group(4) =
OwnerOne
Set Group(5) =
OwnerTwo
Set Group(6) =
OwnerThree
For i = 1 To
SizeGroup
Group(i).SayWhoIs
Next i
Set Гараж(1)
= carOne
Set Гараж(2)
= carTwo
Set Гараж(3)
= carThree
Set Гараж(4)
= OwnerOne
Set Гараж(5)
= OwnerTwo
Set Гараж(6)
= OwnerThree
For i = 1 To
SizeGarage
Гараж(i).PrintDataCar
Next i
End Sub
Public Sub ЛюдиИМашины()
Люди
Cars
CarOwners
Группа
PolyMorf FriendTwo
PolyMorf OwnerTwo
End Sub
Public Sub PolyMorf(One As Личность)
One.SayWhoIs
End Sub
Дадим теперь комментарии к этому тексту:
1. В модуле Примеры вначале объявляются по три объекта классов Личность, Машина и ВладелецМашины. В методах Люди, Cars, CarOwners происходит инициализация этих объектов. Некоторые из этих личностей уже встречались в предыдущих примерах, но появился Остап Бендер и его знаменитая машина.
2. Обратите внимание на создание объектов Owner класса ВладелецМашины, они создаются разными конструкторами.
3. Основную работу выполняет метод Группа. Здесь создается массив элементов Group из трех объектов Friend и трех объектов Owner, принадлежащих разным классам. Тем не менее все эти объекты обрабатываются в едином цикле и для них вызывается метод SayWhoIs, который, как мы говорили ранее по-разному работает для объектов класса Личность и класса ВладелецМашины. Так что простой цикл по элементам Group демонстрирует реализацию полиморфизма.
4. В методе Группа создана и продемонстрирована работа с еще одной группой элементов, ¾ массивом Garage. Эту группу составляют объекты класса Машина и объекты класса ВладелецМашины. Обратите внимание, одни и те же объекты Owner входят в обе группы элементов и могут быть обработаны нужным образом.
5. В процессе обработки массива Garage вызывается метод PrintDataCar, который унаследован объектами класса ВладелецМашины.
6. В модуле Примеры приведена полиморфная функция PolyMorf(One As Личность), с формальным параметром родительского класса Личность. Она дважды вызывается в процедуре ЛюдиИМашины с фактическими параметрами разных классов.
7. Процедура ЛюдиИМашины является процедурой, организующей всю работу, она поочередно запускает все перечисленные ранее процедуры. Сама она вызывается в обработчике события Click командной кнопки, встроенной в наш тестовый документ.
В процессе выполнения этой процедуры откроется серия диалоговых окон, где вначале будет сказано, кто такой Федотов и Катя, будет спрошено отчество Вегеры Юрия, затем опять будет рассказано о Федотове, Кате и Бендере, после чего появится информация о владельцах машин разных марок. На последнем этапе опять будет рассказано о Кате и владельце машины «Антилопа». Нам осталось привести результаты отладочной печати, появляющиеся при работе этой процедуры:
Станислав
Федотов
родился
21.05.39
Катя
Павлова
родилась 22.03.79
Станислав
Федотов
родился 21.05.39
владеет машиной:
Марка = Москвич
ДатаВыпуска =
12.11.98
Цвет =
Морская волна
Остап
Бендер
родился
23.07.1910
владеет машиной:
Марка = Антилопа
ДатаВыпуска =
12.12.12
Цвет =
Неопределенный
Юрий
Алексеевич Вегера
родился
21.08.34
владеет машиной:
Марка = Газ69
ДатаВыпуска =
20.01.76
Цвет =
Зеленый
Марка = Антилопа
ДатаВыпуска =
12.12.12
Цвет =
Неопределенный
Марка = Москвич
ДатаВыпуска =
12.11.98
Цвет =
Морская волна
Марка = Jeep
ДатаВыпуска =
23.05.97
Цвет =
Orange
Марка = Москвич
ДатаВыпуска =
12.11.98
Цвет =
Морская волна
Марка = Антилопа
ДатаВыпуска =
12.12.12
Цвет =
Неопределенный
Марка = Газ69
ДатаВыпуска =
20.01.76
Цвет =
Зеленый
Как мы уже отмечали, родительский класс Личность реагирует на события. Это не мешает наследовать его интерфейсы, но не означает, что потомки будут наследовать события своего родителя. Чтобы убедиться, что зажигание событий не мешает работе потомков и не влияет на их работу, мы включили процедуру CallEvents. В процессе ее работы возникают события у объектов родительского класса и они правильно обрабатываются. Но, заметьте, после того как объект FriendOne был связан с объектом – потомком FriendOwner, при смене фамилии соответствующее событие уже не возникало, так что ничто не помешало Остапу Бендеру сменить свою фамилию. По ходу работы этой процедуры в появляющихся диалоговых окнах будет сообщено об отказе смены фамилии Федотову и появится поздравление Кате Павловой в связи с замужеством. Приведем результаты отладочной печати при работе этой процедуры:
21.05.39
22.03.79
Станислав Федотов
родился
21.05.39
Катя
Волконская
родилась 22.03.79
Остап
Воробьянинов
родился
23.07.1910
Этот пример демонстрирует основные возможности работы с семейством классов, появившиеся в связи с введением механизма наследования интерфейсов.