Дизайн класса C ++, имеющий несколько интерфейсов для каждого отдельного поведения - PullRequest
5 голосов
/ 06 августа 2011

Это мой первый пост, так что будьте добры. Это вопрос интервью, который я недавно получил, но не смог найти ответ после поиска (Google, C ++ FAQ и т. Д.).

Есть интерфейс I1 с поведением b1 (). Есть 3 класса A, B, C. Все эти классы реализуют интерфейс I1 путем переопределения b1 (). Существует четвертый класс D, который имеет поведение (b1), определенное в интерфейсе I1, и дополнительное поведение b2

Вопрос в том, как вы проектируете класс D.

Мой ответ состоял в том, чтобы создать еще один интерфейс I2, который определяет поведение b2 (), и заставить класс D реализовать как I1, так и I2 (множественное наследование в C ++) путем переопределения обоих b1 () и b2 ()

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

Я мог только подумать о добавлении большего количества интерфейсов (I3, I4 и т. Д.) И выполнении множественного наследования, но я знаю, что здесь вы в конечном итоге получаете огромное количество производных классов с соответствующими интерфейсами

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

PS: После серьезного размышления об этом, я думаю, что ответ может заключаться в использовании шаблона проектирования, но, глядя на общие шаблоны проектирования, я не смог найти ни одного, соответствующего этой проблеме

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

Сначала позвольте мне поблагодарить @Nawaz, @Alexandre и @Sjoerd за ваш ценный вклад. Я только начинаю изучать аспекты проектирования в C ++ / шаблонах проектирования, поэтому извините за незнание этого вопроса.

Пример паттерна Vistor от @Nawaz был действительно полезным, но я думаю, что это только частный случай исходной проблемы, заданной интервьюером. @ Александр правильно указал сценарии здесь.

Позвольте мне объяснить еще один аспект этого. Когда мы говорим о поведении, нам нужно сгруппировать их на основе

1) распространенное поведение, связанное с группой объектов или объектом. (Это интуитивно понятно или можно наблюдать как в реальном мире) Например: поведение Чувака (на примере @Nawaz) - я гуляю, ем, учусь и т. д.

2) необычное или очень своеобразное поведение, связанное с группой (это противоречит интуиции) Например: просто ради аргумента, рассмотрим Чувак, который сочиняет музыку (я знаю, этот пример не идеален)

3) совершенно не связанное с группой поведение. Я не могу придумать пример, но я хочу сказать, что по какой-то странной причине нам нужно придать объекту такое поведение.

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

Если взять пример IDude, нам потребуются следующие изменения, чтобы создать Чувака, который может сочинять музыку.

    class IComposerBehavior;

    class IComposer
    {
       public:
          virtual ~IComposer() {}
          virtual void accept(IComposerBehavior *behaviour) = 0 ;
    };

    class IComposerBehavior
    {
       public:
          virtual ~IComposerBehavior() {}
          virtual void visit(IComposer * ) = 0;
    };

    class Dude : public IDude, public IComposer
    {
        public:
          virtual void accept(IDudeBehavior *behaviour)
          {
              behaviour->visit(this);
          }
          virtual void accept(IComposerBehavior *behaviour)
          {
              behaviour->visit(this);
          }
    };

    class SymphonyComposerBehavior : public IComposerBehavior
    {
      public:
         virtual void visit(IComposer *dude) { cout << "Dude is composing a symphony" << endl;   }
    };

Точно так же нам нужно изменить код клиента также для учета SymphonyComposerBehavior.

Таким образом, в итоге мы изменили и код класса Dude, и код клиента, что сводит на нет влияние шаблона.

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

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

- Initially We are only producing Toy Robots
- Then Human helper Robots
- Then Self Healing Robots (would just correct itself when defective)
- Then Humanoid Robots
- Then machine Robots (that are not human like but as a substitute for any machine you can think of) . I have deliberately put this here even though its place should be before with a correct evolution scheme.
- finally Humanoid Robots with life (atleast we can dream :-) )

Так что, если мы знаем полный список роботов до разработки приложения, мы могли бы придумать лучший дизайн,но как мы проектируем, когда каждый новый тип вводится последовательно в указанном выше порядке.Суть в том, что мы знаем, что у робота есть определенное поведение или характеристики, но когда нам нужно внедрить необычные функции (например, самоисцеление, робот-машина), как мы поступим?

Спасибо.

Ответы [ 2 ]

6 голосов
/ 06 августа 2011

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

Да, шаблон посетителя позволяет добавлять новое поведение (я) к существующей структуре классов без какого-либо дальнейшего добавления / вывода класса / интерфейса к структуре. Все, что требует от вас реализации только поведения, и шаблон посетителя позволяет добавить это поведение в структуру классов.

Прочитайте эту запись в вики; это объясняет закономерность:

Вот одна простая реализация шаблона посетителя:

class IDudeBehavior;

class IDude
{
   public:
      virtual ~IDude() {}
      virtual void accept(IDudeBehavior *behaviour) = 0 ;
};

class IDudeBehavior
{
   public:
      virtual ~IDudeBehavior() {}
      virtual void visit(IDude * ) = 0;
};

class Dude : public IDude
{
    public:
      virtual void accept(IDudeBehavior *behaviour)
      {
          behaviour->visit(this);
      }
};

class LaughDudeBehavior : public IDudeBehavior
{
  public:
     virtual void visit(IDude *dude) { cout << "Dude is Laughing" << endl; }
};

class WalkDudeBehavior : public IDudeBehavior
{
  public:
     virtual void visit(IDude *dude) { cout << "Dude is Walking" << endl; }
};
int main() {
        IDude *dude = new Dude();
        dude->accept(new LaughDudeBehavior());
        dude->accept(new WalkDudeBehavior());
        return 0;
}

Онлайн демо: http://ideone.com/Kqqdt

На данный момент класс Dude имеет только два поведения, а именно LaughDudeBehavior и WalkDudeBehavior, но, поскольку он является шаблоном посетителя, вы можете добавить любое количество поведения в Dude, не редактируя класс Dude. Например, если вы хотите добавить EatDudeBehavior и StudyCplusCplusDudeBehavior, тогда все, что вам нужно для реализации IDudeBehavior:

class EatDudeBehavior : public IDudeBehavior
{
  public:
     virtual void visit(IDude *dude) { cout << "Dude is Eating" << endl; }
};

class StudyCplusCplusDudeBehavior : public IDudeBehavior
{
  public:
     virtual void visit(IDude *dude) { cout << "Dude is Studying C++" << endl; }
};

И тогда вам нужно принять это поведение как:

dude->accept(new EatDudeBehavior ()); 
dude->accept(new StudyCplusCplusDudeBehavior ());

Демо после добавления новых вариантов поведения: http://ideone.com/9jdEv


Избегать утечки памяти

Существует одна проблема с приведенным выше кодом. Все выглядит хорошо, за исключением того, что утечка памяти. Программа создает много экземпляров классов, используя new, но никогда не освобождает их, используя delete. Так что вам нужно подумать и об этом.

Утечка памяти может быть легко исправлена ​​как:

int main() {
        IDude *dude = new Dude();

        std::vector<IDudeBehavior*>  behaviours;
        behaviours.push_back(new LaughDudeBehavior());
        behaviours.push_back(new WalkDudeBehavior());
        behaviours.push_back(new EatDudeBehavior());
        behaviours.push_back(new StudyCplusCplusDudeBehavior());

        for(size_t i = 0 ; i < behaviours.size() ; i++ )
           dude->accept(behaviours[i]);

        //deallcation of memory!
        for(size_t i = 0 ; i < behaviours.size() ; i++ )
           delete behaviours[i];
        delete dude;
        return 0;
}

Теперь утечки памяти нет . :-)

2 голосов
/ 06 августа 2011

(...) в будущем появятся новые классы с новым набором поведений

Здесь есть две разные вещи.Новые классы и новый набор поведений .Давайте рассмотрим каждый из них по очереди.


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

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

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


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

См. Также шаблон Template Method, который можно применить здесь.

Пожалуйста, посмотрите также этот вопрос о невероятной полезности интерфейсов.


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

Рассмотрим следующий пример.У вас есть n контейнерные классы (вектор, список, deque, set и т. Д.) И m алгоритмы (find_if, count, copy, for_each и т. Д.).

Вы не можете реализовать каждый алгоритм вкаждый контейнерный класс: это будет означать, что вы должны написать O (nm) код.Решение, сохраняемое стандартной библиотекой (оно старше, чем [цитата нужна]), состоит в том, чтобы абстрагировать обход структур: каждый класс контейнера предоставляет пару итераторов и алгоритмовдействуют на пары итераторов .Это позволяет писать код O (n + m).


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

При наличии фиксированного числа классов состояний и увеличивающегося количества поведений, либо правильно абстрагироваться, либо, в крайнем случае, использовать Visitor.

При наличии растущего числа классов состояний и фиксированного числа поведений используйте интерфейсы: для этого они и нужны.

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