Новости/News | | Туториалы/Tutorials | | Форум/Forum | | ЧаВо/FAQ | | Файлы/Downloads | | Ссылки/Links | | Авторы/About |

Объектно-ориентированная логика

Содержание

Вступление

Общаясь на IRC каналах #UnrealED и #UnrealScript я обратил внимание на то, что многие интересующиеся Unreal Script люди не имеют представления об объектно-ориентированной (ОО) логике. В порыве страсти я решил сделать доброе дело и написать туториал в виде небольшого экскурса в объектно-ориентированное программирование (ООП). Надеюсь, что прочтение этого документа сформирует у Вас более или менее "сильное" понятие ООП для безболезненного изучения Unreal Script. Исходя из собственного опыта, я понял, что при решении какой-либо проблемы мы получим блестящие результаты, если подойдем к этой проблеме с точки зрения ОО. Я думаю, этот документ придется Вам по вкусу. Я постараюсь его обновлять по мере необходимости. Если у Вас есть дополнительная информация по этому вопросу или Вы обнаружили неточности/ошибки, я буду счастлив, добавить Ваши комментарии с указанием их автора. Данный документ является первым из планируемой мной серии "полезных" туториалов.

Соглашение об использовании

Данный туториал может распространяться только в виде электронного документа. Он не может быть подвергнут никаким изменениям, дополнениям, преобразованиям без письменного на то разрешение автора. Также он не может являться частью CD-ROM без разрешение автора. Туториал не может быть использован в коммерческих целях без согласия на то автора. Содержимое этого документа защищено: Copyright © 1998, Brandon Reinhart.

Как думают программисты

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

"Breaking It Up"

Предположим, что Вы, программист, и перед Вами стоит проблема. Например, Вам необходимо написать программу, которая строит машину. Звучит довольно грозно. Понятно, что построение машины не тривиальная задача, но основная трудность написания подобной программы лежит в способе мышления о возможном решении данной проблемы. В том, каким образом Вы думаете о ней. Если Вы говорите: "Я хочу написать программу, которая будет строить машину", Вы, скорее всего, будете поражены необъятностью этой задачи! Машина состоит из тысячи частей! Как возможно написать программу, которая будет делать все эти части? Тысячи частей? Ага! Вот здесь мы уже начали разбирать задачу на составляющие. Что, если, вместо того, чтобы говорить: "Я хочу написать программу, которая строит машину", Вы скажете: "Я хочу написать программу, которая изготовит различные части машины, а затем соберет их". Все еще устрашающая задача, но, определенно, более организованная. Это и есть то, что мы называем "Breaking It Up" или "нисходящее программирование". Разбивая основную проблему на последовательные мелкие части, Вам предстоит решать много маленьких проблем вместо одной большой и непомерно сложной. Если мы оставим аналогию с машиной и перейдем к Unreal, то одна из задач может звучать так: "Я хочу написать бота, который будет играть в Unreal". Задача не из простых. Тем не менее, если Вы разобьете ее на составляющие, в конце у Вас получится "Проект по нахождению ботом путей", "Проект по использованию ботом оружия" и т.д. Это первый шаг ОО подхода в программировании.

Становимся организованными

Теперь, когда перед нами лежит много маленьких задач, составляющих одну большую, нам необходимо подумать об их организации. Если бы Вы писали подобную программу на языке C (в "функциональном" стиле), то у Вас было бы огромное количество различных функций и ощущение полного беспорядка. Конечно, Вы бы могли "подчистить" этот беспорядок, затолкав реализацию некоторых частей в отдельные файлы, но, все равно, идеальным выходом это назвать нельзя. Как же нам организовать маленькие части одной большой задачки? Решением является изменение способа, которым мы думаем о частях проблемы. Давайте вернемся к аналогии с машиной. Мы хотели разбить программу на две части: одна будет изготавливать детали машины, другая - их собирать. Давайте подумаем, что же на самом деле представляет собой деталь машины. Возможно, это смесь метала, пластика, мелких деталей, и она, несомненно, обладает какой-то функциональностью... различные части машины одного и того же типа могут несколько отличаться. В данном случае, мы можем сказать, что деталь машины является общим типом объекта. Деталь не является каким-либо специфическим объектом, это просто шаблон (чертеж) придерживаясь которого, Вы можете создать конечную вещь. Назовем это классом (class).

Класс

В ОО логике класс представляет собой описание того, как будет "выглядеть" вещь, если бы она была создана. Класс является общим шаблоном. Он определяет абстрактные свойства (например, деталь машины имеет свой цвет), но, обычно, не определяет реальное проявление этих свойств в природе (например, какого цвета деталь). Класс также определяет поведение объекта. Набор специальных функций однозначно указывает, как будет "вести" себя объект в Вашей программе. На языке программистов, такие абстрактные свойства называются "переменными экземпляра" (instance variables), а функции, описывающие поведение объекта - "методами" (method). Процесс создания объекта на основе класса называется "реализацией" (instantiation). Очень важно понимать, что класс не является объектом, а представляет собой шаблон, на основе которого эти объекты могут быть созданы. Когда Вы создаете объект (экземпляр класса), Вы создаете модель из шаблона.

Механика класса

В Unreal Script мы определяем классы посредством "объявления класса" (class declaration):

class MyClass expands MyParentClass;

Ключевое слово class говорит Unreal Script, что Вы объявляете новый класс. MyClass, в данном случае, является именем нового класса (имя может быть любое, но желательно, со смыслом, например, CarPart). Оставшуюся часть мы рассмотрим несколько позже.

После того, как Вы объявили новый класс в Unreal Script, самое время определить переменные экземпляра. Все что Вам нужно для этого сделать - это указать список необходимых свойств:

var int color;          // Номер цвета детали
var byte manufacturer;  // ID (ссылка на) производителя

Для более детального ознакомления с типами переменных в Unreal Script прочитайте туториал "UnrealScript Language Reference" (автор Tim Sweeney). Довольно изящно будет, если Вы отделите переменные от основной части кода комментариями:

///////////////////////////////////////////////////
// Переменные экземпляра для MyClass

Вообще, это дело вкуса, и общему делу не повредит. Обычно комментарии являются хорошим тоном в программировании. Они делают Ваш код более "читабельным" и понятным постороннему человеку, особенно если Вы собираетесь поделиться исходником с кем-то или вернуться к нему позже сами.

Определение методов в новом классе очень похоже на объявление переменных: просто составьте список необходимых функций:

function doAThing() {

    // Выполнение некоторого кода UnrealScript
}

function doAnotherThing() {

    // Другая функция, которая что-то делает
}

И снова, Вы можете отделить методы от остального кода при помощи комментариев. Также, рекомендуется давать методам "внятные" имена, отражающие его суть. Например, метод, моющий Ваши носки 8) может быть назван cleanSocks()1.

В Unreal Script, создание объектов называется "spawning" (порождение). Вот как можно использовать функцию Spawn() для создания нового объекта из класса-шаблона:

var actor MyObject;            // Переменная, содержащая (ссылающаяся на) объект
MyObject = Spawn(MyClass);     // Порождение объекта

Краткое обсуждение методов

Объект является независимым контейнером данных, которые "висят" в памяти. Ключевым словом является именно независимость. Каждый объект выполняет свою работу. Если Вы описали класс MyBot и создали два объекта (экземпляра) из этого класса с именами BotA и BotB эти два объекта ничего друг о друге не знают и даже не подозревают о существовании друг друга. Это приводит нас к следующей концепции объектно-ориентированного программирования: объекты изменяют сами себя.

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

Методы класса определяют, каким образом будет действовать объект после создания. Если Вы хотите изменить переменную экземпляра (объекта), Вам необходимо описать соответствующий метод, который позволяет это сделать. Наш класс CarPart может иметь, например, такой метод:

function setColor(int newColor) {

    color = newColor;
}

В данном примере, когда вызывается метод setColor(), объект принимает число в качестве аргумента и устанавливает значение цвета равное этому числу. То есть объект изменяет сам себя. Синтаксис вызова метода в Unreal Script выглядит следующим образом:

MyObject.setColor(15);  // Говорим объекту, чтобы он вызвал метод setColor()

Методы, переменные, и защита объектов

Помните, я Вам говорил, что объекты изменяют сами себя? На самом деле это не совсем так. Я так говорил, т.к. хотел, чтобы Вы думали об объектах, как о независимых "существах" в Вашей программе. Говоря на чистоту, переменные экземпляра можно менять напрямую:

MyObject.color = 15;

Обратите внимание на отличие. В случае с методом, мы говорим объекту изменить себя, а в последнем - меняем природу объекта напрямую. Это иллюстрирует очередной момент "Как думают программисты". Вы, возможно, спрашиваете себя: "Если я могу изменять объект напрямую, то зачем мне вообще нужны эти методы, и какая от них польза?". Отлично. Подумаем об этом.

Допустим, у нас наложены некоторые ограничения на значение переменной color. Например, Вы хотите, чтобы переменная color могла принимать значения только от 1 до 10. Значения же не входящие в этот диапазон могут вызвать сбой программы или привести к нежелательным результатам. В этом случае будет не очень хорошо, если пользователю разрешено менять параметры напрямую. Верно? Даже, если Вы будете знать, что значения должны быть из промежутка от 1 до 10, другой человек, использующий Ваш код, может об этом и не подозревать. Выход: сделайте Вашу переменную private ("частной"), т.е. такой, что только самому классу будет дозволено ее изменять2:

var private int color;  // Объявляем private переменную

Ключевое слово private называют "спецификатором доступа". Private указывает на то, что данная переменная доступна только изнутри объекта, а снаружи ее "не видно". Объект может изменить значение такой переменной посредством метода, но если попытаться обратиться к этой переменной с помощью прямого доступа:

MyObject.color = 15;

... то в результате Вы получите ошибку, не зависимо от того, верное число или нет, т.к. спецификатор private не "позволит" Вам ее изменять таким образом. Такой подход предоставляет Вам возможность контролировать получаемые объектом значения и более четко определить их поведение. Теперь, Вы можете, например, проверить вводимое значение и вернуть сообщение об ошибке или описать специальное действие в случае неправильного ввода.

Семейство классов

Не утомились еще? Самое время хлебнуть холодного пивка3. Это всего лишь фундаментальные понятия об объектах!

Как Вы заметили (если Вы конечно творческая личность - что несомненно, раз Вы это читаете), объекты сами по себе обладают огромным потенциалом. Объекты позволяют разбить нашу задачу на более удобоваримые части. Тем не менее, трудности все же есть, т.к. теперь нам нужно управлять множеством объектов. Это приводит нас к следующей важной концепции объектно-ориентированного программирования: отношения между объектами.

Очень удобно проиллюстрировать объектные отношения на основе аналогии с машиной. Класс CarPart не дает нам полную картину, что же представляет собой деталь машины на самом деле. Учитывая то, что мы теперь знаем об объектах, мы, возможно, даже не будем использовать CarPart... вместо этого, мы опишем класс, например, SteeringWheel (руль). Это звучит более точнее и такой класс удобней использовать.

Фактически, лед тронулся. Мы уже мысленно построили отношения между CarPart и SteeringWheel. Действительно, SteeringWheel (руль) является разновидностью CarPart (детали). Верно? Так что, если класс CarPart опишет методы и свойства присущие всем возможным разновидностям деталей, а другие классы, такие как SteeringWheel, "расширят" его функциональность?

На языке программистов такие отношения называются "родительско-дочерние отношения". CarPart, в данном случае, играет роль "родителя" (или super класса) класса SteeringWheel. В Unreal Script дочерний класс объявляется следующим образом:

class SteeringWheel expands CarPart package(MyPackage);

Видите ключевое слово expands? Оно означает что класс, который мы объявляем (SteeringWheel) является "дочерним" классом CarPart. Как только Unreal встретит expands, он сразу сформирует отношения между соответствующими классами.

Принцип I: Наследование

Что же нам дают эти родительско-дочерние отношения, нам, пытающимся решить поставленную задачу? Да это существенно упрощает нам жизнь! При установлении такого рода отношений между двумя классами, дочерний класс мгновенно "наследует" свойства и методы родителя. И все это без написания и строчки кода: SteeringWheel обладает всей функциональностью CarPart. Если класс CarPart имеет метод setColor(), то SteeringWheel также имеет его. Наследование распространяется на переменные, на методы и на состояния (states).

Такой подход позволяет нам построить то, что программисты называют "Иерархия объектов" (или "Семейство классов"). Это выглядит как генеалогическое дерево:

Object
  | expanded by
  Actor
    | expanded by
    CarPart
      | expanded by
      SteeringWheel

Object и Actor являются специальными классами в Unreal (подробнее см. "UnrealScript Language Reference"). Полное дерево классов в Unreal представляет собой огромную "паутину" взаимоотношений, которую, Вы только можете себе представить. В нашем примере, у нас простое отношение типа "is-a":

A SteeringWheel is a  CarPart   (Руль есть Деталь)
      A CarPart is an Actor   (Деталь есть Актер)
       An Actor is an Object   (Актер есть Объект)

Каждый следующий "слой" в дереве наследования расширяет функциональность предыдущего. Это позволяет нам без труда описывать сложные объекты в рамках "входящих" в него объектов. Очень важно понимать, что отношения не коммутативны (взаимнообратны). SteeringWheel (руль) всегда является CarPart (деталью), но CarPart (деталь) не всегда является SteeringWheel (рулем). Двигаясь вверх по иерархии, мы двигаемся к простому, вниз - к более специфичному. Надеюсь, понятно? Отлично!

Э-э-э! Секундочку... если мы строим машину (car) и машина сделана из деталей, где же сам класс Car? Вот мы и нарвались на различия во взаимоотношениях: "is-a" ("есть") против "has-a" ("имеет"). Ясно, что CarPart (деталь) не является Car (машиной). С другой стороны отношение "CarPart expands Car" будут также неверно. Поэтому, лучше всего, построить иерархию таким образом:

          Object
             |
           Actor 
           /   \ 
         Car  CarPart
                  |
               SteeringWheel

Класс Car "происходит" от класса Actor, и, при этом, не имеет прямого отношения с CarPart (Вы можете называть их братьями). Вместо этого, класс Car может содержать в себе переменные типа CarPart. В этом случае, мы имеем отношения "has-a" ("имеет"). Car (машина) имеет SteeringWheel (руль), но SteeringWheel (руль) не является Car (машиной). Если Вы создаете иерархию классов и запутались, то очень удобно представить отношения фразами "имеет" и "есть".

Как видно из вышесказанного, иерархия отношений позволяет нам делать интересные вещи. Если нам вдруг захотелось, например, расширить понятие о машине, мы можем добавить Vehicle (транспорт):

          Object
             |
           Actor
          /    \
      Part     Vehicle 
      / |        | \ 
CarPart AirPart Car Airplane

Правда круто! Не только потому, что это является удобным способом организовать и визуально представить объекты, но и потому, что в силу наследования, отпадает необходимость копирования и переписывания кода!

Принцип II: Полиморфизм

Поли что? Очередное словечко из лексикона этих сумасшедших программистов. (Если Вы до этих пор понимали все, что я пытался втолковать, значит, Вы являетесь больше программистом, чем сами об этом думаете) Полиморфизм - одна из фундаментальных основ объектно-ориентированного программирования. При наследовании, дочерний класс "перенимает" переменные, методы и состояния (states) родительского класса... но что если мы хотим изменить наследованные элементы? В примере с машиной, Вы, возможно, захотите написать класс Pedal (педаль), в котором определите метод PushPedal() (нажатие педали). Когда вызывается PushPedal(), этот метод производит какое-то действие (например, активирует тормоза). Если, затем Вы расширите класс Pedal новым классом, например, AcceleratorPedal (педаль газа), метод PushPedal(), мягко говоря, становится некорректным. При нажатии педали газа, явно не должны включаться тормоза! (Иначе у Вас будут большие проблемы с законом при релизе Вашей программы, уж поверьте мне).

В этом случае, Вы должны заменить унаследованное поведение педали другим. Это можно сделать посредством "полиморфизма" или "перегрузкой функции"4. Вы будете часто использовать этот прием, программируя на Unreal Script. Пояснение что же такое полиморфизм я позаимствовал у Tim Sweeney:

[Перегрузка функции] подразумевает под собой написание новой версии функции в подклассе (дочернем классе). Например, Вы пишете код для нового вида монстра - Demon. Класс Demon, который Вы создали, расширяет класс Pawn. Что происходит, когда Pawn видит игрока - вызывается функция [SeePlayer()] и Pawn начинает его атаковать. Неплохо, но что, если, скажем, Вы хотите по-своему определить поведение Demon в Вашем классе.

Для этого, просто переопределите эту функцию в дочернем классе. Когда создается объект этого класса, он будет обладать новым поведением, а не заимствованным у родителя. Если Вы хотите запретить переопределение функции подклассам, то воспользуйтесь ключевым словом final при объявлении метода:

function final SeePlayer()

Таким образом, Вы застрахуетесь от перегрузки функции в "производных" классах. Очень разумно использовать это для поддержки целостности поведения в Вашем коде. Tim Sweeney, также, указывает на то, что использование final повышает производительность в Unreal.

Заключение

Теперь Вы знакомы с фундаментальными основами объектно-ориентированного программирования и, я надеюсь, имеете хорошее представление об отношениях между объектами. Что же дальше?

Совет: программируйте. Погрузитесь в код, и не выныривайте из него, даже если обещания еды, сна и секса приняли вполне реальные очертания. Обыщите все дно. Или... Вы всегда можете обратиться к руководству "UnrealScript Language Reference" от Tim Sweeney. Там Вы найдете более детальное пояснение относительно синтаксиса присущего ОО в Unreal. В дополнение ко всему, я посоветую Вам найти другие ресурсы, касающиеся ОО логики и ООП. ООП также присущи тонкие моменты, которые можно только выучить. Некоторые из них не реализованы в Unreal, другие - реализованы. Некоторые являются всего лишь способом мышления.

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

Для тех, кто это понимает и наслаждается этим, программирование является, прежде всего, интеллектуальной, физической и духовной задачей. Это может звучать странно, но программирование затрагивает основные пути, какими мы мыслим и решаем проблемы. Если Вы можете думать "на" ОО, значит, Вам не грозят проблемы при решении задач ООП. Чем больше Вы будете использовать такой подход, тем больше Вы будете осознавать, что это реальность.

С помощью ОО, к сожалению, не все можно решить. Как и любой способ мышления, объектно-ориентированной парадигме присуще игнорирование некоторых моментов решения проблемы, связанное с желанием усилить аналогию с реальной системой. Скорее всего, Вы не встретитесь с подобной проблемой, программируя на Unreal Script. Во всяком случае, до тех пор, пока Вы не захотите написать чертовски навороченный мод.

Автор

© 1998-2002 Brandon Reinhart
Перевод сделан 32_Pistoleta.

Примечания

  1. cleanSocks(): здесь под словом Socks подразумевалось Sockets (сокеты), но с носками звучит веселее 8)
  2. Вообще, это не совсем верно. Private не указывает на то, кто может управлять переменной. На самом деле private указывает, что свойство не видно снаружи объекта. Оно доступно только для внутренних структур объекта. Такой прием, позволяющий "спрятать" от внешних глаз переменную (и даже метод), называется "инкапсуляцией";
  3. В оригинальном тексте предлагали хлебнуть Dr. Pepper. Я позволил себе исправить это в соответствии с национальными интересами 8)
  4. Подобную процедуру еще часто называют "подавлением функции". Подавлять можно не только методы, но и переменные и состояния;
Хостинг от uCoz