Как избежать удручения?

12

Мой вопрос касается особого случая суперкласса Animal.

  1. Моя Animalможет moveForward()и eat().
  2. Sealрасширяется Animal.
  3. Dogрасширяется Animal.
  4. И есть специальное существо, которое также расширяется, Animalназывается Human.
  5. Humanреализует также метод speak()(не реализован Animal).

В реализации абстрактного метода, который принимает, Animalя хотел бы использовать speak()метод. Это кажется невозможным без ударов. Джереми Миллер написал в своей статье, что пахнет удрученным.

Каково было бы решение избежать уныния в этой ситуации?

Барт Вебер
источник
6
Исправь свою модель. Использование существующей таксономической иерархии для моделирования иерархии классов обычно является неправильной идеей. Вы должны извлечь свои абстракции из существующего кода, а не создавать абстракции, а затем попытаться вписать в него код.
Эйфорическая
2
Если вы хотите, чтобы животные говорили, то способность говорить является частью того, что делает животное чем-то: artima.com/interfacedesign/PreferPoly.html
JeffO
8
двигаться вперед? как насчет крабов?
Фабио Марколини
Какой пункт # это в Effective Java, кстати?
Златовласка
1
Некоторые люди не могут говорить, поэтому если вы попытаетесь, вы получите исключение.
Тулаинс Кордова

Ответы:

12

Если у вас есть метод, который должен знать, имеет ли конкретный класс тип Human, чтобы что-то сделать, то вы нарушаете некоторые принципы SOLID , в частности:

  • Принцип открытого / закрытого - если в будущем вам нужно добавить новый тип животного, который может говорить (например, попугай), или сделать что-то определенное для этого типа, ваш существующий код должен будет измениться
  • Принцип сегрегации интерфейса - похоже, вы слишком много обобщаете. Животное может охватывать широкий спектр видов.

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

Что-то вроде этого :

public void MakeItSpeak( Human obj );

и не так

public void SpeakIfHuman( Animal obj );
BЈовић
источник
Можно также создать абстрактный метод для Animalвызываемого, canSpeakи каждая конкретная реализация должна определить, может ли он «говорить».
Брэндон
Но мне больше нравится твой ответ. Большая сложность возникает из-за того, что мы пытаемся относиться к чему-то как к чему-то другому
Брэндон
@ Брэндон Полагаю, в таком случае, если он не может «говорить», то выкинуть исключение. Или просто ничего не делать.
BЈовић
Таким образом, вы получаете перегрузку, как это: public void makeAnimalDoDailyThing(Animal animal) {animal.moveForward(); animal.eat()}иpublic void makeAnimalDoDailyThing(Human human) {human.moveForward(); human.eat(); human.speak();}
Барт Вебер
1
Пройдите весь путь - makeItSpeak (ISpeakingAnimal animal) - тогда у вас может быть ISpeakingAnimal, реализующий Animal. Вы также можете сделать makeItSpeak (Animal animal) {говорить, если экземпляр ISpeakingAnimal}, но у него слабый запах.
ptyx
6

Проблема не в том, что вы унижены, а в том, что вы унижаетесь Human. Вместо этого создайте интерфейс:

public interface CanSpeak{
    void speak();
}

public abstract class Animal{
    //....
}

public class Human extends Animal implements CanSpeak{
    public void speak(){
        //....
    }
}

public void mysteriousMethod(Animal animal){
    //....
    if(animal instanceof CanSpeak){
        ((CanSpeak)animal).speak();
    }else{
        //Throw exception or something
    }
    //....
}

Таким образом, условие не в том, что животное есть, а Humanв том, что оно может говорить. Это означает, что они mysteriousMethodмогут работать с другими, не относящимися к человеку подклассами до Animalтех пор, пока они реализуются CanSpeak.

Идан Арье
источник
в этом случае он должен принять в качестве аргумента экземпляр CanSpeak вместо Animal
Newtopian
1
@Newtopian Предполагая, что этому методу ничего не нужно Animal, и что все пользователи этого метода будут содержать объект, который они хотят отправить ему через CanSpeakссылку на тип (или даже Humanссылку на тип). Если бы это было так, этот метод мог бы использоваться Humanв первую очередь, и нам не нужно было бы его вводить CanSpeak.
Идан Арье
Избавление от интерфейса не связано с конкретностью в сигнатуре метода, вы можете избавиться от интерфейса, если и только если есть только Люди и когда-либо будут только люди, которые могут «CanSpeak».
Ньютопия
1
@Newtopian Причина, по которой мы CanSpeakвпервые представили, заключается не в том, что у нас есть что-то, что реализует это ( Human), а в том, что у нас есть то, что использует это (метод). Точка CanSpeakявляется разъединить этот метод из класса бетона Human. Если бы у нас не было методов, позволяющих обращаться с вещами по- CanSpeakдругому, не было бы смысла выделять такие вещи CanSpeak. Мы не создаем интерфейс только потому, что у нас есть метод ...
Идан Арье
2

Вы можете добавить Общение с животными. Собака лает, Человек говорит, Печать ... э-э-э ... Я не знаю, что делает печать.

Но, похоже, ваш метод предназначен для if (Animal is Human) Speak ();

Вопрос, который вы, возможно, захотите задать, - какая альтернатива? Трудно дать предложение, так как я не знаю точно, чего вы хотите достичь. Существуют теоретические ситуации, в которых даункастинг / апкастинг является лучшим подходом.

Эндрю Хоффман
источник
15
Уплотнение идет ой - ой , но лиса не определена.
2

В этом случае реализация по умолчанию speak()в AbstractAnimalклассе будет:

void speak() throws CantSpeakException {
  throw new CantSpeakException();
}

В этот момент у вас есть реализация по умолчанию в классе Abstract - и она ведет себя правильно.

try {
  thingy.speak();
} catch (CantSeakException e) {
  System.out.println("You can't talk to the " + thingy.name());
}

Да, это означает, что у вас есть попытки поймать разбросанные по коду для обработки каждого speak, но альтернативой этому является if(thingy is Human)вместо этого упаковка всех спиц.

Преимущество исключения состоит в том, что если в какой-то момент у вас есть что-то, что говорит (попугай), вам не нужно переопределять все свои тесты.


источник
1
Я не думаю, что это хорошая причина для использования исключения (если это не код Python).
Брайан Чен
1
Я не буду отказываться от голосования, потому что технически это сработает, но мне действительно не нравится такой дизайн. Зачем определять метод на родительском уровне, который родитель не может реализовать? Если вы не знаете, к какому типу относится объект (и если бы вы это сделали - у вас не возникло бы этой проблемы), вам нужно всегда использовать try / catch для проверки полностью исключаемого исключения. Вы могли бы даже использовать canSpeak()метод, чтобы справиться с этим лучше.
Брэндон
2
Я работал над проектом, в котором было множество дефектов, вызванных чьим-то решением переписать кучу рабочих методов для создания исключения, поскольку они используют устаревшую реализацию. Он мог бы исправить реализацию, но решил вместо этого бросать исключения везде. Естественно, мы вошли в QA с сотнями ошибок. Так что я предвзято против создания исключений в методах, которые не должны вызываться. Если они не должны быть вызваны, удалите их .
Брэндон
2
Альтернативой выбрасыванию исключения в этом случае может быть ничего не делать. В конце концов, они не могут говорить :)
BЈовић
@ Брэндон, есть разница между проектированием с самого начала, за исключением того, что позже его модернизировали. Можно также взглянуть на интерфейс с методом по умолчанию в Java8 или не вызывать исключения и просто молчать. Ключевой момент, однако, заключается в том, что во избежание необходимости понижения функции необходимо определить в передаваемом типе.
1

Даункинг иногда необходим и уместен. В частности, это часто уместно в тех случаях, когда у кого-то есть объекты, которые могут иметь или не иметь какую-то способность, и кто-то хочет использовать эту способность, когда она существует, при обработке объектов без этой способности каким-либо способом по умолчанию. В качестве простого примера, предположим, что a Stringспрашивают, равен ли он некоторому другому произвольному объекту. Чтобы одно Stringсовпадало с другим String, оно должно проверять длину и массив вспомогательных символов другой строки. Если a Stringспрашивают, равно ли оно a Dog, он не может получить доступ к длине Dog, но он не должен этого делать; вместо этого, если объект, с которым Stringдолжен сравнивать a, не являетсяStringсравнение должно использовать поведение по умолчанию (сообщая, что другой объект не равен).

Время, когда удушение следует считать наиболее сомнительным, это когда «известный» объект, о котором известно, что он имеет надлежащий тип. В общем, если известно, что объект является a Cat, следует использовать переменную типа Cat, а не переменную типа Animal, чтобы ссылаться на него. Однако бывают случаи, когда это не всегда работает. Например, Zooколлекция может содержать пары объектов в четных / нечетных слотах массива, ожидая, что объекты в каждой паре смогут воздействовать друг на друга, даже если они не могут воздействовать на объекты в других парах. В таком случае объекты в каждой паре все равно должны будут принимать неспецифический тип параметра, чтобы они могли синтаксически передавать объекты из любой другой пары. Таким образом, даже если CatэтоplayWith(Animal other)Метод будет работать только в том случае, если он otherбыл Cat, а Zooдолжен иметь возможность передавать ему элемент Animal[], поэтому его тип параметра должен быть, Animalа не Cat.

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

Supercat
источник
В случае строки у вас должен быть метод Object.equalToString(String string). Тогда у вас есть boolean String.equal(Object object) { return object.equalStoString(this); }Итак, нет необходимости в понижении: вы можете использовать динамическую диспетчеризацию.
Джорджио
@ Джорджио: Динамическая диспетчеризация имеет свое применение, но обычно она даже хуже, чем уныние.
суперкат
Кстати динамическая диспетчеризация вообще ещё хуже, чем даункинг? я думаю, что это наоборот
Брайан Чен
@BryanChen: Это зависит от вашей терминологии. Я не думаю, что Objectесть какой-либо equalStoStringвиртуальный метод, и я признаю, что я не знаю, как приведенный пример будет работать даже в Java, но в C # динамическая диспетчеризация (в отличие от виртуальной диспетчеризации) будет означать, что компилятор по существу имеет выполнить поиск имени на основе отражений при первом использовании метода в классе, который отличается от виртуальной диспетчеризации (которая просто выполняет вызов через слот в таблице виртуальных методов, которая должна содержать действительный адрес метода).
суперкат
С точки зрения моделирования, я бы предпочел динамическую диспетчеризацию в целом. Это также объектно-ориентированный способ выбора процедуры на основе типа ее входных параметров.
Джорджио
1

В реализации абстрактного метода, который принимает Animals, я хотел бы использовать метод speak ().

У вас есть несколько вариантов:

  • Используйте отражение, чтобы позвонить, speakесли он существует. Преимущество: нет зависимости от Human. Недостаток: теперь есть скрытая зависимость от имени «говорить».

  • Ввести новый интерфейс Speakerи снизить интерфейс. Это более гибко, чем в зависимости от конкретного типа бетона. Недостатком является то, что вы должны изменить его Humanдля реализации Speaker. Это не сработает, если вы не можете изменитьHuman

  • Унизить до Human. Это имеет тот недостаток, что вам придется изменять код всякий раз, когда вы хотите, чтобы другой подкласс говорил. В идеале вы хотите расширить приложения, добавляя код, не возвращаясь и не меняя старый код.

Кевин Клайн
источник