|
|||||||||
АвтоАвтоматизацияАрхитектураАстрономияАудитБиологияБухгалтерияВоенное делоГенетикаГеографияГеологияГосударствоДомДругоеЖурналистика и СМИИзобретательствоИностранные языкиИнформатикаИскусствоИсторияКомпьютерыКулинарияКультураЛексикологияЛитератураЛогикаМаркетингМатематикаМашиностроениеМедицинаМенеджментМеталлы и СваркаМеханикаМузыкаНаселениеОбразованиеОхрана безопасности жизниОхрана ТрудаПедагогикаПолитикаПравоПриборостроениеПрограммированиеПроизводствоПромышленностьПсихологияРадиоРегилияСвязьСоциологияСпортСтандартизацияСтроительствоТехнологииТорговляТуризмФизикаФизиологияФилософияФинансыХимияХозяйствоЦеннообразованиеЧерчениеЭкологияЭконометрикаЭкономикаЭлектроникаЮриспунденкция |
Семантика
Ідентичність - це така властивість об'єкта, що відрізняє його від всіх інших об'єктів. У більшості мов програмування й керування базами даних для ідентифікації тимчасових об'єктів їх іменують, тим самим інтегруючи адресованість та ідентичність. Більшість баз даних розрізняють постійні об'єкти за ключовим атрибутом, тим самим змішуючи ідентичність і значення даних. Джерелом помилок в об’єктно-орієнтованому програмуванні є невміння відрізняти ім'я об'єкта від самого об'єкта. Приклади. Почнемо з визначення крапки на площині. struct Point { int x; int y; Point(): x(0), y(0) {} Point(int xValue, int yValue): x(xValue), y(yValue) {} }; Ми визначили Point як структуру, а не як повноцінний клас. Правило, на підставі якого ми так здійснили, дуже просте. Якщо абстракція представляє собою сукупність інших об'єктів без якої-небудь власної поведінки, ми робимо її структурою. Однак, коли наша абстракція визначає складнішу поведінку, ніж простий доступ до полів структури, то потрібно визначати клас. У цьому випадку абстракція Рoint - це просто пара координат (x,y). Для зручності передбачено два конструктори: один ініціалізує точку нульовими значеннями координат, а інший - деякими заданими значеннями. Тепер визначимо екранний об'єкт (DisplayItem). Це абстракція досить звична для систем із графічним інтерфейсом (GUI) - вона є базовим класом для всіх об'єктів, які можна відображати у вікні. Ми хочемо зробити її чимось значнішим, ніж просто сукупністю точок. Треба, щоб клієнти могли рисувати, вибирати об'єкти й переміщати їх по екрані, а також запитувати їх місце знаходження та стан. Ми записуємо нашу абстракцію у вигляді такого оголошення на C++: class DisplayItem { public: DisplayItem(); DisplayItem(const Point& location); virtual ~DisplayItem(); virtual void draw(); virtual void erase(); virtual void select(); virtual void unselect(); virtual void move(const Point& location); int isSelected() const; Point location() const; int isUnder(const Point& location) const; protected: }; У цьому оголошенні ми навмисно опустили конструктори, а також оператори для копіювання, присвоювання й перевірки на рівність. Їх ми залишимо до наступного розділу. Ми очікуємо, що в цього класу буде багато спадкоємців, тому деструктор і всі модифікатори оголошені віртуальними. Особливо це відноситься до draw. Селектори швидше за все не будуть перевизначатися в підкласах. Зверніть увагу, що один з них, isUnder, повинен обчислити, чи накриває цей об'єкт дану точку, а не просто повертати значення якоїсь властивості. Оголосимо екземпляри вказаних класів: DisplayItem item1; DisplayItem* item2 = new DisplayItem(Point(75, 75)); DisplayItem* item3 = new DisplayItem(Point(100, 100)); DisplayItem* item4 = 0; Рис. 15.1а показує, що при виконанні цих операторів виникають чотири імені й три різних об'єкти. Конкретно, у пам'яті будуть відведені чотири місця під імена item1, item2, item3, item4. При цьому item1 буде іменем об'єкту класу DisplayItem, а три інших будуть показниками. Крім того, лише item2 і item3 будуть насправді вказувати на об'єкти класу DisplayItem. В об'єктів, на які вказують item2 і item3, до того ж немає імен, хоча на них можна посилатися: наприклад, *item2. Тому ми можемо сказати, що item2 вказує на окремий об'єкт класу DisplayItem, на ім'я якого ми можемо опосередковано посилатися через *item2. Унікальна ідентичність (але не обов'язкове ім'я) кожного об'єкта зберігається на весь час його існування, навіть якщо його внутрішній стан змінився. Ця ситуація нагадує парадокс Зенона про ріку: чи може ріка бути тієї ж самою, якщо в ній щодня тече різна вода? Розглянемо результат виконання таких операторів (рис. 15.1б): item1.move(item2->location()); item4 = item3; item4->move(Point(38, 100)); Об'єкт item1 і об'єкт, на який вказує item2, тепер відносяться до однієї й тієї ж точки екрану. Показник item4 став вказувати на той же об'єкт, що й item3. До речі, помітьте різницю між виразами "об'єкт item2 " і "об'єкт, на який вказує item2 ". Другий вираз більше точніший, хоча для стислості ми часто будемо використовувати їх як синоніми.
Рис. 15.1. Ідентичність об'єктів Хоча об'єкт item1 і об'єкт, на який вказує item2, мають однаковий стан, вони залишаються різними об'єктами. Крім того, ми змінили стан об'єкта *item3, використавши його нове непряме ім'я item4. Ця ситуація, яку ми називаємо структурною залежністю, маючи на увазі під цим ситуацію, коли об'єкт іменується більше, ніж одним способом декількома синонімічними іменами. Структурна залежність породжує в об’єктно-орієнтованому програмуванні багато проблем. Складність розпізнання побічних ефектів при діях із синонімічними об'єктами часто приводить до "витоків пам'яті", неправильному доступу до пам'яті, і, гірше того, до непрогнозованих змін станів. Наприклад, якщо ми знищимо об'єкт через показник item3, то значення показника item4 виявиться безглуздим; ця ситуація називається повислим посиланням. На рис. 15.1в ілюструється результат виконання таких дій: item2 = &item1; item4->move(item2->location()); У першому рядку створюється синонім: item2 вказує на той же об'єкт, що й item1. У другій доступ до стану item1 отриманий через цей новий синонім. На жаль, при цьому відбувся витік пам'яті, - об'єкт, на який спочатку вказувало посилання item2, ніяк не йменується ні прямо, ні побічно, і його ідентичність загублена. В Smalltalk і CLOS пам'ять, відведена під об'єкти, буде знову повернута системі збирачем сміття. У мовах типу C++ така пам'ять не звільняється, поки не завершиться програма, що створила об'єкт. Такі витоки пам'яті можуть викликати незручність, великі збої, особливо, якщо програма повинна безупинно працювати тривалий час. Копіювання, присвоювання й рівність. Структурна залежність має місце, коли об'єкт має кілька імен. У складних системах, реалізованих за допомогою об’єктно-орієнтованого підходу використання синонімів просто неминуче. Наприклад, розглянемо такі дві функції: void highLight(DisplayItem& i); void drag(DisplayItem i); // Небезпечно Якщо викликати першу функцію з параметром item1, буде створений псевдонім: формальний параметр i означає показник на фактичний параметр, і отже item1 і i іменує той самий об'єкт під час виконання функції. При виклику другої функції з аргументом item1 їй передається новий об'єкт, який є копією item1: i позначає зовсім інший об'єкт, хоча й з тим же станом, що й item1. В C++ відрізняється передача параметрів за посиланням й за значенням. Треба стежити за цим, інакше можна ненавмисно змінити копію об'єкта, бажаючи змінити сам об'єкт. Як ми побачимо в наступному розділі, передача об'єктів за посиланням в C++ необхідна для програмування поліморфної поведінки. У загальному випадку, передача об'єктів за посиланням вкрай бажане для досить складних об'єктів, оскільки при цьому копіюється посилання, а не стан, а отже, досягається більша ефективність (за винятком тих випадків, коли передане значення дуже просте). У деяких випадках, однак, мається на увазі саме копіювання. У мовах типу C++ семантику копіювання можна контролювати. Зокрема, ми можемо ввести копіюючий конструктор у визначення класу, як у наступному фрагменті коду, який можна було б включити в опис класу DisplayItem: DisplayItem(const DisplayItem&); В C++ копіюючий конструктор може бути викликаний явно (як частина опису об'єкта) або неявно (з передачею об'єкта за значенням). Відсутність цього спеціального конструктора викликає копіюючий конструктор, що діє за замовчуванням, і який копіює об'єкт поелементно. Однак, коли об'єкт містить посилання або показники на інші об'єкти, така операція приводить до створення синонімів показників на об'єкти, що робить поелементне копіювання небезпечним. Ми пропонуємо емпіричне правило: дозволяти неявне розмноження шляхом копіювання тільки для об'єктів, що містять винятково примітивні значення, і робити його явним для складніших об'єктів. Це правило пояснює те, що деякі мови називають "поверхневим" і "глибоким" копіюванням. Щоб копіювати об'єкт, у мові Smalltalk введені методи shallowCopy (метод копіює тільки об'єкт, а стан є роздільним) і deepCopy (метод копіює об'єкт і стан, якщо потрібно - рекурсивно). Перевизначаючи ці методи для класів з агрегацією, можна домагатися ефекту "глибокого" копіювання для одних частин об'єкта, і "поверхневого" копіювання для інших частин. Присвоювання - це, загалом кажучи, копіювання. В C++ його зміст теж можна змінювати. Наприклад, ми могли б додати у визначення класу DisplayItem такий рядок: virtual DisplayItem& operator=(const DisplayItem&); Цей оператор навмисно зроблений віртуальним, тому що очікується, що підкласи будуть його перевизначати. Як і у випадку копіюючого конструктора, копіювання можна зробити "глибоким" і "поверхневим". Якщо оператор присвоювання не перевизначений явно, то за замовчуванням об'єкт копіюється поелементно. З питанням присвоювання тісно пов'язане питання рівності. Хоча питання здається простим, рівність можна розуміти двома способами. По-перше, два імені можуть позначати той самий об'єкт. По-друге, це може бути рівність станів двох різних об'єктів. У прикладі на рис. 15.1в обидва варіанти тотожності будуть справедливі для item1 і item2. Однак для item2 і item3 вірним буде тільки другий варіант. В C++ немає визначеного оператора рівності, тому ми повинні визначити рівність і нерівність, оголосивши ці оператори під час опису: virtual int operator=(const DisplayItem&) const; int operator!=(const DisplayItem&) const; Ми пропонуємо описувати оператор рівності як віртуальний (тому що очікуємо, що підкласи можуть перевизначати його поведінку), і описувати оператор нерівності як невіртуальний (тому що хочемо, щоб він завжди був логічним запереченням рівності: підкласам це не слід перевизначати). Аналогічним чином ми можемо створювати оператори порівняння об'єктів типу >= і <=. Час життя об'єктів. Початком часу існування будь-якого об'єкта є момент його створення (відведення пам'яті), а закінченням - повернення відведеної пам'яті назад системі. Об'єкти створюються явно або неявно. Є два способи створити їх явно. По-перше, це можна зробити при оголошенні (як це було з item1): тоді об'єкт розміщується в стеку. По-друге, як у випадку item3, можна розмістити об'єкт, тобто виділити йому пам'ять із "купи". В C++ у кожному випадку викликається конструктор, що виділяє відому йому кількість правильно ініціалізованої пам'яті під об'єкт. В Smalltalk цим займаються метакласи, про семантику яких ми поговоримо пізніше. Часто об'єкти створюються неявно. Так, передача параметра за значенням в C++ створює в стеку тимчасову копію об'єкта. Більше того, створення об'єктів транзитивно: створення об'єкта тягне за собою створення інших об'єктів, що входять у нього. Перевизначення семантики копіюючого конструктора і оператора присвоювання в C++ дозволяє явне керування створюванням і знищенням об'єкта. До того ж в C++ можна перевизначати й оператор new, тим самим змінюючи політику керування пам'яттю в "купі" для окремих класів. В Smalltalk і деяких інших мовах, якщо втрачено останнє посилання на об'єкт його забирає збирач сміття. У мовах без збирання сміття, типу C++, об'єкти, створені в стеку, знищуються при виході із блоку, у якому вони були визначені, але об'єкти, створені в "купі" оператором new, продовжують існувати й займати місце в пам'яті: їх необхідно явно знищувати оператором delete. Якщо об'єкт "забути", не знищити, це викличе, як вже було сказано вище, витік пам'яті. Якщо ж об'єкт спробують знищити повторно (наприклад, через інший показник), наслідком буде повідомлення про порушення пам'яті або повний крах системи. При явному або неявному знищенні об'єкта в C++ викликається відповідний деструктор. Його завдання не тільки звільнити пам'ять, але й вирішити, що робити з іншими ресурсами, наприклад, з відкритими файлами. Деструктори не звільняють автоматично пам'ять, яку вони виділили оператором new, програмісти повинні самі звільнити її. Знищення довгоживучих об'єктів має трохи іншу семантику. Як наголошувалося у попередньому розділі, деякі об'єкти можуть бути довгоживучими; під цим розуміється, що їхній час життя може перевищувати час життя програм, що їх породили. Звичайно такі об'єкти є частиною якоїсь довгострокової об'єктної структури, тому питання їхнього життя й смерті ставляться скоріше до політики відповідної об’єктно-орієнтованої бази даних. У таких системах для забезпечення довгого життя найприйнятнішим є підхід, що базується на основі постійних класів. Всі об'єкти, яким ми хочемо забезпечити довге життя, повинні успадковуватися від цих класів. 15.2. Відношення між об'єктами 15.2.1. Типи відношень Самі по собі об'єкти не представляють ніякого інтересу: тільки в процесі взаємодії об'єктів реалізується система. Замість процесора, що перемелює структури даних, ми одержуємо співтовариство добре вихованих об'єктів, які чемно просять один одного про послуги. Літак, за визначенням - сукупність елементів, кожний з яких за своєю природою прагне впасти на землю, але за рахунок спільних безперервних зусиль він перемагає цю тенденцію. Він летить тільки завдяки погодженим зусиллям своїх компонентів. Відношення між будь-якими двома об'єктами ґрунтуються на припущеннях, якими один володіє відносно іншого: про операції, які можна виконувати, і про очікувану поведінку. Особливий інтерес для об’єкто-орієнтованого аналізу й проектування представляють два типи ієрархічних співвідношень об'єктів: · зв'язкок; · агрегація. Ці два типи відношень називаються відношеннями старшинства й "батько/нащадок" відповідно. 15.2.2. Зв'язки Семантика. Зв'язок - фізичне або концептуальне з'єднання між об'єктами. Об'єкт співпрацює з іншими об'єктами через зв'язки, що з'єднують його з ними. Інакше кажучи, зв'язок - це специфічне співставлення, через яке клієнт запитує послугу в об'єкта-сервера або через яке один об'єкт знаходить шлях до іншого. На рис. 15.2 показано кілька різних зв'язків. Вони відзначені лініями й позначають шляхи проходження повідомлень. Самі повідомлення показані стрілками (відповідно їхньому напрямку) і позначені іменем операції. На рисунку об'єкт aController зв'язаний із двома об'єктами класу DisplayItem (об'єкти a і b). У свою чергу, обидвоє, імовірно, пов'язані з aView, але нам цікавитиме тільки один із цих зв'язків. Тільки вздовж зв'язку один об'єкт може послати повідомлення іншому. Зв'язок між об'єктами й передача повідомлень звичайно одностороння (як на рисунку; хоча технічно вона може бути й взаємною). Подібний поділ прав і обов'язків типовий для добре структурованих об'єктних систем. Зауважте також, що хоча передане повідомлення ініціалізоване клієнтом (у цьому випадку aController), дані передаються в обох напрямках. Наприклад, коли aController викликає операцію move для пересилання даних об'єктові а, дані передаються від клієнта серверу, однак під час виконання операції isUnder над об'єктом b, результат передається від сервера клієнтові. Беручи участь у зв'язку, об'єкт може виконувати одну з таких трьох ролей:
Рис. 15.2. Зв'язки. На рис. 15.2 об'єкт aController виступає як актор, об'єкт a - як агент і об'єкт aView - як сервер. Приклад. У багатьох промислових процесах потрібна безперервна зміна температури. Необхідно підняти температуру до заданого значення, втримати її деякий заданий час і понизити до норми. Профіль зміни температури в різних процесах різний; дзеркало телескопа треба прохолоджувати дуже повільно, а сталь, що загартовується, дуже швидко. Абстракція нагрівання має досить чітку поведінку, що дає нам право на опис такого класу. Спочатку визначимо тип, значення якого задає минулий час у хвилинах. // Число хвилин, що минули typedef unsigned int Minute; Тепер опишемо сам клас TemperatureRamp, що за змістом задає функцію часу від температури: class TemperatureRamp { public: TemperatureRamp(); virtual ~TemperatureRamp(); virtual void clear(); virtual void bind (Temperature, Minute); Temperature TemperatureAt (Minute); protected: ... }; Витримуючи наш стиль, ми описали деякі з операцій віртуальними, тому що очікуємо, що цей клас буде мати підкласи. Насправді в змісті поведінки нам треба щось більше, ніж просто залежність температури від часу. Нехай, наприклад, відомо, що на 60-й хвилині повинна бути досягнута температура 80С, а на 180-й – 50С. Запитується, якою вона повинна бути на 120-й хвилині? Це вимагає лінійної інтерполяції, так що необхідна від абстракції поведінка ускладнюється. Разом з тим, керування нагрівачем, що підтримує необхідний профіль, ми від цієї абстракції не вимагаємо. Ми розділяємо поняття, при яких необхідна поведінка досягається взаємодією трьох об'єктів: екземпляра TemperatureRamp, нагрівача й контролера. Клас TemperatureController можна визначити так: class TemperatureController { public: TemperatureController(Location); ~TemperatureController(); void process(const TemperatureRamp&); Minute schedule(const TemperatureRamp&) const; private: ... }; Тип Location був визначений у розділі 14. Зауважте, що ми не очікуємо успадкування від цього класу й тому не повідомляємо в ньому ніяких віртуальних функцій. Операція process забезпечує основну поведінку цієї абстракції; її призначення - передати графік зміни температури нагрівачу, встановленому в конкретному місці. Наприклад, оголосимо: TemperatureRamp growingRamp; TemperatureController rampController(7); Тепер задамо пару точок і завантажимо план у контролер: growingRamp.bind (250, 60); growingRamp.bind(150, 180); rampController.process(growingRamp); У цьому прикладі rampController - агент, який відповідає за виконання температурного плану, він використовує об'єкт growingRamp як сервер. Цей зв'язок проявляється хоча б в тому, що rampController явно одержує growingRamp як параметр однієї зі своїх операцій. Одне зауваження із приводу нашого стилю. На перший погляд може здатися, що наші абстракції - лише об'єктні оболонки для елементів, отриманих у результаті звичайної функціональної декомпозиції. Приклад операції schedule показує, що це не так. Об'єкти класу TemperatureController мають достатньо інтелекту, щоб визначати розклад для конкретних профілів, і ми включаємо цю операцію як додаткову поведінку нашої абстракції. У деяких енергоємних технологіях (наприклад, плавка металів) можна істотно виграти, якщо враховувати охолодження установки й тепло, що залишається після попередньої плавки. Оскільки існує операція schedule, клієнт може запросити об'єкт TemperatureController, щоб той рекомендував оптимальний момент запуску наступного нагрівання. Видимість. Нехай є два об'єкти А і В і зв'язок між ними. Щоб А міг послати повідомлення В, треба, щоб А у якомусь змісті бачив В. Ми можемо не піклуватися про це на стадії аналізу, але коли справа доходить до реалізації системи, ми повинні забезпечити видимість зв'язаних об'єктів. У попередньому прикладі об'єкт rampController бачить об'єкт growingRamp, оскільки обоє вони оголошені в одній області видимості й тому, що growingRamp передається об'єкту rampController як параметр. У принципі є наступні чотири способи забезпечення видимості. ¨ Сервер глобальний стосовно клієнта. ¨ Сервер (або показник на нього) переданий клієнтові як параметр операції. ¨ Сервер є частиною клієнта. ¨ Сервер локально породжується клієнтом під час виконання якої-небудь операції. Який саме із цих способів вибрати - залежить від тактики проектування. Синхронізація. Коли один об'єкт посилає через зв'язок повідомлення іншому, пов'язаному з ним об’єкту, то кажуть, що вони синхронізуються. У багатопотоковій системі об'єкти вимагають витонченішої схеми передачі повідомлень, щоб розв'язати проблеми взаємного виключення, які типові для паралельних систем. Активні об'єкти самі по собі виконуються як потоки, тому присутність інших активних об'єктів на них звичайно не впливає. Якщо ж активний об'єкт має зв'язок з пасивним, можливі наступні три підходи до синхронізації: ¨ Послідовний - семантика пасивного об'єкта забезпечується в присутності тільки одного активного процесу. ¨ Захищений - семантика пасивного об'єкта забезпечується в присутності багатьох потоків керування, але активні клієнти повинні домовитися й забезпечити взаємне виключення. ¨ Синхронний - семантика пасивного об'єкта забезпечується в присутності багатьох потоків керування; взаємне виключення забезпечує сервер. Всі об'єкти, описані в цьому розділі, були послідовними. 15.2.3. Агрегація Семантика. У той час, як зв'язки позначають рівноправні або "клієнт-серверні" відношення між об'єктами, агрегація описує відношення цілого й частини, що приводить до відповідної ієрархії об'єктів, причому, ідучи від цілого (агрегату), ми можемо прийти до його частин (атрибутів). У цьому змісті агрегація - спеціалізований окремий випадок асоціації. На рис. 15.3 об'єкт rampController має зв'язок з об'єктом growingRamp і атрибут h класу Heater (нагрівач). У цьому випадку rampController - ціле, a h - його частина. Інакше кажучи, h - частина стану rampController. Виходячи з rampController, можна знайти відповідний нагрівач. Однак через h не можна знайти утримуючий його об'єкт (його також називають контейнером), якщо тільки відомості про нього не є випадково частиною стану h. Рис. 15.3. Агрегація. Агрегація може означати фізичне входження одного об'єкта в інший, але не обов'язково. Літак складається із крил, двигунів, шасі й інших частин. З іншої сторони, відношення акціонера до його акціями - це агрегація, що не передбачає фізичного включення. Акціонер монопольно володіє своїми акціями, але вони в нього не входять фізично. Це, безсумнівно, відношення агрегації, але скоріше концептуальне, ніж фізичне за своєю природою. Вибираючи одне із двох - зв'язок або агрегацію - треба мати на увазі таке. Агрегація іноді переконливіша, оскільки дозволяє сховати частини в цілому. Іноді навпаки краще використовувати зв'язок, оскільки він слабший й менше обмежений. Приймаючи рішення, треба зважити все. Об'єкт, що є атрибутом іншого об'єкта (агрегату), має зв'язок зі своїм агрегатом. Через цей зв'язок агрегат може посилати йому повідомлення. Приклад 15.1. Додамо в специфікацію класу TemperatureController опис нагрівача: Heater h; Після цього кожний об'єкт TemperatureController буде мати свій нагрівач. Відповідно до нашого визначення класу Heater у попередній главі ми повинні включити нагрівач при створенні нового контролера, тому що сам цей клас не передбачає конструктора за замовчуванням. Ми могли б визначити конструктор класу TemperatureController у такий спосіб: TemperatureController::TemperatureController(Location 1) : h(1) {} 15.3. Природа класів 15.3.1. Що таке клас? Поняття класу й об'єкта настільки тісно зв'язані, що неможливо говорити про об'єкт безвідносно до його класу. Однак існує важлива відмінність цих двох понять. У той час як об'єкт позначає конкретну сутність, існуючу в часі й у просторі, клас визначає лише абстракцію істотного в об'єкті. Таким чином, можна говорити про клас "Ссавці", що включає характеристики, загальні для всіх ссавців. Для вказівки на конкретного представника ссавців необхідно сказати "це - ссавець". У загально-зрозумілих термінах можна дати наступне визначення класу: група, множина або вид із загальними властивостями або загальною властивістю, різновидами, які відрізняються від всього решта якістю, можливостями або умовами. У контексті об’єктно-орієнтованого аналізу дамо таке визначення класу: Клас - це якась множина об'єктів, що мають спільну структуру й спільну поведінку. Будь-який конкретний об'єкт є просто екземпляром класу. Що ж не є класом? Об'єкт не є класом, хоча надалі ми побачимо, що клас може бути об'єктом. Об'єкти, які не зв'язані спільною структурою й поведінкою, не можна об'єднати в клас, тому що за означенням вони не зв'язані між собою нічим, крім того, що всі вони об'єкти. Важливо відзначити, що класи, як їх розуміють у більшості існуючих мовах програмування, необхідні, але не достатні для декомпозиції складних систем. Деякі абстракції настільки складні, що не можуть бути виражені в термінах простого опису класу. Наприклад, на досить високому рівні абстракції графічний інтерфейс користувача, база даних або система обліку як ціле, це явні об'єкти, але не класи. Краще вважати їх якимись сукупностями (кластерами) класів, що співпрацюють. Такі кластери називають компонентами. 15.3.2. Інтерфейс і реалізація Складні задачі треба розділити на багато маленьких й передоручити їх дрібним субпідрядникам. Ніде ця ідея не проявляє себе так яскраво, як у проектуванні класів. За своєю природою, клас - це генеральний контракт між абстракцією й всіма її клієнтами. Виразником зобов'язань класу служить його інтерфейс, причому в мовах із сильною типізацією потенційні порушення контракту можна виявити вже на стадії компіляції. Ідея контрактного програмування приводить нас до розмежування зовнішнього вигляду, тобто інтерфейсу, і внутрішньої будови класу, реалізації. Головне в інтерфейсі - оголошення операцій, які підтримуються екземплярами класу. До нього можна додати оголошення інших класів, змінних, констант і виняткових ситуацій, що уточнюють абстракцію, яка виражається в класі. Навпаки, реалізація класу нікому, крім нього самого, не цікава. Реалізація складається у визначенні операцій, оголошених в інтерфейсі класу. Ми можемо розділити інтерфейс класу на три частини: ¨ відкриту (public) - видиму всім клієнтам; ¨ захищену (protected) - видиму самому класу, його підкласам і друзям (friends); ¨ закриту (private) - видиму тільки самому класу і його друзям. Різні мови програмування передбачають різні комбінації цих частин. Розробник може задати права доступу до тієї або іншої частини класу, визначивши тим самим зону видимості клієнта. Зокрема, в C++ всі три перерахованих рівні доступу визначаються явно. На додаток до цього є ще й механізм друзів, за допомогою якого стороннім класам можна надати привілей бачити закриту й захищену область класу. Тим самим порушується інкапсуляція, тому, як і в житті, друзів треба вибирати обережно. В Ada оголошення можуть бути зроблені закритими або відкритими. В Smalltalk всі змінні - закриті, а методи – відкриті. В CLOS узагальнені функції відкриті, а слоти можуть бути закритими, хоча довідатися їх значення однаково можна. Стан об'єкта задається в його класі через визначення констант або змінних, що поміщаються в його захищеній або закритій частині. Тим самим вони інкапсульовані, і їхньої зміни не впливають на клієнтів. Уважний читач може запитати, чому ж подання об'єкта визначається в інтерфейсній частині класу, а не в його реалізації. Причини практичні: в іншому випадку необхідно було б об’єктно-орієнтовані процесори або дуже хитромудрі компілятори. Коли компілятор опрацьовує оголошення об'єкта, наприклад, таке: DisplayItem item1; він повинен знати, скільки відвести під нього пам'яті. Якби ця інформація зберігалася в реалізації класу, нам довелося б написати її повністю, перш, ніж ми змогли б задіяти його клієнтів. Тобто, весь смисл відділення інтерфейсу від реалізації був би загублений. Константи й змінні, які складають подання класу, відомі під різними іменами. В Smalltalk їх називають змінні екземпляра, в Object Pascal - поля, в C++ - члени класу, а в CLOS - слоти. Ми часто будемо використовувати ці терміни як синоніми. 15.3.3. Життєвий цикл класу У поведінці простих класів можна розібратися, вивчаючи операції їх відкритої частини. Однак поведінка цікавіших класів (таких як переміщення об'єкта класу DisplayItem або складання розкладу для екземпляра класу TemperatureController) включає взаємодію різних операцій, що входять у клас. Як вже наголошувалося вище, об'єкти таких класів діють як маленькі машини, частини яких взаємодіють одна з одною. Оскільки всі такі об'єкти мають однакову поведінку, то можна використовувати клас для опису їх загальної семантики, впорядкованої за часом і подіями. Ми можемо описувати динаміку поведінки об'єктів, використовуючи модель кінцевого автомата. 15.4. Відношення між класами 15.4.1. Типи відношень Розглянемо подібності й відмінності між такими класами: квіти, маргаритки, червоні троянди, жовті троянди, пелюстки й сонечка. Відмітимо, що: ¨ Маргаритка - квітка. ¨ Троянда - (інша) квітка. ¨ Червона й жовта троянди - троянди. ¨ Пелюсток є частиною обох видів квітів. ¨ Сонечка харчуються шкідниками, що вражають деякі квіти. Із цього простого прикладу випливає, що класи, як і об'єкти, не існують окремо. У кожній проблемній області ключові абстракції взаємодіють багатьма способами, які ми повинні відобразити у в проекті. Відношення між класами можуть означати одне із двох. По-перше, в них може бути щось загальне. Наприклад, і маргаритки, і троянди - це різновиди квітів: і ті, і інші мають яскраво пофарбовані пелюстки, видихають аромат і так далі. По-друге, може бути якийсь семантичний зв'язок. Наприклад, червоні троянди більше схожі на жовті троянди, ніж на маргаритки. Але між трояндами й маргаритками більше спільного, ніж між квітами й пелюстками. Також існує сібіотичний зв'язок між квітами й сонечками: сонечка захищають квіти від шкідників, які, у свою чергу, служать їжею сонечкам. Відомі три основних типи відношень між класами. По-перше, це відношення "узагальнення/спеціалізація" (загальне й часткове), відоме як "is-a". Троянди є спеціалізованим частковим випадком квітів, тобто підкласом загальнішого класу "квіти". По-друге це відношення "ціле/ частина", відоме як "part of". Так, пелюстки є частиною квітів. По-третє, це семантичні, смислові відношення, асоціації. Наприклад, сонечка асоціюються із квітами - хоча, здавалося б, що в них спільного? Або ось: троянди й свічки - і те, і інше можна використовувати для прикраси столу. Мови програмування виробили кілька загальних підходів до вираження відношень цих трьох типів. Зокрема, більшість об’єктно-орієнтованих мов безпосередньо підтримують різні комбінації таких видів відношень: ¨ асоціація, ¨ успадкування, ¨ агрегація, ¨ використання, ¨ інсталювання, ¨ метаклас. Альтернативою успадкуванню є делегування, при цьому об'єкти розглядаються як прототипи, які делегують свою поведінку родинним їм об'єктам. Таким чином, класи стають не потрібними. Із шести перерахованих видів відношень найзагальнішою є асоціація. Зазвичай аналітик констатує наявність асоціації й, поступово уточнюючи проект, перетворює її в якийсь спеціалізованіший зв'язок. Успадкування, ймовірно, варто вважати семантично найцікавішим. Воно виражає відношення загального й частини. Однак одного успадкування недостатньо, щоб виразити всі різноманіття явищ і відношень життя. Корисна також агрегація, яка задає відношення цілого й частини між екземплярами класів. Не зайво додати відношення використання, що означає наявність зв'язку між екземплярами класів. "Метакласові" відношення - це щось зовсім інше, в явному вигляді воно зустрічається тільки в мовах Smalltalk і CLOS. Метаклас - це клас класів, що дозволяє нам трактувати класи як об'єкти. 15.4.2. Асоціація Приклад 15.2. Бажаючи автоматизувати роздрібну торговельну мережу, ми виявляємо дві абстракції - товари й продажі. На рис. 15.4 показана асоціація, яку ми при цьому вбачаємо. Клас Product - це те, що ми продали згідно деякої угоди, а клас Sale - сама угода, в якій продано кілька товарів. Асоціація працює в обидві сторони: знаючи товар, можна вийти на угоду, в якій він був проданий, а маючи угоду, знайти, що було згідно неї продано. Рис. 15.4. Асоціація. В C++ це можна виразити за допомогою прихованих показників. Ось два коди оголошення відповідних класів: class Product; class Sale; class Product { public: ... protected: Sale* lastSale; }; class Sale { public: ... protected: Product** productSold; }; Це асоціація вигляду "один-до-багатьох": кожний екземпляр товару відноситься тільки до однієї останньої продажі, в той час як кожний екземпляр Sale може вказувати на сукупність проданих товарів. Семантичні залежності. Як видно з цього прикладу, асоціація - смисловий зв'язок. За замовчуванням, вона не має напрямків (якщо не зазначено інше, асоціація, як у цьому прикладі, розуміє двосторонній зв'язок) і не пояснює, як класи спілкуються один з одним (ми можемо тільки відзначити семантичну залежність, вказавши, які ролі класи відіграють). Однак саме це нам потрібно на ранній стадії аналізу. Отже, ми фіксуємо учасників, їх ролі й (як буде сказано далі) потужність відношень. Потужність. У попередньому прикладі ми мали асоціацію "один до багатьох". Тим самим ми позначили її потужність (тобто, грубо говорячи, кількість учасників). На практиці важливо розрізняти три випадки потужності асоціації: ¨ "один-до-одного" ¨ "один-до-багатьох" ¨ "багато-до-багатьох". Відношення "один-до-одного" позначає дуже вузьку асоціацію. Наприклад, у роздрібній системі продажів прикладом міг би бути зв'язок між класом Sale і класом CreditCardTransaction: кожний продаж відповідає рівно одному зняттю грошей з цієї кредитної картки. Відношення "багато-до-багатьох" теж рідкісні. Наприклад, кожний об'єкт класу Customer (покупець) може ініціювати транзакцію з декількома об'єктами класу Saleperson (торговельний агент), і кожний торговельний агент може взаємодіяти з кількома покупцями. Всі три види потужності мають різного роду варіації. 15.4.3. Успадкування Приклади. Космічні зонди, що перебувають у польоті, відсилають на наземні станції інформацію про стан своїх основних систем (наприклад, джерел енергопостачання й двигунів) і вимір датчиків (таких як датчики радіації, мас-спектрометри, телекамери, фіксатори зіткнень із мікрометеоритами й т.д.). Вся сукупність переданої інформації називається телеметричними даними. Як правило, вони передаються у вигляді потоку даних, що складає із заголовка (який включає тимчасові мітки й ключі для ідентифікації наступних даних) і декількох пакетів даних від підсистем і датчиків. Все це виглядає як простий набір різнотипних даних, тому для опису кожного типу даних телеметрії напрошуються структури: class Time... struct ElectricalData { Time timeStamp; int id; float fuelCell1Voltage, fuelCell2Voltage; float fuelCell1Amperes, fuelCell2Amperes; float currentPower; }; Однак такий опис має ряд недоліків. По-перше, структура класу ElectricalData не захищена, тобто клієнт може викликати зміну такої важливої інформації, як timeStamp або currentPower (потужність, що розвивається обома електробатареями, яку можна обчислити зі струму й напруги). По-друге, ця структура є повністю відкритою, тобто її модифікація (додавання нових елементів у структуру або зміна типу існуючих елементів) впливають на клієнтів. Як мінімум, доводиться заново компілювати всі описи, зв'язані яким-небудь чином із цією структурою. Ще важливіше, що внесення в структуру змін може порушити логіку відношень із клієнтами, а отже, логіку всієї програми. Крім того, наведений опис структури дуже складий для сприйняття. Стосовно такої структури можна виконати множину різних дій (пересилання даних, обчислення контрольної суми для визначення помилок і т.д.), але всі вони не будуть пов'язані з наведеною структурою логічно. Нарешті, припустимо, що аналіз вимог до системи обумовив наявність декількох сотень різновидів телеметричних даних, що включають показану вище структуру й інші електричні параметри в різних контрольних точках системи. Очевидно, що опис такої кількості додаткових структур буде надлишковим як через повторюваність структур, так і через наявність загальних функцій опрацювання. Краще було б створити для кожного виду телеметричних даних окремий клас, що дозволить захистити дані в кожному класі й погодити їх з виконуваними операціями. Але цей підхід не вирішує проблему надмірності. Значно краще побудувати ієрархію класів, в якій від загальних класів за допомогою успадкування утворяться спеціалізованіші класи; наприклад, у такий спосіб: class TelemetryData { public: TelemetryData(); virtual ~TelemetryData(); virtual void transmit(); Time currentTime() const; protected: int id; Time timeStamp; }; У цьому прикладі введений клас, який має конструктор, деструктор (який нащадки можуть перевизначити) і функції transmit і currentTime, які видні всім клієнтів. Захищені елементи id і timeStamp трохи краще інкапсульовані - вони доступні тільки класу і його підкласам. Зауважте, що функція currentTime є відкритою, завдяки чому значення timeStamp можна читати (але не змінювати). Тепер розглянемо ElectricalData: class ElectricalData: public TelemetryData { public: ElectricalData(float v1, float v2, float a1, float a2); virtual ~ElectricalData(); virtual void.transmit(); float currentPower() const; protected: float fuelCell1Voltage, fuelCell2Voltage; float fuelCell1Amperes, fuelCell2Amperes; }; Цей клас - спадкоємець класу TelemetryData, але вихідна структура доповнена (чотирма новими елементами), а поведінка - перевизначена (змінена функція transmit). Крім того, додана функція currentPower. Одиночне успадкування. Успадкування - це таке відношення між класами, коли один клас повторює структуру й поведінку іншого класу (одиночне успадкування) або інших (множинне успадкування) класів. Клас, структура й поведінка якого успадковуються, називається суперкласом. Так, TelemetryData є суперкласом для ElectricalData. Похідний від суперкласу клас називається підкласом. Це означає, що успадкування встановлює між класами ієрархію загального й часткового. У цьому змісті ElectricalData є спеціалізованішим класом загальнішого класу TelemetryData. Ми вже бачили, що в підкласі структура й поведінка вихідного суперкласу доповнюються й перевизначаються. Наявність механізму успадкування відрізняє об’єктно-орієнтовані мови від об'єктних. Підклас розширює або обмежує існуючу структуру й поведінку свого суперкласу. Наприклад, підклас GuardedQueue може додавати до поведінки суперкласу Queue операції, які захищають стан черги від одночасної зміни декількома незалежними потоками. Зворотний приклад: підклас UnselectableDisplayItem може обмежити поведінку свого суперкласу DisplayItem, заборонивши виділення об'єкта на екрані. Часто підкласи роблять і те, і інше. Відношення одиночного успадкування від суперкласу TelemetryData показані на рис. 15.5. Стрілки позначають відношення загального до часткового. Зокрема, Cameradata - це різновид класу SensorData, який у свою чергу є різновидом класу TelemetryData. Такий же тип ієрархії характерний для семантичних мереж, які часто використовуються фахівцями з розпізнавання образів і штучного інтелекту для організації баз знань. У розділі 33 ми покажемо, що правильна організація ієрархії абстракцій - це питання логічної класифікації. Рис. 15.5. Одиночне спадкування. Для деяких класів на рис. 15.5 можна створити екземпляри, а для інших - ні. Найімовірніше утворення об'єктів самих спеціалізованих класів ElectricalData і SpectrometerData (такі класи називають конкретними класами, або листками ієрархічного дерева). Утворення об'єктів із класів, що займають проміжне положення (SensorData або TelemetryData), менш імовірно. Класи, екземпляри яких не створюються, називаються абстрактними. Підкласи абстрактних класів довизначають їх до життєздатної абстракції, наповнюючи клас змістом. У мові Smalltalk розробник може змусити підклас перевизначити метод, поміщаючи в реалізацію методу суперкласу виклик методу SubclassResponsibility. Якщо метод не перевизначений, то при спробі його виконання генерується помилка. Аналогічно, в C++ існує можливість повідомляти функції віртуальними. Якщо вони не перевизначені, екземпляр такого класу неможливо створити. Самий загальний клас в ієрархії класів називається базовим. У більшості інформаційних систем базових класів буває декілька, і всі вони відбивають найзагальніші абстракції предметної області. Насправді, особливо в C++, добре зроблена структура класів - це скоріше ліс із дерев успадкування, ніж одна багатоповерхова структура успадкування з одним коренем. Але в деяких мовах програмування є базовий клас самого верхнього рівня, що є єдиним суперкласом для всіх інших класів. У мові Smalltalk цю роль грає клас object. Клас зазвичай має два види клієнтів: ¨ екземпляри; ¨ підкласи. Корисно мати для них різні інтерфейси. Зокрема, ми хочемо показати тільки зовні видиму поведінку для клієнтів-екземплярів, але нам потрібно відкрити службові функції й подання клієнтам-підкласам. Цим пояснюється наявність відкритої, захищеної і закритої частин опису класів в мові C++: розробник може чітко розділити, які елементи класу доступні для екземплярів, а які для підкласів. У мові Smalltalk ступінь такого поділу менша: дані видимі для підкласів, але не для екземплярів, а методи загальнодоступні (можна запровадити закриті методи, але мова не забезпечує їхній захист). Є серйозні протиріччя між потребами успадкування й інкапсуляції. Значною мірою успадкування відкриває клас, що успадковує, деякі секрети. На практиці, щоб зрозуміти, як працює якийсь клас, часто треба вивчити всі його суперкласи в їх внутрішніх деталях. Успадкування розуміє, що підкласи повторюють структури їх суперкласів. У попередньому прикладі екземпляри класу ElectricalData містять елементи структури суперкласу (id і timeStamp) і спеціалізованіші елементи (fuelCell1Voltage, fuelCell2Voltage, fuelCell1Amperes, fuelCell2Amperes). Поведінка суперкласів також успадковується. Стосовно об'єктів класу ElectricalData можна використовувати операції currentTime (успадкована від суперкласу), currentPower (визначена в класі) і transmit (перевизначена в підкласі). У більшості мов допускається не тільки успадковувати методи суперкласу, але також додавати нові і перевизначати існуючі методи. В Smalltalk будь-який метод суперкласу можна перевизначити в підкласі. В C++ ступінь контролю за цим трохи вища. Функція, оголошена віртуальною (функція transmit у попередньому прикладі), може бути в підкласі перевизначена, а інші (currentTime) - ні. Одиночний поліморфізм. Нехай функція transmit класу TelemetryData реалізована в такий спосіб: void TelemetryData::transmit() { // передати id // передати timeStamp }; У класі ElectricalData та ж функція перевизначена: void ElectricalData::transmit() { TelemetryData::transmit(); // передати напругу // передати силу струму }; Ця функція спочатку викликає однойменну функцію суперкласу за допомогою її явно кваліфікованого імені TelemetryData::transmit(). Та передасть заголовок пакету (id і timeStamp), після чого в підкласі передаються його власні дані. Визначимо тепер екземпляри двох описаних вище класів: TelemetryData telemetry; ElectricalData electrical(5.0, -5.0, 3.0, 7.0); Тепер визначимо вільну процедуру: void transmitFreshData (TelemetryData& d, const Time& t) { if (d.currentTime() >= t) d.transmit(); ); Що відбудеться, якщо виконати такі два оператори? transmitFreshData(telemetry, Time(60)); transmitFreshData(electrical, Time(120)); У першому операторі буде передано вже відомий нам заголовок. У другому буде переданий він же, плюс чотири числа у форматі із плаваючою крапкою, що містять результати вимірів електричних параметрів. Чому це так? Ніби то функція transmitFreshData нічого не знає про клас об’єкта, вона просто виконує d.transmit()! Це був приклад поліморфізму. Змінна d може позначати об’єкти різних класів. Ці класи мають спільний суперклас і вони, хоча й по різному, можуть реагувати на одне і те ж повідомлення, однаково розуміючи його зміст. Традиційні типізовані мови типу Pascal засновані на тій ідеї, що функції й процедури, а отже, і операнди повинні мати певний тип. Ця властивість називається мономорфізмом, тобто кожна змінна й кожне значення ставляться до одного певного типу. На противагу мономорфізму поліморфізм допускає віднесення значень і змінних до декількох типів. Вперше ідею поліморфізму ad hoc описав Страчі, маючи на увазі можливість перевизначати зміст символів, таких, як "+", згідно потреби. У сучасному програмуванні ми називаємо це перевантаженням. Наприклад, в C++ можна визначити кілька функцій з тим самим іменем, і вони будуть автоматично відрізнятися за кількістю й типами своїх аргументів. Сукупність цих ознак називається сигнатурою функції; у мові Ada до цього списку додається тип значення, що повертається. Страчі говорив також про параметричний поліморфізм, який ми зараз називаємо просто поліморфізмом. При відсутності поліморфізму код програми змушений містити багато операторів вибору case або switch. Наприклад, мовою Pascal неможливо утворити ієрархію класів телеметричних даних; замість цього прийдеться визначити один великий запис із варіантами, який включатиме всі різновиди даних. Для вибору варіанту потрібно перевірити мітку, що визначає тип запису. Мовою Pascal процедура TransmitFreshData може бути написана в таким чином: const Electrical = 1; Propulsion = 2; Spectrometer = 3; Procedure Transmit_Presh_Data(TheData: Data; The_Time: Time); begin if (The_Data.Current_Time >= The_Time) then case TheData.Kind of Electrical: Transmit_Electrical_Data(The_Data); Propulsion: Transmit_Propulsion_Data(The_Data); end; end; Щоб ввести новий тип телеметричних даних, потрібно модифікувати цей варіантний запис, додавши новий тип у кожний оператор case. У такій ситуації збільшується ймовірність помилок, і проект стає нестабільним. Успадкування дозволяє розрізняти різновид абстракцій, і монолітні типи стають не потрібні. Поліморфізм найдоцільніший у тих випадках, коли кілька класів мають однакові протоколи. Поліморфізм дозволяє обійтися без операторів вибору, оскільки об'єкти самі знають свій тип. Успадкування без поліморфізму можливе, але не дуже корисне. Це видно на прикладі Ada, де можна повідомляти похідні типи, але через мономорфізм мови операції жорстко задаються на стадії компіляції. Поліморфізм тісно пов'язаний з механізмом пізнього зв'язування. При поліморфізмі зв'язок методу й імені визначається тільки в процесі виконання програм. В C++ програміст має можливість вибирати між раннім і пізнім зв'язуванням ім'я з операцією. Якщо функція віртуальна, зв'язування буде пізнім і, отже, функція поліморфна. Якщо ні, то зв'язування відбувається при компіляції й нічого змінити потім не можна. Спадкування й типізація. Розглянемо ще раз перевизначення функції transmit: void ElectricalData::transmit() { TelemetryData::transmit(); // передати напруга // передати силу струму }; У більшості об’єктно-орієнтованих мовах програмування при реалізації методу підкласу дозволяється викликати прямо метод якого-небудь суперкласу. Як видно із прикладу, це допускається й у тому випадку, якщо метод підкласу має таке ж ім'я й фактично перевизначає метод суперкласу. В Smalltalk метод вищестоящого класу викликають за допомогою ключового слова super, при цьому можна вказувати на самого себе за допомогою ключового слова self. В C++ метод будь-якого досяжного вищестоящого класу можна викликати, додаючи ім'я класу як префікс, формуючи кваліфіковане ім'я методу (як TelemetryData::transmit() у нашому прикладі). Об'єкт може посилатися на себе за допомогою визначеного показника this. На практиці метод суперкласу викликається до або після додаткових дій. Метод підкласу уточнює або доповнює поведінку суперкласу. Всі підкласи на рис. 15.5 є також підтипами вищестоящого класу. Зокрема, ElectricalData є підтипом TelemetryData. Система типів, що будується паралельно успадкуванню, звичайна для об’єктно-орієнтованих мов із сильною типізацією, включаючи C++. Для Smalltalk, яка не є типізованою, типи не мають значення. Розглянемо приклад на C++: TelenetryData telemetry; ElectrycalData electrical(5.0, -5.0, 3.0, 7.0); Наступний оператор присвоювання вірний: telemetry = electrical; //electrical - це підтип telemetry Хоча він формально правильний, він небезпечний: будь-які доповнення в стані підкласу в порівнянні зі станом суперкласу просто пропадуть. Таким чином, додаткові чотири параметри, визначені в підкласі electrical, будуть загублені під час копіювання, оскільки їх просто нікуди записати в об'єкті telemetry клacу TelemetryData. Наступний оператор неправильний: electrical = telemetry; //неправильно: telemetry - це не підтип electrical Можна зробити висновок, що присвоєння об'єкту y значення об'єкта x припустиме, якщо тип об'єкта x збігається з типом об'єкта y або є його підтипом. У більшості строго типізованих мовах програмування допускається перетворення значень із одного типу в інший, але тільки в тих випадках, коли між двома типами існує відношення клас/підклас. У мові C++ є оператор явного перетворення, який називається приведенням типів. Як правило, такі перетворення використовуються стосовно об'єкту спеціалізованого класу, щоб привласнити його значення об'єкту загальнішого класу. Таке приведення типів вважається безпечним, оскільки під час компіляції здійснюється семантичний контроль. Іноді необхідні операції приведення об'єктів загальнішого класу до спеціалізованих класів. Ці операції не є надійними з погляду строгої типізації, тому що під час виконання програми може виникнути невідповідність (несумісність) об'єкта, що приводиться, з новим типом. Однак такі перетворення досить часто використовуються в тих випадках, коли програміст добре уявляє собі всі типи об'єктів. Наприклад, якщо немає параметризованих типів, часто створюються класи set або bag, що представляють собою набори довільних об'єктів. Їх визначають для деякого базового класу (це набагато безпечніше, ніж використовувати ідіому void*, як ми робили, визначаючи клас Queue). Ітераційні операції, визначені для такого класу, вміють повертати тільки об'єкти цього базового класу. Всередині конкретної програми розробник може використовувати цей клас, створюючи об'єкти тільки якогось спеціалізованого підкласу, і, знаючи, що саме він збирається поміщати в цей клас, може написати відповідний перетворювач. Але вся ця струнка конструкція звалиться під час виконання, якщо в наборі зустрінеться який-небудь об'єкт неочікованого типу. Більшість сильно типізованих мов дозволяють програмам оптимізувати техніку виклику методів, найчастіше зводячи пересилання повідомлення до простого виклику процедури. Якщо, як в C++, ієрархія типів збігається з ієрархією класів, така оптимізація очевидна. Але в неї є недоліки. Зміна структури або поведінки якого-небудь суперкласу може поставити поза законом його підкласи. Якщо правила утворення типів засновані на успадкуванні й ми переробляємо який-небудь клас так, що міняється його положення в ієрархії успадкування, клієнти цього класу можуть виявитися поза законом з погляду типів, незважаючи на те, що зовнішній інтерфейс класу залишається колишнім. Тим самим ми підходимо до фундаментальних питань успадкування. Як було сказано вище, успадкування використовується у зв'язку з тим, що в об'єктів є щось спільне або між ними є змістовна асоціація. Виражаючи ту ж думку іншими словами: успадкування можна розглядати, як спосіб керування повторним використанням програм, тобто, як просте рішення розробника про запозичення корисного коду. У цьому випадку механіка успадкування повинна бути гнучкою й легкою. Інша точка зору: успадкування відбиває принципову спорідненість абстракцій, яку неможливо скасувати. В Smalltalk і CLOS ці два аспекти нероздільні. C++ гнучкіший. Зокрема, при визначенні класу його суперклас можна оголосити public (як ElectricalData у нашому прикладі). У цьому випадку підклас вважається також і підтипом, тобто зобов'язується виконувати всі зобов'язання суперкласу, зокрема забезпечуючи сумісну із суперкласом підмножину інтерфейсу й володіючи нерозрізненим з погляду клієнтів суперкласу поведінкою. Але якщо при визначенні класу оголосити його суперклас як private, це буде означати, що, успадковуючи структуру й поведінку суперкласу, підклас вже не буде його підтипом. Це означає, що відкриті й захищені члени суперкласу стануть закритими членами підкласу, і отже вони будуть недоступні підкласам нижчого рівня. Крім того, той факт, що підклас не буде підтипом, означає, що клас і суперклас мають несумісні (загалом кажучи) інтерфейси з погляду клієнта. Визначимо новий клас: class InternalElectricalData: private ElectricalData { public: InternalElectricalData(float v1, float v2, float a1, float a2); virtual ~InternalElectricalData(); ElectricalData::currentPower; }; Тут суперклас ElectricalData оголошений закритим. Отже, його методи, такі, наприклад, як transmit, недоступні клієнтам. Оскільки клас InternalElectricalData не є підтипом ElectricalData, ми вже не зможемо привласнювати екземпляри підкласу об'єктам суперкласу, як у випадку оголошення суперкласу в якості відкритого. Відзначимо, що функція currentPower є видимою за рахунок її явної кваліфікації. Інакше вона залишилася б закритою. Як можна було очікувати, правила C++ забороняють робити успадкований елемент у підкласі "відкритішим", ніж у суперкласі. Так, член timeStamp, оголошений у класі TelemetryData захищеним, не може бути зроблений у підкласі відкритим шляхом явного згадування (як це було зроблено для функції currentpower). У мові Ada для досягнення аналогічного ефекту замість підтипів використовується механізм похідних типів. Визначення підтипу означає не появу нового типу, а лише обмеження існуючого. А ось визначення похідного типу самостійно створює новий тип, що має структуру, запозичену у вихідного типу. У наступному розділі ми покажемо, що успадкування з метою повторного використання й агрегації певною мірою суперечать один одному. Поиск по сайту: |
Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Студалл.Орг (0.083 сек.) |