C ++ способ внедрения зависимости - шаблоны или виртуальные методы? - PullRequest
3 голосов
/ 31 мая 2009

Интересно, как в C ++ используется внедрение зависимостей? Это с помощью шаблонов или полиморфных классов? Рассмотрим следующий код,

class AbstractReader
{
public:
  virtual void Read() = 0;
};

class XMLReader : public AbstractReader
{
public:
  void Read()  { std::cout << "Reading with a XML reader" << std::endl; }
};

class TextFileReader : public AbstractReader
{
public:
  void Read()  { std::cout << "Reading with a Text file reader" << std::endl; }
};

class Parser
{
public:
  Parser(AbstractReader* p_reader) : reader(p_reader) { }
  void StartParsing() { reader->Read();
    // other parsing logic
  }
private:
  AbstractReader* reader;
};

template<class T>
class GenericParser
{
public:
  GenericParser(T* p_reader) : reader(p_reader) { }

  void StartParsing()
  {
    reader->Read();
  }
private:
  T* reader;
};

1 - Какой метод самый лучший? GenericParser или Parser ? Я знаю, если это GenericParser , наследование может быть удалено.

2 - Если шаблоны - это путь, нормально ли писать весь код в заголовочных файлах? Я видел много классов, использующих шаблоны, которые записывают весь код в заголовочные файлы, а не в комбинацию .h / .cpp. Есть ли какие-либо проблемы с этим, что-то вроде встраивания и т. Д.

Есть мысли?

Ответы [ 4 ]

10 голосов
/ 31 мая 2009

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

Это зависит от того, может ли быть определено соединение во время компиляции или должно быть отложено до времени выполнения .

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

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

2 голосов
/ 31 мая 2009

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

0 голосов
/ 31 мая 2009

GenericParser или Parser?

Зависит от остальной части кода, проблема с универсальным парсером в том, что класс, который вы собираетесь внедрить, также должен быть шаблоном.
Но есть и третий более общий способ ... boost :: function и boost :: lambda. Все, что вам нужно - это function с правильным (с точки зрения пользователя класса) типом возвращаемого значения и параметрами. boost::function< void ()> reader = bind( &TextFile::read, reader ); Теперь пользовательский класс не зависит от класса считывателя и не должен быть шаблоном.

class User
{
const boost::function< void ()>& reader;

public:
    void setReader( const boost::function< void ()>& reader ) 
    : reader(reader) {
    }
};

Записывает весь код в заголовочные файлы, а не в комбинацию .h / .cpp.

Это называется моделью разделения, поскольку есть только один компилятор, который ее поддерживает (компилятор Comeau). Начало чтения Ограничение «Экспорт», часть 1 и Ограничение «Экспорт», часть 2


@ CiscoIPPhone Комментарий: проблема с универсальным анализатором заключается в том, что класс, который вы собираетесь внедрить, также должен быть шаблоном.

template<class T>
class GenericParser
{
public:
    GenericParser(T* p_reader) : reader(p_reader) { }

    void StartParsing()
    {
        reader->Read();
    }
private:
    T* reader;
};

// Now you have a GeniricParser Interface but your Parser is only usable for 
// TextFileReader 
class Parser
{
public:
    Parser( GenericParser<TextFileReader> p_reader) : reader(p_reader) { }
    void StartParsing() { 
        reader->Read();
    }
private:
    GenericParser<RealParser> reader;
};


//Solution is to make Parser also a template class
template<class T>
class Parser
{
public:
    Parser( GenericParser<T> p_reader) : reader(p_reader) { }
    void StartParsing() { 
        reader->Read();
    }
private:
    GenericParser<T> reader;
};
0 голосов
/ 31 мая 2009

Что касается 1. «Лучший» является относительным. Оба метода имеют свои плюсы и минусы. Шаблоны обеспечивают грубую скорость, но неизбежно добавляется больше кода (что приводит к большей связности), и сообщения об ошибках трудно читать. Наследование медленнее и делает объекты больше, но не требует встраивания (менее связанных). Он также имеет относительно лучшие сообщения об ошибках.

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

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

...