Как обращаться с методами, которые были добавлены для подтипов в контексте полиморфизма?

14

Когда вы используете концепцию полиморфизма, вы создаете иерархию классов и, используя родительскую ссылку, вызываете функции интерфейса, не зная, какой конкретный тип имеет объект. Это круто. Пример:

У вас есть коллекция животных, и вы вызываете все функции животных, eatи вам все равно, едят ли вы собаку или кошку. Но в той же иерархии классов у вас есть животные, у которых есть дополнительные - кроме унаследованных и реализованных из класса Animal, например makeEggs, getBackFromTheFreezedStateи так далее. Поэтому в некоторых случаях в вашей функции вы можете захотеть узнать конкретный тип для вызова дополнительных поведений.

Например, если это утреннее время, и если это просто животное, тогда вы звоните eat, в противном случае, если это человек, то сначала позвоните washHands, getDressedа только потом позвоните eat. Как справиться с этим делом? Полиморфизм умирает. Вам нужно выяснить тип объекта, который звучит как запах кода. Есть ли общий подход для решения этих случаев?

Нарек
источник
7
Тип полиморфизма, который вы описали, называется полиморфизмом подтипов , но это не единственный вид (см. Полиморфизм ). Вам не нужно создавать иерархию классов для полиморфизма (и я бы сказал, что наследование не самый распространенный метод для достижения полиморфизма подтипов, реализация интерфейса гораздо более распространена).
Винсент Савард
24
Если вы определяете Eaterинтерфейс с помощью eat()метода, то как клиент вы не заботитесь о том, что его Humanреализация должна сначала вызывать, washHands()и getDressed()это детали реализации этого класса. Если, как клиент, вы заботитесь об этом факте, скорее всего, вы не используете правильный инструмент для этой работы.
Винсент Савард
3
Вы также должны учитывать, что в то время как утром, человек может нуждаться в getDressedних eat, это не относится к обеду. В зависимости от ваших обстоятельств, washHands();if !dressed then getDressed();[code to actually eat]может быть лучшим способом реализовать это для человека. Другая возможность, что если другие вещи требуют washHandsи / или getDressedназываются? Предположим, у вас есть leaveForWork? Возможно, вам придется структурировать поток вашей программы так, чтобы она вызывалась задолго до этого.
Дункан Х Симпсон
1
Имейте в виду, что проверка на соответствие точному типу может быть запахом кода в ООП, но это очень распространенная практика в FP (то есть использование сопоставления с образцом для определения типа различаемого объединения и последующего воздействия на него).
Теодорос Чатзигианнакис
3
Остерегайтесь примеров школьной комнаты иерархии ОО, как животные. Реальные программы почти никогда не имеют таких чистых таксономий. Например, ericlippert.com/2015/04/27/wizards-and-warriors-part-one . Или, если вы хотите идти на попятную и подвергать сомнению всю парадигму: объектно-ориентированное программирование - это плохо .
jpmc26

Ответы:

18

Зависит. К сожалению, нет общего решения. Подумайте о своих требованиях и попытайтесь выяснить, что эти вещи должны делать.

Например, вы сказали, что утром разные животные делают разные вещи. Как насчет вас ввести способ getUp()или prepareForDay()или что - то подобное. Затем вы можете продолжить полиморфизм и позволить каждому животному выполнять свою утреннюю рутину.

Если вы хотите различать животных, то не следует хранить их без разбора в списке.

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

Роберт Бройтигам
источник
33

Это хороший вопрос, и многие люди пытаются понять, как использовать ОО. Я думаю, что большинство разработчиков борются с этим. Я бы хотел сказать, что большинство из них пройдет, но я не уверен, что это так. Большинство разработчиков, по моему опыту, в конечном итоге используют пакеты псевдо-OO-свойствами .

Во-первых, позвольте мне быть ясным. Это не ваша вина. То, как ОО обычно преподают, очень ошибочно. AnimalПример является главным виновным, ИМ. В основном, мы говорим, давайте поговорим об объектах, что они могут сделать. AnimalМожет eat()и это можетspeak() . Супер. Теперь создайте некоторых животных и напишите, как они едят и говорят. Теперь ты знаешь ОО, верно?

Проблема в том, что это идет в ОО с неправильного направления. Почему в этой программе есть животные и почему они должны говорить и есть?

Мне трудно думать о реальном использовании Animalтипа. Я уверен, что это существует, но давайте обсудим кое-что, о чем я думаю, легче думать: моделирование трафика. Предположим, мы хотим смоделировать трафик в различных сценариях. Вот некоторые основные вещи, которые мы должны сделать, чтобы это сделать.

Vehicle
Road
Signal

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

Давайте рассмотрим Vehicle. Какие возможности нужны автомобилю? Нужно путешествовать по дороге. Нужно уметь останавливаться на сигналах. Нужно уметь ориентироваться на перекрестках.

interface Vehicle {
  move(Road road);
  navigate(Road... intersection);
}

Это, вероятно, слишком просто, но это начало. Сейчас. А как насчет других вещей, которые может делать транспортное средство? Они могут свернуть с дороги и в кювет. Это часть симуляции? Нет, это не нужно У некоторых автомобилей и автобусов есть гидравлика, которая позволяет им подпрыгивать или становиться на колени соответственно. Это часть симуляции? Нет, это не нужно Большинство автомобилей сжигают бензин. Некоторые нет. Является ли силовая установка частью симуляции? Нет, это не нужно Размер колеса? Не нужно это GPS навигация? Информационно-развлекательная система? Не нужно им.

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

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

JimmyJames
источник
13
Это хороший ответ. «С этой целью, я думаю, часто лучше создавать ОО-интерфейсы из кода, который взаимодействует с ними». Абсолютно, и я бы сказал, что это единственный способ. Вы не можете узнать публичный контракт интерфейса только через реализацию, он всегда определяется с точки зрения его клиентов. (И, как примечание, это то, чем на самом деле является TDD.)
Винсент Савард
@VincentSavard "Я бы сказал, что это единственный способ." Вы правы. Я полагаю, что причина, по которой я не сделал это абсолютной, заключается в том, что, как только у вас возникла идея, вы можете немного улучшить интерфейс, а затем усовершенствовать его таким образом. В конечном счете, когда вы приступаете к медным действиям, это единственное, что имеет значение.
JimmyJames
@ jpmc26 Возможно, немного сильно сформулировано. Я не уверен, что согласен с тем, что это редко реализуется. Я не уверен, как интерфейсы могут быть полезны, если вы не используете их таким образом, кроме маркерных интерфейсов, которые я считаю ужасной идеей.
JimmyJames
9

TL; DR:

Подумайте об абстракции и методах, которые применяются ко всем подклассам и охватывают все, что вам нужно.

Давайте сначала остаться с вашим eat() примере.

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

Вернуться к программному обеспечению:

Поскольку Humanэкземпляр не будет есть без предварительных условий, я бы сделал метод Human's' eat(), washHands()и getDressed()если это не было сделано. Это не должно быть вашей работой какeat() знать, звонить, об этой особенности. Альтернативой упрямому человеку было бы выбрасывать исключение («Я не готов есть!»), Если предварительные условия не выполняются, оставляя вас разочарованными, но, по крайней мере, информированными о том, что еда не работает.

Как насчет makeEggs()?

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

Ральф Клеберхофф
источник
Я согласен с этим ответом. Нарек прав насчет запаха кода. Это дизайн интерфейса, который вонючий, так что исправьте это и ваше хорошо.
Джонатан ван де Вин
То, что описывает этот ответ, обычно называют принципом замены Лискова .
Филипп
2

Ответ довольно прост.

Как обращаться с объектами, которые могут сделать больше, чем вы ожидаете?

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

Например, если это утреннее время, и если это просто животное, то вы называете есть, в противном случае, если это человек, то сначала вызовите washHands, getDressed и только потом вызывайте eat. Как справиться с этим делом?

Например, в псевдокоде:

interface IEater { void Eat(); }
interface IMorningRoutinePerformer { void DoMorningRoutine(); }
interface IAnimal : IEater, IMorningPerformer;
interface IHuman : IEater, IMorningPerformer; 
{
  void WashHands();
  void GetDressed();
}

void MorningTime()
{
   IList<IMorningRoutinePerformer> items = Service.GetMorningPerformers();
   foreach(item in items) { item.DoMorningRoutine(); }
}

Теперь вы реализуете IMorningPerformerдля того, Animalчтобы просто выполнять прием пищи, а для Humanвас также внедряете его, чтобы мыть руки и одеваться. Вызывающий ваш метод MorningTime может не заботиться о том, человек это или животное. Все, что нужно, - это утренняя рутина, которую каждый объект делает превосходно благодаря ОО.

Полиморфизм умирает.

Или это?

Вам необходимо выяснить тип объекта

Почему предполагаем это? Я думаю, что это может быть неправильным предположением.

Есть ли общий подход для решения этих случаев?

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

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

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

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

Андрей Савиных
источник