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

  • В этой теме 0 ответов, 1 участник, последнее обновление 2 недели, 2 дня назад сделано Васильев Владимир Сергеевич.
Просмотр 0 веток ответов
  • Автор
    Сообщения
    • #6261
      @admin

      Как правило, большинству низкоуровневых программистов объектно ориентированное программирование кажется сложным. В этой статье объясняются некоторые моменты ООП и приводятся примеры их устройства с помощью языков 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);

      Заключение

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

      Избранные материалы wasm.
      [C] vid, пер. DarkWanderer

Просмотр 0 веток ответов
  • Для ответа в этой теме необходимо авторизоваться.