Чи існує мовна чи дизайнерська схема, яка дозволяє * видаляти * поведінку об'єктів чи властивості в ієрархії класів?


28

Відомий недолік традиційних ієрархій класів полягає в тому, що вони погані, коли йдеться про моделювання реального світу. Як приклад, намагаючись представити види тварин класами. У цьому насправді є кілька проблем, але одна, яку я ніколи не бачив, це рішення, коли підклас «втрачає» поведінку або властивість, визначені в суперкласі, як пінгвін, який не може літати (там є, мабуть, кращими прикладами, але це перший, який мені спадає на думку).

З одного боку, ви не хочете визначати для кожної властивості та поведінки якийсь прапор, який вказує, чи він взагалі присутній, і перевіряти його кожен раз, перш ніж отримати доступ до такої поведінки чи властивості. Ви просто хотіли б сказати, що птахи можуть легко і чітко літати в класі Птахи. Але тоді було б добре, якби потім можна було визначити "винятки", не використовуючи скрізь жахливих хак. Це часто відбувається, коли система деякий час була продуктивною. Ви раптом виявляєте "виняток", який зовсім не вписується в оригінальний дизайн, і ви не хочете змінювати велику частину свого коду, щоб пристосувати його.

Отже, чи є якісь мовні чи дизайнерські зразки, які можуть чітко впоратися з цією проблемою, не вимагаючи великих змін у «суперкласі» та всьому коді, який ним використовується? Навіть якщо рішення стосується лише конкретного випадку, кілька рішень можуть разом формувати повну стратегію.

Поміркувавши більше, я розумію, що забув про Принцип заміщення Ліскова. Ось чому ви не можете цього зробити. Якщо припустити, що ви визначаєте "риси / інтерфейси" для всіх основних "функціональних груп", ви можете вільно реалізовувати риси в різних гілках ієрархії, як, наприклад, Летючу рису можна реалізувати Птахи, а також якийсь особливий вид білки та риби.

Тож моє запитання може означати "Як я міг не реалізувати ознаку?" Якщо ваш суперклас - це серіалізація Java, ви також повинні бути такою, навіть якщо немає можливості серіалізувати свій стан, наприклад, якщо ви містили "Socket".

Один із способів зробити це - це завжди визначати всі ваші риси в парах з самого початку: Flying та NotFlying (що може кинути UnsupportedOperationException, якщо це не встановлено). Not-чорта не визначає жодного нового інтерфейсу, і його можна просто перевірити. Звучить як "дешеве" рішення, зокрема, якщо його використовувати з самого початку.


3
"без необхідності використовувати якісь жахливі хаки скрізь": відключення поведінки є жахливим злом: це означатиме, що function save_yourself_from_crashing_airplane(Bird b) { f.fly() }це стане набагато складніше. (як Петро Тьорк сказав, що це порушує LSP)
keppla

Поєднання структури стратегії та успадкування може допомогти вам "скласти над" успадковану поведінку для конкретних супер типів? Коли ви кажете: " it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"чи вважаєте ви фабричний метод контролю поведінки хакі?
StuperUser

1
Можна, звичайно, просто кинути NotSupportedExceptionз Penguin.fly().
Фелікс Домбек

Що стосується мов, то, звичайно, ви можете скасувати реалізацію методу в дитячому класі. Наприклад, у Ruby : class Penguin < Bird; undef fly; end;. Чи варто вам - інше питання.
Натан Лонг

Це порушило би принцип ліскова і, можливо, всю точку ООП.
deadalnix

Відповіді:


17

Як уже згадували інші, вам доведеться йти проти ЛСП.

Однак можна стверджувати, що підклас - це лише довільне розширення суперкласу. Це новий об’єкт сам по собі, і єдине відношення до супер класу полягає в тому, що він використовується фундаментом.

Це може мати логічний сенс, швидше кажучи, що Пінгвін - Птах. Ваша приказка Пінгвін успадковує деяку підмножину поведінки від Птаха.

Як правило, динамічні мови дозволяють легко висловити це, нижче наведений приклад із використанням JavaScript:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

У цьому конкретному випадку Penguinактивно відтіняється Bird.flyметод, який він успадковує, записуючи на об'єкт flyвластивість зі значенням undefined.

Тепер ви можете сказати, що це Penguinвже не можна трактувати як нормальне Bird. Але, як згадувалося, в реальному світі це просто неможливо. Тому що ми моделюємо себе Birdяк літаючу сутність.

Альтернативою є не робити широке припущення, що птахи можуть літати. Було б розумним бути Birdабстракцію, яка дозволяє всім птахам успадковувати від нього безвідмовно. Це означає лише робити припущення, які можуть містити всі підкласи.

Взагалі ідея Mixin's тут добре застосовується. Майте дуже тонкий базовий клас і змішайте в ньому всю іншу поведінку.

Приклад:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Якщо ви цікаві, у мене є реалізаціяObject.make

Доповнення:

Тож моє запитання може означати "Як я міг не реалізувати ознаку?" Якщо ваш суперклас - це серіалізація Java, ви також повинні бути такою, навіть якщо немає можливості серіалізувати свій стан, наприклад, якщо ви містили "Socket".

Ви не "не реалізуєте" ознаку. Ви просто фіксуєте свою ієрахію спадщини. Або ви можете виконати свій договір про суперкласи, або не повинні робити вигляд, що ви такого типу.

Саме тут сяє композиція об’єкта.

Що стосується відстороненості, серіалізація не означає, що все має бути серіалізовано, це лише означає, що "стан, про який ви дбаєте", має бути серіалізовано.

Ви не повинні використовувати ознаку "NotX". Це просто жахливий розмах коду. Якщо функція очікує літаючого об'єкта, вона повинна розбитися і спалити, коли ви дасте йому мамонта.


10
"в реальному світі це просто неможливо". Так, це може. Пінгвін - птах. Здатність літати не є властивістю птахів, це просто випадкова властивість більшості видів птахів. Властивості, які визначають птахів, - це "пернаті, крилаті, двоногі, ендотермічні, яйценосні, хребетні тварини" (Вікіпедія) - нічого про те, що летить туди.
pdr

2
@pdr знову це залежить від вашого визначення птаха. Під час використання терміна "Птах" я мав на увазі абстракцію класу, яку ми використовуємо для представлення птахів, включаючи метод мухи. Я також зазначив, що ви можете зробити свою абстракцію класу менш конкретною. Також пінгвіна не пернат.
Райнос

2
@Raynos: Пінгвіни справді пернаті. Їх пір’я, звичайно, досить короткі і щільні.
Джон Перді

@JonPurdy досить чесно, я завжди думаю, що вони мали хутро.
Райнос

+1 загалом, і зокрема для "мамонта". ЛОЛ!
Себастьєн Діот

28

AFAIK усі мови, що базуються на спадщині, побудовані на принципі заміщення Ліскова . Видалення / відключення властивості базового класу в підкласі явно порушить LSP, тому я не думаю, що така можливість ніде не застосовується. Реальний світ справді безладний, і його неможливо точно змоделювати за допомогою математичних абстракцій.

Деякі мови пропонують риси чи поєднання, саме для вирішення таких проблем більш гнучким способом.


1
LSP призначений для типів , а не для класів .
Йорг W Міттаг

2
@ PéterTörök: Це питання не існувало б інакше :-) Я можу придумати два приклади Рубі. Classє підкласом, Moduleнавіть якщо ClassIS-NOT-A Module. Але все ж має сенс бути підкласом, оскільки він повторно використовує багато коду. OTOH, StringIOIS-A IO, але вони не мають жодного спадкового відносини (крім очевидних обох спадкових Object, звичайно), оскільки вони не поділяють жодного коду. Класи призначені для спільного використання коду, типи - для опису протоколів. IOі StringIOмають один і той же протокол, тому одного типу, але їхні класи не пов'язані.
Йорг W Міттаг

1
@ JörgWMittag, гаразд, тепер я краще розумію, що ти маєш на увазі. Однак, для мене ваш перший приклад звучить скоріше як неправильне використання спадщини, ніж вираження якоїсь фундаментальної проблеми, яку ви, здається, пропонуєте. ІМО публічного успадкування не слід використовувати для повторного використання імплементації, лише для вираження підтипових зв’язків (є-а). І той факт, що його можна зловживати, не дискваліфікує це - я не уявляю жодного корисного інструменту з будь-якого домену, який не можна зловживати.
Péter Török

2
Люди, які підтримують цю відповідь: зауважте, що це насправді не відповідає на питання, особливо після відредагованого уточнення. Я не вважаю, що ця відповідь заслуговує ослаблення, тому що те, що він каже, дуже правдиве і важливе знати, але він насправді не відповів на питання.
поштовх

1
Уявіть собі Java, у якій є лише інтерфейси типів, класи - ні, і підкласи можуть "відреалізувати" інтерфейси свого суперкласу, і, на мою думку, ви маєте приблизну ідею.
Йорг W Міттаг

15

Fly()є в першому прикладі в: Розділіть перші шаблони дизайну для стратегії , і це хороша ситуація щодо того, чому слід "Скористати композицію над спадщиною". .

Ви можете змішати склад і успадкування, маючи супертипи FlyingBird, FlightlessBirdякі мають правильну поведінку, введені Фабрикою, що відповідні підтипи, наприклад, Penguin : FlightlessBirdотримуються автоматично, і будь-що інше, що дійсно конкретно, отримує Фабрика як зрозуміло.


1
У своїй відповіді я згадав про шаблон «Декоратор», але шаблон «Стратегія» працює досить добре.
поштовх

1
+1 за "Вигідний склад над спадщиною". Однак необхідність спеціальних моделей дизайну для реалізації композиції на мовах статичного типу підсилює мій ухил до динамічних мов, таких як Ruby.
Рой Тінкер

11

Чи не є справжньою проблемою, яка, на вашу думку, Birdмає Flyметод? Чому ні:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Тепер очевидною проблемою є багаторазове успадкування ( Duck), тож вам справді потрібні інтерфейси:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}

3
Проблема полягає в тому, що еволюція не відповідає Принципу заміни Ліскова, а успадковує з усуненням функції.
Дональні стипендіати

7

По-перше, так, будь-яка мова, яка дозволяє легко динамічну модифікацію об'єкта, дозволила б вам це зробити. Наприклад, у Ruby можна легко видалити метод.

Але, як сказав Петер Тьорк, це порушить LSP .


У цій частині я забуду про LSP і припускаю, що:

  • Птах - клас з методом fly ()
  • Пінгвін повинен успадкувати від Птаха
  • Пінгвін не може літати ()
  • Мені байдуже, чи це гарний дизайн, чи він відповідає реальному світу, як це є прикладом у цьому питанні.

Ти сказав :

З одного боку, ви не хочете визначати для кожної властивості та поведінки якийсь прапор, який визначає, чи він взагалі присутній, і перевіряти його кожен раз, перш ніж отримати доступ до такої поведінки чи властивості

Схоже, що ви хочете - це " прохання пробачення, а не дозволу Python "

Просто змусьте ваш Пінгвін кидати виняток або успадковувати клас NonFlyingBird, який викидає виняток (псевдо-код):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

До речі, що б ви не вибрали: збільшити виняток або видалити метод, врешті-решт, наступний код (припустимо, що ваша мова підтримує видалення методу):

var bird:Bird = new Penguin();
bird.fly();

викине виняток з виконання.


"Просто змусьте ваш Пінгвін кидати виняток або успадковувати клас NonFlyingBird, який кидає виняток". Це все-таки порушення LSP. Він все ще говорить про те, що Пінгвін може літати, навіть якщо його виконання летить не вдається. Ніколи не повинно бути льотного методу на Пінгвіні.
pdr

@pdr: це не припущення, що Пінгвін може літати, а він повинен літати (це контракт). Виняток скаже вам, що не може . До речі, я не стверджую, що це хороша практика ООП, я просто даю відповідь на частину запитання
Девід

Справа в тому, що Пінгвіна не слід очікувати, що він летить лише тому, що це Птах. Якщо я хочу написати код, який говорить "Якщо x може літати, зробіть це; інакше зробіть це". Мені доводиться використовувати спробу / ловити у вашій версії, де я просто повинен мати можливість запитати об'єкт, чи може він літати (існує метод кастингу чи перевірки). Це може бути лише у формулюванні, але ваша відповідь передбачає, що кидання винятку відповідає LSP.
pdr

@pdr "Мені доводиться використовувати спробу / ловити у вашій версії" -> у цьому вся суть прохання пробачення, а не дозволу (адже навіть Качка могла зламати крила і не змогла літати). Я виправлю формулювання.
Девід

"у цьому вся суть просити прощення, а не дозволу." Так, за винятком того, що це дозволяє рамці викидати один і той же тип винятку для будь-якого відсутнього методу, тому Python "спробувати: крім AttributeError:" точно еквівалентний C # 's ", якщо (X є Y) {} else {}" і миттєво розпізнається як такий. Але якщо ви навмисно кинули CannotFlyException, щоб замінити функцію fly () за замовчуванням у Bird, він стає менш впізнаваним.
pdr

7

Як хтось вказав вище в коментарях, пінгвіни - це птахи, пінгвіни не літають, ерго не всі птахи можуть літати.

Отже, Bird.fly () не повинен існувати або дозволяти йому не працювати. Я віддаю перевагу колишньому.

Зрозуміло, що FlyingBird розширює, Bird, звичайно, буде правильним методом .fly ().


Я згоден, Fly повинен бути інтерфейсом, який може реалізувати птах . Він може бути реалізований як метод, а також з поведінкою за замовчуванням, яку можна переохопити, але більш чистий підхід використовує інтерфейс.
Джон Рейнор

6

Справжня проблема прикладу fly () полягає в тому, що введення та вихід операції не визначені належним чином. Що потрібно, щоб птах летіла? А що відбувається після успішного польоту? Типи параметрів і типи повернення для функції fly () повинні мати цю інформацію. Інакше ваш дизайн залежить від випадкових побічних ефектів і все може статися. Все, що завгодно - це те, що викликає всю проблему, інтерфейс не визначений належним чином, і допускаються всі види реалізації.

Отже, замість цього:

class Bird {
public:
   virtual void fly()=0;
};

У вас повинно бути щось подібне:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

Тепер він чітко визначає межі функціональності - у вашій льотній поведінці є лише один поплавок для вирішення - відстань від землі, коли дається положення. Тепер вся проблема автоматично вирішується. Птах, яка не вміє літати, просто повертає 0,0 з цієї функції, вона ніколи не залишає землі. Це правильна поведінка для цього, і як тільки один плавець вирішено, ви знаєте, що ви повністю реалізували інтерфейс.

Реальну поведінку може бути важко кодувати до типів, але це єдиний спосіб правильно вказати інтерфейси.

Редагувати: Я хочу уточнити один аспект. Ця функція float-> float версія fly () важлива ще й тому, що вона визначає шлях. Ця версія означає, що одна птах не може чарівно дублювати себе під час польоту. Ось чому параметр є одиночним поплавком - це положення на шляху, який проходить птах. Якщо ви хочете більш складні шляхи, то Point2d posinpath (float x); яка використовує той самий х, що і функція fly ().


1
Мені дуже подобається ваша відповідь. Я думаю, що це заслуговує на більше голосів.
Себастьєн Діот

2
Відмінна відповідь. Проблема полягає в тому, що питання просто махає руками щодо того, що муха () насправді робить. Будь-яка реальна реалізація мухи мала би, принаймні, місце призначення - полет (Координатне призначення), яке у випадку з пінгвіном може бути відмінено для здійснення {return currentPosition)}
Кріс Кадмор

4

Технічно ви можете зробити це в будь-якій мові на динамічній основі / качці (JavaScript, Ruby, Lua тощо), але це майже завжди дуже погана ідея. Видалення методів з класу - кошмар технічного обслуговування, схожий на використання глобальних змінних (тобто в одному модулі ви не можете сказати, що глобальний стан не було змінено в іншому місці).

Хорошими зразками описаної вами проблеми є «Декоратор» або «Стратегія», розробка архітектури компонентів. В основному, замість того, щоб видаляти зайві форми поведінки з підкласів, ви створюєте об'єкти, додаючи необхідні форми поведінки. Отже, щоб створити більшість птахів, ви б додали літаючий компонент, але не додайте цей компонент до своїх пінгвінів.


3

Петро згадав Принцип заміщення Ліскова, але я вважаю, що це потрібно пояснити.

Нехай q (x) - властивість, доказувальна щодо об'єктів x типу T. Тоді q (y) має бути доказовим для об'єктів y типу S, де S є підтипом T.

Таким чином, якщо Птах (об’єкт x типу T) може літати (q (x)), то Пінгвін (об'єкт y типу S) може літати (q (y)), за визначенням. Але це явно не так. Є й інші істоти, які можуть літати, але не належать до типу Птах.

Як ви з цим справляєтеся, залежить від мови. Якщо мова підтримує багатократне успадкування, то вам слід використовувати абстрактний клас для істот, які можуть літати; якщо мова віддає перевагу інтерфейсам, то це рішення (а реалізація мухомора повинна бути інкапсульована, а не успадкована); або, якщо мова підтримує Duck Typing (не каламбур), ви можете просто реалізувати метод fly на тих класах, які можуть і викликати його, якщо він є.

Але кожна властивість суперкласу має стосуватися всіх його підкласів.

[У відповідь на редагування]

Застосування "риси" CanFly до Bird - не краще. Все ще пропонується зателефонувати за тим, що всі птахи можуть літати.

Рис у визначених вами термінах - це саме те, що мав на увазі Лісков, коли вона сказала "власність".


2

Почну з згадування (як і всі) Принципу заміни Ліскова, який пояснює, чому ви не повинні цього робити. Однак питання, що вам слід зробити, - це один із дизайну. У деяких випадках може не бути важливим те, що Пінгвін насправді не може літати. Можливо, ви можете змусити Penguin кидати InsufficWingsException, коли його попросять літати, якщо ви зрозуміли в документації Bird :: fly (), що це може кинути це для птахів, які не можуть літати. У вас є тест, щоб перевірити, чи дійсно він може літати, хоча це роздуває інтерфейс.

Альтернативою є реструктуризація класів. Давайте створимо клас "FlyingCreature" (або краще інтерфейс, якщо ви маєте справу з мовою, яка це дозволяє). "Bird" не успадковується від FlyingCreature, але ви можете створити "FlyingBird", що це робить. Жайворонок, гриф та орел успадковують від FlyingBird. Пінгвін ні. Він просто успадковується від Птаха.

Це трохи складніше, ніж наївна структура, але вона має перевагу в точності. Ви зауважите, що всі очікувані класи є там (Птах), і користувач зазвичай може ігнорувати «винайдені» (FlyingCreature), якщо не важливо, може ваша істота літати чи ні.


0

Типовий спосіб вирішення такої ситуації - кинути щось на зразок UnsupportedOperationException(Java) відповіді. NotImplementedException(C #).


Поки ви документируєте таку можливість у Bird.
DJClayworth

0

Багато хороших відповідей з багатьма коментарями, але вони не всі згодні, і я можу вибрати лише одну, тому я підсумую тут усі погляди, з якими я згоден.

0) Не припускайте "статичного набору тексту" (я це робив, коли просив, тому що я роблю Java майже виключно). В основному проблема дуже залежить від типу мови, якою ви користуєтесь.

1) Ієрархію типів слід відокремлювати від ієрархії повторного використання коду в дизайні та в голові, навіть якщо вони переважно перетинаються. Зазвичай використовуйте класи для повторного використання та інтерфейси для типів.

2) Причина, чому зазвичай Bird IS-A Fly, полягає в тому, що більшість птахів може літати, тому це практично з точки зору повторного використання коду, але сказати, що Bird IS-A Fly насправді помиляється, оскільки існує хоча б один виняток (Пінгвін).

3) І в статичній, і в динамічній мовах ви можете просто кинути виняток. Але це слід використовувати лише в тому випадку, якщо це прямо заявлено в "договорі" класу / інтерфейсу, що декларує функціональність, інакше це "порушення договору". Це також означає, що тепер ви повинні бути готові виловлювати виняток скрізь, тому ви пишете більше коду на сайті виклику, і це некрасивий код.

4) У деяких динамічних мовах фактично можливо "видалити / приховати" функціональність суперкласу. Якщо перевірка наявності функціональності - це те, як ви перевіряєте "IS-A" на цій мові, то це адекватне та розумне рішення. Якщо, з іншого боку, операція "IS-A" - це щось інше, що все ще говорить, що ваш об'єкт "повинен" реалізувати відсутню функціональність, то ваш код виклику повинен припускати, що функціонал присутній, і викликати його і збивати, так це види суми до викиду виключення.

5) Краща альтернатива - насправді відокремити рису мухи від ознаки Птаха. Тож літаюча птах повинна явно розширювати / застосовувати як Птах, так і Літати / Летять. Це, мабуть, найчистіша конструкція, оскільки не потрібно нічого "знімати". Єдиним недоліком є ​​те, що майже кожному птаху доводиться реалізовувати і Bird, і Fly, тому ви пишете більше коду. Шляхом цього є створення посередницького класу FlyingBird, який реалізує і Bird і Fly, і являє собою загальний випадок, але ця обробка може мати обмежене використання без багатократного успадкування.

6) Ще одна альтернатива, яка не потребує багаторазового успадкування - використовувати склад замість спадкування. Кожен аспект тварини моделюється незалежним класом, а конкретна Птах - це композиція з Птаха, а можливо, з Fly або Swim, ... Ви отримуєте повне повторне використання коду, але вам потрібно зробити один або кілька додаткових кроків, щоб отримати функція Flying, коли у вас є посилання на конкретного Птаха. Крім того, природні мови "об'єкт IS-A Fly" та "об'єкт AS-A (cast) Fly" більше не працюватимуть, тому вам доведеться вигадувати власний синтаксис (деякі динамічні мови можуть обходити це). Це може зробити ваш код більш громіздким.

7) Визначте свою рису Fly таким чином, щоб вона мала чіткий вихід для чогось, що не може літати. Fly.getNumberOfWings () може повернути 0. Якщо Fly.fly (напрямок, currentPotinion) повинен повернути нову позицію після польоту, тоді Penguin.fly () може просто повернути поточнуПозицію, не змінюючи її. Можливо, ви отримаєте код, який технічно працює, але є деякі застереження. По-перше, якийсь код може не мати очевидної поведінки "нічого не робити". Крім того, якщо хтось телефонує x.fly (), він очікує, що він щось зробить , навіть якщо коментар каже, що fly () не може робити нічого . Нарешті, пінгвін IS-A Flying все-таки повернеться правдою, що може заплутати програміста.

8) Робіть як 5), але використовуйте склад, щоб обійти справи, які потребували б багаторазового успадкування. Це варіант, який я вважаю за краще для статичної мови, оскільки 6) здається більш громіздким (і, ймовірно, потребує більше пам’яті, оскільки у нас більше об’єктів). Динамічна мова може зробити 6) менш громіздкою, але я сумніваюся, вона стане менш громіздкою, ніж 5).


0

Визначте поведінку за замовчуванням (позначте її як віртуальну) у базовому класі та замініть її як необхідну. Таким чином кожна птах може «літати».

Навіть пінгвіни літають, ковзаючи по льоду на нульовій висоті!

Поведінка польоту може бути відмінена за необхідності.

Ще одна можливість - мати Fly Interface. Не всі птахи реалізують цей інтерфейс.

class eagle : bird, IFly
class penguin : bird

Властивості неможливо видалити, тому важливо знати, які властивості є загальними для всіх птахів. Я думаю, що це більше проблема дизайну, щоб переконатися, що загальні властивості реалізовані на базовому рівні.


-1

Я думаю, що шаблон, який ви шукаєте, - це добрий старий поліморфізм. Хоча ви, можливо, зможете вилучити інтерфейс із класу на деяких мовах, це, мабуть, не є хорошою ідеєю з причин, поданих Петером Тьореком. У будь-якій мові ОО, однак, ви можете змінити метод змінити його поведінку, і це не робить нічого. Щоб запозичити свій приклад, ви можете надати метод Penguin :: fly (), який виконує будь-що з наступного:

  • нічого
  • кидає виняток
  • називає метод Penguin :: swim ()
  • стверджує, що пінгвін під водою (вони начебто "літають" через воду)

Властивості можна трохи простіше додати та видалити, якщо ви плануєте заздалегідь. Ви можете зберігати властивості у масиві карт / словник / асоціативний елемент замість використання змінних екземплярів. Ви можете використовувати заводський зразок для створення стандартних екземплярів таких структур, тому Bird, що надходить від BirdFactory, завжди запускатиметься з однаковим набором властивостей. Ключове значення кодування Key-Value від Objective-C є хорошим прикладом такого роду речей.

Примітка . Серйозний урок із наведених нижче коментарів полягає в тому, що, хоча переосмислення видалення поведінки може спрацювати, це не завжди найкраще рішення. Якщо вам потрібно зробити це будь-яким значущим способом, ви повинні вважати, що це сильний сигнал про те, що ваш графік спадкування є недоліком. Не завжди можливо переробити класи, від яких ви успадковуєте, але коли це часто, краще рішення.

Використовуючи ваш приклад Пінгвінів, одним із способів реабілітації було б відокремлення льотної здатності від класу Птахів. Оскільки не всі птахи можуть літати, у тому числі метод fly () у Bird був недоречним і призводив безпосередньо до тієї проблеми, про яку ви ставите. Отже, перемістіть метод fly () (і, можливо, takeoff () та land ()) до класу або інтерфейсу Aviator (залежно від мови). Це дозволяє створити клас FlyingBird, який успадковується як від Bird, так і від Aviator (або успадковується від Bird та реалізує Aviator). Пінгвін може продовжувати успадковувати безпосередньо від Птаха, але не від Авіатора, тим самим уникаючи проблеми. Така домовленість може також полегшити створення класів для інших літаючих речей: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect тощо.


2
-1, навіть пропонуючи зателефонувати Penguin :: swim (). Це порушує принцип найменшого здивування і змусить програмістів підтримувати всюди прокляття вашого імені.
DJClayworth

1
@DJClayworth Як приклад був в першу чергу смішною стороною, нахил за порушення поведінки fly () та плавання () здається небагато. Але якщо ви дійсно хочете поглянути на це серйозно, я погоджуюся, що більш ймовірно, що ви підете іншим шляхом та застосуєте плавання () в плані fly (). Качки плавають, розводячи ноги; пінгвіни плавають, махаючи крилами.
Калеб

1
Я погоджуюся, що питання було нерозумним, але проблема полягає в тому, що я бачив, як люди роблять це в реальному житті - використовуйте існуючі дзвінки, які "насправді нічого не роблять", щоб реалізувати рідкісні функції. Він дійсно накручує код і, як правило, закінчується необхідністю писати "if (! (MyBird instanceof Penguin)) fly ();" у багатьох місцях, сподіваючись, що ніхто не створить клас страусів.
DJClayworth

Ствердження ще гірше. Якщо у мене є масив Birds, у якому всі є метод fly (), я не хочу заперечення твердження, коли я викликаю на них fly ().
DJClayworth

1
Я не читав документацію на Пінгвіна , тому що мені передали масив Птахів, і я не знав, що Пінгвін буде в масиві. Я читав документацію про птахів, яка говорила, що коли я кличу муху (), птах летить. Якби в цій документації було чітко сказано, що викид може бути кинутий, якщо птах буде без польоту, я б це допустив. Якби це сказало, що дзвінок fly () іноді змусив би його плавати, я змінив би використання бібліотеки іншого класу. Або пішов на дуже великий напій.
DJClayworth
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.