Существует ли язык или шаблон проектирования, который позволяет * удалять * поведение объекта или свойства в иерархии классов?

28

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

С одной стороны, вы не хотите определять для каждого свойства и поведения какой-либо флаг, который указывает, присутствует ли он вообще, и проверять его каждый раз перед доступом к этому поведению или свойству. Вы просто хотите сказать, что птицы могут просто и ясно летать в классе птиц. Но тогда было бы хорошо, если бы потом можно было определить «исключения», не прибегая к каким-либо ужасным взломам. Это часто происходит, когда система некоторое время была продуктивной. Вы неожиданно находите «исключение», которое вообще не вписывается в оригинальный дизайн, и вы не хотите изменять большую часть своего кода, чтобы приспособить его.

Итак, существуют ли какие-то языковые или конструктивные шаблоны, которые могут чисто решить эту проблему, не требуя серьезных изменений в «суперклассе» и во всем коде, который его использует? Даже если решение обрабатывает только конкретный случай, несколько решений могут вместе сформировать полную стратегию.

Подумав больше, я понимаю, что забыл о принципе замены Лискова. Вот почему вы не можете сделать это. Предполагая, что вы определяете «черты / интерфейсы» для всех основных «групп объектов», вы можете свободно реализовывать черты в разных ветвях иерархии, например, черта «Летающий» может быть реализована птицами и некоторыми особыми видами белок и рыб.

Таким образом, мой вопрос может быть таким: «Как я могу не реализовать черту?» Если ваш суперкласс является Java Serializable, вы должны быть им тоже, даже если у вас нет возможности сериализовать ваше состояние, например, если вы содержали «Socket».

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

Себастьян Диот
источник
3
«без необходимости использовать какие-то ужасные хаки везде»: отключение поведения - это ужасный хак: это будет означать, что все function save_yourself_from_crashing_airplane(Bird b) { f.fly() }станет намного сложнее. (как сказал Питер Тёрёк, это нарушает LSP)
keppla 15.11.11
Комбинация шаблона Стратегии и наследования может позволить вам «составлять» наследуемое поведение для определенных супертипов? Когда вы говорите: " 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

Как уже упоминали другие, вы должны пойти против LSP.

Однако можно утверждать, что подкласс - это просто произвольное расширение суперкласса. Это новый объект сам по себе, и единственное отношение к суперклассу состоит в том, что он использует основу.

Это может иметь логический смысл, вместо того, чтобы говорить, что Пингвин - Птица. Вы говорите, что Пингвин унаследовал некоторое подмножество поведения от Птицы.

Как правило, динамические языки позволяют легко выразить это, например, с помощью 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 Serializable, вы должны быть им тоже, даже если у вас нет возможности сериализовать ваше состояние, например, если вы содержали «Socket».

Вы не «не реализуете» черту. Вы просто исправляете свое наследие. Либо вы можете выполнить свой контракт на суперклассы, либо не должны притворяться, что вы принадлежите к этому типу.

Вот где сияет объектная композиция.

Кроме того, Serializable не означает, что все должно быть сериализовано, это только означает, что «состояние, которое вам небезразлично», должно быть сериализовано.

Вы не должны использовать черту NotX. Это просто чудовищный код. Если функция ожидает летающий объект, она должна разбиться и сгореть, когда вы дадите ей мамонта.

Raynos
источник
10
«В реальном мире это просто невозможно». Да, оно может. Пингвин это птица. Способность летать - это не свойство птицы, это просто случайное свойство большинства видов птиц. Свойства, которые определяют птиц: «пернатые, крылатые, двуногие, эндотермические, яйцекладки, позвоночные животные» (Википедия) - ничего о полете там нет.
фунтовые
2
@pdr снова, это зависит от вашего определения птицы. Поскольку я использовал термин «птица», я имел в виду абстракцию класса, которую мы используем для представления птиц, включая метод fly. Я также упомянул, что вы можете сделать абстракцию вашего класса менее конкретной. Также пингвин не пернатый.
Райнос
2
@Raynos: Пингвины действительно пернатые. Их перья довольно короткие и плотные, конечно.
Джон Перди
@JonPurdy достаточно справедливо, я всегда представляю, что у них был мех.
Райнос
+1 вообще, а в частности за "мамонта". СМЕШНО!
Себастьян Диот
28

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

Некоторые языки предоставляют черты или миксины, именно для более гибкого решения таких проблем.

Петер Тёрёк
источник
1
LSP для типов , а не для классов .
Йорг Миттаг
2
@ PéterTörök: в противном случае этот вопрос не существовал бы :-) Я могу вспомнить два примера из Ruby. Classэто подкласс, Moduleхотя ClassIS-NOT-A Module. Но все же имеет смысл быть подклассом, так как он много использует кода. OTOH, StringIOIS-A IO, но у этих двух нет никаких отношений наследования (кроме очевидного того, что оба наследуются от Object, конечно), потому что они не разделяют никакого кода. Классы предназначены для совместного использования кода, а типы - для описания протоколов. IOи StringIOимеют тот же протокол, следовательно, того же типа, но их классы не связаны.
Йорг Миттаг
1
@ JörgWMittag, хорошо, теперь я понимаю, что ты имеешь в виду. Однако для меня ваш первый пример звучит скорее как злоупотребление наследованием, чем как выражение какой-то фундаментальной проблемы, которую вы, похоже, предлагаете. Публичное наследование IMO не следует использовать для повторного использования реализации, только для выражения отношений подтипа (is-a). И тот факт, что он может быть использован не по назначению, не дисквалифицирует его - я не могу представить ни одного полезного инструмента из любой области, который не может быть использован неправильно.
Петер Тёрёк
2
Людям, голосующим против этого ответа: обратите внимание, что это на самом деле не отвечает на вопрос, особенно после отредактированного разъяснения. Я не думаю, что этот ответ заслуживает отрицательного ответа, потому что то, что он говорит, очень верно и важно знать, но на самом деле он не ответил на вопрос.
Джоккинг
1
Представьте себе Java, в котором только интерфейсы являются типами, а классы - нет, а подклассы могут «не реализовывать» интерфейсы своего суперкласса, и я думаю, у вас есть грубое представление.
Йорг Миттаг
15

Fly()находится в первом примере в: Head First Design Patterns для шаблона стратегии , и это хорошая ситуация для того, почему вы должны «отдавать предпочтение композиции, а не наследованию». ,

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

StuperUser
источник
1
Я упомянул шаблон Decorator в своем ответе, но шаблон Strategy тоже работает довольно хорошо.
Джоккинг
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
Проблема в том, что эволюция не следует принципу подстановки Лискова, а наследует с удалением признаков.
Donal Fellows
7

Во-первых, ДА, любой язык, который допускает легкое динамическое изменение объекта, позволит вам сделать это. Например, в Ruby вы можете легко удалить метод.

Но, как сказал Петер Тёрёк, это нарушит LSP .


В этой части я забуду о LSP и предположу, что:

  • Bird - это класс с методом fly ()
  • Пингвин должен унаследовать от Птицы
  • Пингвин не может летать ()
  • Мне все равно, хороший ли это дизайн или соответствует ли он реальному миру, как это показано в этом вопросе.

Вы сказали :

С одной стороны, вы не хотите определять для каждого свойства и поведения какой-либо флаг, который указывает, присутствует ли он вообще, и проверять его каждый раз перед обращением к этому поведению или свойству

Похоже, что вы хотите, чтобы Python « просил прощения, а не разрешения »

Просто заставьте ваш Penguin генерировать исключение или наследовать от класса 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: это не говорит о том, что Пингвин может летать, а о том, что он должен летать (это контракт). Исключение скажет вам, что не может . Кстати, я не утверждаю, что это хорошая практика ООП, я просто даю ответ на часть вопроса
Дэвид
Дело в том, что нельзя ожидать, что пингвин будет летать только потому, что это птица. Если я хочу написать код, который говорит: «Если х может летать, делай это; иначе делай это». Я должен использовать try / catch в вашей версии, где я должен просто спросить объект, может ли он летать (существует метод приведения или проверки). Это может быть только в формулировке, но ваш ответ подразумевает, что создание исключения соответствует LSP.
фунтовые
@pdr «Я должен использовать try / catch в вашей версии» -> в этом весь смысл просить прощения, а не разрешения (потому что даже утка могла сломать крылья и не иметь возможности летать). Я исправлю формулировку.
Дэвид
«В этом весь смысл просить прощения, а не разрешения». Да, за исключением того, что он позволяет фреймворку генерировать исключения одного и того же типа для любого отсутствующего метода, поэтому Python "try: Кроме AttributeError:" в точности эквивалентен C # "если (X - Y) {} else {}" и мгновенно распознается в качестве таких. Но если вы намеренно сгенерировали исключение CannotFlyException, чтобы переопределить функциональность fly () по умолчанию в Bird, тогда она станет менее узнаваемой.
фунтовые
7

Как отмечалось выше в комментариях, пингвины - это птицы, пингвины не летают, поэтому не все птицы могут летать.

Таким образом, Bird.fly () не должен существовать или иметь возможность не работать. Я предпочитаю первое.

Конечно, наличие FlyingBird расширяет Bird, и метод .fly () будет правильным.

Алекс
источник
Я согласен, Fly должен быть интерфейсом, который может реализовать птица . Он также может быть реализован как метод с поведением по умолчанию, которое может быть переопределено, но более чистый подход использует интерфейс.
Джон Рейнор
6

Реальная проблема с примером fly () заключается в том, что входные и выходные данные операции определены неправильно. Что требуется для полета птицы? А что происходит после удачного полета? Типы параметров и возвращаемые типы для функции fly () должны иметь эту информацию. В противном случае ваш дизайн зависит от случайных побочных эффектов, и все может случиться. Часть any - это то, что вызывает всю проблему, интерфейс не определен должным образом и разрешены все виды реализации.

Итак, вместо этого:

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

У вас должно быть что-то вроде этого:

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

Теперь он явно определяет пределы функциональности - ваше поведение в полете может быть определено только одним поплавком - расстояние от земли, если задано положение. Теперь вся проблема автоматически решается сама собой. Птица, которая не может летать, просто возвращает 0.0 из этой функции, она никогда не покидает землю. Это правильное поведение для этого, и как только будет решено, что один float, вы знаете, что полностью реализовали интерфейс.

Реальное поведение может быть трудно закодировать для типов, но это единственный способ правильно указать ваши интерфейсы.

Изменить: я хочу уточнить один аспект. Эта версия функции fly () float-> float важна еще и потому, что она определяет путь. Эта версия означает, что одна птица не может магически дублировать себя во время полета. Вот почему параметр «одиночный поплавок» - это позиция на пути, по которому идет птица. Если вы хотите более сложные пути, то Point2d posinpath (float x); который использует тот же x, что и функция fly ().

ТР1
источник
1
Мне очень нравится ваш ответ. Я думаю, что это заслуживает большего количества голосов.
Себастьян Диот
2
Отличный ответ. Проблема в том, что вопрос просто машет руками о том, что на самом деле делает fly (). Любая реальная реализация fly будет иметь, по крайней мере, пункт назначения - fly (координата назначения), который в случае пингвина может быть переопределен для реализации {return currentPosition)}
Крис Кадмор
4

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

Хорошие шаблоны для проблемы, которую вы описали - это Decorator или Strategy, проектирующие архитектуру компонентов По сути, вместо удаления ненужных поведений из подклассов, вы строите объекты, добавляя необходимые поведения. Поэтому для создания большинства птиц вы бы добавили летающий компонент, но не добавляйте этот компонент к своим пингвинам.

jhocking
источник
3

Питер упомянул принцип замещения Лискова, но я чувствую, что это нужно объяснить.

Пусть q (x) свойство, доказуемое для объектов x типа T. Тогда q (y) должно быть доказуемым для объектов y типа S, где S является подтипом T.

Таким образом, если птица (объект x типа T) может летать (q (x)), то пингвин (объект y типа S) может летать (q (y)) по определению. Но это явно не тот случай. Есть и другие существа, которые могут летать, но не относятся к типу птиц.

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

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

[В ответ на редактирование]

Применение «черты» CanFly к Bird не лучше. В коде вызова все еще предлагается, чтобы все птицы могли летать.

Особенность в терминах, которые вы определили, это именно то, что имел в виду Лисков, когда она сказала "собственность"

прецизионный самописец
источник
2

Позвольте мне начать с упоминания (как и всех остальных) принципа замены Лискова, который объясняет, почему вы не должны этого делать. Однако вопрос о том, что вы должны сделать, это вопрос дизайна. В некоторых случаях это может быть не важно, что Пингвин не может летать. Возможно, вы можете сделать так, чтобы Penguin выдавал при запросе «полет» «Недостаточное количество крыльев», если в документации Bird :: fly () ясно, что это может быть сделано для птиц, которые не могут летать. Есть тест, чтобы увидеть, действительно ли он может летать, хотя это раздувает интерфейс.

Альтернатива - реструктурировать ваши классы. Давайте создадим класс «FlyingCreature» (или лучше интерфейс, если вы имеете дело с языком, который это позволяет). «Птица» не наследует от FlyingCreature, но вы можете создать «FlyingBird», который делает. Ларк, Стервятник и Орел все наследуют от FlyingBird. Пингвин нет. Это просто наследуется от птицы.

Это немного сложнее, чем наивная структура, но преимущество в том, что она точна. Вы заметите, что все ожидаемые классы есть (Bird), и пользователь обычно может игнорировать «изобретенные» классы (FlyingCreature), если не важно, может ли ваше существо летать или нет.

DJClayworth
источник
0

Типичный способ справиться с такой ситуацией - создать что-то вроде UnsupportedOperationException(Java) соотв. NotImplementedException(С #).

user281377
источник
Пока вы документируете эту возможность в Bird.
DJClayworth
0

Много хороших ответов с большим количеством комментариев, но они не все согласны, и я могу выбрать только один, поэтому я суммирую здесь все мнения, с которыми я согласен.

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

1) Следует отделить иерархию типов от иерархии повторного использования кода в проекте и в голове, даже если они в основном пересекаются. Как правило, используйте классы для повторного использования и интерфейсы для типов.

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

3) В статических и динамических языках вы можете просто сгенерировать исключение. Но это следует использовать только в том случае, если это явно объявлено в «контракте» класса / интерфейса, объявляющего функциональность, в противном случае это «нарушение контракта». Это также означает, что теперь вы должны быть готовы к тому, чтобы перехватывать исключения везде, поэтому вы пишете больше кода на сайте вызовов, и это ужасный код.

4) В некоторых динамических языках фактически возможно «удалить / скрыть» функциональность суперкласса. Если проверка на наличие функциональности является способом проверки «IS-A» на этом языке, то это адекватное и разумное решение. Если, с другой стороны, операция «IS-A» - это что-то еще, что все еще говорит, что ваш объект «должен» реализовать недостающую функциональность, тогда ваш вызывающий код будет предполагать, что эта функциональность присутствует, и будет вызывать ее и падать, поэтому это разные виды броска исключения.

5) Лучшая альтернатива - отделить черту Fly от черты Bird. Таким образом, летящая птица должна явно расширять / реализовывать как Bird, так и Fly / Flying. Это, наверное, самый чистый дизайн, так как вам не нужно ничего «удалять». Единственным недостатком является то, что почти каждая птица должна реализовывать как Bird, так и Fly, поэтому вы пишете больше кода. Чтобы обойти это, нужно иметь промежуточный класс FlyingBird, который реализует как Bird, так и Fly и представляет общий случай, но этот обходной путь может иметь ограниченное использование без множественного наследования.

6) Другой альтернативой, которая не требует множественного наследования, является использование композиции вместо наследования. Каждый аспект животного моделируется независимым классом, а конкретный Bird - это композиция Bird, и, возможно, Fly или Swim ... Вы получаете полное повторное использование кода, но вам нужно сделать один или несколько дополнительных шагов, чтобы добраться до Летающий функционал, когда у вас есть ссылка на конкретную птичку. Кроме того, естественный язык «объект IS-A Fly» и «объект AS-A (приведение) Fly» больше не будут работать, поэтому вам придется придумывать собственный синтаксис (некоторые динамические языки могут обойти это). Это может сделать ваш код более громоздким.

7) Определите свою черту Мухи так, чтобы она предлагала ясный выход для чего-то, что не может летать. Fly.getNumberOfWings () может вернуть 0. Если Fly.fly (direction, currentPotinion) должен вернуть новую позицию после полета, тогда Penguin.fly () может просто вернуть currentPosition, не изменяя его. Вы можете получить код, который технически работает, но есть некоторые предостережения. Во-первых, некоторый код может не иметь очевидного поведения «ничего не делать». Кроме того, если кто-то вызовет x.fly (), он будет ожидать, что он что-то сделает , даже если в комментарии сказано, что fly () разрешено ничего не делать . Наконец, пингвин IS-A Flying все равно вернет true, что может запутать программиста.

8) Делайте как 5), но используйте композицию, чтобы обойти случаи, которые требуют множественного наследования. Это вариант, который я бы предпочел для статического языка, так как 6) кажется более громоздким (и, вероятно, требует больше памяти, потому что у нас больше объектов). Динамический язык может сделать 6) менее громоздким, но я сомневаюсь, что он станет менее громоздким, чем 5).

Себастьян Диот
источник
0

Определите поведение по умолчанию (пометить его как виртуальное) в базовом классе и переопределите его по необходимости. Таким образом, каждая птица может «летать».

Даже пингвины летают, скользя по льду на нулевой высоте!

Поведение полета может быть изменено по мере необходимости.

Другая возможность - иметь интерфейс Fly. Не все птицы будут реализовывать этот интерфейс.

class eagle : bird, IFly
class penguin : bird

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

Джон Рейнор
источник
-1

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

  • ничего такого
  • бросает исключение
  • вместо этого вызывает метод Penguin :: swim ()
  • утверждает, что пингвин находится под водой (они как бы «летают» через воду)

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

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

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

Калеб
источник
2
-1 даже за предложение вызова Penguin :: swim (). Это нарушает принцип наименьшего удивления и заставит программистов по всему миру проклинать ваше имя.
DJClayworth
1
@DJClayworth Так как пример был на смешной стороне, во-первых, понижение голосования за нарушение предполагаемого поведения fly () и swim () кажется немного большим. Но если вы действительно хотите взглянуть на это серьезно, я бы согласился, что более вероятно, что вы пойдете другим путем и реализуете swim () в терминах fly (). Утки плавают, грести их ногами; Пингвины плавают, хлопая крыльями.
Калеб
1
Я согласен, что вопрос был глупым, но проблема в том, что я видел, как люди делают это в реальной жизни - используют существующие вызовы, которые «ничего не делают» для реализации редких функций. Это действительно испортит код, и обычно заканчивается написанием «if (! (MyBird instanceof Penguin)) fly ();» во многих местах, надеясь, что никто не создает класс страуса.
DJClayworth
Утверждение еще хуже. Если у меня есть массив Birds, у каждого из которых есть метод fly (), я не хочу ошибки подтверждения при вызове fly () для них.
DJClayworth
1
Я не читал документацию Пингвина , потому что мне передали множество Птиц, и я не знал, что Пингвин будет в массиве. Я прочитал документацию Bird, в которой говорилось, что когда я вызываю fly (), птица летит. Если бы в этой документации было четко указано, что исключение может быть выдвинуто, если птица не летает, я бы это допустил. Если бы он сказал, что вызов fly () иногда заставил бы его плавать, я бы переключился на другую библиотеку классов. Или пошел на очень большой напиток.
DJClayworth