Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
22
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать
  1. Агрегация

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

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

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

Например:

Interface

Type //класс, объекты которого агрегируются TInteriorObject = class(TObject) ... end;

//класс, в который агрегирован объект класса TInteriorObject TExteriorObject = class(TObject) private //агрегированный объект InteriorObject: TInteriorObject; ... public constructor Create; destructor Destroy; ... end;

implementation constructor TExteriorObject.Create; begin //создание агрегированного объекта InteriorObject := TInteriorObject.Create; //только после этого можно работать со свойствами //и методами агрегированного объекта ... end;

destructor TExteriorObject.Destroy; begin //уничтожение агрегированного объекта InteriorObject.Free; ... end;

В данном простом примере объект класса TInteriorObject агрегирован в объект класса TExteriorObject. Поле InteriorObject представляет собой указатель. Сам агрегированный объект создается, когда в теле конструктора TExteriorObject.Create вызывается его конструктор и уничтожается, соответственно, в деструкторе.

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

TPerson = class //имеет свойства имя и фамилия //а также свойство только для чтения – полные данные private //поля для хранения значений свойств FSurname: string; FName: string; protected //методы доступа к свойствам function GetFullData: string; virtual; procedure SetSurname(const Value: string); procedure SetName(const Value: string); public //конструкторы constructor Create(FileHandler: integer); overload; constructor Create(vName,vSurname: string); overload; //метод записи данных в файл procedure WriteData(FileHandler: integer); virtual; //свойство фамимлия property Surname: string read FSurname write SetSurname; //свойство имя property Name: string read FName write SetName; //свойство, возвращающее полные данные property FullData: string read GetFullData; end;

TStudent = class(TPerson) //добавляется свойство код группы private FGroup: string; protected //метод чтения свойства FullData перекрывается //как виртуальный function GetFullData: string; override; procedure SetGroup(const Value: string); public constructor Create(FileHandler: integer); overload; constructor Create(vName,vSurname,vGroup: string); overload; //метод записи данных в файл прекрывается как виртуальный procedure WriteData(FileHandler: integer); override; //свойство код группы property Group: string read FGroup write SetGroup; end;

TEmployee = class(TPerson) //добавляется свойство табельный номер private FTabNum: string; protected //метод чтения свойства FullData перекрывается //как виртуальный function GetFullData: string; override; procedure SetTabNum(const Value: string); public constructor Create(FileHandler: integer); overload; constructor Create(vName,vSurname,vTabNum: string); overload; //метод записи данных в файл прекрывается как виртуальный procedure WriteData(Filehandler: integer); override; //свойство табельный номер property TabNum: string read FTabNum write SetTabNum; end;

В этих классах предусмотрено по два конструктора инициализации. Один будет получать значения, инициализирующие данные объекта, как параметры. Второй в качестве параметра получает файловую переменную, указывающую на место в уже открытом файле, откуда объект должен считать свои данные. Метод записи в файл WriteData, соответственно, получает файловую переменную, указывающую на то место открытого для записи файла, куда объект должен сохранить свои данные. Метод WriteData перекрывается в потомках как виртуальный, поскольку, совпадая по назначению для всех классов, он различается по реализации, и будет сохранять в файле разные наборы данных. Для удобства использования объектов данных классов предусмотрено свойство только для чтения FullData. Оно представляет все данные об объекте в виде одной строки. Поскольку набор данных разный для разных классов, метод чтения этого свойства перекрывается как виртуальный.

Далее приводится возможная реализация методов этих классов. TPerson:

//конструктор, инициализирующий данные полученными значениями constructor TPerson.Create(vName, vSurname: string); begin Name := vName; Surname := vSurname; end;

//конструктор, инициализирующий данные значениями из файла constructor TPerson.Create(FileHandler: integer); var n,i: integer; sName,sSurName: string [100]; begin FileRead(FileHandler,sName,100); FileRead(FileHandler,sSurname,100); //вызываем конструктор инициализации как обычную процедуру Create(sName,sSurname); end;

//Метод, возвращающий полные данные как одну строку function TPerson.GetFullData: string; begin result := 'Имя ' + Name + ' Фамилия ' + Surname; end;

//метод записи свойства имя procedure TPerson.SetName(const Value: string); begin //все буквы преобразуются к верхнему регистру для общности FName := AnsiUpperCase(Value); end;

//метод записи свойства фамилия procedure TPerson.SetSurname(const Value: string); begin //все буквы преобразуются к верхнему регистру для общности FSurname := AnsiUpperCase(Value); end;

//метод записи данных в файл procedure TPerson.WriteData(FileHandler: integer); var s: string[100]; begin //данные записываются как строки s := Name; FileWrite(FileHandler,s,100); s := Surname; FileWrite(FileHandler,s,100); end;

Методы класса TStudent:

//конструктор, инициализирующий данные полученными значениями constructor TStudent.Create(vName,vSurname,vGroup: string); begin //вызывам конструктор предка для инициализации его данных inherited Create(vName,vSurname); Group := vGroup; end;

//конструктор, инициализирующий данные значениями из файла constructor TStudent.Create(FileHandler: integer); var s: string[100]; begin //вызывам конструктор предка для инициализации его данных inherited Create(FileHandler); FileRead(FileHandler,s,100); Group := s; end;

//Метод, возвращающий полные данные как одну строку function TStudent.GetFullData: string; begin //вызывам перекрытый метод предка и добавляем код группы result := inherited GetFullData + ' Группа ' + Group; end;

//метод записи свойства код группы procedure TStudent.SetGroup(const Value: string); begin //все буквы преобразуются к верхнему регистру для общности FGroup := AnsiUpperCase(Value); end;

//метод записи данных в файл procedure TStudent.WriteData(FileHandler: integer); var s: string[100]; begin //вызывам перекрытый метод предка для записи его данных inherited WriteData(FileHandler); //записываем собственные данные класса s := Group; FileWrite(Filehandler,s,100); end;

Методы класса TEmployee:

//конструктор, инициализирующий данные значениями из файла constructor TEmployee.Create(FileHandler: integer); var s: string[100]; begin //вызывам конструктор предка для инициализации его данных inherited Create (FileHandler); FileRead(FileHandler,s,100); TabNum := s; end;

//конструктор, инициализирующий данные полученными значениями constructor TEmployee.Create(vName,vSurname,vTabNum: string); begin //вызывам конструктор предка для инициализации его данных inherited Create(vName,vSurname); TabNum := vTabNum; end;

//Метод, возвращающий полные данные как одну строку function TEmployee.GetFullData: string; begin //вызывам перекрытый метод предка и добавляем //табельный номер result := inherited GetFullData+ ' Табельный номер ' + TabNum; end;

//метод записи свойства табельный номер procedure TEmployee.SetTabNum(const Value: string); begin //все буквы преобразуются к верхнему регистру для общности FTabNum := AnsiUpperCase(Value); end;

//метод записи данных в файл procedure TEmployee.WriteData(FileHandler: integer); var s: string[100]; begin //вызывам перекрытый метод предка для записи его данных inherited WriteData(FileHandler); //записываем собственные данные класса s := TabNum; FileWrite(Filehandler,s,100); end;

Добавим в этот пример класс список TList, агрегирующий сведения обо всех людях, связанных с данным учебным заведением. Предположим также, что в то время, когда программа не запущена, все сведения обо всех уже зарегистрированных людях хранятся в одном общем файле. Эти данные читаются из файла в конструкторе объекта класса TList и записываются в файл в деструкторе:

TList = class private //агрегированные объекты классов, потомков TPerson Persons: array[1..100] of TPerson; //количество реально созданных объектов FPersonsNumber: integer; //Имя файла FFileName: string; protected //методы чтения свойств function GetData(i: integer): TPerson; function GetStringData(i: integer): string; public //конструкторы и деструктор constructor Create; overload; constructor Create(s: string); overload; destructor Destroy; override; //добавляет человека к списку procedure AddPerson(Value: TPerson); //свойство колическтво человек property PersonsNumber: integer read FPersonsNumber; //векторное свойство, возвращающее данные об i-ом человеке //в виде строки property PersonListS[i: integer]: string read GetStringData; //векторное свойство, возвращающее данные об i-ом человеке //как указатель на объект типа TPerson или его потомка property PersonListP[i: integer]: TPerson read GetData; end;

Для хранения сведений о людях используется следующий формат файла: сначала хранится число типа integer, представляющее собой количество записей в файле. Далее идут собственно записи. Перед каждой записью хранится однобайтовая величина, содержащая сведения о типе объекта: студент, сотрудник или преподаватель. Имя файла передается конструктору в качестве параметра. Предусмотрен также конструктор по умолчанию, который считывает данные из файла с именем temp.dat. Организованы три свойства. Все только для чтения. Два векторных свойства позволяют получить данные об i-ом человеке в виде строки или объекта. Третье свойство позволяет прочитать количество человек в списке. Организация количества как свойства только для чтения гарантирует, что оно не будет некорректно изменено.

Рассмотрим возможную реализацию методов класса TList:

//Конструктор инициализации constructor TList.Create(s: string); var i: integer; FileHandler: integer; TypeOfPerson: char; begin //записываем в поле объекта имя файла FFileName := s; //открываем файл FileHandler := FileOpen(s, fmOpenRead); //считываем количество записей FileRead(FileHandler,FPersonsNumber, sizeof(FPersonsNumber)); //для всех записей for i := 1 to FPersonsNumber do begin //считываем тип объекта fileread(FileHandler,TypeOfPerson,1); //анализируем тип объекта case TypeOfPerson of //создаем агрегированный объект соответствующего типа //вызываем конструктор, считывающий данные из файла '0': Persons[i] := TPerson.Create(FileHandler); '1': Persons[i] := TStudent.Create(FileHandler); '2': Persons[i] := TEmployee.Create(FileHandler); end; end; //закрываем файл FileClose(FileHandler); end;

//конструктор по умолчанию constructor TList.Create; begin //чтобы не дублировать код вызывыаем конструктор //инициализации с именем файла по умолчанию Create('temp.dat'); end;

//метод, добавляющий человека к списку procedure TList.AddPerson(Value: TPerson); //в качестве параметра получает уже созданный объект класса //TPerson или одного из его потомков begin if FPersonsNumber<100 then begin inc(FPersonsNumber); Persons[FPersonsNumber] := Value; end; end;

//деструктор destructor TList.Destroy; var i: integer; FileHandler: integer; TypeOfPerson: char; begin inherited; //заносим данные в файл FileHandler := FileCreate(fFileName); FileWrite(FileHandler,fPersonsNumber, sizeof(fPersonsNumber)); for i := 1 to FPersonsNumber do begin TypeOfPerson := -1; //обратите внимане на порядок сравнения if (Persons[i] is TEmployee) then TypeOfPerson := '2' else if (Persons[i] is TStudent) then TypeOfPerson := '1' else if (Persons[i] is TPerson) then TypeOfPerson := '0'; FileWrite(FileHandler,TypeOfPerson,1); Persons[i].WriteData(FileHandler); //уничтожаем i-ый агрегированный объект Persons[i].Free; end; FileClose(FileHandler); end;

//метод возвращает данные i-го человека как объект function TList.GetData(i: integer): TPerson; begin //проверка допустимости значения i пропущена для краткости result := Persons[i]; end;

//метод возвращает данные i-го человека как строку function TList.GetStringData(i: integer): string; begin //проверка допустимости значения i пропущена для краткости result := Persons[i].GetFullData; end;

Данные о людях добавляются в список двумя путями: часть из них считывается из файла в конструкторе класса TList, другие добавляются с помощью метода AddPerson. В конструкторе TList явно вызываются конструкторы для всех создаваемых агрегированных объектов классов потомков TPerson. В метод AddPerson передаются указатели на уже созданные объекты. Вне зависимости от способа создания все агрегированные объекты TPerson разрушаются в деструкторе класса TList.

Рассмотрим возможное использование этих классов. Будут использоваться следующие переменные:

var List: TList; Person: TPerson; FileName: string; n,i: integer;

и вспомогательные подпрограммы:

Procedure WriteText(s: string);//вывод на экран одной строки Procedure EditText(var s: string);//вывод на экран и //редактирование одной строки

Создание списка:

FileName := ’’; ...//ввод имени файла if (FileName = ’’) then List := TList.Create else List := TList.Create(FileName);

Уничтожение списка:

List.Free;

Добавление данных в список:

List.AddPerson(TPerson.Create(’Иван’,’Петров’)); List.AddPerson(TStudent.Create(’Петр’,’Иванов’,’группа 1’)); List.AddPerson(TEmployee.Create(’Сергей’,’Васильев’,’257’));

Вывод имеющихся данных:

for i := 1 to List.PersonsNumber do WriteText(List.PersonListS[i]);

Редактирование данных i-го человека:

if (i>0) and (i<=List.PersonsNumber) then begin Person := List.PersonListP[i]; EditString(Person.Name); EditString(Person.Surname); //Проверяем тип реально созданного объекта if (Person is TStudent) EditString((Person as TStudent).Group); if (Person is TEmployee) EditString((Person as TEmployee).TabNum);

Как уже было сказано, язык Object Pascal допускает только одиночное наследование, то есть каждый класс может непосредственно наследовать поля методы и свойства только от одного класса. Что же делать, если необходимо, чтобы класс мог использовать поля, методы и свойства нескольких классов? В этом случае можно подменить множественное наследование агрегацией.

Например, мы имеем два класса: класс прямоугольник TRectangle и класс текст TText. Класс прямоугольник способен отображать себя на экране в заданной позиции. Класс текст предоставляет возможности по вводу и хранению текста, а также выводу его на экран. Допустим, нам необходим объект – рамка текста TTextRectangle, который должен быть способен выводить на экран рамку с надписью внутри нее. С одной стороны, у этого класса должны быть все возможности класса прямоугольник TRectangle, с другой – все возможности класса текст TText. Но сделать его потомком их обоих сразу невозможно. В этом случае можно сделать класс TTextRectangle потомком класса TRectangle и агрегировать в него объект класса TText:

Interface type

TRectangle = class(TObject) private ... protected ... public constructor Create(vTop, vLeft, vHeight, vWidth: integer); //положение и размеры property Top: integer read GetTop write SetTop; property Left: integer read GetLeft write SetLeft; property Height: integer read GetHeight write SetHeight; property Width: integer read GetWidth write SetWidth; //метод отображения procedure Draw; virtual; end;

TText = class(TObject) private ... protected ... public constructor Create(vTop, vLeft: integer; vText: string); //положение property Top: integer read GetTop write SetTop; property Left: integer read GetLeft write SetLeft; //текст property Text: string read GetText write SetText; procedure WriteText; end;

TTextRectangle = class(TRectangle) //класс объявлен как потомок TRectangle private //объект TText агрегируется TextObject: TText; protected procedure SetText(Value: string); function GetText: string; public constructor Create(vTop, vLeft, vHeight, vWidth: integer; vText: string); destructor Destroy; override; //перекрывается метод отображения procedure Draw; override; //свойство текст property Text: string read GetText write SetText; end;

Реализация методов класса TTextRectangle:

//конструктор constructor TTextRectangle.Create(vTop, vLeft, vHeight, vWidth: integer; vText: string); begin //вызываем конструктор предка inherited Create(vTop, vLeft, vHeight, vWidth); //создаем агрегированный объект TextObject := TText.Create(vTop, vLeft, vText); end;

//деструктор destructor TTextRectangle.Destroy; begin //разрушаем агрегированный объект TextObject.Destroy; //вызываем деструктор предка inherited; end;

//метод отображения на экране procedure TTextRectangle.Draw; begin //вызываем перекрытый метод, который нарисует рамку inherited; //вызываем метод агрегированного объекта TText, //который выведет текст TextObject.WriteText; end;

//метод записи свойства текст procedure TTextRectangle.SetText(Value: string); begin //устанавливаем свойство агрегированного объекта TextObject.Text := Value; end;

//метод чтения свойства текст function TTextRectangle.GetText: string; begin //возвращаем значение свойства агрегированного объекта result := TextObject.Text; end;

В этом примере класс TTextRectangle связан отношением наследования с классом TRectangle и агрегирует объект типа TText. В реализации конструктора и деструктора класса TTextRectangle необходимо вызвать конструкторы и деструкторы как класса-предка, так и агрегированного объекта, но формат вызова различен и происходящие при этом действия также различны. При вызове конструктора класса-предка не происходит распределения динамической памяти. Под агрегируемый объект память распределяется, возвращаемый указатель присваиваются соответствующему полю TextObject. Аналогичные действия выполняются в деструкторе. Вызов деструктора агрегированного объекта через его указатель приводит к разрушению этого объекта и освобождению динамической памяти. Вызов перекрытого деструктора предка приводит только к тому, что выполнятся описанные в нем действия.

Также в классе TTextRectangle перекрывается виртуальный метод Draw, который выводит текст на экран. При этом вызывается как унаследованная от предка реализация, которая выводит на экран рамку, так и метод агрегированного объекта WriteText, который выводит на экран текст.

Свойства Top, Left, Width, Height унаследованы классом TTextRectangle от класса TRectangle. Для доступа к свойству Text, описанному в классе TText создано новое свойство TTextRectangle.Text. Его методы чтения и записи просто обращаются к соответствующему свойству агрегированного объекта TextObject.

    1. Агрегация в C++

Язык С++ допускает как агрегацию по ссылке, так и по значению. В случае агрегации по ссылке в состав объектов внешнего класса в качестве полей будут входить указатели на агрегируемые объекты:

class CSomeClass { ... };

class CBase { public: CBase(); ~CBase(); CSomeClass * aggregated1; ... };

В этом случае агрегированные объекты должны быть созданы и разрушены явно в каких либо методах внешнего класса – например, в конструкторе и деструкторе:

CBase::CBase() { aggregated1 = new CSomeClass(); ... }

CBase::~CBase() { delete aggregated1; ... }

В случае агрегации по значению в объект внешнего класса будут входить непосредственно экземпляры агрегированных объектов. Память под них при этом будет распределяться и освобождаться автоматически при создании и уничтожении внешнего объекта.

Как известно, поля объекта могут инициализироваться путем присваивания в теле конструктора, а также в списке инициализации:

class CTop : public CBase1, public CBase2 { int a,b; public: CTop(int i, int j): CBase1(i*5),CBase2(j+i), a(i) {b = j;}; };

Список инициализации обеспечивает возможность инициализации полей объекта, являющихся в свою очередь объектами класса:

class CTop : public CBase1, public CBase2 { int a,b; CBase1 aggregated2; //агрегированный объект public: CTop(int i, int j): CBase1(i*5), CBase2(j+i), a(i), aggregated2(i*10) {b = j;}; //объект aggregated2 инициализируется в списке //инициализации значением i*10 };

При создании агрегированных объектов в этом случае будет вызван конструктор инициализации, и ему будут переданы соответствующие параметры.

Если для агрегированного объекта не нужна инициализация, то есть для него должен вызываться конструктор по умолчанию, нет необходимости указывать этот объект в списке инициализации – конструктор будет вызван автоматически.

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