В своих разработках я постепенно перехожу от объектно-ориентированного подхода к подходу на основе интерфейса. Точнее:
- в прошлом я уже был удовлетворен, если бы мог группировать логику в классе
- Теперь я склонен уделять больше внимания интерфейсу и позволить фабрике создать реализацию
Простой пример проясняет это.
В прошлом я писал эти классы:
Теперь я пишу эти классы:
- 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 может быть определен внешним фактором: настройкой конфигурации, параметром командной строки, выбором в диалоговом окне, ... И каждая реализация имеет свой вид оптимизации (например, автор книги не не меняется, так что это может быть довольно статичный кеш, популярность может меняться ежедневно, что может означать совершенно другую структуру данных).