ООП в VBA
В VBA для описания объекта используется модуль класса. Для каждого абстрактного объекта надо использовать отдельный модуль класса.
Вставить модуль класса в проект:
Редактор VB – Меню – Insert – Class Module
Модулю класса надо присвоить разумное имя, которое станет именем объекта в тексте программы на VBA. Основная «смысловая» проблема: выбор между единственным и множественным числом. Будем считать, что наш класс описывает не множество полос, а абстрактную полосу и сделаем выбор в пользу единственного числа.
Имя модуля присваивается в Редакторе VB, в окне свойств, как значение свойства (Name). В нашем примере в качестве значения надо ввести Polosa.
Свойства объекта описываются как переменные уровня модуля. Это логично: они должны быть видны во всех методах объекта, т.е. во всех процедурах модуля. Здесь есть принципиальный вопрос: хотим ли мы, чтобы свойства были видны за пределами объекта, в основных модулях. Если хотим, то переменные надо объявлять как глобальные с ключевым словом Public (вместо Dim). Мы в нашем решении будем придерживаться принципа инкапсуляции и закроем свойства для внешнего окружения, поэтому объявление будет таким:
Dim a1 As Integer ‘ (начальная клетка)
Dim d As Integer ‘ (шаг прогрессии)
Методы объекта реализуются с помощью процедур модуля класса. Как правило первым методом является метод create, который задает начальные значения свойств нового (пустого) объекта. В нашем случае, чтобы создать, например, первую вертикальную полосу (т.е. полосу от клетки 1 с шагом 3), надо будет написать:
create 1, 3
Т.е. у процедуры должны быть два параметра, они имеют тот же смысл, что свойства a1 и d, но их имена должны отличаться от a1 и d. Решим это просто: добавим к каждому имени начало «p_» (от «параметр»). В результате текст процедуры будет такой.
Sub create(p_a1 As Integer, p_d As Integer)
a1 = p_a1
d = p_d
End Sub
Работа с объектом в основных модулях программы
В вашем проекте я рекомендую завести отдельный модуль (не модуль класса, а просто «Module») и присвоить ему имя «ООП» (или латинскими буквами “OOP”). По двум причинам:
§ в основном модуле нашего проекта уже много текста, и не стоит его перегружать еще;
§ по прошествии времени вам будет удобней увидеть пример работы с классами в виде отдельного модуля.
Как правило абстрактный объект создается потому, что таких объектов в программе должно быть несколько. Так, в нашем проекте будет 8 полос. Соответственно, обычно объекты объединяются в массивы или в коллекции. В предыдущем предложении «массив» и «коллекция» - это не синонимы, а два разных понятия и две разных структуры. Мы используем вариант коллекции.
Коллекция – это структура, которая с одной стороны объединяет однородные объекты, а с другой – сама является объектом. Теоретически элементы коллекции нумеруются, однако существует возможность работать с коллекцией и ее элементами без указания номеров, что удобнее, чем при использовании массива.
Для ссылки на коллекцию используется объектная переменная с универсальным типом Object либо более узким Collection. Я рекомендую первый вариант. Эта переменная должна быть объявлена как минимум как переменная уровня модуля (т.е. вначале модуля, до описания процедур), а, поскольку мы работаем с двумя модулями, то как переменная уровня проекта, т.е. с ключевым словом Public.
Public Poloski As Object.
Логика создания коллекции такова: сначала эта переменная должна быть привязана к новой (пустой) коллекции, а потом к этой коллекции должны быть добавлены ее элементы. Это будет происходить в процедуре, выполняющей «инициализацию» коллекции, поэтому дадим ей имя Poloski_Ini. Создадим эту процедуру и включим в нее (пока) начальную привязку переменной Poloski:
Sub Poloski_InI()
Set Poloski = New Collection
End Sub
В единственной строке процедуры используется уже известный вам формат работы с объектными переменными:
Set <имя> = <ссылка на объект>
В ней есть синтаксическое новшество: использование ключевого слова New для указания на новый, еще не существующий объект. Объект будет создан в момент выполнения этой строки.
Для добавления элементов в коллекцию создадим процедуру AddPolosa. Сразу представим себе ее вызов (пример для первой вертикальной полосы):
AddPolosa 1, 3
В этой процедуре вы увидите принцип работы с объектами пользовательского класса.
Sub AddPolosa (a1 As Integer, d As Integer) | Заголовок процедуры. Это одна строчка программы. Совпадение имен параметров и свойств объекта Polosa проблемой не является, т.к. свойства объекта Polosa не видны на уровне текущего модуля |
Dim pls As Object | Объявляем объектную переменную для ссылки на объект Polosa. Используем универсальный тип Object. Имя переменной должно отличаться от имени класса Polosa, поэтому используем вариант «без гласных». |
Set pls = New Polosa | Привязываем объектную переменную к новому (New) объекту Polosa. Объект будет создан в момент выполнения этой строки |
pls.create a1, d | Для заполнения свойств объекта используем метод create класса Polosa. Напомним: мы создали этот метод в модуле класса.Обратите внимание на формат «через точку»: <объект>.<метод> <аргументы> Аргументы метода берем из параметров процедуры. |
Poloski.Add pls | Добавляем объект (полосу) к коллекции. Здесь используется метод Add объекта «коллекция». Уточним, что этот метод создан разработчиками VBA и всегда присутствует в языке. Мы используем метод Add в простейшем варианте, при котором объект добавляется в конец коллекции. |
End Sub |
Теперь в процедуру Poloski_InI надо включить 8 строчек вида
AddPolosa 1, 3
с параметрами каждой из 8 полос. Впрочем, можно придумать и более «изящный» вариант и добавить 3 горизонтальные и 3 вертикальные полосы с помощью двух циклов.
Процедура Poloski_InI должна выполняться один раз при запуске проекта или, как компромисс, каждый раз при запуске новой игры. Реализуем «компромисс»: вызов процедуры Poloski_InI надо включить в текст процедуры Start основного модуля, поближе к началу процедуры. Можно проверить инициализацию полос щелчком по кнопке «Старт»: происходить ничего не будет, но и сообщений об ошибках быть не должно.
Вернемся к нашей основной задаче – программированию умного хода компьютера. Вспомним один из элементов стратегии: «если есть полоса с двумя нашими знаками и пустой клеткой (с картой, равной «20»), то надо ставить знак в пустую клетку, мы выиграли, СТОП». Для реализации такой идеи нам надо создать два метода объекта
§ подсчет количества крестиков и ноликов в виде «карты» полосы (см. об этом выше);
§ определение номера первой пустой клетки полосы (в ситуациях, которые нас интересуют, «первая» пустая клетка будет единственной)
Оба метода должны быть описаны как процедуры в модуле класса Polosa. Поскольку оба метода возвращают по одному значению, они должны быть оформлены как функции. Займемся ими.
Первая функция – это практически типовая задача подсчета количества вхождений элемента в последовательность, усложненная тем, что надо параллельно считать два количества.
Function karta() As String | |
Dim i As Integer | счетчик цикла |
Dim kx As Integer, ko As Integer | счетчики количества крестиков и ноликов |
Dim kl_nom As Integer | объявление переменных, см. далее |
Dim s As String | |
For i = 1 To 3 | запускаем цикл по 3 клеткам полосы |
kl_nom = a1 + d * (i - 1) | Определяем номер клетки по правилу арифметической прогрессии. Обратите внимание: при запуске функции VBA автоматически подставит a1 и d той полосы, для которой будет вызван метод |
s = kl_znak(kl_nom) | Узнаем знак, стоящий в клетке. Напомним: - kl_znak это функция-оболочка, написанная нами ранее. Отметим это: в модуле класса могут использоваться процедуры, описанные в других модулях. |
If s = "o" Then ko = ko + 1 | Стандартные строки подсчета по условию. Использован сокращенный формат If, с одним действием на ветви. |
If s = "x" Then kx = kx + 1 | |
Next i | |
karta = kx & ko | Выход функции – текстовое сцепление двух чисел (цифр). Явное преобразование типов не обязательно. |
End Function |
Вторая функция (поиск пустой клетки) – это также типовая задача поиска позиции элемента в последовательности. Особенностью этой задачи является немедленный выход из функции в тот момент, когда элемент найден, а также использование значения «по договоренности», если элемент вообще не будет найден. В качестве значения «по договоренности» будем использовать ноль.
Function kl_free() As Integer | |
Dim i As Integer | счетчик цикла |
Dim kl_nom As Integer | объявление переменных, см.далее |
For i = 1 To 3 | запускаем цикл по 3 клеткам полосы |
kl_nom = a1 + d * (i - 1) | Определяем номер клетки по правилу арифметической прогрессии (см. предыдущую задачу) |
If is_pusto(kl_nom) Then | «Если клетка пустая». В условии используем функцию-оболочку is_pusto, написанную нами ранее. |
kl_free = kl_nom | номер клетки – на выход функции |
Exit Function | нашли, немедленный выход из функции |
End If | |
Next i | |
kl_free = 0 | отправляем на выход функции значение по договоренности |
End Function |
Методы объекта Polosa созданы, и мы можем возвращаться к работе в основном модуле. Перед реализацией основной стратегии нам надо решить одну техническую задачу. Дело в том, что компьютер (от лица которого мы сейчас думаем) может играть и крестиками, и ноликами, и нам надо уметь «переворачивать» карту полосы, чтобы первым было количество знаков, которыми играет компьютер. Создадим для этого функцию karta_by_clr (карта по цвету) и поместим ее в основной модуль, перед процедурой PC_hod.
У функции два параметра: карта в формате «крестики-нолики» (xo) и «цвет» интересующего нас игрока (clr, 1 – крестики, 2 – нолики). Если цвет равен единице, то карта остается без изменений, иначе мы соединяем правый символ карты (функция Right, т.е. справа, от xo, 1 символ) и левый символ карты (функция Left, т.е. слева, от xo, 1 символ).
Function karta_by_clr(xo As String, clr As Integer) As String
If clr = 1 Then
karta_by_clr = xo
Else
karta_by_clr = Right(xo, 1) & Left(xo, 1)
End If
End Function
Перебор коллекции
Это последняя теоретическая информация перед реализацией стратегии. В ходе проверки возможных ситуаций нам надо будет рассматривать все 8 полос, то есть перебирать элементы коллекции. Для перебора коллекции в VBA существует специальный цикл For Each. Его формат («ОП» - объектная переменная):
For Each <ОП> In <коллекция>
Действия (с использованием ОП для ссылки на элемент коллекции)
Next <ОП>
Объектная переменная должна быть объявлена до начала цикла с универсальным типом Object, либо более точным типом конкретной коллекции (в нашем случае – Polosa).
Вот теперь можно переходить к реализации стратегии. Мы будем работать с процедурой PC_hod и включать в нее новые фрагменты текста. Сначала – объявление новых переменных (после строки Dim nom As Integer)
Dim pls As Object | объектная переменная для перебора коллекции полос |
Dim xo As String | карта полосы (крестики-нолики) |
Dim pc_pl As String | карта в формате «компьютер-игрок» |
Текст процедуры PC_hod, относящийся к проверке начала игры (распределение цветов) остается без изменений, а после него можно начинать проверку ситуаций.
Итак, ситуация-1, проверка выигрыша пользователя: если карта полосы равна «03», то мы проиграли, СТОП:
For Each pls In Poloski | Запускаем цикл «перебор коллекции». Объектная переменная pls обозначает текущую полосу внутри цикла. |
xo = pls.karta | В переменной xo запоминаем карту полосы. Здесь вызывается метод karta по формату <объект>.<метод>. Метод является функцией, поэтому используется справа от знака равенства. |
pc_pl = karta_by_clr(xo, pc_clr) | В переменной pc_pl запоминаем карту полосы с поправкой на «цвет» компьютера |
If pc_pl = "03" Then | По тексту стратегии: если карта полосы равна «03» |
MsgBox "You win" | Выводим сообщение |
Exit Sub | Выходим из процедуры |
End If | Конец If |
Next pls | Конец цикла For Each |
Ключевая строка с точки зрения ООП, это
xo = pls.karta
Оцените простоту и лаконичность вызова метода объекта. Ради такой простоты и придуман принцип ООП.
Ситуация-2, проверка выигрыша компьютера: если карта полосы равна «20», то надо ставить знак в пустую клетку, мы выиграли, СТОП. Прокомментируем только новые моменты
For Each pls In Poloski | |
xo = pls.karta | |
pc_pl = karta_by_clr(xo, pc_clr) | |
If pc_pl = "20" Then | По тексту стратегии: если карта полосы равна «20» |
nom = pls.kl_free | В переменной nom запоминаем номер пустой клетки. Здесь вызывается метод kl_free объекта полоса, по тем же правилам, что и метод karta. |
hod_kletka nom, pc_clr | Делаем ход в найденную клетку своим «цветом». Напомним: hod_kletka – это процедура, написанная нами ранее. |
MsgBox "I win" | Выводим сообщение |
Exit Sub | |
End If | |
Next pls |
Ситуация-3, защита от выигрыша игрока: если карта полосы равна «02», то надо ставить знак в пустую клетку, СТОП. Эту ситуацию реализуйте самостоятельно, по аналогии с двум предыдущими. Обратите внимание, что в этом случае не будет никакого сообщения, т.к. игра будет продолжаться.
Если ни одна из ситуаций не встретилась, то компьютер по-прежнему будет делать случайный ход. Соответственно, начиная со строки
nom = kl_free_rnd()
останется без изменений старый текст процедуры PC_hod.
На этом нововведения можно считать реализованными. Можно проверять работу проекта.