ООП и Object Pascal

      Комментарии к записи ООП и Object Pascal отключены

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 3 мес., 2 нед. назад.

  • Автор
    Сообщения
  • #4080

    Введение в ООП и Object Pascal

    Настало время разобраться, а что же такое Object Pascal, и чем он отличается от простого Паскаля, разработанного Н.Виртом и который изучают в школах и университетах?

    Object Pascal
    – это надмножество (т.е. дальнейшее развитие) языка Pascal, в который введено понятие объекта и классов объектов и, таким образом, реализована концепция объектно-ориентированного программирования (ООП). Хотя объектно-ориентрованные расширения синтаксиса Паскаля существовали уже с версий Turbo Pascal 5.5, в полной мере идеи ООП нашли отражение лишь в Delphi.

    Но для начала разберемся, что же такое объектно-ориентированное программирование. Решая задачу на компьютере или моделируя на нем какой-либо процесс, программист строит модель задачи, которую он решает, или процесса (объекта), который он моделирует. Задача (процесс, объект) описывается некими абстрактными формулами или алгоритмами, которые затем кодируются в текст программы. Далее эта программа выполняются компьютером. Обычно процесс моделируется в виде некоторого набора изменяемых параметров, над которыми производятся различные действия (вычисления). Такой способ описания задачи не лишен недостатков, так как реальную задачу или поведение реальных объектов необходимо довести до такого уровня абстракции, когда их можно описать неким алгоритмом, но при этом от самого процесса или объекта ничего не остается. Программист в этом случае работает только с наборами переменных. Моделирование поведения сколько-нибудь сложного реального объекта становится весьма затруднительным.

    Допустим, поставлена задача смоделировать участок автотрассы для анализа загруженности дороги и автоматического управления потоком автомашин (используя светофоры). Для этого нам следует ввести понятие модели трассы, ее протяженности, ее качества, ширины. Значит, следует ввести соответствующие переменные. Не следует забывать, что на трассе существуют опасные участки, пешеходные переходы, мосты, участки с разной шириной дороги. Для этого придется писать соответствующие функции, анализирующие эти участки дороги. А как же автомобили? Ведь каждый едет со своей скоростью, по своей полосе. Придется вводить еще массу переменных величин, над которыми надо произвести еще множество действий для моделирования движений автомашин: Да и самих автомобилей существует множество различных типов. Кроме того, необходимо управлять светофорами. А ведь время тоже не является неизменным – есть день, ночь, рабочие дни, выходные: Уффф! Используя стандартный подход к решению такой задачи, т.е. анализ переменных при помощи процедур и функций, можно легко запутаться и увязнуть в дебрях формул.

    Не лучше ли все эти объекты реального мира смоделировать такими, какими они есть? Каждый объект обладает какими-либо свойствами (длина, ширина, вес, цвет, материал и т.д.). Над ними можно произвести какие-либо действия (покрутить, передвинуть, открыть, потрясти, толкнуть, разбить :) или дать им команду что-то выполнить (повернуться, погудеть, открыться). Более того, над совершенно разными объектами можно осуществит одинаковые действия, например, мы можем открыть ящик, окно, дверь или шкатулку. Или заставить выполнить одну и ту же команду, например, заставить прозвенеть в 7 утра электронную пищалку и старый механический будильник. При этом нас далеко не всегда интересует, а что же у него внутри и как он это делает. Мы умеет им пользоваться, и этого вполне достаточно.

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

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

    Объект-транспортное средство имеет общие для всех типов транспорта свойства – массу, скорость, размеры. И опять же у каждого типа автомобиля есть свои уникальные свойства, описывающие его поведение на дороге. Например, автобус или троллейбус имеют преимущество в движении, их следует пропустить. А трамвай вообще умеет ездить только по участку с рельсами.

    Ну и, наконец, объекты-светофоры. Все они одинаковые, и все они имеют 3 объекта-лампочки (красная, желтая, зеленая).

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

    Все. Собираем автотрассу из разных участков, устанавливаем светофоры, ставим на автотрассу автомобили разных типов, и запускаем время. Поехали!

    Таким образом, ООП включает в себя следующие базовые принципы:

    • Инкапсуляция. Долой переменные, массивы, процедуры, функции! Мы имеем дело с объектом, неким “черным ящиком”, который обладает набором свойств и методами работы с ним. Что происходит внутри объекта, от нас скрыто, да нам это и не нужно знать. (пример: автомобиль, автобус, часы).
    • Полиморфизм. Над разными классами объектов можно осуществлять похожие действия, которые называются одинаково. (Завести автомобиль, Завести автобус, Завести часы).
    • Наследование. Один класс объектов можно создать на базе уже существующего класса объектов, при этом новый класс унаследует все свойства и действия, которые можно над ним совершить, от класса-родителя, добавляя при этом свои свойства или детализируя уже существующие (Пассажирский автомобиль – это частный случай транспортного средства, который ездит по асфальтовым дорогам и предназначен для перевозки людей, а автобус – это частный случай автомобиля, который предназначен для перевозки большого количества людей и обладающего бОльшими размерами).

    ООП – языками в настоящее время являются C++, Object Pascal, SmallTalk, Java, ADA. Собственно, “самым-самым” ООП языком является SmallTalk, который самый первый реализовал принципы объектно-ориентированного программирования. Но этот язык является интерпретируемым. К тому же программы, написанные на SmallTalk, выполняются в среде виртуальной SmallTalk-машины, что не подходит для создания эффективных приложений. А вот любимый многими Visual Basic вообще не является ООП-языком, хотя на первый взгляд этого не скажешь. Обилие различных визуальных “объектов”, “объектов”-списков, визуальное построение форм и многое другое являются просто встроенными возможностями языка и среды разработки. В VB нет главного – возможности создания своих классов и наследования свойств и методов у уже существующих классов. А расширение возможностей осуществляется только за счет подключения библиотек настоящих объектов, реализованных с помощью других языков программирования, в частности Object Pascal или C++, или создания экземпляров классов, предоставляемых операционной системой (например, создание экземпляра Excel’а).

    Описание классов в Object Pascal

    Для создания классов в Object Pascal введена конструкция class:

    Type
       TmyClassA = class(имя класса-родителя)
       End;

    или

       TmyClassB = class
       End;

    “Так просто?” – скажете вы. Да, действительно просто – мы создали новый тип данных – класс TmyClassA (TmyClassB). И хотя мы еще ничего не сказали о том, что это за класс объектов, как будут вести себя объекты этого класса, какие они будет иметь свойства и многое другое, это все же настоящий класс объектов, который умеет уже очень многое!

    Дело в том, что в Object Pascal все классы наследуются от базового класса TObject, который представляет из себя корень всей будущей иерархии классов, эдакий “первокирпичик”. Этот “первокирпичик” умеет создавать свои экземпляры, уничтожать их, давать информацию о своем имени, имеет диспетчер сообщений и еще массу полезных функций, без которых не может существовать ни один класс. Если в заголовке класса не указан “родитель”, то класс просто наследуется от TObject. Т.е. описание класса TmyClassB в данном случае равносильно следующей конструкции:

    Type
       TmyClassB = class(TObject)
       End;

    Попробуем теперь наш класс в действии. Но для начала следует подготовить плацдарм для боевых действий.

    Запускаем Delphi, создаем новый проект (File -> New application). Затем убираем из проекта модуль Unit1 (Project -> Remove from project, выбираем Unit1 и нажимаем OK). В проекте остается только файл project.dpr. Стираем весь текст и оставляем только следующие строки:

    Program ClassTest;
    {$AppType Console}
    Uses Windows;
    {$R *.RES}
    
    Begin
    End.

    Все готово. Обратите внимание на 2-ю строку: {$AppType Console} . Это – команда компилятору собрать консольное приложение, т.е. приложение, работающее в текстовом режиме, подобно старым DOS-программам, написанным на Turbo Pascal. При этом программа остается полноценным Win32-приложением. Этого же результата можно добиться, открыв Опции проекта (Project -> Options…) и на вкладке Linker поставить галочку в пункте Generate console application. Таким образом мы на время избавили себя от необходимости изучения графического интерфейса Windows и получили простую возможность вывести результаты нашей работы на дисплей.

    Ну а теперь приступим к испытаниям. Описание класса подобно проекту автомобиля, по которому на заводе собирают готовые экземпляры автомобилей. На проекте ездить нельзя :), а вот на автомобиле, собранном по этому проекту – пожалуйста. Точно так же следует поступить и нам:

    Type
       TmyClassA = class(TObject)
       End;
    
       TmyClassB = class
       End;
    
    Var
       ObjA: TmyClassA; { Описание переменной типа TmyClassA }
       ObjB: TmyClassB; { Описание переменной типа TmyClassB }
    
    Begin
       ObjA := TmyClassA.Create;
       ObjB := TmyClassB.Create;

    Мы создали экземпляры классов TmyClassA и TmyClassB (или собственно объекты) при помощи конструктора Create. Вообще каждый класс имеет конструктор (один или несколько) и деструктор. Цель конструктора – создание экземпляра (объекта) указанного класса. Ну а цель деструктора, как вы догадались – уничтожение объекта, надобность в котором отпала. В данном случае мы воспользовались конструкторами, унаследованными обоими классами от базового класса TObject. Конструктор класса создает объект в динамической памяти и возвращает ссылку на него, которую мы помещаем в переменную ObjА (ObjB).

    У каждого класса есть имя и родительский класс, которые можно получить, вызвав методы ClassName (Имя Класса) и ClassParent (Родительский Класс):

    Writeln('Class name:',objA.Classname, ' ',
        'Parent:',objA.ClassParent.ClassName);
    Writeln('Class name:',objB.Classname, ' ',
        'Parent:',objB.ClassParent.ClassName);
    readln;

    Как мы видим, оба класса имеют “родителя” – TObject. Естественно, возникает вопрос, а каким образом мы можем узнать, в каких “родственных отношениях” находятся созданные объекты? Это можно узнать несколькими способами.

    при помощи методов ClassType (Тип Класса) и InheritsFrom (Унаследован От):

        Writeln('Is the ObjA instance of TmyClassA?',
            ObjA.ClassType = TmyClassA);
        Writeln('Is the ObjA instance of TmyClassB?',
            ObjA.ClassType = TmyClassB);
        Writeln('Is the ObjA instance of TObject?',
            ObjA.ClassType = TObject);
        Writeln('Is the ObjA descendant of TObject?',
            ObjA.InheritsFrom(TObject));
        Writeln('Is the ObjA descendant of TmyClassB?',
            ObjA.InheritsFrom(TmyClassB));

    при помощи оператора is:

        Writeln('Is the ObjA instance of myClassA?',
            ObjA is TmyClassA);
        Writeln('Is the ObjA descendant of TObject?',
            ObjA is TObject);

    Следует отметить, что выражение:

    Writeln('Is the ObjA instance of myClassB?',
        ObjA is TmyClassB);

    будет отметено компилятором еще во время компиляции, т.к. ObjA действительно не может быть экземпляром класса TmyClassB. Зато возможна следующая ситуация:

    var
       Obj1, Obj2: TObject;
    
    begin
       Obj1 := TmyClassA.Create;
       Obj2 := TmyClassB.Create;

    Мы описали две переменных типа TObject, в которые затем поместили ссылки на созданные экземпляры классов TmyClassA и TmyClassB. Да-да, и такая ситуация возможна! Представьте себе две коробки, на которых написано – “Яблоки”. В одну мы положим антоновку, а в другую – белый налив. И это будет вполне естественно. А вот положить сливы в коробку из-под яблок будет несколько нечестно :). В таком случае мы можем не знать, какие яблоки лежат в коробках, но мы точно знаем, что лежат там именно яблоки.

    В этой ситуации компилятор спокойно “переварит” приведенный текст:

    Writeln('Is the Obj1 instance of myClassA?',
       Obj1 is TmyClassA);
    Writeln('Is the Obj1 descendant of TObject?',
       Obj1 is TObject);
    Writeln('Is the Obj1 instance of myClassB?',
       Obj1 is TmyClassB);

    так как реальный тип объекта, содержащийся в Obj1, еще не известен, но точно известно, что это будет наследник от TObject. A т.к. TmyClassB тоже унаследован от TObject, то последняя строчка вполне корректна.

    Уфф! Наработались? Теперь надо за собой убрать.

    ObjA.Destroy;
    ObjB.Destroy;

    Destroy – это деструктор. Деструктор производит все завершающие действия по корректному завершению работы объекта и затем уничтожает его. После вызова деструктора ссылка, лежащая в переменной ObjA (ObjB) становится недействительной, и ее использование может привести к непредсказуемым результатам!

    Но непосредственное использование деструктора в Object Pascal не рекомендуется. Более правильно для уничтожения объекта использовать метод free:

    ObjA.free;
    ObjB.free;

    Free проверяет, не является ли ссылка на объект пустой (nil), и если ссылка не пуста, вызывает деструктор.

    Но пока классы TmyClassA и TmyClassB остаются вещью в себе. Чтобы экземпляры наших новых классов обрели хоть какой-то смысл, надо наполнить их содержимым. Этим мы и займемся далее.

    Пример реализации класса очередь на Object Pascal

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

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

    Реализовать стек и очередь можно и в обычном, процедурном Pascal’е. Но в Object Pascal эта задача решается гораздо проще и даже интереснее.

    Итак, что же представляет из себя очередь? Это некий массив данных определенного размера m, который характеризует максимальную длину очереди. В каждый момент времени очередь может содержать n объектов, причем n <= m, или же вовсе быть пустой, т.е. n = 0. Каждый новый объект помещается в хвост очереди, т.е. в конец массива, а извлекаются объекты с самого начала массива. После извлечения объекта из очереди весь массив сдвигается к началу. Ну что ж, пора взяться за реализацию класса "очередь" или TQueue (символом T в начале имени принято обозначать названия типов, а символом P - типы -указатели, в отличие от названия переменной). Создадим пустой проект Queues.dpr (как это сделать, было описано ранее):

    program Queues;
    {$AppType Console}
    uses Windows, SysUtils;
       { модуль SysUtils содержит множество вспомогательных функций }
    
    type
    { Описание массива целых чисел и указателя на этот массив }
       TIntArray = array[1..65535] of Integer;
       PIntArray = ^TIntArray;
    Первым делом следует объявить тип-массив, содержащий элементы очереди. В нашем случае это будет очередь целых чисел, поэтому массив тоже будет содержать целые числа. Т.к. максимальная длина очереди на данном этапе нам неизвестна (ее длина будет указываться во время исполнения программы, при создании очереди), то фактический размер массива нам тоже неизвестен. Чтобы избежать данной неопределенности, мы объявляем массив максимально возможного размера, а в дальнейшем создадим массив нужного нам размера в динамической памяти. Далее объявляем класс:
    TQueue = class
       Capacity: Integer;   // Максимальная длина очереди
       Size: Integer;       // Текущая длина очереди (кол-во элементов)
       Items: PIntArray;    // Указатель на массив элементов.
    End;
    Вспомним, какие действия нам надо произвести над очередью? Поместить в очередь новый элемент (Push) и извлечь из очереди элемент (Pop). Эти действия называются методами, и похожи на обычные процедуры и функции Паскаля, но помещаются они внутри описания класса:
    TQueue = class
       Capacity: Integer;   // Максимальная длина очереди
       Size: Integer;       // Текущая длина очереди (кол-во элементов)
       Items: PIntArray;    // Указатель на массив элементов.
       Procedure Push(Value: Integer);
       Function Pop:Integer;
    End;
    Еще нам может понадобиться проверка, не является ли очередь пустой. Ну и, конечно же, мы забыли о конструкторе и деструкторе. Именно они "знают", как правильно выделить память для очереди и затем ее уничтожить. Конструктор и деструктор объявляются соответствующими ключевыми словами:
    TQueue = class
       Capacity: Integer;   // Максимальная длина очереди
       Size: Integer;       // Текущая длина очереди (кол-во элементов)
       Items: PIntArray;    // Указатель на массив элементов.
    // при создании очереди мы указываем ее максимальный размер
       Constructor Create(aCapacity:Integer);
       Destructor Destroy; override;
       Procedure Push(Value: Integer);
       Function Pop:Integer;
       Function IsEmpty:Boolean;
    End;
    Обратите внимание на директиву override. Это указание на то, что новый деструктор перекрывает старый, который класс TQueue унаследовал от своего "родителя" TObject. В результате этого везде, где ранее вызывался деструктор Destroy класса TObject, применительно к классу TQueue будет вызываться TQueue.Destroy. Более подробно об этом мы узнаем, когда будем рассматривать виртуальные методы. Теперь следует реализация класса:
    { TQueue }
    constructor TQueue.Create(aCapacity: Integer);
    begin
       Inherited Create;
    Ключевое слово Inherited (Унаследованный) перед именем метода означает, что нам требуется вызвать метод с тем же именем, но который достался нам в наследство от "родителя", в противном случае у нас получится обыкновенная рекурсивная процедура, которая будет бесконечно вызывать саму себя. Вспомним, что унаследованный конструктор TObject.Ctreate выполняет необходимые действия по инициализации экземпляра класса.
       Capacity := aCapacity;
       { Получить необходимую память }
       GetMem(Items, aCapacity*SizeOf(Integer));
       { Очистка "мусора" }
       ZeroMemory(Items, aCapacity*SizeOf(Integer));
    end;
    Деструктор освободит выделенную память и корректно завершит работу экземпляра класса:
    destructor TQueue.Destroy;
    begin
       FreeMem(Items);
       Inherited Destroy;
    end;
    Теперь следует реализация методов Push, Pop и IsEmpty:
    procedure TQueue.Push(Value: Integer);
    begin
       if Size < Capacity   // Если позволяет место,
       then begin
          Inc(Size);        // увеличить длину очереди
          Items[Size] := Value;   // и поместить в ее конец новый элемент.
       end;
    end;
    
    function TQueue.Pop: Integer;
    begin
       if Size > 0   // Если очередь непустая,
       then begin
          Result := Items[1];   // вернем первый элемент,
          Dec(Size);            // уменьшим длину очереди
          // и переместим все оставшиеся элементы к началу очереди
          Move(Items[2], Items[1], Size*SizeOf(Integer));
       end;
    end;
    
    function TQueue.IsEmpty: Boolean;
    begin
       IsEmpty := (Size = 0);
       { Или Result := (Size = 0); }
    end;
    Следует отметить, что в Object Pascal результат выполнения функции можно помещать в специальную локальную переменную Result. Помимо удобства, переменная Result обладает еще одним полезным качеством - она позволяет считывать из нее помещенное значение, что позволяет избавиться от лишних временных переменных. Но мы забыли о стеке. Стек очень похож на очередь, только метод извлечения элемента у него несколько другой. Опишем класс TStack, который будет почти полной копией TQueue, за исключением метода Pop:
    Type
    
    TStack = class
        ...
       function Pop: Integer;
        ...
    End;
    
    function TStack.Pop: Integer;
    begin
       if Size > 0         // Если стек непустой,
       then begin
          Result := Items[Size]; // вернем последний элемент,
          Dec(Size);      // уменьшим длину стека
       end;
    end;
    
    А теперь опробуем наши новые классы в действии:
    procedure PrintQueue(Queue: TQueue);
    var i:Integer;
    Begin
       Write(' Size: ', Queue.Size,'; ');
       Write(' Items: ');
       if Queue.IsEmpty then
          Write('no items.')
       else
          for i:=1 to Queue.Size do Write(Queue.Items[i],' ');
       Writeln;
    End;
    
    procedure PrintStack(Stack: TStack);
    var i:Integer;
    Begin
       Write(' Size: ', Stack.Size,'; ');
       Write(' Items: ');
       if Stack.IsEmpty then
          Write('no items.')
       else
          for i:=1 to Stack.Size do Write(Stack.Items[i],' ');
       Writeln;
    End;
    
    Var
       Stack: TStack;
       Queue: TQueue;
       I, Value: Integer;
    Begin
       Stack := TStack.Create(5); // Выделим место для хранения 5-ти элементов
       Queue := TQueue.Create(5); // Выделим место для хранения 5-ти элементов
       // Заполним стек
       For i:=1 to 5 do
       Begin
          Write('Enter element:'); readln(Value);
          Stack.Push(Value); PrintStack(Stack);
       End;
    
       // Теперь извлечем элементы из стека
       while not Stack.IsEmpty do
          begin Write(Stack.Pop); PrintStack (Stack); End;
    
       // Заполним очередь
       For i:=1 to 5 do
       Begin
          Write('Enter element:'); readln(Value);
          Queue.Push(Value); PrintQueue(Queue);
       End;
    
       // Теперь извлечем элементы из очереди
       while not Queue.IsEmpty do
          begin Write(Queue.Pop); PrintQueue(Queue); End;
    
       Queue.free;
       Stack.free;
    End.
    Все работает! Но... Что-то здесь не так... Чем же написанная программа лучше обычной процедурной программы? Все та же громоздкость, та же незащищенность внутреннего устройства класса от внешних "посягательств". Неужели это все, на что способны классы? Конечно, нет! Мы забыли о двух главных вещах - возможности наследования общих свойств и методов классов, и инкапсуляции, т.е. сокрытии внутренних деталей работы класса.

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