Взгляд на ООП из низкого уровня — Архив WASM.RU
Как правило, большинству низкоуровневых программистов объектно ориентированное программирование кажется сложным. В этой статье объясняются некоторые моменты ООП и приводятся примеры их устройства с помощью языков C и Assembler. Замечу, что примеры не всегда сто процентно читаемы, и не всегда правильны. Примеры не подходят для немедленного выполнения, они служат только иллюстрациями к тексту. Главное достоинство ООП – возможность получить доступ к объектам посредством сходного интерфейса. Эта мысль поясняется в остальной части статьи.
Простой объект (First object)
Создадим простой объект. Каким он будет? Хороший пример, когда ООП действительно нужно, это разработка игр. Представьте себе, что мы разрабатываем компьютерную игру. В нашей игре будут враги, оружие, ракеты и т.п. Вещи, которые вы привыкли видеть в компьютерных играх. И все они будут представлены как объекты, которые, в свою очередь, как структуры данных в памяти. Естественно, типов объектов будет множество. Например, аптечка (medikit), для восстановления здоровья игрока, или ружье, из которого можно стрелять. Объектов какого-либо типа в игре тоже может быть много. Все эти объекты могут выглядеть одинаково и вести себя сходным образом, но, в тоже время, отличаться между собой. Иметь, например, различное местоположение на карте (в игровом мире) или отличаться количеством восстанавливаемого здоровья. В терминологии ООП тип объекта называют классом. Тогда объект – это экземпляр какого-либо класса. Характеристики объекта называют свойствами (реже полями). Внутри каждого объекта сохраним идентификатор, позволяющий установить, к какому классу он относится. В примере это первое поле структуры. Другие поля структуры будут хранить свойства аптечки, это координаты x:y и количество здоровья, которое она восстановит при использовании. Описание аптечки, которая находится в точке [10;15] и восстанавливает 50 пунктов здоровья:
Определим теперь другой объект – бомбу (mine). Она также должна иметь координаты на карте. Но вместо количества здоровья понадобится количество урона, которое она нанесет. Кроме того, бомба имеет еще одну характеристику – время, через которое она сработает после активации. Объект может выглядеть следующим образом:Код (Text):
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};
Код (Text):
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)
Когда игрок встретит в мире один из таких предметов, мы получим сообщение об этом вместе с указателем на структуру данных, описывающих объект. Тогда мы прочитаем первое поле и узнаем, к какому классу относится встреченный объект. Если это аптечка, мы восстанавим здоровье игрока. А если бомба, начнем отсчет до взрыва.
Код (Text):
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; ...Этот подход прост, но имеет недостатки. Части кода, где происходит обращение к объекту, могут быть разбросаны по всей программе. И если мы хотим изменить что-то в описании класса (изменить структуру данных, описывающую объект), мы должны изменить все места, где происходит обращение к объекту. Это неудобно, вдобавок, можно легко допустить ошибку.
Будет лучше, если мы сгруппируем код. Напишем функции, которые будут работать с объектами и сохраним их в одном месте. В нашем примере есть функция, восстанавливающая здоровье игрока при использовании аптечки, и другая функция, активирующая взрыватель при использовании бомбы.
Код (Text):
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)
Однако, такой подход тоже нельзя назвать идеальным. Если нам понадобится добавить новый класс или новый метод, придется сделать слишком много исправлений. Но мы можем разместить указатель на метод внутри самого объекта. Тогда, вызывая метод мы не будем определять тип объекта, а сразу вызовем функцию:
Код (Text):
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', он будет обрабатывать выстрел игрока по объекту. Теперь объекты аптечка и бомба выглядят следующим образом:
Код (Text):
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' взорвет бомбу (таймер устанавливается на ноль).
Код (Text):
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; }Хранить указатели на методы удобно, но ведь для всех объектов одного класса они одинаковы. Будет лучше, если мы создадим статичные таблицы для каждого класса. Тогда внутри объекта можно будет хранить только указатель на такую таблицу. Кроме того, таблица будет уникальной для класса, а значит, хранить тип объекта тоже не нужно.
Код (Text):
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);Заключение
Хотя ООП поддерживается многими языками программирования на уровне архитектуры, сам по себе метод является лишь парадигмой (методологией) программирования; наряду с такими методами, как процедурное программирование. Его назначение - обеспечить более удобную разработку больших проектов. © vid, пер. DarkWanderer
Взгляд на ООП из низкого уровня
Дата публикации 29 авг 2008