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

Дата публикации 29 авг 2008

Взгляд на ООП из низкого уровня — Архив WASM.RU

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

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

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

Код (Text):
  1.  
  2. Asm:
  3. dd TYPE_MEDIKIT ;класс - аптечки
  4. dd 10           ;координата x = 10
  5. dd 15           ;           y = 15
  6. dd 50           ;пункты здоровья = 50
  7.  
  8. C:
  9. struct MEDIKIT {
  10.     int type;
  11.     int x, y;
  12.     int hp;
  13. };
  14. MEDIKIT mk = {TYPE_MEDIKIT, 10, 15, 50};
Определим теперь другой объект – бомбу (mine). Она также должна иметь координаты на карте. Но вместо количества здоровья понадобится количество урона, которое она нанесет. Кроме того, бомба имеет еще одну характеристику – время, через которое она сработает после активации. Объект может выглядеть следующим образом:

Код (Text):
  1.  
  2. Asm:
  3. dd TYPE_MINE        ;тип объекта = mine
  4. dd 10           ;x = 10
  5. dd 15           ;y = 15
  6. dd 5                 ;время срабатывания = 5 секунд
  7. dd 50           ;урон = 50
  8.  
  9. C:
  10. struct MINE {
  11.     int type;
  12.     int x, y;
  13.     int timeout;
  14.     int hp;
  15. };
  16. MEDIKIT mk = {TYPE_MINE, 10, 15, 5, 50};

Методы (Methods)

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

Код (Text):
  1.  
  2. Asm:
  3. ;esi = адрес структуры данных (объекта)
  4. cmp     dword [esi], TYPE_MEDIKIT
  5. je      touched_medikit
  6. cmp     dword [esi], TYPE_MINE
  7. je      touched_mine
  8.  
  9. C:
  10. switch(obj->type) {
  11. case TYPE_MEDIKIT:
  12.         MEDIKIT *mk = (MEDIKIT*)obj;
  13.         ...
  14. case TYPE_MINE:
  15.         MINE *mn = (MINE*)obj;
  16.         ...

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

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

Код (Text):
  1.  
  2. Asm:
  3. cmp     dword [esi], TYPE_MEDIKIT
  4. jne     not_medikit
  5. push    esi
  6. call    medikit_touched
  7. jmp     done
  8. not_medikit:
  9.  
  10. cmp     dword [esi], TYPE_MINE
  11. jne     not_mine
  12. push    esi
  13. call    mine_touched
  14. jmp     done
  15. not_mine:
  16.  
  17. C:
  18. switch(obj->type) {
  19. case TYPE_MEDIKIT:
  20.         medikit_touched((MEDIKIT*)obj);
  21.         break;
  22. case TYPE_MINE:
  23.         mine_touched((MINE*)obj);
  24.         break;

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

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

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

Код (Text):
  1.  
  2. Asm:
  3. ; medikit object
  4. dd TYPE_MEDIKIT         ;тип объекта = аптечка
  5. dd medikit_touched      ;указатель на "touched" метод
  6. dd 10                   ;x = 10
  7. dd 15                   ;y = 15
  8. dd 50                   ;HP = 50
  9.  
  10. ; mine object
  11. dd TYPE_MINE            ;тип объекта = бомба
  12. dd mine_touched         ;указатель на "touched" метод
  13. dd 10                   ;x = 10
  14. dd 15                   ;y = 15
  15. dd 5                    ;таймер = 5 секунд
  16. dd 50                   ;урон = 50
  17.  
  18. ; вызов "touched" метода, указатель на объект передается в ESI
  19.     push    esi   ;адрес объекта, для которого вызывается метод
  20.     call    dword [esi+4]   ;вызов метода
  21.  
  22. C:
  23. struct MEDIKIT {
  24.    int type;
  25.    void (*touched)(OBJECT* this);//указатель на "touched" метод
  26.    int x, y;
  27.    int hp;
  28. };
  29. struct MINE {
  30.    int type;
  31.    void (*touched)(OBJECT* this);//указатель на "touched" метод
  32.    int x, y;
  33.    int timeout;
  34.    int hp;
  35. };
  36. struct OBJECT {  //общая часть, есть во всех объектах
  37.    int type;   //тип объекта
  38.    void (*touched)(OBJECT* this);//указатель на "touched" метод
  39. } *obj;
  40.  
  41. //вызов метода для объекта
  42.     obj->touched(obj);

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

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

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

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

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

Код (Text):
  1.  
  2. Asm:
  3. ; medikit object
  4. dd TYPE_MEDIKIT
  5. dd medikit_touched
  6. dd medikit_shot     ;мы добавили указатель на новый метод
  7. dd 10
  8. dd 15
  9. dd 50
  10.  
  11. ; mine object
  12. dd TYPE_MINE
  13. dd mine_touched
  14. dd mine_shot    ;указатель на метод mine_shot
  15. dd 10
  16. dd 15
  17. dd 5
  18. dd 50
  19.  
  20. ; вызов "touched" метода для объекта, адрес которого в ESI
  21. push    esi    
  22. call    dword [esi+4]
  23.  
  24. ; вызов "shot" метода
  25. push    esi
  26. call    dword [esi+8]
  27.  
  28. C:
  29. struct MEDIKIT {
  30.         int type;
  31.         void (*touched)(OBJECT* this);
  32.         void (*shot)(OBJECT* this); //мы добавили указатель на                              //метод
  33.         int x, y;
  34.         int hp;
  35. };
  36. struct MINE {
  37.         int type;
  38.         void (*touched)(OBJECT* this);  
  39.         void (*shot)(OBJECT* this);//указатель на новый метод
  40.         int x, y;
  41.         int timeout;
  42.         int hp;
  43. };
  44. struct OBJECT {       //общая часть
  45.         int type;
  46.         void (*touched)(OBJECT* this);          
  47.         void (*shot)(OBJECT* this);
  48. } *obj;
  49.  
  50. //вызов методов
  51. obj->touched(obj);
  52. obj->shot(obj);

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

Код (Text):
  1.  
  2. Asm:
  3. medikit_shot:
  4.         mov     esi, [esp+4]
  5.         mov     eax, [esi + MEDIKIT.HP]
  6.         shr     eax, 1
  7.         mov     [esi + MEDIKIT.HP], eax
  8.         retn    4
  9. mine_shot:
  10.         mov     esi, [esp+4]
  11.         mov     dword [esi + MINE.TIMEOUT], 0
  12.         retn    4
  13. C:
  14. void medikit_shot(MEDIKIT *this)
  15. {
  16.         this->hp = this->hp / 2;
  17. }
  18. void mine_shot(MINE *this)
  19. {
  20.         this->timeout = 0;
  21. }

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

Код (Text):
  1.  
  2. Asm:
  3. ; виртуальная таблица для класса MEDIKIT
  4. medikit_vtab:
  5.         dd medikit_touched
  6.         dd medikit_shot
  7.  
  8.         ; экземпляр класса MEDIKIT (объект аптечка)
  9. mk:
  10.         dd medikit_vtab       ;указатель на виртуальную таблицу
  11.         dd 10                 ;x
  12.         dd 15                 ;y
  13.         dd 50                 ;hp
  14.  
  15. ;виртуальная таблица для класса MINE
  16. mine_vtab:
  17.         dd mine_touched
  18.         dd mine_shot
  19.  
  20. ;экземпляр класса
  21. mn:
  22.         dd mine_vtab          ;указатель на виртуальную таблицу
  23.         dd 20                 ;x
  24.         dd 20                 ;y
  25.         dd 30                 ;таймер
  26.         dd 50                 ;hp
  27.  
  28. ;вызов "touched" метода для объекта, адрес которого в ESI
  29. push    esi              ;аргумент для метода – адрес объекта
  30. mov     eax, dword [esi] ;EAX = адрес виртуальной таблицы
  31. call    dword [eax]      ;EAX+0 = адрес 'touched' метода
  32.  
  33. push    esi                    
  34. mov     eax, dword [esi]      
  35. call    dword [eax+4]           ;EAX+4 = адрес 'shot' метода
  36.  
  37. C:
  38. //описание виртуальной таблицы
  39. struct VTAB {
  40.         void (*touched)(OBJECT* this);
  41.         void (*shot)(OBJECT* this);
  42. };
  43.  
  44. // аптечка
  45. struct MEDIKIT {
  46.         VTAB *vtab;
  47.         int x,y;
  48.         int hp;
  49. };
  50. VTAB medikit_vtab = { //виртуальная таблица для MEDIKIT
  51.   &medikit_touched,
  52.   &medikit_shot
  53. };  
  54. MEDIKIT mk1 = {  //аптечка
  55.   &medikit_vtab, 10, 15, 50
  56. };
  57. MEDIKIT mk2 = {  //вторая аптечка
  58.   &medikit_vtab, 20, 20, 10
  59. };
  60.  
  61. // mine
  62. struct MINE {
  63.         VTAB *vtab;    
  64.         int x,y;
  65.         int timeout;
  66.         int hp;
  67. };
  68. VTAB mine_vtab = { //виртуальная таблица для MINE
  69.   &mine_touched,
  70.   &mine_shot
  71. };  
  72. MINE mn1 = {  //объект MINE
  73.   &mine_vtab, 10, 15, 30, 30
  74. };
  75.  
  76. //общая часть всех объектов
  77. struct OBJECT {        
  78.         VTAB *vtab;    
  79. };
  80. OBJECT *obj;
  81.  
  82. //вызов методов
  83. obj->vtab->touched(obj);
  84. obj->vtab->shot(obj);

Заключение

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


0 2.133
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532