
- •Тема 5.3. Средства объектно-ориентированного программирования в vb
- •5.3.1. Две роли классов в ооп и типы данных в vb
- •5.3.2. Средства создания классов в vb
- •5.3.2.1. Средства создания пользовательских классов
- •5.3.2.2. Пошаговое руководство для создания приложения с классами
- •Определение класса
- •Создание кнопки для тестирования класса
- •Запуск приложения
- •5.3.3. Взаимодействие, взаимное различие и сходство форм, модулей и классов
- •5.3.4. Создание объектной модели и приложений с использованием классов
- •5.3.5. Иерархия классов и наследование
- •Наследование и тождественность
- •Базовые классы и повторное использование кода
- •Взаимозаменяемые производные классы
- •Неполные иерархии классов
- •Глобальные изменения производных классов через базовый класс
- •Изменение структуры базовых классов после развертывания
- •Проблема уязвимости базовых классов
- •Сведение к минимуму проблем уязвимости базовых классов
- •5.3.6. Задачи для самостоятельного решения по теме «Средства объектно-ориентированного программирования в Visual Basic»
- •Практикум
- •5.3.7. Тестовые задания по теме «Средства объектно-ориентированного программирования в vb»
- •Тема 5.3. Средства объектно-ориентированного программирования в Visual Basic Страница 85
5.3.4. Создание объектной модели и приложений с использованием классов
Итак, напомним, что такое объект.
Объект - это синтез данных (в частности, переменных и констант) и действий (процедур), которые эти данные обрабатывают.
Объекты в программировании напоминают объекты реального мира, которые в некотором смысле представляют синтез данных и действий.
Почти все приложения Windows и сама операционная система Windows создана на основе объектного подхода. Типичные примеры объектов в Windows это окна, кнопки и пр. Все они характеризуются, с одной стороны, данными (размер, цвет и пр.), а с другой стороны, действиями, которые определяют их поведение.
Пример 5.3-7. Определение объектной модели игрушечного радиоуправляемого автомобиля.
Определим, где у игрушечного радиоуправляемого автомобиля данные и где действия.
Данных у игрушечного автомобиля множество. Например, следующие:
цвет кузова;
номер автомобиля;
скорость движения в данный момент;
громкость звукового сигнала;
высота кресел;
величина электрического тока в двигателе в данный момент;
толщина гайки в таком-то месте внутри корпуса и т. д.
Действий тоже достаточно. Например:
поворот по команде с пульта управления;
торможение по команде с пульта управления;
подпрыгивание автомобиля на маленьком камушке;
изменение скорости вращения электродвигателя при изменении в нем тока;
возникновение жужжания двигателя при трении шестеренок друг о друга и т. д.
Определим "внешние" и "внутренние" данные и действия. Они бывают крупные и мелкие, важные и менее важные. Нас будут интересовать:
данные и действия, что видны или доступны для запуска автомобиля со стороны внешних объектов, например мальчика, держащего в руках пульт управления (такие данные и действия близки к понятию глобальных переменных и методов).
данные и действия, что не видны или недоступны (т.е. близки к понятию модульных).
Проведем дальнейшую детализацию и определим, какие данные являются свойствами. Таким образом, данные будем делить на те, что видны снаружи (это первые 5 свойств из списка), и те, что не видны (последние два). Данные, видимые снаружи, назовем свойствами объекта
Итак, свойства - это данные, видимые снаружи. Теперь будем подразделять свойства по доступности к изменению. Мы их разделим на две категории:
те, что можно произвольно менять снаружи (назовем их свойствами для чтения- записи);
те, что снаружи менять нельзя - последние 4 данных из списка (назовем их свойствами только для чтения).
Очевидно, что данные, которые не видны снаружи, и менять снаружи тоже нельзя. В программировании это локальные переменные или модульные переменные, объявленные модификаторами Dim или Private.
Действия разделим на те, которые можно вызывать снаружи (первые два действия из списка), и на те, что вызываются внутренним строением автомобиля (остальные). Действия, вызываемые снаружи, назовем методами объекта. В программировании это процедуры, объявленные, например, словом Public. Если процедуры объявлены словом Private, то снаружи они не видны и методами не являются.
Создатель любого объекта, в том числе игрушечного автомобиля, при его конструировании стремится к тому, чтобы объект был надежен, защищен и просто управлялся. Для этого он должен придерживаться, прежде всего, двух принципов:
количество методов должно быть минимально необходимым (например, разгон, торможение, поворот налево и направо и этого достаточно);
количество свойств для чтения-записи должно быть минимальным (если все данные сделать свойствами, да еще и для чтения-записи, тогда любой, кому не лень, сможет в любой момент менять все свойства).
При создании объектов необходимо придерживаться принципа инкапсуляции, который заключается в следующем.
Данные и действия объекта представляют собой единое целое, образующее всю механику объекта, и хранятся они в одной "упаковке". Упаковкой этой является класс. Они должны быть как можно меньше видимы снаружи. Хорошо инкапсулированный объект представляет собой некий "черный ящик". Вся работа идет внутри. Внутренние данные меняются при помощи внутренних действий. Никто снаружи не может вмешаться в эту внутреннюю работу. Наружу показывается лишь тот минимум, который необходим для связи с окружающим миром.
Влиять снаружи на работу объекта можно только тремя способами:
методами;
изменяя значения свойств для чтения-записи;
изменяя свойства окружающей среды.
Таким образом, инкапсуляция – то, что объединяет объекты в программировании с объектами реального мира.
Теперь можно уточнить терминологию.
Данные, видимые снаружи, делятся на свойства и на поля.
Полем (Field) класса будем называть переменную, объявленную в этом классе при помощи Public или Friend:
-
Public С As Integer = 10
Свойства (Properties) определяются при помощи ключевого слова Property и описаны в примере 5.3-4. Пока же будем иметь дело только с полями.
Снаружи класса поля и свойства неотличимы, обращение к ним одинаково. Это позволило в начале данной темы неформально называть и то и другое свойствами.
Пример 5.3-8. Создание и использование двух объектов одного класса.
Пусть вы директор только что родившегося садового товарищества, в нем всего два участка. Однако вы уже решили использовать компьютер для решения повседневных задач.
Надо создать класс Участок, в котором собрать все данные, характеризующие участок (например, размеры), и действия для вычисления нужных величин, касающихся участка (например, его площади).
Полный программный код приведен на рис. 5.3-9.
Предполагается, что на форме имеется 6 текстовых полей (TextBox) и 3 кнопки (Button), а проект должен действовать следующим образом.
В 5 текстовых полей вводится следующая информация, касающаяся первого участка:
TextBox1 Владелец участка;
TextBox2 Длина участка;
TextBox3 Ширина участка;
TextBox4 Высота забора на участке;
TextBox5 Расход краски на 1 кв. м забора.
После этого пользователь нажатием на кнопку Button1 создает из класса Участок объект Участок1, который принимает в себя все данные из текстовых полей в качестве своих полей.
Затем пользователь вводит в текстовые поля информацию, касающуюся второго участка и нажатием на кнопку Button2 создает из класса Участок объект Участок2, который также принимает в себя все данные из текстовых полей в качестве своих полей.
С этого момента в памяти находятся два объекта-экземпляра класса Участок. Программист теперь как угодно может использовать их в своих целях. В качестве примера такого использования он может нажать на кнопку Button3 для решения следующей задачи: "Правда ли, что на покраску забора первого участка уйдет больше краски, чем для второго?"
-
Public Class Участок
Public Владелец As String
Public Длина, Ширина, Высота_забора As Integer
Public Расход_краски_на_кв_м As Integer
Private Периметр As Integer
Private Sub Вычисляем_периметр( )
Периметр = 2 * (Длина + Ширина)
End Sub
Private Function Площадь_забора( ) As Integer
Вычисляем_периметр( )
Return Периметр * Высота_забора
End Function
Public Function Расход_краски_на_забор( ) As Integer
Return Расход_краски_на_кв_м * Площадь_забора()
End Function
End Class
Public Class Form1
Dim Участок1, Участок2 As Участок
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)_
Handles Button1.Click
Участок1 = New Участок 'Из класса рождается объект.
Участок1.Владелец = TextBox1.Text Участок1.Длина = CInt(TextBox2.Text)
Участок1.Ширина = CInt(TextBox3.Text)
Участок1.Высота_забора = CInt(TextBox4.Text)
Участок1.Расход_краски_на_кв_м = CInt(TextBox5.Text)
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)_
Handles Button2.Click
Участок2 = New Участок 'Из класса рождается объект
Участок2.Владелец = TextBox1 Text
Участок2.Длина = TextBox2.Text
Участок2.Ширина = TextBox3.Text
Участок2.Высота_забора = TextBox4.Text
Участок2.Расход_краски_на_кв_м = TextBox5.Text
End Sub
Private Sub Button3_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)_
Handles Button3.Click
If Участок1.Расход_краски_на_забор > Участок2.Расход_краски_на_забор Then
TextBox6.Text="Пepвомy участку нужно больше краски, чем второму"
End If
End Sub
End Class
Рис. 5.3-9
В классе объявлено 5 переменных с ключевым словом Public. Следовательно, они видны снаружи объекта. Значит, это поля объекта. К тому же Public без употребления других запретительных слов допускает свободное изменение значений поля снаружи объекта. Следовательно, это поля для чтения-записи.
Внутренняя механика объекта определяется его процедурами и функциями.
Так как участки прямоугольные и не соприкасаются, значит, длина забора вокруг участка равна периметру участка, а это две ширины плюс две длины. Площадь забора равна периметру, умноженному на высоту забора. Расход краски на весь забор равен ее расходу на 1 кв. м, умноженному на площадь забора.
Процедура Вычисляем_периметр( ) вычисляет периметр. Сделали ее Private, так как пока никому снаружи она не нужна, всех интересует только расход краски. Если в будущем кому-то захочется вычислять периметр участка, поменяем Private на Public. По той же причине объявлена Private и переменная Периметр, которая ввиду этого полем не является и снаружи не видна. Класс Участок использует ее только как промежуточный результат вычислений.
Функция Площадь_забора( ) возвращает площадь забора. Мы сделали ее Private из аналогичных соображений.
Функция Расход_краски__на_забор( ) возвращает результат, который нужен снаружи (в нашем конкретном случае - процедуре Button3_Click( ), принадлежащей форме), поэтому сделали ее Public. Следовательно, эта функция является методом.
Итак, был создан класс с пятью полями и одним методом. Кроме этого, в нем есть переменная, процедура и функция, каждая из которых невидима снаружи.
Пример 5.3-9. Создание и использование массива объектов одного класса.
Предположим, что садовое товарищество растет, и в нем уже несколько десятков участков. Надо менять проект. Теперь у нас на форме будет только две кнопки. Рождение каждого нового объекта будет происходить при нажатии одной и той же Button1. Когда все объекты будут рождены, их можно как угодно использовать в своих целях. В качестве примера конкретной реализации, рассмотрим ситуацию, когда от нажатия на Button2 распечатываются имена владельцев тех участков, на заборы которых ушло больше 200 кг краски (рис. 5.3-10).
-
Public Class Участок
Public Номер_участка As Integer
Public Владелец As String
Public Длина, Ширина As Integer
Public Высота_забора As Integer
Public Расход_краски_на_кв_м As Integer
Private Периметр As Integer
Private Sub Вычисляем_периметр()
Периметр = 2 * (Длина + Ширина)
End Sub
Private Function Площадь_забора() As Integer
Вычисляем_периметр()
Return Периметр * Высота_забора
End Function
Public Function Расход_краски() As Integer
Return Расход_краски_на_кв_м * Площадь_забора()
End Function
End Class
Public Class Form1
Dim Участки(100) As Участок
Dim k As Integer = 1
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)_
Handles Button1.Click
Участки(k) = New Участок
Участки(k).Номер_участка = k
Участки(k). Владелец = TextBox1.Text
Участки(k).Длина = CInt(TextBox2.Text)
Участки(k).Ширина = CInt(TextBox3.Text)
Участки(k).Высота_забора = CInt(TextBox4.Text)
Участки(k).Расход_краски_на_кв_м = CInt(TextBox5.Text)
k = k + 1
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs)_
Handles Button2.Click
Dim i As Integer
For i = 1 To k -1 'Всего участков k-1
If Участки(i).Расход_краски > 200 Then
TextBox6.Text = Учacтки(i).Владелец
End If
Next
End Sub
End Class
Рис. 5.3-10
Здесь вместо двух участков (Участок1 и Участок2) был объявлен массив из 101 участка.
От нажатия к нажатию Button1 значение переменной к увеличивается на единицу оператором k = k + 1.
Поэтому оператор
-
Участки(k) = New Участок
рождает новый объект - экземпляр класса Участок - и заодно присваивает его очередному элементу массива Участки( ).
В классе Участок прибавилось новое поле - Номер_участка. В принципе снаружи оно никому не нужно, так как если мы обращаемся, скажем, к элементу массива Участки(8), то тем самым мы уже знаем, что это участок № 8.
Пример 5.3-10. Использование функции вместо поля.
Вернемся к предыдущему примеру и предположим, что кому-то снаружи класса понадобился периметр участка, а он его не видит, так как он объявлен Private.
Сделаем его полем: Public Периметр As Integer. Теперь он виден всем. И это хорошо. Плохо то, что теперь все смогут менять наш периметр, например, оператором Участки(k).Периметр = 25. Тогда вернем, все как было: Private Периметр As Integer.
Но теперь недоволен заказчик: ему нужно знать периметр. Что же делать? Нужно устроить так, чтобы периметр был все-таки виден и в то же время защищен от посягательств. Читать - читайте, а менять извне его значение нельзя!
Для этого создадим функцию Периметр_участка( ):
-
Public Function Периметр_участка( ) As Integer
Return Периметр
End Function
Теперь любой желающий извне может узнать значение периметра.
Поскольку такие обращения к переменной и функции без параметров синтаксически неотличимы, то снаружи могут сколько угодно думать, что обращаются к переменной Периметр_участка, когда на самом деле это функция. Цель достигнута, так как присваивать значение функции мы можем только в ее теле, а уж никак не из других модулей.
Поскольку в теле функции можно написать любой программной код, открывается возможность, как угодно ограничивать доступ любопытных к значению:
-
Public Function Периметр_участка() As Integer
If Периметр > 10000 Then
MsgBox("Периметр участка засекречен")
Return 0
Else
Return Периметр + 1
End If
End Function
Идея «функция вместо поля» в VB оформлена синтаксически в виде следующей заготовки:
-
Public Property Периметр_участка( ) As Integer
Get
End Get
Set(ByVal Value As Integer)
End Set
End Property
Слово Property означает свойство. Периметр участка перестал быть просто переменной, перестал быть полем, перестал быть функцией, он стал свойством.
Слово Get означает «Получи значение свойства», а слово Set означает «Установи значение свойству».
Эта конструкция состоит из двух частей. Внутри части Get - End Get пишется код, показывающий значение свойства наблюдателю извне. Внутри части Set - End Set пишется код, позволяющий наблюдателю извне менять значение свойства. Об этой части поговорим чуть позже, а поскольку никто не собирается давать возможность изменения значений свойства извне, просто пока сотрем эту часть, а добавим в объявление свойства слово ReadOnly (только для чтения):
-
Public ReadOnly Property Периметр_участка( ) As Integer
Get
Return Периметр
End Get
End Property
Поставленная задача решена.
Теперь займемся полями нашего класса, например, полем Длина. Хорошо бы при присвоении этому полю значения из текстового поля осуществлялся некоторый контроль значений. Например, мы хотим запретить участки длиннее 500 м. Однако поле Длина имеет тип Integer и поэтому допускает очень большие числа. Можно написать ограничение в коде формы, но это будет нарушением принципа инкапсуляции. Общепринятое средство - создать свойство.
Превратим переменную Длина из поля в модульную (локальную) переменную, защитив ее тем самым от воздействия извне:
-
Private Длина As Integer
а затем создадим свойство Длина_участка:
-
Public Property Длина_участка() As Integer
Set (ByVal Value As Integer)
If Value < 500 Then
Длина = Value
Else
MsgBox("Cлишком длинный участок")
Длина = 0
End If
End Set
Get
Return Длина
End Get
End Property
Таким образом, предоставлена возможность каждому извне узнавать без ограничений значение свойства, поэтому часть Get - End Get заполняется стандартно.
Поговорим о части Set - End Set. Внутренним хранителем значения длины в нашем классе является модульная переменная Длина. Свойство Длина_участка придумано только для того, чтобы показывать вовне значение этой переменной и по возможности безопасным и контролируемым образом разрешать извне это значение менять. Часть Set - End Set поэтому имеет своей конечной целью присвоение переменной Длина нужного значения.
Если часть Get - End Get - это функция, то часть Set - End Set - это процедура с параметром Value. Каждый раз, когда кто-то пытается извне присвоить свойству Длина_участка какое-нибудь значение, это значение приходит в объект-участок в качестве значения параметра Value свойства Длина_участка. Остается только написать в части Set - End Set единственный оператор Длина = Value.
И если бы он остался единственным, то в этом случае мы получили бы свойство для чтения-записи безо всяких ограничений. Но ведь поле Длина обеспечивало, то же самое. Однако мы помнили о нашем запрете на длинные участки и поэтому часть Set - End Set сделали длиннее. Теперь каждый желающий из текстового поля задать длину участка, равную 500 и выше, столкнется с сообщением «Слишком длинный участок» и вынужден будет вводить данные вновь.
Обратите внимание, что классу совершенно все равно, что физически является передатчиком значения извне внутрь свойства. Это может быть текстовое поле, как в нашем случае, или InputBox( ), или файл. В любом случае значение приходит в объект в качестве значения параметра Value и анализируется согласно коду, написанному в теле свойства программистом.
Итак, обычная последовательность создания свойств:
организуется модульная переменная Private – хранительница значения свойства;
организуется свойство Property для связи этой переменной с внешним миром.
Если же эта переменная вам внутри класса не нужна, то вы можете обойтись и без нее и вычислять значение свойства прямо в теле свойства в части Get - End Get.
Предположим, что не хотим, чтобы снаружи знали, кто является владельцем участка.
Для этого превращаем поле Владелец в модульную переменную:
-
Private Владелец As String
Тогда свойство Владелец_участка будет следующим:
-
Public WriteOnly Property Владелец_участка( ) As String
Set (ByVal Value As String)
Владелец = Value
End Set
End Property
Теперь рассмотрим конструкторы (специальные процедуры с именем New). Обратите внимание, что эти процедуры могут выполняться автоматически при создании объекта.
Для чего же нужен New? Для того же, для чего, и процедура Form1_Load( ) (ведь форма загружалась при отсутствии этой процедуры), а именно для того, чтобы произвести некоторые действия, которые программист считает нужным произвести в момент создания объекта из класса. Обычно это присвоение начальных значений переменным, открытие нужных файлов и т. п.
Пример 5.3-11. Использование конструктора для присваивания значения.
Рассмотрим использование конструктора для присвоения в каждом объекте нужного значения полю Номер_участка. Ранее оно присваивалось в модуле формы, что противоречит принципу инкапсуляции.
Уберем из кода формы строку,
-
Участки(k).Номер_участка = k
а вместо этого дополним наш класс:
-
Public Номер_участка As Integer
Private Shared Число_созданных_объектов As Integer = 0
Public Sub New()
Число_созданных_объектов = Число_созданных_объектов + 1
Номер_участка = Число_созданных_объектов
End Sub
При создании очередного участка увеличивается на единицу статическая переменная класса Число_созданных_объектов. Поэтому к моменту создания очередного участка эта переменная автоматически приобретает нужное значение, которое оператором:
-
Номер_участка = Число_созданных_объектов
придается полю Номер_участка.
Конструктор, как и всякая процедура, может иметь параметры. Обычно параметры используются для задания начальных значений переменным, полям и свойствам класса. Зададим через параметры имя владельца, длину, ширину участка и высоту забора.
В учебных целях сделаем переменную Ширина модульной переменой.
-
Public Class Участок
Public Владелец As String
Private Длина As Integer
Public Property Длина_участка() As Integer
Get
Return Длина
End Get
Set (ByVal Value As Integer)
If Value < 500 Then
Длина = Value
Else
MsgBox("Cлишком длинный участок") : Длина = 0
End If
End Set
End Property
Private Ширина As Integer
Public Высота_забора As Integer
Public Shared Расход_краски_на_кв_м As Integer
Private Периметр As Integer
Public Номер_участка As Integer
Private Shared Число_созданных_объектов As Integer = 0
Public Sub New (ByVal Влад As String, ByVal Дл As Integer, _
ByVal Шир As Integer, ByVal Выс As Integer)
Владелец = Влад
Длина_участка = Дл
Ширина = Шир
Высота_забора = Выc
Число_созданных_объектов = Число_созданных_объектов +1
Номер_участка = Число_созданных_объектов
End Sub
Private Sub Вычисляем_периметр()
Периметр = 2 * (Длина + Ширина)
End Sub
Private Function Площадь_забора() As Integer
Вычисляем_периметр()
Return Периметр * Высота_забора
End Function
Public Function Расход_краски() As Integer
Return Расход_краски_на_кв_м * Площадь_забора()
End Function
End Class
‘Для проверки работы конструктора необходимо нажать на кнопку:
Dim Участки(100) As Участок
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim SS As String = ""
Участок.Расход_краски_на_кв_м = 3
Участки(1) = New Участок("Иванов", 5,4,2)
SS=SS & (Учacтки(1).Pacxoд_кpacки)
Участки(2) = New Участок("Петров", 800,5,3)
SS = SS & Учacток(2).Pacxoд_кpacки)
TextBox1.Text=SS
End Sub
Как видите, при помощи параметров конструктора мы можем задать значения поля Владелец и Высота_забора свойству Длина_участка, а также добраться извне до модульной переменной Ширина, невидимой снаружи.