C ++ - Чрезмерное использование виртуальных методов? - PullRequest
5 голосов
/ 25 ноября 2010

Недавно мне дали задание, в котором я должен был реализовать нечто похожее на следующее:

Есть некоторые животные с определенными атрибутами, такими как:

Dog1: имя: tery, цвет: белый, любимый напиток: виноградный сок
Dog2: имя: чива, цвет: черный, любимый напиток: лимонад
Птица1: имя: tweety, canfly: да, cansing: нет
Bird2: имя: парирование, canfly: нет, cansing: да

Как бы вы сделали это в C ++ эффективно, используя практики ООП?

Я сделал что-то вроде этого:

class Animal {  
    Animal(...);  
    ...  
    public String getName() const;  
    public void setName(string s);  
    ...  
    private:  
    String name;  
}  

class Bird : public Animal {  
    Bird(...);  

    public bool canFly() const;  
    public void setCanFly(bool b);  
    ...

    private:  
    bool canFly;  
    bool canSing;  
}  

class Dog : public Animal {  
    ...  
}  

Проблема с этой реализацией в том, что я не могу извлечь выгоду из полиморфизма:

Animal* p = new Anima(...);  
...  
p->canFly();  

и я должен использовать кастинг:

((Bird*)p)->canFly();  

В конце меня критиковали за то, что я не использовал виртуальные функции в базовом классе и использовал приведение вместо ООП.

Но, по моему мнению, здесь не имеет смысла использовать виртуальные функции, потому что getName () должен быть в базовом классе, чтобы избежать нескольких реализаций одного и того же метода. И canFly не является допустимым свойством для собак, например.

Тогда мне нужно было бы определить нечто абсурдное, подобное этому, для каждого другого (будущего) животного, которое также наследует от базового класса, что привело бы к ненужным накладным расходам:

bool Dog::canFly () const {
return false;
}

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

Ответы [ 9 ]

7 голосов
/ 25 ноября 2010

Ну, вам не нужен ни один базовый класс.Учтите это:

Animal
  |
  |--Flying Animal
  |        |---Bird
  |
  |--Non Flying Animal
           |---Dog

где:

class Animal
{
public:
  virtual bool CanFly () = 0;
  String Name ();
};

class Flying : public Animal
{
public:
  virtual bool CanFly () { return true; }
};

class NonFlying : public Animal
{
public:
  virtual bool CanFly () { return false; }
};

class Bird : public Flying
{
};

class Dog : public NonFlying
{
};

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

РЕДАКТИРОВАТЬ: Композиция,Наличие иерархии, где каждый уровень в иерархии представляет меньшую группу членов (собак меньше, чем животных), представляет проблему того, как разделить множество всех возможных типов на иерархию.Как отметил в комментариях Лагербаер, некоторые птицы не могут летать.

Поэтому вместо создания сложного дерева создайте простое дерево (или его нет), и каждое животное будет содержать список характеристик этого животного.:

class Animal
{
public:
   String Name ();
   List <Characteristic> Characteristics ();
};

class Characteristic
{
public:
   String Name ();
};

class CanFly : public Characteristic
{
public:
  bool CanFly ();
};

class Legs : public Characteristic
{
public:
  int NumberOfLegs ();
};

И затем, чтобы создать собаку:

Animal *CreateDog ()
{
  Animal *dog = new Animal;
  dog->Characteristics ()->Add (new CanFly (false));
  dog->Characteristics ()->Add (new NumberOfLegs (4));
  return dog;
}

и создать птицу:

Animal *CreateBird ()
{
  Animal *bird = new Animal;
  bird->Characteristics ()->Add (new CanFly (true));
  bird->Characteristics ()->Add (new NumberOfLegs (2));
  return bird;
}

У этого есть два преимущества:

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

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

7 голосов
/ 25 ноября 2010

Конечно, 'canFly' является допустимым свойством для собаки, она просто вернет false.

Нет смысла использовать canFly вообще, если вы реализуете его только тогда, когда оно должно быть истинным.В вашем примере, к тому моменту, как вы применили кейс в стиле c к летающему животному, вы уже указали на тип животного, и вам не нужно вызывать canFly, потому что вы уже знаетеответ.

Если вы действительно не хотите использовать canFly на большом количестве нелетных животных, то реализуйте виртуальный bool canFly () const {return false;} в вашем базовом классе, и просто переопределите его у летающих животных.

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

5 голосов
/ 25 ноября 2010

Для решения технической проблемы это неверно:

((Bird*)p)->canFly(); 

Это приведение в стиле C выполняет static_cast;если p указывает на Dog, приведение будет успешным, но результат будет неверным.Плохие вещи случаются.

Если вы не знаете наиболее производный тип указанного объекта и не можете определить его тип с помощью указателя базового класса, вам нужно использовать dynamic_cast:

if (Bird* bp = dynamic_cast<Bird*>(p)) {
    // p points to a Bird
}
else {
    // p points to something else
}

dynamic_cast возвращает нулевой указатель в случае неудачного приведения, позволяя проверить тип объекта.


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

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

3 голосов
/ 25 ноября 2010
struct Animal {
  std::string name;
  std::string favoriteDrink;
  bool canFly;
  bool canSing;
};

Не стесняйтесь обернуть get / setters вокруг участников, если это делает вас счастливыми.

Но люди склонны забывать одну вещь: полиморфизм - это поведение .Речь идет о создании разных классов, которые выглядят одинаково, но ведут себя по-разному.

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

Фактическое поведение не требуется ни для одного из животных.Единственные операции, которые нам нужны, - это возможность спросить «как его зовут», «может ли он летать», «может ли он петь» (и, конечно, «будет ли он смешиваться?»)

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

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

Если вам когда-либо понадобитсяЧтобы добавить метод Fly(), вам могут потребоваться другие классы.Механика полета различна для воробья, орла и летучей мыши (хотя даже это зависит от цели. В зависимости от того, на каком уровне абстракции работает приложение, процедура «полета» может состоять не более чем из установки другого * 1028).* флаг где-нибудь, или, возможно, предоставление животному положительной ненулевой высоты, и в этом случае та же самая реализация может использоваться для любого летающего животного).

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

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

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

См. также мой пост в блоге на тему

3 голосов
/ 25 ноября 2010

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

В случае использования canFly и canSing, где члены данных в базовом классе поддерживают инвариантную реализацию во всех подклассах , поэтому использование этих методов get / set virtual не имеет никакого смысла для меня.

Лучшим кандидатом на virtual будут соответствующие методы fly и sing, в которых реализация базового класса может выдаваться, и только когда свойства установлены true, будет предоставлена ​​реализация для конкретного животного.в подклассе.

1 голос
/ 25 ноября 2010

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

Просто представьте себе простую игру, в которой базовый класс - GameObject, а методы update() и draw() - виртуальные.Затем вы наследуете другие классы, например PlayerObject, EnemyObject, PowerUpObject и т. Д.

В своем основном цикле вы можете сделать что-то вроде этого:

GameObject *p = firstObject;
while(p)
{
    p->update();
    p = p->nextObject;
}

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

0 голосов
/ 25 ноября 2010

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

0 голосов
/ 25 ноября 2010

Объявление чего-то виртуального не мешает вам реализовать это в базовом классе.

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

Почему возвращение false из canFly () для собаки может быть проблемой?Некоторые птицы не могут летать, и есть не птицы, которые могут летать.

0 голосов
/ 25 ноября 2010

Я думаю, что вы правы. Добавление всех мыслимых свойств, которые какое-либо семейство животных может иметь к базовому классу Animal, просто глупо и приводит к чрезмерным накладным расходам.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...