Вызов метода производного класса из цикла списка базовых указателей (OOD)

Проблема

Я столкнулся с простой проблемой, хотя я не могу придумать для нее подходящего OOD.

Что у меня есть:

  • Базовый класс
  • Подкласс, добавляющий новый метод foo()
  • Список указателей на экземпляры базового класса

Что мне нужно:

Мне нужно просмотреть этот список и вызвать foo() для объектов, поддерживающих этот метод, то есть объектов (или производных) вышеупомянутого подкласса. Или вообще говоря, мне нужен "не вонючий" полиморфный доступ к подклассу через список указателей на базовый класс.

Пример кода

class Entity {
    // ...
    // This class contains methods also needed by subclasses.
};

class SaveableEntity : public Entity {
public:
    virtual void save() = 0;
};

// SaveableEntity has multiple subclasses with specific save() implementations.

std::vector<Entity *> list;
for (Entity *entity : list) {
    // Here I need to save() the descendants of a SaveableEntity type.
}

Я придумал несколько идей, однако ни одна из них не кажется мне правильной. Вот некоторые из них:

Способ 1: dynamic_cast

Поскольку некоторые элементы можно сохранить, а некоторые нет, наиболее очевидным способом, который я вижу, является динамическое приведение:

std::vector<Entity *> list;
for (Entity *entity : list) {
    auto saveable = dynamic_cast<SaveableEntity *>(entity);
    if (saveable) {
        saveable->save();
    }
}

Однако использование dynamic_cast в этой ситуации выглядит как плохой OOD (поправьте меня, если я ошибаюсь). Кроме того, такой подход может легко привести к нарушению LSP.

Способ 2: переместите save() в базовый класс

Я мог бы удалить SaveableEntity и переместить метод save() в базу Entity. Однако это заставляет нас реализовать фиктивный метод:

class Entity {
    virtual void save() {
        // Do nothing, override in subclasses
    }
};

Это устраняет использование dynamic_cast, но фиктивный метод по-прежнему не кажется правильным: теперь базовый класс содержит информацию (метод save()), совершенно не связанную с ним.

Способ 3: применение шаблонов проектирования

  • Шаблон Стратегия: класс SaveStrategy и его подклассы, такие как NoSaveStrategy, SomeSaveStrategy, SomeOtherSaveStrategy и т. д. Опять же, наличие NoSaveStrategy возвращает нас к недостатку предыдущего метода: базовый класс должен знать конкретные детали о его подкласс, который кажется плохим дизайном.
  • Шаблоны Proxy или Decorator могут легко инкапсулировать dynamic_cast, однако это только скроет нежелательный код, но не избавит от самого плохого дизайна.
  • Добавьте несколько слоев «композиция вместо наследования» и т. д. и т. д.

Вопрос

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

Так какой подход к дизайну подходит в такой ситуации?


person kefir500    schedule 22.11.2018    source источник
comment
вы все еще можете реализовать чистую виртуальную функцию. это сделало бы это более или менее поведением по умолчанию. хотя я не уверен насчет вонючести :-/   -  person MauriceRandomNumber    schedule 22.11.2018
comment
Посмотрите на свою проблему под этим углом: интерфейс базового класса не поддерживает foo. И вы хотите сделать через указанный интерфейс (доступ к реальным объектам через указатели базового класса) что-то ( foo ), что он не поддерживает.   -  person Andrew Kashpur    schedule 22.11.2018


Ответы (2)


Существует решение № 4, поощряемое программированием, ориентированным на данные (о нем было отличное обсуждение на cppcon 2018, доступное на YouTube): иметь два списка. Один список предназначен для всех SavableEntity, а другой — для Entity, которые нельзя сохранить.

Теперь вы перебираете первый список и ->save() эти элементы.

Основное преимущество заключается в том, что вы выполняете итерацию только по релевантным объектам. С некоторым (вероятно, серьезным) рефакторингом у вас может быть коллекция объектов, а не указатели на некоторые из них. Это повысит локальность данных и резко уменьшит количество промахов кэша.

person YSC    schedule 22.11.2018

Моей первой идеей было дополнить базовый класс методом virtual bool trySave(). По умолчанию может быть return false;, а SaveableEntity обеспечивает следующее переопределение:

class SaveableEntity : public Entity {
public:
    virtual bool trySave() final override
    {
        save();
        return true;
    }

    // You could also make this protected.
    virtual void save() = 0;
};

Является ли это более подходящим, чем предложение @YSC для вашего конкретного случая, вы должны решить сами. Это похоже на перемещение save() в базовый класс, но значительно менее запутанно для пользователя.

person Max Langhof    schedule 22.11.2018
comment
Спасибо за ответ. Однако, хотя он улучшает метод № 2, он по-прежнему не решает всезнающий базовый класс, который (в идеале) ничего не должен знать о производных классах. - person kefir500; 26.11.2018