Сказати правду, спочатку я цієї статті не планував. Я вважав питання, які хочу тут обговорити, тривіальними, не вартими навіть згадки. Однак у процесі написання статей для цього сайту я підняв в одному з форумів обговорення множинного успадкування. Внаслідок чого з'ясувалося, що більшість розробників має досить невиразне уявлення про успадкування. І, відповідно, припускається дуже багато помилок. Оскільки спадкування є однією з найважливіших рис ООП (якщо не найважливішою!) – я вирішив присвятити цьому явищу окрему статтю. * * * Спочатку я хочу розмежувати два поняття – об'єкт та клас. Ці поняття постійно плутають. Тим часом вони є центральними в ООП. І знати різницю між ними, на мій погляд, необхідно. Отже, об'єкт. По суті, це будь-що. Ось кубик лежить. Дерев'яний синій. Довжина ребра 5 см. Це об'єкт. А он пірамідка. Пластмасова, червона. 10 см ребро. Це також об'єкт. Що з-поміж них спільного? Різні розміри. Різна форма. Різний матеріал. Проте спільне у них є. Насамперед, і кубик, і пірамідка – правильні багатогранники. Тобто. сума кількості вершин та кількості граней на 2 більше за кількість ребер. Далі. В обох фігур є грані, ребра та вершини. Обидва фігури мають таку характеристику, як розмір ребра. Обидві фігури можна крутити. Обидві фігури можна малювати. Два останні властивості – це поведінка. Ну і таке інше. Практика програмування показує, що з однорідними об'єктами оперувати значно простіше, ніж з різнорідними. А оскільки між цими фігурами таки є щось спільне, виникає бажання це спільне якось виділити. Ось тут і випливає таке поняття, як клас. Отже, визначення.
Клас – це описувач загальних властивостей групи об'єктів. Цими властивостями може бути як характеристики об'єктів (розмір, вага, колір тощо), і поведінки, ролі тощо.
Зауваження. Слово " всіх " (описувач всіх властивостей) сказано був. Що означає, що будь-який об'єкт може належати до кількох різних класів.
Візьмемо за основу той самий приклад із геометричними фігурами. Найзагальніший опис -
правильний багатогранник . Безвідносно розміру ребра, кількості граней та вершин. Єдине, що ми знаємо – що ця фігура має вершини, ребра і грані, і що довжини ребер рівні.
Далі. Ми можемо конкретизувати опис. Допустимо, ми хочемо намалювати цей
багатогранник . Введемо таке поняття як
правильний багатогранник, що відмальовується . Що нам потрібне для малювання? Опис загального способу відтворення, який залежить від конкретних координат вершин. Можливо колір об'єкта. Тепер введемо класи
Куб і
Тетраедр . Об'єкти, що належать до цих класів, безумовно, є правильними багатогранниками. Єдина відмінність – числа вершин, ребер та граней вже жорстко фіксовані для кожного з нових класів. Далі, знаючи вигляд конкретної фігури, ми можемо дати опис способу відтворення. А значить, будь-який об'єкт класів
Куб або
Тетраедр також є і об'єктом класу
правильний багатогранник, що відмальовується . В наявності ієрархія класів. У цій ієрархії ми спускаємося від найзагальнішого опису до найбільш конкретизованого. Зауважте, що об'єкт будь-якого класу також підходить під опис будь-якого більш загального класу з ієрархії. Таке ставлення класів і називається
успадкуванням . Кожен дочірній клас успадковує всі властивості батьківського, більш загального, і (можливо) додає до цих властивостей якісь свої. Або перевизначає якісь властивості батьківського класу. Тут я хочу навести цитату із класичної вже книги Граді Буча з об'єктно-орієнтованого дизайну:
Уповноваження, там, defines an "is a" hierarchy among classes, в яких subclass inherits from 1 or more superclasses. Це fact in litmus test for inheritance. Given classes A і B, якщо A "не є" дитинство з B, то повинні бути subclass of B.
У перекладі це звучить так:
Спадкування, таким чином, визначає ієрархію "є" між класами, в якій підклас успадковує від одного або більше суперкласів. Це фактично визначальний тест (дослівно – лакмусовий тест, прим. моє) для успадкування. Якщо ми маємо класи А і В і якщо клас А "не є" різновидом класу В, то А не повинен бути підкласом В.
Ті, хто дочитав до цього місця, можливо, здивовано покрутять пальцем біля скроні. Перша думка – це тривіально! Так і є. Але якби ви знали, скільки божевільних ієрархій спадкування я бачив! У тій дискусії, про яку я згадав на самому початку, один із учасників серйозно успадкував танк від... кулемета!!! На тій простій підставі, що у танка Є кулемет. І це – найпоширеніша помилка. Спадкування плутають з агрегуванням - включенням одного об'єкта до складу іншого. Танк не є кулеметом, він містить його. І через цю помилку найчастіше і виникає бажання скористатися множинним успадкуванням. Перейдемо тепер безпосередньо до Java. Що тут є у плані наслідування? У мові є два типи класів – здатні утримувати реалізацію, і нездатні цього. Другі називаються інтерфейсами, хоча по суті це абстрактні класи.
Так от, мова дозволяє успадкувати клас від іншого класу, що потенційно містить реалізацію. АЛЕ ТІЛЬКИ ВІД ОДНОГО! Поясню, навіщо це зроблено. Справа в тому, що кожна реалізація може мати справу лише зі своєю частиною – з тими змінними та методами, про які вона знає. І якщо навіть ми успадкуємо клас
С від
А і
В , то метод
processA , успадкований з класу А, може працювати тільки з внутрішньою змінною
а , бо про
b він нічого не знає, так само як нічого він не знає і про
c , і про метод
processC . Так само і метод
processB
може працювати лише зі змінною b. Тобто, по суті, успадковані частини є ізольованими. Клас С, безумовно, може з ними працювати, але так само він може працювати з цими частинами, якщо вони будуть просто включені до його складу, а не успадковані. Однак тут є ще одна неприємність, яка полягає у перекритті імен. Якби методи
processA і
processB називалися однаково, припустимо, process, який ефект дало б звернення до методу
process класу
З ? Який із двох методів був би викликаний? Зрозуміло, в С++ є засоби управління у цій ситуації, проте стрункості мови це не додає. Отже, переваг успадкування реалізації не дає, а недоліки є. З цієї причини це успадкування реалізації Java відмовабося. Однак, розробникам залишабо такий варіант множинного успадкування, як успадкування від інтерфейсу. У термінах Java – реалізація інтерфейсу. Що таке інтерфейс? Набір методів. (Визначення в інтерфейсах констант ми зараз не розглядаємо, докладніше про це
тут ). А що є метод? А метод, за своєю суттю, визначає поведінку об'єкта. Не випадково в назві практично кожного методу міститься дія -
getXXX ,
drawXXX ,
countXXX , і т.д. Оскільки інтерфейс – це сукупність методів, то
інтерфейс є, власне,
визначник поведінки . Інший варіант застосування інтерфейсу – визначник ролі об'єкта.
Спостерігач, слухач і т.п. У цьому випадку метод фактично є втіленням реакції на якусь зовнішню подію. Тобто, знову ж таки, поведінкою. Об'єкт, безумовно, може мати кілька різних поведінок. Якщо йому потрібно малюватись – він малюється. Якщо потрібно зберегтися – він зберігається. Ну і т.д. Відповідно, можливість успадкування від класів, визначальних поведінка – дуже корисна. Так само об'єкт може мати кілька різних ролей. Проте
реалізаціяповедінки – повністю на совісті дочірнього класу. Спадкування від інтерфейсу (його реалізація) говорить, що об'єкт цього класу повинен вміти робити те й те. А ЯК він це робить – кожен інтерфейс, що реалізує, клас визначає самостійно. Повернемося до помилок під час наслідування. Мій досвід розробки різних систем показує, що маючи успадкування від інтерфейсів, можна реалізувати будь-яку систему, при цьому не використовуючи успадкування множини реалізації. І тому, коли я зустрічаюся з наріканнями на відсутність множинного успадкування у тому вигляді, в якому воно є в С++, для мене це вірна ознака неправильного дизайну. Найчастіше відбувається помилка, про яку я вже згадував – успадкування плутається з агрегуванням. Іноді це відбувається через невірні передумови. Тобто. береться, наприклад, спідометр, стверджується, що виміряти швидкість можна тільки вимірявши відстань і час, після чого спідометр благополучно успадковується від лінійки і годинника, стаючи таким чином лінійкою і годинником, згідно з визначенням спадкування. (На мої прохання виміряти спідометром час зазвичай відповідали жартами. Або взагалі не відповідали.) А в чому тут помилка? У передумові. Справа в тому, що спідометр не вимірює часу. І відстані, до речі, також. Одометр, який є у будь-якому спідометрі – це класичний приклад другого приладу у тому корпусі, тобто. агрегування. Для вимірювання швидкості не потрібен. Його можна взагалі прибрати – на вимірювання швидкості це ніяк не вплине. Іноді такі помилки роблять свідомо. Це набагато гірше. "Так, я знаю, що так неправильно, але мені так зручніше". На що це може обернутися? А ось у що: успадкуємо танк від гармати та кулемета. Найзручніше так. В результаті танк стає гарматою та кулеметом. Далі ми обладнаємо літак двома кулеметами та гарматою. Що отримуємо? Літак із підвісним озброєнням у вигляді трьох танків! Тому що обов'язково знайдеться людина, яка, не розібравшись, використовує танк як кулемет. Виключно згідно з ієрархією успадкування. І буде абсолютно правим, бо помилку зробив той, хто таку ієрархію спроектував.
Взагалі, я не дуже розумію підхід " мені так зручніше". Зручні писати як чутка, а ті, хто каже пра граматність – козли. Я утрирую, звичайно, але основна думка залишається - крім миттєвої зручності є таке поняття як грамотність. Це поняття визначено виходячи з дуже великого досвіду дуже багато людей. Фактично це те, що в англійській мові називається "best practice" – найкраще рішення. І найчастіше рішення, які здаються більш простими, дають надалі чимало проблем.
Приклад цей, звичайно, сильно перебільшений і тому абсурдний. Проте трапляються менш явні випадки, які призводять до катастрофічних наслідків. Успадкувавшись від об'єкта, замість того, щоб його агрегувати, розробник дає можливість використовувати функціональність батьківського об'єкта безпосередньо. З усіма наслідками, що з цього випливають. Уявіть, що у вас є клас, що працює з базою даних,
DBManager . Ви створюєте ще один клас, який буде працювати вже з вашими даними, використовуючи
DBManager –
DataManager . Цей клас здійснюватиме контроль даних, перетворення, додаткові дії тощо. Загалом, прошарок між бізнес-рівнем та рівнем бази. Якщо успадкувати DataManager від DBManager, то кожен, хто використовує його, отримає доступ до бази безпосередньо. І, отже, зможе виконати будь-які дії в обхід контролю, перетворень тощо. Гаразд, припустимо, що навмисної шкоди ніхто завдавати не хоче і прямі дії будуть грамотними. Але! Припустимо, що база змінилася. У сенсі змінабося якісь принципи контролю чи перетворень. DataManager змінабо. Але той код, який раніше працював із базою безпосередньо – так і працюватиме. Про нього з великою ймовірністю не згадають. В результаті з'явиться помилка такого класу, що ті, хто її шукатиме, посивіють. Адже нікому на думку не спаде, що з базою працюють в обхід DataManager. До речі, приклад із реального життя. Помилку шукали ДУЖЕ довго. Насамкінець повторю ще раз.
Спадкування необхідно застосовувати ТІЛЬКИ за наявності відношення "є". Тому що в цьому полягає сама суть наслідування – можливість використовувати об'єкти дочірнього класу як об'єкти базового. Якщо ж відносини "є" між класами немає - успадкування бути НЕ ПОВИННО! Ніколи і за жодних обставин. І тим більше – лише тому, що так зручно. Посилання на першоджерело: http://www.skipy.ru/philosophy/inheritance.html
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ