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

Я прочитал несколько тем о динамических приведениях в С++, и все они полны людей, утверждающих, что это указывает на плохой дизайн. В других языках я никогда особо не задумывался при проверке типа объекта. Я никогда не использую его как альтернативу полиморфизму и только тогда, когда сильная связь кажется вполне приемлемой. С одной из таких ситуаций я сталкиваюсь довольно часто: у меня есть список (я использую std::vector в C++) объектов, производных от общего базового класса. Список управляется объектом, которому разрешено знать различные подклассы (часто это небольшая иерархия частных классов внутри класса управляющих объектов). Сохраняя их в одном списке (массив, вектор, ..), я все еще могу извлечь выгоду из полиморфизма, но когда операция предназначена для воздействия на объекты определенного подкласса, я использую динамическое приведение или что-то подобное.

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

Если мое описание слишком абстрактно, я могу написать простой пример на C++ (редактирование: см. ниже).

class EntityContacts {
private:
  class EntityContact {
  private:
    virtual void someVirtualFunction() { };            // Only there to make dynamic_cast work
  public:
      b2Contact* m_contactData;
  };

  class InternalEntityContact : public EntityContact {
  public:
    InternalEntityContact(b2Fixture* fixture1, b2Fixture* fixture2){
        m_internalFixture1 = fixture1;
        m_internalFixture2 = fixture2;
    };

    b2Fixture* m_internalFixture1;
    b2Fixture* m_internalFixture2;
  };

  class ExternalEntityContact : public EntityContact {
  public:
    ExternalEntityContact(b2Fixture* internalFixture, b2Fixture* externalFixture){
        m_internalFixture = internalFixture;
        m_externalFixture = externalFixture;
    };

    b2Fixture* m_internalFixture;
    b2Fixture* m_externalFixture;
  };

  PhysicsEntity* m_entity;
  std::vector<EntityContact*> m_contacts;
public:
  EntityContacts(PhysicsEntity* entity)
  {
    m_entity = entity;
  }

  void addContact(b2Contact* contactData)
  {
    // Create object for internal or external contact
    EntityContact* newContact;
    if (m_entity->isExternalContact(contactData)) {
        b2Fixture* iFixture;
        b2Fixture* eFixture;
        m_entity->getContactInExFixtures(contactData, iFixture, eFixture);
        newContact = new ExternalEntityContact(iFixture, eFixture);
    }
    else
        newContact = new InternalEntityContact(contactData->GetFixtureA(), contactData->GetFixtureB());

    // Add object to vector
    m_contacts.push_back(newContact);
  };

  int getExternalEntityContactCount(PhysicsEntity* entity)
  {
    // Return number of external contacts with the entity
    int result = 0;
    for (int i = 0; i < m_contacts.size(); ++i) {
        ExternalEntityContact* externalContact = dynamic_cast<ExternalEntityContact*>(m_contacts[i]);
        if (externalContact != NULL && getFixtureEntity(externalContact->m_externalFixture) == entity)
            result++;
    }
    return result;
  }
};

Это упрощенная версия класса, который я использую для обнаружения столкновений в игре, использующей физику box2d. Я надеюсь, что детали box2d не слишком отвлекают от того, что я пытаюсь показать. У меня есть очень похожий класс Event, который создает различные типы обработчиков событий, которые структурированы одинаково (с подклассами базового класса EventHandler вместо EntityContact).


person Double Dan    schedule 21.03.2013    source источник
comment
не зная, какие другие языки вы используете, обратите внимание, что, например, instanceof в Java также не одобряется. Есть законные способы использования, но в целом это не соответствует концепциям OO. С точки зрения объектно-ориентированного программирования можно спросить, зачем объекту-менеджеру знать о подклассах, если действие над конкретным подклассом должно быть абстрагировано и т. д.   -  person msam    schedule 21.03.2013
comment
dynamic_cast кажется мне ярлыком дизайна. Все можно сделать производным от общего базового класса (class Base {};) и сохранить в списке (std::list<Base> l). Но когда мы используем dynamic_cast позже, это приводит к нескольким вопросам: должны ли мы использовать наследование? Или мы достаточно абстрагировались? - у нас есть другие доступные инструменты (шаблоны и интерфейсы)   -  person andre    schedule 21.03.2013


Ответы (6)


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

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

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

Если вы часто видите это, это, как правило, указывает на отсутствие абстракции в ваших классах — например, вместо fly или attempt_to_fly вам действительно может понадобиться член travel, и это зависит от конкретного животного. делать ли это плаванием, ползанием, ходьбой, полетом и т. д.

person Jerry Coffin    schedule 21.03.2013

Этот

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

похоже, что моделирование объекта неправильно. У вас есть список, содержащий экземпляры подклассов, но на самом деле они не являются подклассами, поскольку вы не можете воздействовать на них все одинаково (Лисков и т. д.).

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

person Brian Agnew    schedule 21.03.2013

Вопрос довольно широкий, поэтому следует учитывать некоторые общие моменты:

  • Это особенность языка по какой-то причине, а это означает, что есть допустимые варианты использования.
  • В общем, это не хорошо и не плохо.
  • Проверьте, подходит ли оно для вашей проблемы.
  • Знайте альтернативы для решения вашей проблемы.
  • Сравнение его с другими языками должно включать вопрос о том, возможны ли альтернативы C++/C++11 и в этом другом языке.
  • dynamic_cast дает определенную стоимость. Если речь идет о производительности во время выполнения, сравните ее со стоимостью альтернатив.
  • Проверки выполняются во время выполнения, что создает определенный риск того, что вы доставите программное обеспечение с ошибками, если оно не будет протестировано должным образом. При статической проверке типов компилятор может помочь вам и даже гарантировать, что определенные проблемы/ошибки не возникнут.
person Daniel Frey    schedule 21.03.2013

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

person SomeWittyUsername    schedule 21.03.2013

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

person Joel Falcou    schedule 21.03.2013

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

Поскольку мы не знаем вашей конкретной проблемы, я могу только дать совет, исходя из того, что вы нам сказали. Вы говорите, что у вас есть контейнер полиморфных базовых типов. Например:

std::vector<Base*> bases;

Если вы собираетесь использовать какие-либо объекты, которые вы помещаете в этот контейнер, специфичным для производных образом, то на самом деле это не контейнер базовых объектов, не так ли? Весь смысл наличия контейнера указателей на Base объектов заключается в том, что вы можете перебирать их и обрабатывать их все как Base объекты. Если вам нужно dynamic_cast привести к какому-то Derived типу, значит, вы злоупотребили полиморфным поведением Base*.

Если Derived наследуется от Base, то Derived является Base. Проще говоря, если вы используете полиморфный указатель Base на тип Derived, то вы должны использовать только те функции Derived, которые делают его Base.

person Joseph Mansfield    schedule 21.03.2013