Интерфейсное программирование на C ++ в сочетании с итераторами. Как тоже держать это просто? - PullRequest
9 голосов
/ 19 октября 2010

В своих разработках я постепенно перехожу от объектно-ориентированного подхода к подходу на основе интерфейса. Точнее:

  • в прошлом я уже был удовлетворен, если бы мог группировать логику в классе
  • Теперь я склонен уделять больше внимания интерфейсу и позволить фабрике создать реализацию

Простой пример проясняет это.

В прошлом я писал эти классы:

  • Библиотека
  • Книга

Теперь я пишу эти классы:

  • ILibrary
  • Библиотека
  • LibraryFactory
  • IBook
  • Книга
  • BookFactory

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

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

Предположим, в моей Библиотеке есть коллекция книг, и я хочу перебрать их. В прошлом это не было проблемой: Library :: begin () и Library :: end () возвращали итератор (Library :: iterator), на котором я мог легко написать цикл, например:

for (Library::iterator it=myLibrary.begin();it!=mylibrary.end();++it) ...

Проблема в том, что в подходе на основе интерфейса нет гарантии, что разные реализации ILibrary используют один и тот же тип итератора. Например, если OldLibrary и NewLibrary наследуются от ILibrary, затем:

  • OldLibrary может использовать std :: vector для хранения своих книг и возвращать std :: vector :: const_iterator в своих методах begin и end
  • NewLibrary может использовать std :: list для хранения своих книг и возвращать std :: list :: const_iterator в своих методах begin и end

Требование обеих реализаций ILibrary для возврата одного и того же типа итератора также не является решением, поскольку на практике операцию приращения (++ it) необходимо реализовывать по-разному в обеих реализациях.

Это означает, что на практике я должен сделать итератор также интерфейсом, а это означает, что приложение не может поместить итератор в стек (типичная проблема срезки в C ++).

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

Есть ли лучшие способы решения этой проблемы?

EDIT: Некоторые уточнения после замечаний Мартина.

Предположим, у меня есть класс, который возвращает все книги, отсортированные по популярности: LibraryBookFinder. У него есть методы begin () и end (), которые возвращают LibraryBookFinder :: const_iterator, который ссылается на книгу.

Чтобы заменить мою старую реализацию новой, я хочу поместить старый LibraryBookFinder за интерфейсом ILibraryBookFinder и переименовать старую реализацию в OldSlowLibraryBookFinder.

Тогда моя новая (невероятно быстрая) реализация под названием VeryFastCachingLibraryBookFinder может наследоваться от ILibraryBookFinder. Отсюда проблема итератора.

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

ILibraryBookFinder *myFinder = LibraryBookFinderFactory (FINDER_POPULARITY);
for (ILibraryBookFinder::const_iterator it=myFinder->begin();it!=myFinder.end();++it) ...

или, если я хочу использовать другой критерий:

ILibraryBookFinder *myFinder = LibraryBookFinderFactory (FINDER_AUTHOR);
for (ILibraryBookFinder::const_iterator it=myFinder->begin();it!=myFinder.end();++it) ...

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

Ответы [ 3 ]

3 голосов
/ 19 октября 2010

Здесь вы смешиваете метафоры.

Если библиотека является контейнером, ей нужен собственный итератор, она не может повторно использовать итератор члена.Таким образом, вы бы обернули член-итератор в реализацию ILibraryIterator.

Но, строго говоря, библиотека - это не контейнер, а библиотека.
Таким образом, методы в библиотеке - это действия (представьте здесь глаголы)Вы можете выполнять в библиотеке.Библиотека может содержать контейнер, но, строго говоря, она не является контейнером и, следовательно, не должна показывать begin () и end ().

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

class ILibrary
{
    public:
         IBook const& getBook(Index i) const;

         template<R,A>
         R checkBooks(A const& librarianAction);
};
3 голосов
/ 19 октября 2010

Если в ваших библиотеках много книг, вам следует подумать о том, чтобы поместить свои "агрегатные" функции в свои коллекции и передать действие, которое необходимо выполнить.

Нечто в природе:

class ILibrary
{
public:
  virtual ~Ilibrary();
  virtual void for_each( boost::function1<void, IBook> func ) = 0;
};

LibraryImpl::for_each( boost::function1<void, IBook> func )
{
    std::for_each( myImplCollection.begin(), myImplCollection.end(), func );
}

Хотя, вероятно, не совсем так, потому что вам, возможно, придется иметь дело с использованием shared_ptr, constness и т. Д.

1 голос
/ 19 октября 2010

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

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

struct IIterator {
   // iterator stuff here
   // ---
   // now for the handling on the stack
   virtual size_t size() = 0; // must return own size
   virtual void copyTo(IIterator* pt) = 0;
};

и ваша оболочка:

struct IteratorWrapper {
   IIterator* pt;
   IteratorWrapper(IIterator* i) {
       pt = alloca(i->size());
       i->copyTo(pt);
   }
   // ...
};

или около того.


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

template<typename T>
do_sth_with_library(T& library) {
   for(typename T::iterator i = library.begin(); i != library.end(); ++i)
      // ...
}

Но код может стать довольно уродливым, если вы слишком сильно полагаетесь на это.


Еще одно приятное решение (повышение функциональности кода - реализация for_each интерфейс) был предоставлен CashCow.

С текущим C ++ это может сделать код также немного сложным / уродливым для использования.Благодаря будущим функциям C ++ 0x и лямбда это решение может стать намного более чистым.

...