Допустимое использование экземпляра
Я новичок в Java и борюсь с проблемой дизайна. Я знаю, что использование instanceof
может указывать на дефект дизайна, и я понимаю часто заданные классы Animal/Dog/Cat
в качестве примера, заменяя bark()
и meow()
на makenoise()
и т.д.
Мой вопрос в том, что такое разумный дизайн, если мне нужно вызвать методы, которые не имеют соответствующего метода в зависимости от типа подкласса? Например, что, если я хочу вызвать новый метод biteleash()
, если класс является Dog
, но ничего не делать, если он Cat
?
Я считал, что biteleash()
в Animal
ничего не делает и переопределяет его в Dog
, но есть методы, подобные многим, так что это кажется неуклюжим решением. В аналогичном ключе, что, если вызывающему нужно сделать что-то другое в зависимости от того, какой подкласс он имеет, например. если подкласс является Cat
? Является ли instanceof
приемлемым здесь, или есть лучший способ?
public class Animal {
String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void makeNoise() {
System.out.println("Some noise for a generic animal!");
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeNoise() {
System.out.println("Meow");
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeNoise() {
System.out.println("Woof");
}
public void biteLeash() {
System.out.println("Leash snapped!");
}
}
import java.util.Random;
public class CodeExample {
public static void main(String[] args) {
Animal animal = getSomeAnimal();
System.out.println("My pet is called " + animal.getName());
animal.makeNoise();
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.biteLeash();
// do lots of other things because animal is a dog
// eg. sign up for puppy training lessons
}
}
private static Animal getSomeAnimal() {
Animal animal;
Random randomGenerator = new Random();
int randomInt = randomGenerator.nextInt(100);
if (randomInt < 50) {
animal = new Dog("Rover");
}
else {
animal = new Cat("Tiddles");
}
return animal;
}
}
Ответы
Ответ 1
Композиция поможет вам здесь и является идиоматической в Java.
Создайте интерфейс, называемый Leashable
. Это выполняется с помощью Dog
, но не Cat
.
Вместо того, чтобы использовать instanceof
, вы можете попытаться выполнить привязку ссылки к Leashable
, чтобы убедиться, что она реализована вашим конкретным объектом.
По-моему, вы должны продолжить в том же ключе: Создайте интерфейс NoisyAnimal
. Возможно, даже просто Noisy
, почему шум должен быть уместным только для животных? Реализация того, что для Parrot
, скажем, будет иметь технические проблемы за пределами Cat
или Dog
. Хорошие поддерживаемые программы изолируют области сложности и состава, которые помогут вам достичь этого.
Ответ 2
Вы не должны использовать конкретные классы. Сам экземпляр не является проблемой. Он существует по какой-то причине. Вы должны использовать интерфейсы для свободной связи, т.е. Ваш код не должен зависеть от конкретных реализаций класса. Я предлагаю вам использовать интерфейсы там, где это возможно (т.е. IAnimal вместо Animal)
Вместо проверки на Dog вы должны использовать интерфейс типа ILeashable (да, немного смешно для имени lol), а затем:
public interface ILeashable {
//add other methods which is connected to being on a leash
void biteLeash();
}
class Dog implements ILeashable {...}
Также нет никакого способа сделать это, есть определенные шаблоны, то есть декораторы или инверсия зависимостей, которые могут помочь вам в этом случае.
Ответ 3
Просто, чтобы вы знали, эта проблема, с которой вы сталкиваетесь, - это не то, с чем вы, как правило, сталкиваетесь в реальном мире. Если для какого-либо класса, который реализует интерфейс или абстрактный базовый класс, вам нужно иметь специфичную для реализации логику, это обычно происходит потому, что на каком-то более высоком уровне вам нужно получить производное свойство. Этот psuedocode для иллюстрации:
interface ISellable {
decimal getPrice();
}
class CaseItem : ISellable {
int numItemsInCase;
decimal pricePerUnit;
decimal getPrice() {
return numItemsInCase*pricePerUnit;
}
}
class IndividualItem : ISellable{
decimal pricePerUnit;
decimal getPrice() {
return pricePerUnit;
}
}
main() {
aCaseItem = new CaseItem { pricePerUnit = 2, numItemsInCase=5 }; //getPrice() returns 10
anIndividualItem = new IndividualItem { pricePerUnit = 5 }; //getPrice() returns 5
List<ISellable> order = new List<ISellable>();
order.Add(aCaseItem);
order.Add(anIndividualItem);
print getOrderTotal(order);
}
function getOrderTotal(List<ISellable> sellableItems) {
return sellableItems.Sum(i => i.getPrice());
}
Обратите внимание, что я использую интерфейс, чтобы абстрагировать концепцию цены товара, но когда я нахожусь в основном методе, я могу легко создавать экземпляры определенного типа, чтобы управлять поведением двух классов.
Однако, когда мне нужно получить цену, я ссылаюсь на элементы как список ISellable, который только предоставляет метод getPrice() для моего удобства.
Лично я всегда считал, что метафоре животных не хватает. Он не объясняет эту концепцию таким образом, что имеет смысл, и она не подсказывает вам, как ее использовать в реальном мире.
Ответ 4
Подумайте об этом так: какое событие заставляет Dog
укусить его поводок? Или иначе говоря, какова ваша мотивация, чтобы заставить это выполнить это действие?
В вашем примере фактически нет. Так получилось, что в вашем основном методе вы решили поставить чек, и если животное, которое вы случайно создали, - это Dog
, вы делаете это для определенных вещей собаки. Это не то, как работает код в реальном мире.
Когда вы пишете код, вы хотите решить какую-то проблему. Чтобы придерживаться примера животных, пусть притворяется, что вы пишете игру, в которой у вас есть домашние животные, которые реагируют на определенные события, такие как включение вакуума или получение удовольствия. Из этого предложения мы можем создать разумную иерархию классов:
interface Animal {
void reactToVacuum();
void receiveTreat();
}
class Dog implements Animal {
public void biteLeash() {
System.out.println("Leash snapped!");
}
public void wiggleTail() {
System.out.println("Tail is wiggling!");
}
@Override
public void reactToVacuum() {
biteLeash();
}
@Override
public void receiveTreat() {
wiggleTail();
}
}
Как вы можете видеть, укусы поводка происходят в ответ на событие, а именно на включение вакуума.
Для более реалистичного примера возьмите иерархию представлений Android. A View
- базовый класс для каждого элемента управления на экране, например. a Button
, a EditText
и так далее. View.onDraw()
определяется для каждого вида, но в зависимости от того, что у вас есть, произойдет что-то другое. EditText
, например, сделает что-то вроде drawCursor()
и drawText()
.
Как вы можете видеть, ответ на вопрос "Какое событие вызывает EditText, чтобы нарисовать курсор" - "Это нужно сделать на экране". Нет instanceof
или проверка условий.
Ответ 5
Как и в вашем примере - некоторые подклассы могут иметь разные методы.
Это типичный сценарий, в котором вы должны использовать instanceof
.
Ответ 6
Если ваши подклассы исправлены, вы можете использовать animalType
, чтобы избежать экземпляра.
Я предпочитаю, чтобы u мог использовать интерфейс, чтобы проверить, доступна ли эта функция в подклассе, но это не представляется возможным для большого количества зависимых от подкласса методов. (решение задано user2710256
).
Enum Type {
DOG,CAT ;
}
public abstract class Animal {
String name;
Type type;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void makeNoise() {
System.out.println("Some noise for a generic animal!");
}
abstract Type getType();
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeNoise() {
System.out.println("Meow");
}
public Type getType() {
return Type.CAT;
}
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeNoise() {
System.out.println("Woof");
}
public void biteLeash() {
System.out.println("Leash snapped!");
}
public Type getType(){
return Type.DOG;
}
}
import java.util.Random;
public class CodeExample {
public static void main(String[] args) {
Animal animal = getSomeAnimal();
System.out.println("My pet is called " + animal.getName());
animal.makeNoise();
switch(animal.getType())
{
case DOG:
{
Dog dog = (Dog) animal;
dog.biteLeash();
// do lots of other things because animal is a dog
// eg. sign up for puppy training lessons
}
case CAT:
{
// do cat stuff
}
default:
throw new Exception("Invalid Animal");
}
}
private static Animal getSomeAnimal() {
Animal animal;
Random randomGenerator = new Random();
int randomInt = randomGenerator.nextInt(100);
if (randomInt < 50) {
animal = new Dog("Rover");
}
else {
animal = new Cat("Tiddles");
}
return animal;
}
}
Ответ 7
Конечно, наличие biteleash()
в Cat
не имеет смысла, потому что вы не ходите кошками на поводке. Поэтому Animal
не должен иметь biteleash()
. Возможно, вы должны поместить метод playWith()
в Animal
, в котором Dog
будет biteleash()
. Я пытаюсь сказать, что вам, вероятно, нужно быть более общим. Вы не должны заботиться о том, чтобы собака вручную вызывала biteleash()
. Добавление Ferret
в ваш зоопарк потребует добавления возможности, чтобы животное было Ferret
, так как оно также может biteleash()
.
И, возможно, метод shouldTerminate()
, который возвращает true
в Cat
s. Вы, вероятно, захотите также закончить работу над Tortoise
.
Как правило, люди считают это плохой практикой, если для добавления нового подкласса потребуются изменения в коде с использованием этого класса.
Ответ 8
Если я могу, это очень непрактичный способ сделать это, я не уверен, что вам нужно, но instanceof будет утомительным, то, что у вас получается, - это высокая связь, которая не идет, когда вы делаете OOD,
Вот что я сделал бы:
Сначала избавьтесь от класса Animal и сделайте его интерфейсом следующим
public interface Animal {
public String getName();
public void makeNoise();
public void performAggression();
}
а затем в песке:
public class Dog implements Animal {
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void makeNoise() {
System.out.println("Woof");
}
@Override
public void performAggression(){
biteLeash();
}
private void biteLeash() {
System.out.println("Leash snapped!");
}
}
Наконец, cat:
public class Cat implements Animal {
private String name;
public cat(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void makeNoise() {
System.out.println("Meow");
}
@Override
public void performAggression(){
hiss();
}
private void hiss() {
System.out.println("Hisssssss");
}
}
Таким образом, вы можете в своем основном классе просто сделать следующее:
public class test{
public test(){
dog = new Dog("King");
cat = new Cat("MissPrincess");
}
public void performAggression(Animal animal){
animal.performAggression();
}
Бонус, который вы получаете, вы можете передать любому классу, который вы хотите использовать, до тех пор, пока он реализует тот же интерфейс, что и в тестовом классе выше в методе performAggression(Animal animal)
Все тестовые классы должны знать эти три метода, все остальное можно сделать внутренне в соответствующих классах, если тестовый класс не должен знать об этом, поэтому видимость "private" на biteLeash()
и hiss()
методы.
В итоге вы получите очень низкое сцепление и легко отредактируете позже.
Выполняя это, вы также эффективно достигаете высокой сплоченности, потому что вам не нужно втягиваться в милю длиной, если /ifelse/ifelse... (так далее), чтобы определить, с каким классом вы имеете дело.
Ответ 9
Оператор instanceof
имеет тенденцию масштабироваться только тогда, когда применяется к интерфейсам. Причина в том, что интерфейс разделяет ваш домен на два: некоторые объекты являются экземплярами классов, которые реализуют интерфейс, а другие - нет. Вполне вероятно, что введение нового класса не нарушит ваши текущие алгоритмы, которые полагаются на if (x instanceof ISomething)
, потому что новый класс либо реализует ISomething
, либо нет. Если это так, тело if
должно отличать x
до ISomething
и эффективно использовать его; если это не так, то тело if
, вероятно, не представляет интереса к x
.
Это отличается от подхода, в котором instanceof
применяется к классам. Например, тюлени коры, но они не собаки: вы не можете наследовать Seal
от Dog
или наоборот. Следовательно, как вы намекали, вы осознаете, что, если вы используете instanceof
, чтобы проверить, можете ли вы сделать кору x
, вы можете закончиться такими мерзостями, как это:
if (x instanceof Dog) {
((Dog)x).bark();
}
else if (x instanceof Seal) {
((Seal)x).bark();
}
Решение состоит в том, чтобы иметь интерфейс ICanBark
и проверить его:
if (x instanceof ICanBark) {
((ICanBark)x).bark();
}
Это хорошо масштабируется, если вы добавите больше животных, могут ли они лаять или нет. Но он не очень хорошо масштабируется, если вы хотите добавить больше шумов, потому что у вас может возникнуть соблазн ввести больше интерфейсов, таких как ICanMeow
и т.п.
Решение не должно быть в такой ситуации; ICanBark
и ICanMeow
- это, очевидно, одно и то же поведение IMakeNoise
, так что существует один интерфейс, реализованный Dog
, Cat
и Seal
.
Я не являюсь поклонником примера животного/собаки/кошки/шума в контексте полиморфизма. Вместо этого я покажу вам пример реального мира системы компонентов, над которой я работал некоторое время назад. В составной системе компоненты устанавливаются на сущности, чтобы дать им дополнительное поведение. Например, монстр в видеоигре - это объект с установленным компонентом (этот материал вдохновлен движком Unity 5):
- IAudioSource
- IRenderer
- IMovementAI
- ILiving
Четыре класса, которые могут реализовать это поведение, могут быть чем-то вроде:
- ScaryGruntingSound
- MeshOpenGLRender
- StupidQuadrupedAI
- Armorless
Если у вас есть коллекция объектов, тогда она полностью приемлема (и она будет хорошо масштабироваться), если вы instanceof
для интерфейсов:
for (Entity e : entities) {
for (Component c : e.getComponents()) {
if (c instanceof IAudioSource) {
((IAudioSource)c).play();
}
...
if (c instanceof IUserInput) {
((IUserInput)c).poll();
}
}
}
Поскольку монстры не контролируются игроком, c instanceof IUserInput
терпит неудачу, и все счастливы. Причина, по которой это использование пород instanceof
и ICanBark/ICanMeow
отстой, состоит в том, что IAudioSource/IRenderer/IMovementAI/ILiving
- это четыре совершенно несвязанных аспекта того, что значит быть монстром, тогда как ICanBark/ICanMeow
являются двумя проявлениями одного и того же аспекта (и должны быть вместо этого обрабатываются путем объединения их в IMakeNoise
).
Edit
I did consider having biteleash() in Animal which does nothing, and overriding it in Dog
Это неуклюжие решения, и я считаю, что это может быть вызвано только соображениями производительности. Если я не ошибаюсь, Swing для Java иногда использует такой шаблон.