Можно ли избежать использования проверки типов в этом примере? - PullRequest
5 голосов
/ 02 ноября 2009

Извините за плохой заголовок, не могу придумать краткий способ выразить это ..

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

Т.е. 3 интерфейса:

public interface IAnimal { ... }
public interface IEggLayer { public Egg layEgg(); }
public interface IMammal { public void sweat(); }

это будет сохранено как

private List<IAnimal> animals= new ArrayList<IAnimal>();

Таким образом, экземпляры, добавленные в список, могут также иметь тип IEggLayer или IMammal, которые имеют совершенно не связанные методы.

Мой первоначальный инстинкт должен был бы потом сделать

for(IAnimal animal : animals) {
  if(animal instanceof IEggLayer) {
    egg = ((IEggLayer)animal).layEgg();
  }
  if(animal instance of IMammal) {
    ((IMammal)animal).sweat();
  }
}

Но мне всегда говорили, что проверка типов является признаком того, что код действительно должен быть реорганизован.

Поскольку для одного объекта может быть возможным, например, [утконос], что означает, что один doFunction() здесь не подходит, возможно ли избежать использования проверки типов в этом случае, или это экземпляр, где проверка типов классифицируется как приемлемая?
Может быть, есть шаблон дизайна для этого?

Я также прошу прощения за надуманный пример ...
[Игнорируйте любые синтаксические ошибки, пожалуйста - он предназначен только для Java-подобного псевдокода]

Я добавил lvalue к использованию EggLayer, чтобы показать, что иногда возвращаемый тип важен

Ответы [ 6 ]

5 голосов
/ 02 ноября 2009

Очевидно, что вашему IAnimal интерфейсу (или некоторому его расширению) нужен метод callAllMethods, который каждый разработчик интерфейса может закодировать для полиморфного выполнения этой задачи - кажется, единственный подход OO-звука!

1 голос
/ 02 ноября 2009

Но мне всегда говорили, что проверка типов является признаком того, что код действительно должен быть реорганизован.

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

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

Но если вы не можете этого сделать, вам нужно будет выполнить некоторую проверку типов. Даже isEggLayer() / isMammal() включает проверку типа; например

if (x.isEggLayer()) {
    ((IEggLayer) x).layEgg();  // type cast is required.
}

Я полагаю, что вы можете скрыть проверку типа с помощью метода asEggLayer(); например,

public IEggLayer asEggLayer() {
    return ((IEggLayer) this);
}

или

// Not recommended ...
public IEggLayer asEggLayer() {
    return (this instanceof IEggLayer) ? ((IEggLayer) this) : null;
}

Но всегда происходит проверка типа и вероятность того, что она потерпит неудачу. Кроме того, все эти попытки скрыть проверку типов влекут за собой добавление «знаний» о подтипах в интерфейс супертипов, что означает, что их необходимо менять при добавлении новых подтипов.

1 голос
/ 02 ноября 2009

Я думаю, что вопрос об этом вопросе таков: что делает цикл? Цикл имеет цель и пытается что-то сделать с этими объектами. Это что-то может иметь метод в интерфейсе IAnimal, и реализации могут потеть или откладывать яйца по мере необходимости.

С точки зрения вашей проблемы с возвращаемым значением, вы будете возвращать ноль, вы ничего не можете с этим поделать, если поделитесь методами. Не стоит приводить внутри цикла, чтобы избежать дополнительного возврата null; чтобы удовлетворить компилятор. Вы можете, однако, сделать это более явным, используя дженерики:

  public interface IAnimal<R> {

         public R generalMethod();

  }

  public interface IEggLayer extends IAnimal<Egg> {

         public Egg generalMethod(); //not necessary, but the point is it works.

  }

  public interface IMammal extends IAnimal<Void> {

        public Void generalMethod();

  }

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

1 голос
/ 02 ноября 2009

в C #, вы должны сделать это прозрачно.

foreach(IEggLayer egglayer in animals) {
    egglayer.layEgg();
}

foreach(IMammal mammal in animals) {
    mammal.sweat();
}
0 голосов
/ 02 ноября 2009

Есть много способов сделать это. Точность, которая наиболее подходит, зависит от вашего контекста. Я собираюсь предложить ввести дополнительный слой косвенности. Это решает большинство проблем. Я предпочитаю проекты, которые избегают множественного наследования интерфейса (если бы я был президентом языка, я бы запретил это).

Я не думаю, что layEggsOrSweatOrDoBothIfYoureWeirdOrNoneIfYouCant() - это отличный метод для игры на Animal. Поэтому вместо добавления каждого Animal непосредственно к animals оберните каждый в отдельную оболочку. Я говорю «обертка» как общее имя - случайная операция, которую мы пытаемся выполнить, не имеет никакого смысла.

private final List<AnimalWrapper> animals =
    new ArrayList<AnimalWrapper>();

public void doStuff() {
    for (AnimalWrapper animal : animals) {
        animal.doStuff();
    }
}

Тогда нам нужен способ добавить обертки. Что-то вроде:

public void addPlatypus(final Platypus platypus) {
    animals.add(new AnimalWrapper() { public void doYourStuff() {
        platypus.sweat();
        platypus.layEgg();
    }});
}

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

/*** Poor context -> trouble ***/

public void addNormalMamal(final Mamal mamal) {
    animals.add(new AnimalWrapper() { public void doYourStuff() {
        mamal.sweat();
    }});
}
public void addNormalEggLayer(final EggLayer eggLayer) {
    animals.add(new AnimalWrapper() { public void doYourStuff() {
        eggLayer.layEgg();
    }});
}
public <T extends Mamal & EggLayer> void addMamalEggLayer(final T animal) {
    animals.add(new AnimalWrapper() { public void doYourStuff() {
        animal.sweat();
        animal.layEgg();
    }});
}
0 голосов
/ 02 ноября 2009

Почему бы не добавить методы в isAnimal:

public interface IAnimal {
   bool isEggLayer();
   bool isMammal();
}

Затем вы можете выполнить цикл и просто запрашивать этот логический аргумент каждый раз.

Обновление: Если это было рисование животного, то разумно иметь полностью закрытый класс, просто позвоните drawVehicle, и он нарисует корвет, сессну, мотоцикл, что угодно.

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

Если вы поместите аннотацию на каждый класс, вы можете иметь

@IsEggLayer
@IsMammal
public class Platypus() implements EggLayer, Mammal {
...
}

Это позволит вам создавать аспекты, которые вытаскивают все яйцекладки, и выполнять любые необходимые операции.

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

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

...