"Віддавайте перевагу складу над спадщиною" - це просто хороша евристика
Ви повинні врахувати контекст, оскільки це не є універсальним правилом. Не сприймайте це означає, що ніколи не використовуйте спадщину, коли можете робити композицію. Якби це було так, ви виправите це, заборонивши спадщину.
Я сподіваюсь, що ця точка з’ясується на цій посаді.
Я не буду намагатися відстояти достоїнства композиції самостійно. Що я вважаю поза темою. Натомість я розповім про деякі ситуації, коли розробник може вважати спадщину, що було б краще використовувати склад. З цього приводу, у спадщині є свої достоїнства, які я також вважаю поза темою.
Приклад транспортного засобу
Я пишу про розробників, які намагаються робити речі німим способом, для розповідних цілей
Ходімо варіант класичних прикладів , що деякі курси ООП використовувати ... У нас є Vehicle
клас, то ми отримаємо Car
, Airplane
, Balloon
і Ship
.
Примітка . Якщо вам потрібно заземлити цей приклад, зробіть вигляд, що це види об’єктів у відеоігри.
Тоді Car
і Airplane
може бути якийсь загальний код, оскільки вони обидва можуть кататись на колесі по суші. Розробники можуть розглянути можливість створення посередницького класу в ланцюжку спадкування для цього. Тим не менш, насправді є і деякий загальний код між Airplane
і Balloon
. Вони могли б розглянути можливість створення іншого посередницького класу в ланцюжку спадкування для цього.
Таким чином, розробник шукав би багаторазового успадкування. У той момент, коли розробники шукають багаторазове спадкування, дизайн вже пішов не так.
Краще моделювати цю поведінку як інтерфейси та композицію, щоб ми могли використовувати її повторно, не впадаючи у спадщину кількох класів. Якщо розробники, наприклад, створюють FlyingVehicule
клас. Вони б сказали, що Airplane
це FlyingVehicule
(спадкування класу), але ми могли б замість цього сказати, що Airplane
має Flying
компонент (склад) і Airplane
є IFlyingVehicule
(інтерфейс успадкування).
Використовуючи інтерфейси, якщо потрібно, ми можемо мати багатократне успадкування (інтерфейсів). Крім того, ви не пов'язані з конкретною реалізацією. Підвищення повторної використання та перевіреності коду.
Пам’ятайте, що успадкування - це інструмент поліморфізму. Крім того, поліморфізм є інструментом для повторного використання. Якщо ви можете збільшити багаторазове використання свого коду за допомогою складу, зробіть це. Якщо ви не впевнені, що незалежно від того, чи композиція забезпечує кращу багаторазовість використання, "Віддавайте перевагу композиції над успадкуванням" є хорошою евристикою.
Все це без згадки Amphibious
.
Насправді нам можуть не знадобитися речі, які сходять із землі. Стівен Гарн має красномовніший приклад у своїх статтях «Композиція користі над спадщиною», частина 1 та частина 2 .
Заміна та інкапсуляція
Чи слід A
успадковувати чи складати B
?
Якщо A
спеціалізація, B
яка повинна відповідати принципу заміщення Ліскова , успадкування є життєздатним, навіть бажаним. Якщо є ситуації, коли A
це не є дійсною заміною, B
ми не повинні використовувати спадщину.
Нас може зацікавити композиція як форма оборонного програмування для захисту похідного класу . Зокрема, щойно ви починаєте використовувати B
для інших цілей, може виникнути тиск, щоб змінити або розширити його, щоб бути більш придатним для цих цілей. Якщо є ризик, який B
може виявити методи, які можуть призвести до недійсного стану, A
ми повинні використовувати склад замість спадкування. Навіть якщо ми є автором обох B
і A
, варто турбуватися про одне, тому композиція полегшує повторне використання B
.
Ми можемо навіть стверджувати, що якщо є функції, B
які A
не потрібні (і ми не знаємо, чи ці функції можуть призвести до недійсного стану для A
, або в теперішній реалізації, або в майбутньому), це гарна ідея використовувати склад замість спадкування.
Композиція також має переваги, що дозволяє перемикати виконання та полегшувати глузування.
Примітка . Є ситуації, коли ми хочемо використовувати композицію, незважаючи на те, що заміна є дійсною. Ми архівуємо цю замінюваність за допомогою інтерфейсів або абстрактних класів (який слід використовувати, коли інша тема), а потім використовуємо композицію з ін'єкцією залежності від реальної реалізації.
Нарешті, звичайно, є аргумент, що ми повинні використовувати склад для захисту батьківського класу, оскільки успадкування порушує інкапсуляцію батьківського класу:
Спадкування розкриває підклас до деталей про реалізацію батьків, часто говорять про те, що "спадкування порушує інкапсуляцію"
- Шаблони дизайну: елементи багаторазового об’єктно-орієнтованого програмного забезпечення, банда з чотирьох
Ну, це погано розроблений батьківський клас. Ось чому ви повинні:
Розробити на спадщину або заборонити це.
- Ефективна Java, Джош Блок
Проблема Йо-Йо
Ще один випадок, коли композиція допомагає - це проблема Йо-Йо . Це цитата з Вікіпедії:
У розробці програмного забезпечення проблема йо-йо є антидіаграмою, яка виникає, коли програмісту доводиться читати та розуміти програму, графік спадкування якої настільки довгий і складний, що програмісту доводиться постійно переходити між багатьма різними визначеннями класу, щоб слідувати за цим контрольний потік програми.
Ви можете вирішити, наприклад: Ваш клас C
не успадкує від класу B
. Натомість у вашого класу C
буде член типу A
, який може бути, а може і не бути (або мати) його об'єктом B
. Таким чином, ви не будете програмувати не на деталі реалізації B
, а на контракт, який пропонує інтерфейс A
.
Лічильні приклади
Багато рамок виступають за спадкування над складом (що протилежне тому, що ми сперечалися). Розробник може зробити це, тому що вони вкладають багато роботи в свій базовий клас, щоб його реалізація зі складом збільшила розмір клієнтського коду. Іноді це пов’язано з обмеженнями мови.
Наприклад, рамка PHP ORM може створити базовий клас, який використовує магічні методи, щоб дозволити написання коду так, як якщо б об'єкт мав реальні властивості. Натомість код, який обробляється магічними методами, буде переходити до бази даних, запитувати для конкретного запитуваного поля (можливо, кешувати його для майбутнього запиту) та повертати його. Якщо це зробити з композицією, замовник повинен або створити властивості для кожного поля, або написати якусь версію коду магічних методів.
Додаток : Є деякі інші способи, які можна дозволити розширювати ORM-об'єкти. Таким чином, я не думаю, що спадкування в цьому випадку не потрібне. Це дешевше.
Для іншого прикладу, двигун відеоігор може створити базовий клас, який використовує нативний код залежно від цільової платформи для виконання 3D-рендерінгу та обробки подій. Цей код складний і залежить від платформи. Користувачеві двигуна-розробника двигуна було б дорого та схильно до помилок мати справу з цим кодом, адже це є частиною причини використання двигуна.
Крім того, без частини 3D-рендерінга, саме стільки функціонують рамки віджетів. Це звільняє вас від занепокоєння щодо обробки повідомлень ОС… насправді, багатьма мовами ви не можете написати такий код без певної форми власних ставок. Більше того, якби ви це зробили, це обмежує вашу портативність. Натомість, з успадкуванням, за умови, що розробник не порушує сумісність (занадто багато); ви зможете легко перенести свій код на будь-які нові платформи, які вони підтримують у майбутньому.
Крім того, врахуйте, що багато разів ми хочемо лише переотримати кілька методів і залишити все інше з типовими реалізаціями. Якби ми використовували композицію, нам довелося б створити всі ці методи, навіть якщо тільки делегувати обернутий об'єкт.
За цим аргументом є момент, коли композиція може бути гіршою за ремонтабельність, ніж успадкування (коли базовий клас занадто складний). Але пам’ятайте, що збереження успадкування може бути гіршим, ніж склад (коли дерево спадкування занадто складне), на що я посилаюсь у проблемі йо-йо.
У представлених прикладах розробники рідко мають намір повторно використовувати код, сформований за допомогою спадкування в інших проектах. Це пом'якшує зменшену можливість використання спадщини замість складу. Крім того, використовуючи успадкування, розробники фреймворку можуть забезпечити багато простого у користуванні та простого відкриття коду.
Заключні думки
Як бачимо, композиція має певну перевагу перед успадкуванням у деяких ситуаціях, не завжди. Важливо враховувати контекст та різні фактори, що беруть участь (такі як повторне використання, ремонтопридатність, доказовість тощо) для прийняття рішення. Назад до першого пункту: "Віддавайте перевагу складу над спадщиною" - це просто хороша евристика.
Можливо, ви також помітили, що багато описуваних мною ситуацій можна певною мірою вирішити за допомогою «Характеристики» або «Міксінс». На жаль, це не звичайні риси у великому списку мов, і зазвичай вони мають певну вартість виконання. На щастя, їхні популярні методи розширення двоюрідних братів та реалізація за замовчуванням полегшують деякі ситуації.
У мене є нещодавнє повідомлення, де я розповідаю про деякі переваги інтерфейсів, чому нам потрібен інтерфейс між інтерфейсом користувача, бізнесом та доступом до даних у C # . Це допомагає роз’єднати та полегшує повторне використання та тестабельність, можливо, вам буде цікаво.