JavaRush /Java блог /Random UA /Спадкування як явище
articles
15 рівень

Спадкування як явище

Стаття з групи Random UA
Сказати правду, спочатку я цієї статті не планував. Я вважав питання, які хочу тут обговорити, тривіальними, не вартими навіть згадки. Однак у процесі написання статей для цього сайту я підняв в одному з форумів обговорення множинного успадкування. Внаслідок чого з'ясувалося, що більшість розробників має досить невиразне уявлення про успадкування. І, відповідно, припускається дуже багато помилок. Оскільки спадкування є однією з найважливіших рис ООП (якщо не найважливішою!) – я вирішив присвятити цьому явищу окрему статтю. * * * Спочатку я хочу розмежувати два поняття – об'єкт та клас. Ці поняття постійно плутають. Тим часом вони є центральними в ООП. І знати різницю між ними, на мій погляд, необхідно. Отже, об'єкт. По суті, це будь-що. Ось кубик лежить. Дерев'яний синій. Довжина ребра 5 см. Це об'єкт. А он пірамідка. Пластмасова, червона. 10 см ребро. Це також об'єкт. Що з-поміж них спільного? Різні розміри. Різна форма. Різний матеріал. Проте спільне у них є. Насамперед, і кубик, і пірамідка – правильні багатогранники. Тобто. сума кількості вершин та кількості граней на 2 більше за кількість ребер. Далі. В обох фігур є грані, ребра та вершини. Обидва фігури мають таку характеристику, як розмір ребра. Обидві фігури можна крутити. Обидві фігури можна малювати. Два останні властивості – це поведінка. Ну і таке інше. Практика програмування показує, що з однорідними об'єктами оперувати значно простіше, ніж з різнорідними. А оскільки між цими фігурами таки є щось спільне, виникає бажання це спільне якось виділити. Ось тут і випливає таке поняття, як клас. Отже, визначення.
Клас – це описувач загальних властивостей групи об'єктів. Цими властивостями може бути як характеристики об'єктів (розмір, вага, колір тощо), і поведінки, ролі тощо.
Зауваження. Слово " всіх " (описувач всіх властивостей) сказано був. Що означає, що будь-який об'єкт може належати до кількох різних класів. Спадкування як явище - 1 Візьмемо за основу той самий приклад із геометричними фігурами. Найзагальніший опис - правильний багатогранник . Безвідносно розміру ребра, кількості граней та вершин. Єдине, що ми знаємо – що ця фігура має вершини, ребра і грані, і що довжини ребер рівні. Далі. Ми можемо конкретизувати опис. Допустимо, ми хочемо намалювати цей багатогранник . Введемо таке поняття як правильний багатогранник, що відмальовується . Що нам потрібне для малювання? Опис загального способу відтворення, який залежить від конкретних координат вершин. Можливо колір об'єкта. Тепер введемо класи Куб і Тетраедр . Об'єкти, що належать до цих класів, безумовно, є правильними багатогранниками. Єдина відмінність – числа вершин, ребер та граней вже жорстко фіксовані для кожного з нових класів. Далі, знаючи вигляд конкретної фігури, ми можемо дати опис способу відтворення. А значить, будь-який об'єкт класів Куб або Тетраедр також є і об'єктом класу правильний багатогранник, що відмальовується . В наявності ієрархія класів. У цій ієрархії ми спускаємося від найзагальнішого опису до найбільш конкретизованого. Зауважте, що об'єкт будь-якого класу також підходить під опис будь-якого більш загального класу з ієрархії. Таке ставлення класів і називається успадкуванням . Кожен дочірній клас успадковує всі властивості батьківського, більш загального, і (можливо) додає до цих властивостей якісь свої. Або перевизначає якісь властивості батьківського класу. Тут я хочу навести цитату із класичної вже книги Граді Буча з об'єктно-орієнтованого дизайну:
Уповноваження, там, 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. Що тут є у плані наслідування? У мові є два типи класів – здатні утримувати реалізацію, і нездатні цього. Другі називаються інтерфейсами, хоча по суті це абстрактні класи. Спадкування як явище - 2 Так от, мова дозволяє успадкувати клас від іншого класу, що потенційно містить реалізацію. АЛЕ ТІЛЬКИ ВІД ОДНОГО! Поясню, навіщо це зроблено. Справа в тому, що кожна реалізація може мати справу лише зі своєю частиною – з тими змінними та методами, про які вона знає. І якщо навіть ми успадкуємо клас С від А і В , то метод processA , успадкований з класу А, може працювати тільки з внутрішньою змінною а , бо про b він нічого не знає, так само як нічого він не знає і про c , і про метод processC . Так само і метод processB може працювати лише зі змінною b. Тобто, по суті, успадковані частини є ізольованими. Клас С, безумовно, може з ними працювати, але так само він може працювати з цими частинами, якщо вони будуть просто включені до його складу, а не успадковані. Однак тут є ще одна неприємність, яка полягає у перекритті імен. Якби методи processA і processB називалися однаково, припустимо, process, який ефект дало б звернення до методу process класу З ? Який із двох методів був би викликаний? Зрозуміло, в С++ є засоби управління у цій ситуації, проте стрункості мови це не додає. Отже, переваг успадкування реалізації не дає, а недоліки є. З цієї причини це успадкування реалізації Java відмовабося. Однак, розробникам залишабо такий варіант множинного успадкування, як успадкування від інтерфейсу. У термінах Java – реалізація інтерфейсу. Що таке інтерфейс? Набір методів. (Визначення в інтерфейсах констант ми зараз не розглядаємо, докладніше про це тут ). А що є метод? А метод, за своєю суттю, визначає поведінку об'єкта. Не випадково в назві практично кожного методу міститься дія - getXXX , drawXXX , countXXX , і т.д. Оскільки інтерфейс – це сукупність методів, то інтерфейс є, власне, визначник поведінки . Інший варіант застосування інтерфейсу – визначник ролі об'єкта. Спостерігач, слухач і т.п. У цьому випадку метод фактично є втіленням реакції на якусь зовнішню подію. Тобто, знову ж таки, поведінкою. Об'єкт, безумовно, може мати кілька різних поведінок. Якщо йому потрібно малюватись – він малюється. Якщо потрібно зберегтися – він зберігається. Ну і т.д. Відповідно, можливість успадкування від класів, визначальних поведінка – дуже корисна. Так само об'єкт може мати кілька різних ролей. Проте реалізаціяповедінки – повністю на совісті дочірнього класу. Спадкування від інтерфейсу (його реалізація) говорить, що об'єкт цього класу повинен вміти робити те й те. А ЯК він це робить – кожен інтерфейс, що реалізує, клас визначає самостійно. Повернемося до помилок під час наслідування. Мій досвід розробки різних систем показує, що маючи успадкування від інтерфейсів, можна реалізувати будь-яку систему, при цьому не використовуючи успадкування множини реалізації. І тому, коли я зустрічаюся з наріканнями на відсутність множинного успадкування у тому вигляді, в якому воно є в С++, для мене це вірна ознака неправильного дизайну. Найчастіше відбувається помилка, про яку я вже згадував – успадкування плутається з агрегуванням. Іноді це відбувається через невірні передумови. Тобто. береться, наприклад, спідометр, стверджується, що виміряти швидкість можна тільки вимірявши відстань і час, після чого спідометр благополучно успадковується від лінійки і годинника, стаючи таким чином лінійкою і годинником, згідно з визначенням спадкування. (На мої прохання виміряти спідометром час зазвичай відповідали жартами. Або взагалі не відповідали.) А в чому тут помилка? У передумові. Справа в тому, що спідометр не вимірює часу. І відстані, до речі, також. Одометр, який є у будь-якому спідометрі – це класичний приклад другого приладу у тому корпусі, тобто. агрегування. Для вимірювання швидкості не потрібен. Його можна взагалі прибрати – на вимірювання швидкості це ніяк не вплине. Іноді такі помилки роблять свідомо. Це набагато гірше. "Так, я знаю, що так неправильно, але мені так зручніше". На що це може обернутися? А ось у що: успадкуємо танк від гармати та кулемета. Найзручніше так. В результаті танк стає гарматою та кулеметом. Далі ми обладнаємо літак двома кулеметами та гарматою. Що отримуємо? Літак із підвісним озброєнням у вигляді трьох танків! Тому що обов'язково знайдеться людина, яка, не розібравшись, використовує танк як кулемет. Виключно згідно з ієрархією успадкування. І буде абсолютно правим, бо помилку зробив той, хто таку ієрархію спроектував.
Взагалі, я не дуже розумію підхід " мені так зручніше". Зручні писати як чутка, а ті, хто каже пра граматність – козли. Я утрирую, звичайно, але основна думка залишається - крім миттєвої зручності є таке поняття як грамотність. Це поняття визначено виходячи з дуже великого досвіду дуже багато людей. Фактично це те, що в англійській мові називається "best practice" – найкраще рішення. І найчастіше рішення, які здаються більш простими, дають надалі чимало проблем.
Приклад цей, звичайно, сильно перебільшений і тому абсурдний. Проте трапляються менш явні випадки, які призводять до катастрофічних наслідків. Успадкувавшись від об'єкта, замість того, щоб його агрегувати, розробник дає можливість використовувати функціональність батьківського об'єкта безпосередньо. З усіма наслідками, що з цього випливають. Уявіть, що у вас є клас, що працює з базою даних, DBManager . Ви створюєте ще один клас, який буде працювати вже з вашими даними, використовуючи DBManagerDataManager . Цей клас здійснюватиме контроль даних, перетворення, додаткові дії тощо. Загалом, прошарок між бізнес-рівнем та рівнем бази. Якщо успадкувати DataManager від DBManager, то кожен, хто використовує його, отримає доступ до бази безпосередньо. І, отже, зможе виконати будь-які дії в обхід контролю, перетворень тощо. Гаразд, припустимо, що навмисної шкоди ніхто завдавати не хоче і прямі дії будуть грамотними. Але! Припустимо, що база змінилася. У сенсі змінабося якісь принципи контролю чи перетворень. DataManager змінабо. Але той код, який раніше працював із базою безпосередньо – так і працюватиме. Про нього з великою ймовірністю не згадають. В результаті з'явиться помилка такого класу, що ті, хто її шукатиме, посивіють. Адже нікому на думку не спаде, що з базою працюють в обхід DataManager. До речі, приклад із реального життя. Помилку шукали ДУЖЕ довго. Насамкінець повторю ще раз. Спадкування необхідно застосовувати ТІЛЬКИ за наявності відношення "є". Тому що в цьому полягає сама суть наслідування – можливість використовувати об'єкти дочірнього класу як об'єкти базового. Якщо ж відносини "є" між класами немає - успадкування бути НЕ ПОВИННО! Ніколи і за жодних обставин. І тим більше – лише тому, що так зручно. Посилання на першоджерело: http://www.skipy.ru/philosophy/inheritance.html
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ