Первое знакомство с объектно-ориентированным программированием

А.А.Семенов , А.Г.Юдина

Продолжение. Начало см. в № 14/99

Занятие № 3

Определим нашу ближайшую цель.

Создадим некий родительский объектный тип (на Си он назывался бы классом).

Это будет прародитель целого семейства разнообразных по сложности графических образов, обладающих некоторыми общими характеристиками, а именно: x, y — координаты левого верхнего угла прямоугольника, описанного около объекта; color — его цвет.

Кроме этого, наши объекты будут обладать следующими свойствами:

каждый из них может быть

· создан вызовом специальной процедуры-конструктора Init;

· нарисован цветом color вызовом процедуры Show;

· спрятан вызовом процедуры Hide (т.е. нарисован, например, цветом фона);

· перемещен вызовом процедуры Move (сперва спрятан, затем, после увеличения координат объекта на величину перемещения по каждой из осей, нарисован вновь).

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

Для того чтобы конкретный тип потомка можно было бы задавать на этапе выполнения программы, процедура Draw объявлена виртуальной. Это позволяет менять вид объектов в нашем скрин-сейвере “на лету”.

type
GOb=object
x,y: integer;
color: byte;
constructor Init(ax,ay:integer;acolor: byte);
procedure Draw(acolor: byte); virtual;
procedure Show;
procedure Hide;
procedure Move(dx,dy: integer);
end;

Характеристики (x,y,color) называются ПОЛЯМИ объекта, а процедуры/функции (Init, Draw, Show, Hide), определяющие его свойства, — МЕТОДАМИ объекта.

В объектно-ориентированном программировании считается дурным тоном менять напрямую поля объекта (хотя Турбо Паскаль это допускает). Лучше использовать для этого специально разработанные методы.

Итак, опишем все методы.

uses graph13h;
type
GOb=object
x,y: integer;
color: byte;
constructor Init(ax,ay: integer;acolor: byte);
procedure Draw(acolor: byte); virtual;
procedure Show;
procedure Hide;
procedure Move(dx,dy: integer);
end;

constructor GOb.Init(ax,ay: integer;acolor: byte);
begin
  x:=ax; y:=ay;
  color:=acolor;
end;

procedure GOb.Draw(acolor: byte);
begin
end;

procedure GOb.Show;
begin
  Draw(color);
end;

procedure GOb.Hide;
begin
  Draw(0);
end;

procedure GOb.Move(dx,dy: integer);
begin
  Hide;
  x:=x+dx;
  y:=y+dy;
  Show;
end;

var
a: GOb;
begin
  Screen13h;
  a.init(160,100,14);
  a.show;
  readln;
end.

Запускаем программу, и... видим на экране августовскую ночь. Ничего удивительного — ведь наш объектный тип-прародитель имеет пустой метод Draw. Кстати, в Паскале даже есть процедура Abstract, генерирующая ошибку при обращении к экземплярам абстрактных объектов. Следовательно, надо породить от нашего GOb’а жизнеспособных потомков.

Простейшим потомком абстрактного объекта GOb будет, по всей видимости, просто точка.

Тип Point объекта-потомка (в Си и Java-класс) GOb опишем следующим образом после описания объекта-родителя:

type
{... здесь будет описание родительского типа GOb}
Point=object(GOb);
procedure Draw; virtual;
end;

Нас вполне устраивает поведение предка (его методы) и его данные (поля).

Нужно всего лишь перекрыть (т.е. заменить одноименным) единственный метод Draw. В предке метод Draw ничего не делает. Новый Draw виртуальный — так же, как и в объекте-предке. Напомним, что объявление метода виртуальным дает возможность потомкам различных типов обращаться к методам общего предка на этапе выполнения программы. В нашем случае различные методы Draw потомков будут вызываться методами Show и Hide, как предписано в объекте GOb.

Все остальные методы и поля наш объект Point наследует от предка.

Содержание метода Draw элементарно:

procedure Point.Draw;
begin

PutPixel(x,y,color)

end;

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

{$R-,Q-}
uses graph13h;
type
GOb=object
x,y: integer;
color: byte;
constructor Init(ax,ay: integer;acolor: byte);
procedure Draw(acolor: byte); virtual;
procedure Show;
procedure Hide;
procedure Move(dx,dy:integer);
end;
Point=object(GOb)
procedure Draw(acolor: byte); virtual;
end;
constructor GOb.Init(ax,ay:integer;acolor: byte);
begin
  x:=ax; y:=ay;
  color:=acolor;
end;

procedure GOb.Draw(acolor: byte);
begin
end;

procedure GOb.Show;
begin
Draw(color);
end;

procedure GOb.Hide;
begin
  Draw(0);
end;

procedure GOb.Move(dx,dy: integer);
begin
  Hide;
  x:=x+dx; y:=y+dy;
  Show;
end;

procedure Point.Draw(acolor: byte);
begin
  PutPixel(x,y,acolor)
end;
const
max=100;
var
  a: array [1..max] of Point;
  i: integer;
begin
  Screen13h;
   for i:=1 to max do
    a[i].Init(Random(320),Random(200),Random(256));
   for i:=1 to max do a[i].Show;
  readln;
end.

Запускаем программу, и... видим на экране августовскую ночь, но уже звездную.

Раздел описаний (намного больший, чем раздел операторов) включает:

· описание типов (одного предка и одного потомка);

· описание процедур (методов наших объектов);

· описание констант;

· описание переменных.

Примечание

Вопрос: Как использовать процедуру Abstract?

Ответ: Нужно подключить модуль Objects библиотеки Turbo Vision, входящей в комплект поставки Турбо Паскаля, начиная с версии 6 (файл Objects.tpu). После этого достаточно вызвать процедуру Abstract из абстрактного метода. При прямом вызове абстрактного метода процедура генерирует ошибку времени выполнения номер 211, напоминая, что такое действие некорректно.

Задание на дом

1. Сделать так, чтобы звезды двигались с одной скоростью паралелльно диагонали экрана или случайным образом (как броуновские частицы).

2. Дополнить листинг объектным типом Cross, экземпляры которого на экране выглядят как крестики 3ґ3 точки. Если новый тип создан, то простой заменой описания

var
a: array [1..max] of Point;
на
var
a: array [1..max] of Cross;

точки превращаются... точки превращаются... превращаются точки... в элегантные крестики! А поведение крестиков останется тем же, что и у точек. Это и неудивительно, ведь у объектов Point и Cross общий предок GOb.

Занятие № 4

Наверное, читатель уже оценил удобство объектного подхода, выполняя домашнее задание.

Добавляя аналогичным образом все новых потомков, можно легко разнообразить “ассортимент” живущих на экране объектов. Попробуйте поселить одновременно на экране и точки, и крестики. И пусть, например, точки изображают падающие снежинки, а крестики (или другие придуманные вами объекты) движутся случайным образом.

Но сначала избавимся от необходимости переносить из программы в программу описание Адама нашей иерархии объектов — создадим еще один модуль с описанием родительского объекта GOb:

{$R-,Q-}
unit MyObj;
Interface
type
GOb=object
x,y:word;
color:byte;
constructor Init(ax,ay:integer;acolor:byte);
procedure Draw(acolor:byte);virtual;
procedure Show;
procedure Hide;
procedure Move(dx,dy:integer);
end;

Implementation
constructor GOb.Init(ax,ay:integer;acolor:byte);
begin
  x:=ax;y:=ay;color:=acolor
end;

procedure GOb.Draw(acolor:byte);
begin
end;

procedure GOb.Show;
begin
Draw(color)
end;

procedure GOb.Hide;
begin
Draw(0)
end;

procedure GOb.Move(dx,dy:integer);
begin
  Hide;
  x:=x+dx; y:=y+dy;
  Show
end;
end.

Файл надо сохранить как MyObj.pas и затем откомпилировать. Получим еще один модуль — MyObj.tpu.

Теперь программы будут выглядеть не так “тяжеловесно”.

{$R-,Q-}
uses graph13h,MyObj,crt;
type
Point=object(GOb)
procedure Draw(acolor:byte);virtual;
end;
Cross=object(GOb)

procedure Draw(acolor:byte);virtual;
end;

procedure Point.Draw(acolor:byte);
begin
PutPixel(x,y,acolor)
end;

procedure Cross.Draw(acolor:byte);
begin
  HLine(x,y+2,x+4,acolor);VLine(x+2,y,y+4,acolor)
end;
const max=100;
var a:array[1..max] of Point;b:array[1..max] of Cross;
i:integer;
begin
  Screen13h;
    for i:=1 to max do
    begin
     a[i].Init(random(320),random(200),14);
     {это 100 желтых точек}
     b[i].Init(random(320),random(200),random(256));
     {а это 100 разноцветных крестиков}
    end;
    for i:=1 to max do begin a[i].Show;b[i].Show end;
     readln;
     while not keypressed do
      for i:=1 to max do
      begin
       a[i].Move(-2+random(5),-2+random(5));
       {точки изображают броуновское движение}
       b[i].Move(1,1); delay(1)
       {а крестики дружно плывут на юго-восток}
   end;
   readln
end.

А если мы захотим существенно изменить поведение наших объектов? Пусть нам необходимо, чтобы объекты летали внутри некоторой области и отражались от ее границ. Тогда нашим объектам понадобятся дополнительные характеристики — скорости по осям vx и vy. При достижении вертикальной границы скорость vx объекта должна менять знак, а при “столкновении” с горизонтальной границей меняет знак вертикальная составляющая vy.

В ООП не принято “тревожить” первичный объект в иерархии без крайней необходимости. Нужно порождать потомков, имеющих все необходимые поля и методы, определяющие их поведение. Нам требуется, чтобы точка и крестик отражались от стенок.

Давайте попробуем создать потомка объекта Gob по имени Ball, который “знает”, как обеспечивать зеркальное отражение, а от него уже породим Point и Cross.

Итак, изменим схему наследования.

{$R-,Q-}
uses graph13h,MyObj,crt;
type
Ball=object(GOb)
vx,vy:integer;
constructor Init(ax,ay,avx,avy:integer;acolor:byte);
procedure Move;
end;
Cross=object(Ball)

procedure Draw(acolor:byte);virtual;
end;

constructor Ball.Init(ax,ay,avx,avy:integer;acolor:byte);
begin
  inherited Init(ax,ay,acolor);
  {унаследованная процедура инициализации}
  vx:=avx;
  vy:=avy;
  {плюс еще два поля (две характеристики объекта): скорости по осям}
end;

procedure Ball.Move;
begin
  inherited Move(vx,vy);
  if (x<20) or (x>299) then vx:=-vx;
  if (y<20) or (y>179) then vy:=-vy;
  {добавление к унаследованной процедуре движения}
  {отражения от вертикальных и горизонтальных "стенок"}
end;

procedure Cross.Draw(acolor:byte);
begin
HLine(x,y+2,x+4,acolor);VLine(x+2,y,y+4,acolor)
end;
const max=100;
var a:array[1..max] of Cross;
i:integer;
begin
  Randomize;
  Screen13h;
  for i:=1 to max do
   begin
    a[i].Init(20+random(280),20+random(160),random(5),random(3), random(255));
    a[i].Show;
    {каждый крестик при "рождении" в случайном месте наделяется
     случайными скоростями по осям}
   end;
  readln;
  while not keypressed do
  for i:=1 to max do
   begin
    a[i].Move;delay(1)
   end;
readln
end.

Уф! Ну, достаточно на этот раз.

На следующем занятии мы создадим более сложный тип-потомок под освежающим именем Sprite.

Примечание

Вопрос: Почему меню и стандартные диалоги (открыть/сохранить файл, получить подтверждение на серьезное действие) в Windows-программах все “на одно лицо”?

Ответ: Дело в том, что все они — экземпляры или прямые потомки стандартных объектов Windows. Так проще жить и программистам, и пользователям. Достаточно вспомнить муки бухгалтеров начала 90-х, привыкших вызывать меню в электронной таблице Supercalc клавишей </>, затем вынужденных переучиться на <F10>   QuattroPro, а затем <ALT> на ¦в Excel, если вдруг вышла из строя мышка. Дела давно минувших дней...

Домашнее задание в стиле “ретро”.

Попробуйте заставить звездочки и крестики бежать по радиусам из центра экрана в духе скрин-сейвера “Летящие звезды”.

Окончание - в   № 16/99

TopList