Иерархия наследования против множественного наследования (C ++) - PullRequest
5 голосов
/ 08 мая 2011

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

Ситуация следующая: у меня есть пара различных классов интерфейса, абстрагирующих несколько устройств связи. Поскольку эти устройства различаются по своей природе, они также различаются по интерфейсу и, таким образом, на самом деле не связаны между собой. Давайте назовем их IFooDevice и IBarDevice . Со временем может быть добавлено больше типов устройств. Язык C ++.

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

Я подумал о двух возможных решениях:

Первым будет какая-то иерархия переходов. Все устройства будут иметь подкласс общего интерфейса IDevice , который обеспечит (виртуальные) методы, необходимые для управления и запроса устройства (например, getProperties () , hasProperties () , ...). DeviceManager затем имеет набор указателей на IDevice , и в какой-то момент будет необходимо приведение от Base до Derived - либо с шаблонный метод в менеджере или после запроса на стороне клиента.

С точки зрения дизайна, я думаю, было бы более элегантно разделить проблемы управления устройством и интерфейсом конкретного устройства. Таким образом, это привело бы к двум несвязанным интерфейсам: IManagedDevice и, например, IFooDevice . Реальное устройство должно быть унаследовано от обоих, чтобы «быть» конкретным типом устройства и быть управляемым. Менеджер будет управлять только указателями на IManagedDevice . Однако в какой-то момент возникнет необходимость в приведении между теперь не связанными классами (например, от IManagedDevice до IFooDevice ), если клиент захочет использовать устройство, предоставленное менеджером.

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

Edit:

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

Ответы [ 5 ]

1 голос
/ 08 мая 2011

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

struct Device
{
    static Device *first;  // Pointer to first available device
    Device *prev, *next;   // Links for the doubly-linked list of devices

    Device() : prev(0), next(first)
    {
        if (next) next->prev = this;
        first = this;
    }

    virtual ~Device()
    {
        if (next) next->prev = prev;
        if (prev) prev->next = next; else first = next;
    }

    private:
        // Taboo - the following are not implemented
        Device(const Device&);
        Device& operator=(const Device&);
 };

Тогда вы можете просто извлечь все устройства из Device, и они будут автоматически помещены в глобальный список по строительству и удалены из глобального списка по уничтожению.

Все ваши клиенты смогут просматривать список всех устройств, начиная с Device::first и следуя device->next. Делая dynamic_cast<NeededDeviceType*>(device) клиенты могут проверить, совместимо ли устройство с тем, что им нужно.

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

1 голос
/ 08 мая 2011

Я думаю, что предложенный Томом может быть немного изменен в соответствии с вашими потребностями:

class IManagedDevice
{
    IDevice* myDevice;

    /* Functions for managing devices... */
};

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

Затем вы можете запросить конкретное устройство (IFooDevice или IBarDevice), возможно, через какой-то тип устройства.ID.

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

class IDevice
{
    virtual void Handle() = 0;
};

class IFooDevice : public IDevice
{
    virtual void Handle()
    {
        this->doFoo();
    }

    virtual void doFoo() = 0;
}

class IBarDevice : public IDevice
{
    virtual void Handle()
    {
        this->doBar();
    }

    virtual void doBar() = 0;
}

С менеджером, вызывающим функцию Handle.

1 голос
/ 08 мая 2011

Я думаю, что шаблон посетителя - лучший выбор для этого.

0 голосов
/ 15 сентября 2015

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

Однако, если вам нужно разделить интерфейс между управлением и функциями устройства, вы можете использовать виртуальное наследование . IManagedDevice и IFooDevice оба являются интерфейсами одного и того же конкретного устройства, поэтому они оба имеют общую виртуальную базу IDevice .

Конкретно (код запуска) :

#include <cassert>

class IDevice {
public:
  // must be polymorphic, a virtual destructor is a good idea
  virtual ~IDevice() {}
};

class IManagedDevice : public virtual IDevice {
    // management stuff
};

class IFooDevice : public virtual IDevice {
    // foo stuff
};

class ConcreteDevice : public IFooDevice, public IManagedDevice {
    // implementation stuff
};

int main() {
    ConcreteDevice device;
    IManagedDevice* managed_device = &device;
    IFooDevice* foo_device = dynamic_cast<IFooDevice*>(managed_device);
    assert(foo_device);
    return 0;
}
0 голосов
/ 08 мая 2011

при связи с устройствами я полностью отделил устройство и менеджер связи.

У меня был простой менеджер связи, основанный на Boost.Asio. Интерфейс был что-то вроде

/** An interface to basic communication with a decive.*/
class coms_manager
{
public:
  virtual 
  ~coms_manager();
  /** Send a command. */
  virtual 
  void 
  send(const std::string& cmd) = 0;

  /** Receive a command.
   *  @param buffsize The number of bytes to receive.
   *  @param size_exactly True if exactly buffsize bytes are to be received.  If false, then fewer bytes may be received.
   */
  virtual 
  std::string 
  recv( const unsigned long& buffsize = 128, 
    const bool& size_exactly = false) = 0 ;

  /** Timed receive command.
   *  @param buffsize The number of bytes to receive.
   *  @param seconds The number of seconds in the timeout.
   *  @param size_exactly True if exactly buffsize bytes are to be received.  If false, then fewer bytes may be received.
   */
  virtual 
  std::string 
  timed_recv( const unsigned long& buffsize = 128, 
      const double& seconds = 5, 
      const bool& size_exactly = false) = 0;
};

Затем я реализовал этот интерфейс для tcp (ethernet) и последовательной связи.

class serial_manager : public coms_manager {};
class ethernet_manager : public coms_manager {};

Каждое из устройств затем содержало (или указывало) (а не унаследовано) объект coms_manager Например:

class Oscilloscope
{
  void send(const std::string& cmd)
  {  
     m_ComsPtr->send(cmd);
  }
private:
  coms_manager* m_ComsPtr;
};

Затем вы можете поменять метод связи, изменив то, на что указывает указатель.

Для меня это не имело особого смысла (Осциллограф ЛИБО был подключен через последовательный ИЛИ через Ethernet, поэтому я выбрал

template<class Manager>
class Oscilloscope
{
  void send(const std::string& cmd)
  {  
     m_Coms.send(cmd);
  }
private:
  Manager m_Coms;
};

и теперь я использую

Oscilloscope<serial_manager> O1(/dev/tty1);   // the serial port
Oscilloscope<ethernet_manager> O2(10.0.0.10); //The ip address

, что имеет больше смысла.

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

...