Взгляд на ООП из низкого уровня


Как правило, большинству низкоуровневых программистов объектно ориентированное программирование кажется сложным. В этой статье объясняются некоторые моменты ООП и приводятся примеры их устройства с помощью языков C и Assembler. Замечу, что примеры не всегда сто процентно читаемы, и не всегда правильны. Примеры не подходят для немедленного выполнения, они служат только иллюстрациями к тексту. Главное достоинство ООП – возможность получить доступ к объектам посредством сходного интерфейса. Эта мысль поясняется в остальной части статьи.

Простой объект (First object)

Создадим простой объект. Каким он будет? Хороший пример, когда ООП действительно нужно, это разработка игр. Представьте себе, что мы разрабатываем компьютерную игру. В нашей игре будут враги, оружие, ракеты и т.п. Вещи, которые вы привыкли видеть в компьютерных играх. И все они будут представлены как объекты, которые, в свою очередь, как структуры данных в памяти. Естественно, типов объектов будет множество. Например, аптечка (medikit), для восстановления здоровья игрока, или ружье, из которого можно стрелять. Объектов какого-либо типа в игре тоже может быть много. Все эти объекты могут выглядеть одинаково и вести себя сходным образом, но, в тоже время, отличаться между собой. Иметь, например, различное местоположение на карте (в игровом мире) или отличаться количеством восстанавливаемого здоровья. В терминологии ООП тип объекта называют классом. Тогда объект – это экземпляр какого-либо класса. Характеристики объекта называют свойствами (реже полями). Внутри каждого объекта сохраним идентификатор, позволяющий установить, к какому классу он относится. В примере это первое поле структуры. Другие поля структуры будут хранить свойства аптечки, это координаты x:y и количество здоровья, которое она восстановит при использовании. Описание аптечки, которая находится в точке [10;15] и восстанавливает 50 пунктов здоровья:

Asm:
dd TYPE_MEDIKIT	;класс - аптечки
dd 10			;координата x = 10
dd 15			;           y = 15
dd 50			;пункты здоровья = 50

C:
struct MEDIKIT {
	int type;
	int x, y;
	int hp;
};
MEDIKIT mk = {TYPE_MEDIKIT, 10, 15, 50};

Определим теперь другой объект – бомбу (mine). Она также должна иметь координаты на карте. Но вместо количества здоровья понадобится количество урона, которое она нанесет. Кроме того, бомба имеет еще одну характеристику – время, через которое она сработает после активации. Объект может выглядеть следующим образом:

Asm:
dd TYPE_MINE		;тип объекта = mine
dd 10			;x = 10
dd 15			;y = 15
dd 5			     ;время срабатывания = 5 секунд
dd 50			;урон = 50

C:
struct MINE {
	int type;
	int x, y;
	int timeout;
	int hp;
}; 
MEDIKIT mk = {TYPE_MINE, 10, 15, 5, 50};

Методы (Methods)

Когда игрок встретит в мире один из таких предметов, мы получим сообщение об этом вместе с указателем на структуру данных, описывающих объект. Тогда мы прочитаем первое поле и узнаем, к какому классу относится встреченный объект. Если это аптечка, мы восстанавим здоровье игрока. А если бомба, начнем отсчет до взрыва.

Asm:
;esi = адрес структуры данных (объекта)
cmp     dword [esi], TYPE_MEDIKIT
je      touched_medikit
cmp     dword [esi], TYPE_MINE
je      touched_mine

C:
switch(obj->type) {
case TYPE_MEDIKIT:
        MEDIKIT *mk = (MEDIKIT*)obj;
        ...
case TYPE_MINE:
        MINE *mn = (MINE*)obj;
        ...

Этот подход прост, но имеет недостатки. Части кода, где происходит обращение к объекту, могут быть разбросаны по всей программе. И если мы хотим изменить что-то в описании класса (изменить структуру данных, описывающую объект), мы должны изменить все места, где происходит обращение к объекту. Это неудобно, вдобавок, можно легко допустить ошибку.

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

Asm:
cmp     dword [esi], TYPE_MEDIKIT
jne     not_medikit
push    esi
call    medikit_touched
jmp     done
not_medikit:

cmp     dword [esi], TYPE_MINE
jne     not_mine
push    esi
call    mine_touched
jmp     done
not_mine:

C:
switch(obj->type) {
case TYPE_MEDIKIT:
        medikit_touched((MEDIKIT*)obj);
        break;
case TYPE_MINE:
        mine_touched((MINE*)obj);
        break;

Функции, которые работают с объектами, называют методами.

Виртуальные методы (Virtual Methods)

Однако, такой подход тоже нельзя назвать идеальным. Если нам понадобится добавить новый класс или новый метод, придется сделать слишком много исправлений. Но мы можем разместить указатель на метод внутри самого объекта. Тогда, вызывая метод мы не будем определять тип объекта, а сразу вызовем функцию:

Asm:
; medikit object 
dd TYPE_MEDIKIT         ;тип объекта = аптечка
dd medikit_touched      ;указатель на "touched" метод
dd 10                   ;x = 10
dd 15                   ;y = 15
dd 50                   ;HP = 50

; mine object
dd TYPE_MINE            ;тип объекта = бомба
dd mine_touched         ;указатель на "touched" метод
dd 10                   ;x = 10
dd 15                   ;y = 15
dd 5                    ;таймер = 5 секунд
dd 50                   ;урон = 50

; вызов "touched" метода, указатель на объект передается в ESI
    push    esi   ;адрес объекта, для которого вызывается метод
    call    dword [esi+4]   ;вызов метода

C:
struct MEDIKIT {
   int type;
   void (*touched)(OBJECT* this);//указатель на "touched" метод
   int x, y;
   int hp;
}; 
struct MINE {
   int type;
   void (*touched)(OBJECT* this);//указатель на "touched" метод
   int x, y;
   int timeout;
   int hp;
}; 
struct OBJECT {  //общая часть, есть во всех объектах
   int type;   //тип объекта
   void (*touched)(OBJECT* this);//указатель на "touched" метод
} *obj;

//вызов метода для объекта
    obj->touched(obj);

Инкапсуляция

Здесь мы подошли к основному принципу объектно ориентированного программирования. Согласно нему объект закрыт для других объектов, а взаимодействие осуществляется только через методы. То есть, если другому объекту потребуется узнавать, сколько пунктов здоровья восстанавливает аптечка (чтобы отображать на экране, например) мы напишем еще одну функцию-метод, которая будет возвращать значение свойства hp. Если другому объекту потребуется изменять это свойство, мы также добавим еще один метод. Быть может, это кажется излишней расточительностью, но оно оправдывает себя. Разработав класс мы можем возвращаться к его коду только если хотим что-то добавить (есть специальные средства, чтобы не делать даже этого). А все взаимодействие сводится к вызову однажды разработанных методов, которые не изменяются вместе с измененным классом. Это также позволяет лучше организовать разработку больших программ. Однажды договорившись о взаимодействии классов программисты могут разрабатывать их независимо друг от друга.

Принцип объект-черный ящик называют инкапсуляцией.

Виртуальные таблицы (Virtual Table)

Давайте создадим другой метод, назовем его ‘shot’, он будет обрабатывать выстрел игрока по объекту. Теперь объекты аптечка и бомба выглядят следующим образом:

Asm:
; medikit object
dd TYPE_MEDIKIT
dd medikit_touched
dd medikit_shot     ;мы добавили указатель на новый метод
dd 10
dd 15
dd 50

; mine object
dd TYPE_MINE
dd mine_touched
dd mine_shot    ;указатель на метод mine_shot
dd 10
dd 15
dd 5
dd 50

; вызов "touched" метода для объекта, адрес которого в ESI
push    esi     
call    dword [esi+4]

; вызов "shot" метода
push    esi
call    dword [esi+8]

C:
struct MEDIKIT {
        int type;
        void (*touched)(OBJECT* this);
        void (*shot)(OBJECT* this); //мы добавили указатель на 							 //метод
        int x, y;
        int hp;
}; 
struct MINE {
        int type;
        void (*touched)(OBJECT* this);  
        void (*shot)(OBJECT* this);//указатель на новый метод
        int x, y;
        int timeout;
        int hp;
}; 
struct OBJECT {       //общая часть
        int type;
        void (*touched)(OBJECT* this);          
        void (*shot)(OBJECT* this);
} *obj;

//вызов методов
obj->touched(obj);
obj->shot(obj);

Вот примеры этого метода. ‘medikit.shot’ снижает запас здоровья в аптечке в два раза, а ‘mine.shot’ взорвет бомбу (таймер устанавливается на ноль).

Asm:
medikit_shot:
        mov     esi, [esp+4]
        mov     eax, [esi + MEDIKIT.HP]
        shr     eax, 1
        mov     [esi + MEDIKIT.HP], eax
        retn    4
mine_shot:
        mov     esi, [esp+4]
        mov     dword [esi + MINE.TIMEOUT], 0
        retn    4
C:
void medikit_shot(MEDIKIT *this)
{
        this->hp = this->hp / 2;
}
void mine_shot(MINE *this)
{
        this->timeout = 0;
}

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

Asm:
; виртуальная таблица для класса MEDIKIT
medikit_vtab:
        dd medikit_touched
        dd medikit_shot

        ; экземпляр класса MEDIKIT (объект аптечка)
mk:
        dd medikit_vtab       ;указатель на виртуальную таблицу
        dd 10                 ;x
        dd 15                 ;y
        dd 50                 ;hp

;виртуальная таблица для класса MINE
mine_vtab:
        dd mine_touched
        dd mine_shot

;экземпляр класса
mn:
        dd mine_vtab          ;указатель на виртуальную таблицу
        dd 20                 ;x
        dd 20                 ;y
        dd 30                 ;таймер
        dd 50                 ;hp

;вызов "touched" метода для объекта, адрес которого в ESI
push    esi              ;аргумент для метода – адрес объекта
mov     eax, dword [esi] ;EAX = адрес виртуальной таблицы
call    dword [eax]      ;EAX+0 = адрес 'touched' метода

push    esi                    
mov     eax, dword [esi]       
call    dword [eax+4]           ;EAX+4 = адрес 'shot' метода

C:
//описание виртуальной таблицы
struct VTAB {
        void (*touched)(OBJECT* this);
        void (*shot)(OBJECT* this);
};

// аптечка
struct MEDIKIT {
        VTAB *vtab;
        int x,y;
        int hp;
};
VTAB medikit_vtab = { //виртуальная таблица для MEDIKIT
  &medikit;_touched, 
  &medikit;_shot
};  
MEDIKIT mk1 = {  //аптечка
  &medikit;_vtab, 10, 15, 50
};
MEDIKIT mk2 = {  //вторая аптечка
  &medikit;_vtab, 20, 20, 10
};

// mine
struct MINE {
        VTAB *vtab;     
        int x,y;
        int timeout;
        int hp;
};
VTAB mine_vtab = { //виртуальная таблица для MINE
  &mine;_touched, 
  &mine;_shot
};   
MINE mn1 = {  //объект MINE
  &mine;_vtab, 10, 15, 30, 30
};

//общая часть всех объектов
struct OBJECT {         
        VTAB *vtab;    
};
OBJECT *obj;

//вызов методов
obj->vtab->touched(obj);
obj->vtab->shot(obj);

Заключение

Хотя ООП поддерживается многими языками программирования на уровне архитектуры, сам по себе метод является лишь парадигмой (методологией) программирования; наряду с такими методами, как процедурное программирование. Его назначение — обеспечить более удобную разработку больших проектов.

[C] vid, пер. DarkWanderer

 

Источник WASM.RU /29.08.2008/


Поделиться в соц сетях

Подписаться
Уведомить о
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+