Архангельский А.Я. Программирование в Delphi. Учебник по классическим версиям Delphi. 2006 г. - 1152 с.
Спадкування
Концепція об’єктно-орієнтованого програмування передбачає можливість визначати нові класи за допомогою додавання полів, властивостей і методів до вже існуючих класів. Такий механізм отримання нових класів називається новоутворенням. При цьому новий, новоутворений клас (називається нащадком) успадковує властивості і методи свого базового, батьківськогокласу.
Таким чином спадкування (Inheritance) – це процес, за допомогою якого один об’єкт може отримувати властивості іншого. Об’єкт може успадковувати основні властивості іншого об’єкта і добавляти до них риси, характерні тільки для нього. Спадкування є важливим, оскільки воно дозволяє підтримувати концепцію ієрархії класів. Використання ієрархії класів робить керованими великі потоки інформації. Наприклад, представимо опис житлового будинку. Будинок - це частина загального класу, що називається будови. З іншого боку, будова - це частина більш загального класу - конструкції, що є частиною ще більш загального класу об’єктів, який можна назвати створенням рук людини. В кожному випадку новоутворений клас успадковує всі властивості що пов’язані з батьком і додає до них свої власні, характеристики. Без використання ієрархії класів для кожного об’єкта необхідно було задати всі характеристики, які би його визначали. Однак при використанні спадкування можна описати об’єкт шляхом визначення того спільного класу (або класів), до якого від відноситься, з тими спеціальними рисами, які роблять об’єкт унікальним.
При оголошенні класа-нашадка вказують клас батька.
Наприклад, клас TEmployee [емплоии](конкретний співробітник компанії) може бути утворений від розглянутого попередньо класу TPersonal шляхом додавання поля FDepartment (відділ). Оголошення класу TEmployee в цьому випадку може бути, як показано в лістінгу 4.7.
Лістинг 4.7[3] Оголошення класу-нашадка TEmployee
TEmployee = class(TPersonal) // Клас TEmployee створений на основі TPersonal. private FDepartment: byte; // Номер відділу співробітника. function GetDepartment: byte; procedure SetDepartment(New_Department: byte); public constructor Create(Name:TName; Age:TAge; Department: byte); property Department:byte read GetDepartment write SetDepartment; end; |
Ім’я класу Tpersonal, що заключено в дужки показує, що клас Temployee є похідним від класу TPersonal. В свою чергу клас TPersonal є базовим для класу TEmployee.
Клас TEmployee повинен мати свій власний конструктор, який забезпечує ініціалізацію класа-батька і своїх полів. В лістингу 4.8 наведений приклад реалізації конструктора класу TEmployee.
Лістинг 4.8 Конструктор для класу Temployee
// Конструктор для класу TEmployee. constructor TEmployee.Create(Name:TName;Age:TAge;Department:byte); begin inherited Create(Name, Age); // Ініціалізація конструктора // класа-батька. FDepartment := Department; end ; |
В наведеному прикладі директивою inherited викликається конструктор батьківського класу. Після цього присвоювання значення полю класу-нащадка.
Після створення об’єкта похідного класу в програмі можна використовувати поля і методи батьківського класу. В лістингу 4.9 наведений текст програми, демонструє цю можливість.
Лістинг 4.9 Приклад реалізації принципу спадкування
unit WinForm; interface uses System.Drawing, System.Collections, System.ComponentModel, System.Windows.Forms, System.Data; type TName = string[30]; TAge = byte; TWinForm = class(System.Windows.Forms.Form) {$REGION 'Designer Managed Code1} strict private Components: System.ComponentModel.Container; Button1: System.Windows.Forms.Button; procedure InitializeComponent; procedure Button1Click(sender: System.Object;e: System.EventArgs); {$ENDREGION} strict protected procedure Dispose(Disposing: Boolean); override; private { Private Declarations } public constructor Create; end; // Оголошення классу. TPersonal = class private fname:TName; // Значення властивості Name – ім’я співробітника. fage:TAge; // Значение свойства Age - вік співробітника. function GetName:TName; function GetAge:TAge; procedure SetAge(new_age:TAge); public // Конструктор - створення нового об’єкта (экземпляра класса) constructor Creat e(Name:TName;Age:TAge); procedure Showlnfo; // Цей метод класу - показує інформацію // про співробтника. // Властивість об’єкта property Name:TName // Властивість тілько для читання. read GetName; property Age:TAge // Властивість для зчитeвання та запису. read GetAge write SetAge; end; // Клас TEmployee створений на основі TPersonal. TEmployee = class(TPersonal) private FDepartment:byte; // Номер відділу співробітника. function GetDepartment:byte; procedure SetDepartment(New_Department:byte); public constructor Create(Name:TName;Age:TAge;Department:byte); property Department:byte read GetDepartment write SetDepartment; end; [assembly: RuntimeRequiredAttribute(TypeOf(TWinForm))] implementation {$AUTOBOX ON} {$REGION 'Windows Form Designer generated code'} procedure TWinForm.Dispose(Disposing: Boolean); begin if Disposing then begin if Components <> nil then Components.Dispose() ; end ; inherited Dispose(Disposing); end ; // Конструктор для форми. constructor TWinForm.Create; begin inherited Create; InitializeComponent; end; procedure TWinForm.Button1Click(sender: System.Object; e: System.EventArgs); var worker: TPersonal; dep_worker:TEmployee; begin // Створення нового об’єкта TPersonal. worker:=TPersonal.Create('Шупрута Володимир',0); // Виклик метода об’єкта - вивід інформації про співробітника. worker.showinfо; // На екрані повідомлення Шупрута Володимир О'. // Зміна властивості об’єкта. worker.age:=2 5; // Виклик метода об’єкта - вивід інформації про співробітника. worker.showinfо // На екрані повідомлення 'Шупрута Володимир 25'. // Створення співробітника - нащадка TEmployee. dep_worker:=TEmployee.Create('Захаров Сергій',24,1); // Відображення інформації - метод успадкований від TPersonal. dep_worker.showinfо; // На екрані повідомлення 'Захаров Сергій 24' // Відображення інформації - поля Name, Age і Department. messagebox.Show(dep_worker.Name + ' '+ dep_worker.age.tostring+ ' ' + dep_worker.FDepartment.tostring); // Видалення об’єктів. worker.free; dep_worker.free; end ; // Конструктор для класу TPersonal. // При виклику створюється новий об’єкт з ім’ям Name і вік Age. constructor TPersonal.Create(Name:TName;Age:TAge); begin inherited Create; fname:=Name; fage:=Age; end ; // Метод отримання значення властивості Name. function TPersonal.GetName; begin result:=fname; end; // Метод отримання значення властивості Age. function TPersonal.GetAge; begin result:=fage end; // Метод запису значення властивості Age. procedure TPersonal.SetAge(new_age:TAge); begin // Приклад обробки значення на запис. // Якщо значення, що присвоюється значенню, <20, то // властивість не змінить своего значення. if new_age<2 0 then exit else fage:=new_age; end; // Метод класу TPersonal - процедура показує дані про співробітника. procedure TPersonal.showinfо; begin messagebox.Show(Name+' '+Age.Tostring) ; end; // Конструктор для класу TEmployee. constructor TEmployee.Create(Name:TName;Age:TAge;Department:byte); begin inherited Create(Name,Age); //Ініціалізація конструктора // класа-батька. FDepartment:=Department; end; // Метод отримання значеня властивості Department. function TEmployee.GetDepartment; begin result:=FDepartment; end ; // Метод запису значення в властивості Department. procedure TEmployee.SetDepartment(New_Department:byte); begin FDepartment:=New_Department; end; end. |
Директива inherited вказує компілятору, що треба викликати метод базового (батьківського) класу з тим самим ім’ям, що і метод, в якому вона вказана. При цьому якщо сигнатура методів батьківського класу і його нащадків співпадає (нагадаємо, що сигнатурою методу є його ім’я і набір параметрів), то вказувати повний формат виклику не треба. Наприклад, створимо нащадка класу TEmployee, який відрізняється від свого батька тільки тим, що при виклику конструктора зразу викликається повідомлення з інформацією з полів об’єкта:
TNewEmployee = class(TEmployee) public constructor Create(Name:TName;Age:TAge;Department:byte); end; constructor TNewEmployee.Create(Name:TName;Age:TAge;Department:byte); begin inherited; Showlnfo; end; |
Як бачимо, в конструкторі вказується директива inherited,a компілятор сам підставляє за нею виклик конструктора батьківського об’єкта з правильними параметрами.
При розгляді лістингу 4.9, а точніше – опису об’єкта TPersonal і реалізації його конструктора - може виникнути наступне питання: чому, в описі об’єкта відсутня вказівка класу-батька, а в конструкторі ми звертаємось до конструктора базового класу?
Справа в тому, що в Delphi всі класи вишикувані в єдину ієрархію, на вершині якої стоїть клас TObject. Коли при описі класу пропускається вказівка його батька, то в цьому випадку компілятор рахує, що батьком такого класу є TObject.
4.4.3 Поліморфізм[4]
З попереднього прикладу ми, побачили одну важливу особливість.
Клас TEmployee успадковує метод Showlnfo класу TPersonal, але даний метод для класу TEmployee не дуже корисний, в зв’язку з тим що відображається тільки частина інформації – ім’я і вік. Бажано, щоб, при використанні даного методу для класу TEmployee відображалася повна інформація – ім’я, вік та відділ. Така заміна методів при відсутності зовнішніх відмінностей у викликах може бути реалізована за допомогою третьої концепції ООП - поліморфізму.
Поліморфізм (Polymorphism) - це можливість використовувати однакові імена для методів, що входять в різні класи. Концепція поліморфізму забезпечує у випадку використання методів до об’єкту використання саме того методу, який відповідає класу об’єкта.
З попереднього прикладу в нас визначені два класи: TPersonal і TEmployee, причому перший є базовим для другого.
В базовому класі визначений метод Showlnfo, що забезпечує вивід інформації (ім’я і вік) про співробітника на екран. Щоб дочірній клас (нащадок) міг використовувати метод з тим самим ім’ям, зміст якого складали би деякі інші дії, даний метод в базовому класі треба оголосити з директивою virtual. Оголошення методу віртуальним дає можливість дочірньому класу здійснити заміну віртуального методу своїм власним. Нижче в лістингу 4.10 наведений приклад опису базового класу з використанням директиви virtual.
Лістинг 4.10Метод showinfо оголошений в базовому класу як віртуальний
// Оголошення класу. TPersonal = class private fname:TName; // Значення властивості Name – ім’я співробітника. fage:TAge; // Значення властивості Age - вік співробітника. function GetName:TName; function GetAge:TAge; procedure SetAge(new_age:TAge); public // Конструктор - створення нового (екземпляра класу). constructor Create(Name:TName;Age:TAge); // Це метод класу - показує інформацію про співробітника – // віртуальний. procedure Showinfо; virtual; // Свойства объекта property Name:TName // Свойство тільки для читання. read GetName; property Age:TAge // Свойство для чтения и записи. read GetAge write SetAge; end; |
В дочірньому класі TEmployee також визначений свій метод Showlnfo (лістинг 4.11), який заміщає відповідний метод батьківського класу. Метод породженого класу, що заміщає віртуальний метод батьківського класу, помічається директивою override.
Лістинг 4.11Метод showinfо в описі класу TEmployee відмічений директивою override
// Класс TEmployee створений на основі TPersonal. TEmployee = class(TPersonal) FDepartment:byte; // Номер відділу співробітника. function GetDepartment:byte; procedure SetDepartment(New_Department:byte); public constructor Create(Name:TName;Age:TAge;Department:byte); procedure showinfо; override;// Метод Showlnfo замінений // на власний. // Властивість об’єкта. property Department:byte read GetDepartment write SetDepartinent ; end; |
4.4.4. Директиви protected і private(додатково)
Крім оголошених елементів класу (полів, методів, властивостей) опис класу, як правило, містить директиви protected (захищений) і private (приватними), які встановлюють ступінь видимості елементів класу в програмі.
Елементи класу, оголошенні в секції protected, доступні тільки в утворених від нього класах. Область видимості елементів класу цієї секції не обмежується модулем, в якому знаходиться опис класу.
Зазвичай в секцію protected розміщають опис методів класу.
Елементи класу, оголошенні в секції private, видимі тільки всередині модуля. Ці елементи не доступні за межами модуля, навіть в похідних класах. Зазвичай в секції private розміщають опис полів класу, а методи, забезпечують доступ до цих полів, розміщують в секцію protected.
В тих випадках, коли необхідно повністю закрити елементи класу, виділення класу необхідно помістити в окремий модуль, а в програму, яка використовує об’єкти цього класу, розмістити в секції uses вказати посилання на цей модуль.
4.4.5. Області видимості елементів класу
Для розділення доступу до властивостей і методів екземплярів класів між різними частинами програми передбачені модифікатори доступу («видимості»), приведені в табл. 5.1.
Модифікатор доступу в Delphi (як і в Pascal, але на відміну від деяких інших мов програмування) відноситься не до конкретної властивості або методу класу, а до всіх елементів класу (властивостям і методам), опис яких розташовується після вказівки модифікатора. Один і той же модифікатор може указуватися в описі класу більше одного разу.
Порівняльна таблиця модифікаторів доступу в Pascal і Delphi Таблиця 5.1
Модифікатор | Область видимості властивості або методу, поміченого модифікатором | |
Pascal | Delphi | |
private | опис класу і модуль у якому описаний клас у тому числі і для інших класів | Так само |
protected | не використовується | як у private + у класах успадкованих від цього класу |
public | будь-яка частина програми | Так само |
published | не використовується | як у public + у Інспекторові об'єктів Delphi для візуальної побудови програм |
Стандартний опис класу містить властивості і методи, розташовані в порядку зростання їх видимості, але це правило не є обов'язковим.
Турe ..... <Ім'я класу> = class private <Ім'я властивості 1>: <Тип властивості 1>; (Опис властивостей класу, що мають область видимості private) ..... <Ім'я властивості N>: <Тип властивості N>; <Заголовок методу 1>; {Опис методів класу, що мають область видимості private} ..... <Заголовок методу М>; protected <Ім'я властивості 1>: <Тип властивості 1>; {Опис властивостей класу, що мають область видимості protected} <Ім'я властивості N>: <Тип властивості Nl>; <3заголовок методу 1>; {Опис методів класу, що мають область видимості protected} ..... <заголовок методу М>; public <Ім'я властивості 1>: <Тип властивості 1>; {Опис властивостей класу, що мають область видимості public) <Ім'я властивості N>: <Тип властивості N>; <заголовок методу 1>; {Опис методів класу, що мають область видимості public} ….... <3заголовок методу М>; published (Опис спеціальних властивостей класу (property), що мають область видимості published} End; |
Різні області видимості властивостей і методів об'єкту призначені для спрощення підтримки цілісності інформації в об'єктах. Розглянемо клас, в якому містяться три властивості, — а, b і c, причому c є добутком а і b. Якщо властивості а, b і с матимуть широку область видимості public, тобто будуть доступні з будь-якого фрагмента програми, то об'єкт — екземпляр такого класу може містити некоректні дані, оскільки існує можливість зміни властивостей а і b без перерахунку властивості с. Для виходу з такої ситуації можна запропонувати два підходи, і обидва вони зв'язані з використанням областей видимості властивостей об'єкту.
Перший підхід (див. лістинг 5.5) полягає в тому, щоб приховати властивості а і b від підпрограм які викликають, призначивши ним вузькі області видимості, наприклад private або protected. Вибір того або іншого модифікатора визначається необхідністю використання приховуваних властивостей в класах-нащадках даного класу. Якщо такої необхідності немає, то призначається область видимості private, якщо об'єкти-нащадки повинні мати прямий доступ до властивостей, то призначається область видимості protected.
У будь-якому випадку, зовнішні підпрограми не зможуть звернутися до властивостей а і b тому доведеться передбачити методи, які їх встановлюють. У цих методах, крім установки значень властивостей, повинен проводитися перерахунок властивості с. Це гарантуватиме цілісність даних об'єкту при зміні властивостей а і b.
Відмітимо, що методи установки повинні мати широку область видимості, щоб до них можна було звернутися з будь-якого місця програми.
Лістинг5.5. Завдання областейвидимості. Перший підхід | |
UnitUsingPrivatel; {Заголовок модуля} Interface{Початок інтерфейсної частини) Турe ABC = class{Заголовок класу ABC) private{Початок області видимості private} а, b: Double;{Властивості а і Ь мають вузьку область видимості} public(Початок області видимості public}. c: Double;{Властивість з має широку область видимості) Procedure SETA(NEWA: Double);{Метод SETA має широку область видимості} Procedure SETB(NEWB: Double);{Метод SETB має широку область видимості} end; Implementation(Початок частини реалізації) Procedure ABC.SetA(NEWA: Double);{Опис методу. SETA класу ABC} Begin а := NEWA;(Установка значення властивості а} c := а * b; (Оновлення значення властивості c — підтримка цілісності даних} end; Procedure ABC.SetB(NEWB: Double);{Опис методу SETB класу ABC} Begin b := NEWB;{Установка значення властивості b} c := а * b; {Оновлення значення властивості c — підтримка цілісності даних} end; end.(Закінчення модуля} |
При такій конструкції класу викликаючи підпрограма не зможе змінити значення властивостей а і b, не вплинувши тим самим на значення властивості c, тому цілісність даних не буде порушена. Проте властивість c доступно для зміни, навіть якщо її значення перераховане при установці значень а і b. У результаті воно може змінитися, викликавши невідповідність даних. Таким чином, краще було б приховати властивість, реалізувавши спеціальний метод доступу до його значення (лістинг 5.6).
Лістинг 5.6. Завдання областей видимості. Другий підхід | |
Unit UsingPrivate2;{Заголовок модуля} Interface{Початок інтерфейсної частини} Турe Авс2 = class{Заголовок класу ABC} Private{Початок області видимості private} c: Double;{Властивість c має вузьку область видимості} Public {Початок області видимості public} а, b: Double;// Властивості а і b мають широку область //видимості Function GETC: Double;//Метод GETC має широку область //видимості end; Implementation{Початок описової частини} Function ABC2.GetC: Double;{Опис методу GETC класу Авс2} Begin c := а * b; {Оновлюємо значення властивості c} Result := c;{Повертаємо значення із викликаючої підпрограмі} end; end.{Закінчення модуля}. |
При такій конструкції класу цілісність даних не може бути зіпсуватися підпрограмами які викликають, якщо тільки вони не знаходяться в одному модулі з описуваним класом, тобто, в даному випадку, в модулі UsingPrivate2. Єдиним недоліком такого підходу є постійний перерахунок значення с при кожному зверненні до методу GETC. Тому найбільш оптимальним варіантом рішення наший завдання буде заховання всіх властивостей класу і реалізація методів доступу до них через методи (див. лістинг 5.7).
Лістинг 5.7.Завдання областей видимості. Третій підхід (оптимальний) | |
unit UsingPrivateS;{Заголовок модуля} Interface Туре АВСЗ = class private а, b, с: Double;{Всі властивості мають вузьку область видимості} public Procedure SETA(NEWA: Double);{Всі методи мають широку область видимості} Procedure SETB(NEWB: Double); Function GETC: Double; end; Implementation Procedure ABC3.SetA(NEWA: Double);{Опис методу SETA класу АВСЗ} Begin а := NEWA; с := а * b; end; Procedure АВСЗ.SetB(NEWB: Double);{Опис методу SETB класу АВСЗ} Begin b := NEWB; с := а * b; end; Function ABC3.GetC: Double;{Опис методу GETC класу АВСЗ} Begin Result:= с;{Просто повертаємо значення с} end; end. |
Питання:
1. Для чого призначений поліморфізм.
2. Для чого призначено спадкування.
3. Для чого використовуються директиви private, public.
Література:
Архангельский А.Я. Программирование в Delphi. Учебник по классическим версиям Delphi. 2006 г. - 1152 с.
2. Архангельский А.Я. Delphi 2006. Справочное пособие: язык Delphi, классы, функции Win32 и .NET, 2006 г. - 1152 с.