Объектно-ориентированное программирование. Объектно-ориентированное программирование в действии. Объектноориентированное программирование
Скачать 5.29 Mb.
|
Глава 6: Учебный пример: игра «Бильярд» Во втором примере мы построим простую имитацию бильярдного стола. Программа написана на языке Object Pascal для Macintosh 1 . Как и в случае с восемью ферзями, разработка делает упор на создание автономных агентов, взаимодействующих между собой для достижения желаемого результата. 6.1. Элементы бильярда Для пользователя бильярдный стол представляет собой окно, содержащее прямоугольник с лузами по углам, 15 черных шаров и 1 белый шар. Нажатием кнопки мыши пользователь имитирует удар кием по шару, сообщая ему некоторую энергию. Шар движется в сторону, противоположную указателю мыши. Получив энергию, шар начинает катиться, отскакивая от стенок, ударяя другие шары, и, наконец, попадает в лузу. 1 Программа «Бильярд» была разработана на компьютере PowerPC Macintosh с использованием компилятора CodeWarrior Pascal версии 1.1. В версии для PowerPC движение столь быстро, что я решил вставить в процедуру Ball.update замедляющий цикл, несколько раз вызывающий метод draw. Игра, реализованная программой из этой главы, не соответствует никакой настоящей игре. Это не пул, это не бильярд, это просто движение шаров по столу со стенками и лузами. Когда один шар сталкивается с другим, часть энергии первого передается второму и в результате направление движения обоих шаров меняется. PDF created with pdfFactory Pro trial version www.pdffactory.com 6.2. Графические объекты Основу имитации составляют три списка графических объектов, представляющих стенки, лузы и шары. Каждый графический объект включает в себя поле ссылки и поле, описывающее местоположение объекта на экране 1 Мы ввели упрощающее предположение, что все графические объекты занимают прямоугольную область. Это, конечно, совершенно неверно для круглых объектов наподобие шара. Более реалистичной альтернативой было бы написать процедуру, определяющую, пересеклись ли два шара, на основе их действительной геометрии. Но сложность процедуры только отвлекла бы нас от того главного, чего мы хотим добиться, приводя этот пример, а именно понять способ наделения объектов ответственностью за их поведение. Каждый графический объект знает не только как изображать себя, но и как взаимодействовать с другими объектами нашей модели мира. 6.2.1. Графический объект Wall (стенка) Первым из наших трех графических объектов является стенка Wall. Она определяется следующим описанием класса: 1 Неясно, куда поместить изучение этого примера. С одной стороны, читателю важно как можно быстрее увидеть применение объектных принципов; поэтому желательно, чтобы этот пример встретился в книге пораньше. С другой стороны, эта программа только выиграла бы от более изощренной техники, которая обсуждается ниже. В частности, графические объекты было бы лучше представлять в виде иерархии наследования, как описано в главе 7. Кроме того, считается плохим стилем программирования размещение ссылочных полей в области данных объектов, объединенных в список; лучше отделить контейнер и элементы списка. Решение этих проблем нетривиально и содержит сложности. Мы обсудим классы контейнеров в главе 15. Wall = object (* поля данных *) link : Wall; region : Rect; PDF created with pdfFactory Pro trial version www.pdffactory.com (* угол отскока шаров *) convertFactor : real; (* инициализирующая функция *) procedure initialize (left, top, right, bottom : integer; cf : real); (* изображение стенки *) procedure draw; (* сообщение стенке, что о нее ударился шар *) procedure hitBy (aBall : Ball); end; Поле link (ссылка) служит для поддержания списка объектов Wall. Инициализирующий метод просто задает местоположение (region) стенки и параметр отскока (convert factor): procedure Wall.initialize (left, top, right, bottom : integer; cf : real); begin (* инициализация convertFactor *) convertFactor := cf; (* установить область для стены *) SetRect (region, left, top, right, bottom); end; Стенка может быть нарисована просто как сплошной прямоугольник. Это выполняется стандартной процедурой для Macintosh: procedure Wall.draw; begin PaintRect (region); end; Самое интересное происходит со стенкой, когда о нее ударяется шар. Направление его движения изменяется, основываясь на значении параметра convertFactor для стенки. (Переменная convertFactor равна или нулю, или pi, в зависимости от того, горизонтальная стенка или вертикальная.) В результате столкновения шар будет двигаться в новом направлении. procedure Wall.hitBy (aBall : Ball); begin (* оттолкнем шар от стенки *) aBall.setDirection(convertFactor — aBall.direction); end; 6.2.2. Графический объект Hole (луза) Hole (луза) определяется следующим описанием класса: Hole = object (* поля данных *) link : Hole; region : Rect; (* инициализирующая функция *) procedure initialize (x, y : integer); (* изображение лунки *) procedure draw; (* сообщение лузе, что в нее попал шар *) procedure hitBy (aBall : Ball); end; PDF created with pdfFactory Pro trial version www.pdffactory.com Как и в случае стенок, инициализация и изображение лузы в основном состоят из вызова соответствующих стандартных подпрограмм: procedure Hole.initialize (x, y : integer); begin (* определить область с центром в x, y *) SetRect(region, x-5, y-5, x+5, y+5); end; procedure Hole.draw; begin PaintOval (region); end; Больший интерес представляет происходящее при «ударе» шара о лузу. Есть два случая. Если шар оказался белым (он идентифицируется глобальной переменной CueBall), то он возвращается назад в игру на определенную позицию. В остальных случаях шар лишается энергии и убирается со стола в специальную область. procedure Hole.hitBy (aBall : Ball); begin (* остановить шар; убрать его со стола *) aBall.energy := 0.0; aBall.erase; (* передвинуть шар *) if aBall = CueBall then aBall.setCenter(50.100); else begin saveRack := saveRack + 1; aBall.setCenter (10 + saveRack * 15, 250); end; (* перерисовать шар *) aBall.draw; end; 6.2.3. Графический объект Ball (шар) Последним графическим объектом является шар, определяемый следующим описанием класса: Ball = object (* поля данных для шара *) link : Ball; region : Rect; direction : real; (* направление в радианах *) energy : real; (* инициализирующая функция *) procedure initialize (x, y : integer); (* методы *) procedure draw; procedure erase; procedure update; procedure hitBy (aBall : Ball); procedure setDirection (newDirection : real); (* возвращают x, y — координаты центра шара *) function x : integer; function y : integer; end; В дополнение к полям ссылки (link) и местоположения (region), общими с остальными объектами, шар имеет два новых поля данных: direction (направление), вычисленное в PDF created with pdfFactory Pro trial version www.pdffactory.com радианах, и energy (энергия), представляющее собой вещественное значение. Как и в случае лузы, шар инициализируется аргументами, описывающими координаты его центра. Первоначально шар не имеет энергии и его направление нулевое. procedure Ball.initialize (x, y : integer); begin SetRect (region, x-5, y-5, x+5, y+5); setDirection (0.0); energy := 0.0; end; Шар изображается либо окружностью, либо сплошным кругом, в зависимости от того, является ли он белым или нет. procedure Ball.draw; begin if self = CueBall then (* рисуем окружность *) FrameOval (region); else (* рисуем круг *) PaintOval (region); end; procedure Ball.erase; begin EraseRect (region); end; Метод update используется для изменения позиции шара. Если он имеет заметную энергию, то слегка сдвигается, а затем проверяет, не задел ли он другой объект. Глобальная переменная ballMoved устанавливается в true, если какой-либо шар на столе сдвинулся. Если шар задел другой объект, шар сообщает об этом объекту. Сообщения бывают трех видов; они соответствуют ударам по лузе, стенке и другим шарам. Наследование, которое мы изучаем в главе 7, предоставляет методы объединения этих трех тестов в один цикл. procedure Ball.update; var hptr : Hole; wptr : Wall; bptr : Ball; dx, dy : integer; theIntersection : Rect; begin if energy > 0.5 then begin ballMoved := true; (* удалить шар *) erase; (* уменьшить энергию *) energy := energy — 0.05; (* сдвинуть шар *) dx := trunc(5.0 * cos(direction)); dy := trunc(5.0 * sin(direction)); offsetRect(region, dx, dy); (* перерисовать шар *) draw; (* проверить, не попали ли в лузу *) hptr := listOfHoles; while (hptr <> nil) do PDF created with pdfFactory Pro trial version www.pdffactory.com if SectRect (region, hptr.region, theIntersection) then begin hptr.hitBy(self); hptr := nil; end else hptr := hptr.link; (* проверить, не ударились ли в стенку *) wptr := listOfWalls; while (wptr <> nil) do if SectRect (region, wptr.region, theIntersection) then begin wptr.hitBy(self); wptr := nil; end else wptr := wptr.link; (* проверить, не ударили ли шар *) bptr := listOfBalls; while (bptr <> nil) do if SectRect (region, bptr.region, theIntersection) then begin bptr.hitBy(self); bptr := nil; end else bptr := bptr.link; end; end; Когда один шар ударяет другой, энергия первого делится пополам между ними. Также меняются направления движения обоих шаров. procedure Ball.hitBy (aBall : Ball); var da : real; begin (* уменьшить энергию ударяющего шара наполовину *) aBall.energy := aBall.energy / 2; (* и добавить ее к нашему шару *) energy := energy + aBall.energy; (* установить наше новое направление *) setDirection(hitAngle(self.x aBall.x, self.y — aBall.y); (* и направление ударяющего шара *) da := aBall.direction — direction; aBall.setDirection (aBall.direction + da); end; function hitAngle (dx, dy : real) : real; const PI = 3.14159; var na : real; begin if (abs(dx) < 0.05) then na := PI / 2; else na := arctan (abs(dy / dx)); if (dx < 0) then na := PI — na; PDF created with pdfFactory Pro trial version www.pdffactory.com if (dy < 0) then na := -na; hitAngle := na; end; 6.3. Основная программа В предыдущем параграфе описывались статические характеристики программы. Динамика начинается при нажатии кнопки мыши. При этом вызывается следующая процедура: procedure mouseButtonDown (x, y : integer); var bptr : Ball; begin (* присвоим белому шару некоторую энергию *) CueBall.energy := 20.0; (* и направление *) CueBall.setDirection(hitAngle (CueBall.x — x, CueBall.y — y)); (* изменения происходят, пока движется хотя бы один шар *) ballMoved := true; while ballMoved do begin ballMoved := false; bptr := listOfBalls; while bptr <> nil do begin bptr.update; bptr := bptr.link; end; end; end; Оставшаяся часть программы относительно прямолинейна и не представлена здесь. Весь текст находится в Приложении Б. Основная часть кода связана с инициализацией новых объектов и организацией цикла ожидания события, то есть действия пользователя. Главное — понять то, как было децентрализовано управление и как сами объекты были наделены возможностями влиять на ход выполнения программы. Все, что происходит при нажатии кнопки мыши, — это наделение белого шара некоторой энергией. В дальнейшем модель руководствуется исключительно взаимодействием шаров. 6.4. Использование наследования В главе 1 мы описали наследование неформально, а в главе 7 обсудим, как оно работает в каждом из рассматриваемых нами языков. Здесь мы поясним, как наследование используется для упрощения имитации бильярда. Думается, что читателю лучше вернуться к этому параграфу после ознакомления с общими положениями о наследовании в следующей главе. Первым шагом в использовании наследования в нашей имитации бильярда является описание общего класса «графический объект». Он породит трех потомков: шары, стенки и лузы. Родительский класс определяется следующим образом: GraphicalObject = object (* поля данных *) PDF created with pdfFactory Pro trial version www.pdffactory.com link : GraphicalObject; region : Rect; (* инициализирующая функция *) procedure setRegion (left, top, right, bottom : integer); (* операции, выполняемые графическими объектами *) procedure draw; procedure erase; procedure update; function intersect (anObj : GraphicalObject) : boolean; procedure hitBy (anObj : GraphicalObject); end; Инициализирующая функция setRegion просто устанавливает область, занимаемую объектом. Методы draw и update ничего не делают, так как их фактическое поведение определено в дочерних классах. Программа erase очищает область, занимаемую объектом. intersect возвращает значение true, если объект-аргумент пересекается с рассматриваемым объектом. И наконец, метод hitBy также переопределяется в дочерних классах. Хотя двигаются только шары и, следовательно, аргументом этой функции всегда будет шар, тот факт, что класс Ball еще не определен, означает, что мы должны объявить аргумент как имеющий более общий тип GraphicalObject: procedure GraphicalObject.setRegion (left, top, right, bottom : integer); begin SetRect(region, left, top, right, bottom); end; procedure GraphicalObject.draw; begin (* переопределяется в дочернем классе *) end; procedure GraphicalObject.erase; begin EraseRect (region); end; procedure GraphicalObject.update; begin (* переопределяется в дочернем классе *) end; procedure GraphicalObject.hitBy(anObject : GraphicalObject); begin (* переопределяется в дочернем классе *) end; function GraphicalObject.intersect (anObject : GraphicalObject) : boolean; var theIntersection : Rect; begin intersect := SectRect (region, anObject.region, theIntersection); end; Теперь Ball, Wall и Hole объявляются как подклассы общего класса GraphicalObject, и внутри них ни к чему объявлять данные или функции, если только они не переопределяются: Hole = object (GraphicalObject) (* инициализация местоположения лузы *) procedure initialize (x, y : integer); (* изображение лузы *) PDF created with pdfFactory Pro trial version www.pdffactory.com procedure draw; override; (* сообщить лузе, что в нее попал шар *) procedure hitBy (anObject : GraphicalObject); override; end; Процедура hitBy должна преобразовать тип аргумента в Ball. Благоразумно проверить тип до приведения: procedure Wall.hitBy (anObj : GraphicalObject); var aBall : Ball; begin if Member (anObj, Ball) then begin aBall := Ball(anObj); aBall.setDirection(convertFactor — aBall.direction); end; end; Делая класс CueBall подклассом Ball, мы ликвидируем условный оператор в программе изображения шара. CueBall = Object (Ball) procedure draw; override; end; procedure Ball.draw; begin (* рисуем круг *) PaintOval (region); end; procedure CueBall.draw; begin (* рисуем окружность *) FrameOval (region); end; Наибольшее упрощение достигается тем, что теперь можно держать все графические объекты в одном списке. Программа, рисующая весь экран, записывается так: procedure drawBoard; var gptr : GraphicalObject; begin SetPort (theWindow); gptr := listOfObjects; while gptr <> nil do begin gptr.draw; gptr := gptr.link; end; end; Наиболее важным местом этого кода является вызов функции draw внутри цикла. Несмотря на то что вызов написан один, иногда будет вызываться функция класса Ball, а в других случаях — класса Wall или Hole. Тот факт, что одно обращение к функции может привести к вызовам различных функций, относится к понятию полиморфизма. Мы обсудим его в главе 14. PDF created with pdfFactory Pro trial version www.pdffactory.com Часть подпрограммы Ball.update, проверяющая, ударился ли движущийся шар обо что- нибудь, также упрощается аналогичным образом. Это можно увидеть в полном исходном тексте в Приложении Б. Упражнения 1. Предположим, вы хотите производить определенное действие каждый раз, когда программа «Бильярд» выполняет цикл обработки события. В каком месте лучше всего поместить этот код? 2. Предположим, вы хотите сделать шары цветными. Какие части программы вам придется изменить? 3. Предположим, вы хотите добавить лузы на боковых стенках, как на обычном бильярдном столе. Какие части программы вам придется изменить? 4. Программа «Бильярд» использует метод, при котором в цикле просматривается список шаров и каждый шар, имеющий энергию, немного сдвигается. Альтернативный и более объектно-ориентированный подход заключается в том, чтобы позволить каждому шару, пока он имеет энергию, изменять свое состояние и состояние шаров, которые он задевает. Тогда для запуска модели бильярда необходимо только придать движение белому шару. Измените программу, чтобы использовать этот подход. Что дает более реальную модель? Почему? Глава 7: Наследование Первым шагом при изучении объектно-ориентированного программирования было осознание задачи как взаимодействия программных компонент. Этот организационный подход был главным при разборе примеров в главах 5 и 6. Следующим шагом в изучении объектно-ориентированного программирования станет организация классов в виде иерархической структуры, основанной на принципе наследования. Под наследованием мы понимаем возможность доступа представителей дочернего класса (подкласса) к данным и методам родительского класса (надкласса). 7.1. Интуитивное описание наследования Давайте вернемся к Фло — хозяйке цветочного магазина из первой главы. Мы вправе ожидать от нее вполне определенного поведения не потому, что она хозяйка именно цветочного магазина, а потому, что она хозяйка магазина. Например, Фло попросит вас оплатить заказ, а затем даст вам квитанцию. Эти действия не являются уникальными для владельца цветочного магазина; они общие для булочников, бакалейщиков, продавцов канцелярских товаров и автомобилей и т. д. Таким образом, мы как бы связали определенное поведение с общей категорией «хозяева магазинов» Shopkeeper, и поскольку хозяева (и хозяйки) цветочных магазинов (Florist) являются частным случаем категории Shopkeeper, поведение для данного подкласса определяется автоматически. В языках программирования наследование означает, что поведение и данные, связанные с дочерним классом, всегда являются расширением (то есть большим множеством) свойств, связанных с родительскими классами. Подкласс имеет все свойства родительского класса и, кроме того, дополнительные свойства. С другой стороны, поскольку дочерний класс является более специализированной (или ограниченной) формой родительского класса, он также, в определенном смысле, будет сужением родительского класса. Это диалектическое противоречие между наследованием как расширением и наследованием как сужением является источником большой силы, присущей данной технике, и в то же PDF created with pdfFactory Pro trial version www.pdffactory.com время вызывает некоторую путаницу. Мы это увидим в следующих разделах при практическом изучении наследования. Наследование всегда транзитивно, так что класс может наследовать черты надклассов, отстоящих от него на несколько уровней. Например, если собаки Dog являются подклассом класса млекопитающих Mammal, а млекопитающие Mammal являются подклассом класса животных Animal, то класс собак Dog наследует свойства и млекопитающих Mammal, и всех животных Animal. Усложняющим обстоятельством в нашем интуитивном описании наследования является тот факт, что подклассы могут переопределять поведение, унаследованное от родительского класса. Например, класс утконосов Platypus переопределяет процедуру размножения, унаследованную от класса млекопитающих Mammal, поскольку утконосы откладывают яйца. В этой главе мы коротко коснемся механизма переопределения. К более детальному обсуждению семантики наследования мы вернемся в главе 11. |