Допустимое использование экземпляра

Я новичок в 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 иногда использует такой шаблон.