Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП_ Лекция №10 - Более сложные формы композиции.docx
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
68.53 Кб
Скачать

Классы-родители с применением итераторов контейнеров

Предложенное решение, изолирующее конкретный использованный контейнер за типовыми рекомендованными методами, вполне пригодно на практике. Однако и его можно улучшить, данная форма реализации далеко не является пределом совершенства.

В реализации имеется ряд методов, предполагающих работу с порядковыми номерами дочерних объектов, а именно:

Chapter & getChapter ( int _index ) const;

int findChapterIndex ( Chapter const & _chapter ) const;

void insertChapter ( int _atIndex, Chapter * _pChapter );

void removeChapter ( int _atIndex );

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

Кроме того, работа с порядковыми номерами несколько сковывает программиста в выборе конкретной структуры данных. Хотя зависимости хорошо расщеплены, заменить вектор на связный список или множество в условиях, когда интерфейс требует вернуть i-ый объект по порядку, может быть проблематичным. Безусловно, реализовать такое требование на основе других структур технически возможно, но решение не будет слишком эффективно. Перебор вместо линейного алгоритма превратится в квадратичный (каждый последующий элемент будет извлекаться перебором с начала). Просто и эффективно будет лишь заменить вектор на массив либо любую другую структуру, где поддерживается эффективный произвольный доступ к элементам по индексу и оператор или метод для индексной выборки (например, контейнер std::deque).

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

class Book

{

/*-----------------------------------------------------------------*/

public:

/*-----------------------------------------------------------------*/

// ...

// Метод, возвращающий количество глав

int getChaptersCount () const;

// Метод, добавляющий главу

void addChapter ( Chapter * _pChapter );

// Метод, подтверждающий наличие главы в книге

bool hasChapter ( Chapter const & _chapter ) const;

// Метод удаления указанной главы

void removeChapter ( Chapter const & _chapter ) const;

// Метод удаления всех глав

void clearChapters ();

/*-----------------------------------------------------------------*/

};

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

Однако, в ходе такого преобразования незаметно теряется неявная возможность перебора элементов. В прежних версиях эту роль играла пара методов getXXXCount() и getXXX(int), позволявшие организовать простой цикл for для прохода по контейнеру и перебора всех элементов. В качестве замены этому примитивному механизму перебора следует использовать итераторы. Как уже было показано ранее, итераторы хорошо расщепляют зависимости между реализацией хранения данных и кодом их обработки. Любой контейнер стандартной библиотеки предоставляет итераторы для полного прохода от начала и до конца при помощи пары методов begin и end. Если вернуть итераторы клиентскому коду, будет возможен обход набора дочерних объектов, не использующий порядковые номера, и одновременно не нарушающий инкапсуляцию.

Объявление typedef позволяет скрыть от клиенского кода сложности объявления конкретного типа итератора, относящегося к используемому контейнеру. Если контейнер изменится на другой, достаточно будет обновить объявление typedef, и большинство клиентского кода не будет задето.

class Book

{

/*-----------------------------------------------------------------*/

public:

/*-----------------------------------------------------------------*/

// …

// Синоним для типа итератора, перебирающего вектор глав

typedef std::vector< Chapter * >::const_iterator ChapterIterator;

// Методы, возвращающие начальный и конечный итераторы контейнера глав

ChapterIterator chaptersBegin() const { return m_chapters.begin(); }

ChapterIterator chaptersEnd() const { return m_chapters.end(); }

/*-----------------------------------------------------------------*/

};

Ниже приведен фрагмент кода с перебором глав книги на основе итераторов:

// ...

int main ()

{

// Создаем тестовую книги двумя способами

Book * pBook = createTestBook();

// Перебираем главы первой книги через итераторы

Book::ChapterIterator itChapter = pBook->chaptersBegin();

while ( itChapter != pBook->chaptersEnd() )

{

const Chapter & chapter = ** itChapter;

std::cout << chapter.getTitle() << std::endl;

++ itChapter;

}

// Уничтожаем книгу

delete pBook;

}

Выражение, извлекающее итератор можно записать чуть более компактно:

auto itChapter = pBook->chaptersBegin();

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

Помимо простого цикла while, использующего итераторы, может быть полезным применение нового интервального цикла for. Напомним, такой цикл требует, чтобы итерируемый объект-контейнер имел методы под названием begin/end, которые возвращают итераторы. В классе Book есть похожие методы, однако они носят другие названия. Переименовывать их в требуемые begin и end может быть не совсем логичным, особенно если родительский объект содержит более одного набора дочерних объектов (например, набор авторов книги помимо набора глав). Чтобы добиться работы интервальных циклов без собственных методов begin/end в родительском классе, необходимо небольшое расширение кода - возвращать из книги вспомогательную структуру (IterableChapters), обладающую нужными операциями begin и end, но при этом всю работу должны выполнять стандартные итераторы контейнера глав:

class Book

{

/*-----------------------------------------------------------------*/

public:

/*-----------------------------------------------------------------*/

// …

// Вспомогательный класс для реализации интервального цикла for по главам книги

class IterableChapters

{

public:

// Конструктор - запоминает переданную пару итераторов на контейнер глав

IterableChapters ( ChapterIterator _chaptersBegin, ChapterIterator _chaptersEnd )

: m_begin( _chaptersBegin ), m_end( _chaptersEnd )

{}

// Методы begin/end используются интервальным циклом for

ChapterIterator begin () const { return m_begin; }

ChapterIterator end () const { return m_end; }

private:

// Итераторы на начало и конец контейнера глав соответственно

ChapterIterator m_begin, m_end;

};

// Методы, возвращающий итерируемый объект для перебора глав книги

IterableChapters chapters () const

{

return IterableChapters( chaptersBegin(), chaptersEnd() );

}

/*-----------------------------------------------------------------*/

};

Теперь для перебора и обработки глав можно применить более удобный интервальный цикл for:

int main ()

{

// Создаем тестовую книги двумя способами

Book * pBook = createTestBook();

// Перебираем главы первой книги через интервальный цикл for

for ( Chapter * pChapter : pBook->chapters() )

std::cout << pChapter->getTitle() << std::endl;

// Уничтожаем книгу

delete pBook;

}

Такая запись очень компакта и удобна. Высокий уровень абстракции способствует лучшему восприятию сути решения задачи при чтении кода. Однако не стоит забывать что стоит за такой выразительностью. Кажущаяся простота на самом деле скрывает сразу несколько слоев обработки:

  • интервальный цикл for скрывает за собой эквивалентный цикл while, использующий итераторы объекта из правой части, полученные через вызовы begin()/end();

  • вызов pBook->chapters() возвращает вспомогательную структуру IterableChapters, которая скрывает связь с итераторами контейнера глав, выполняющими перебор элементов;

  • итераторы глав - это итераторы конкретной реализации класса std::vector из стандартной библиотеки, которые скрывают проход через конкретную реализацию структуры данных;

  • фактически, любой вектор содержит массив элементов, а итератор прямо или косвенно задействует указатели на элементы в этом массиве.

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

В то же время, такая сложная многоуровневая структура абстракций не оказывает негативного влияния на производительность генерируемого компилятором машинного кода. В отличие от других объектно-ориентированных языков, С++ предоставляет возможности встраивания функций в место вызова, при чем встраивание может быть многоуровневым. Хотя интервальный цикл for в функции main и задействует 4 уровня абстракций, благодаря встраиванию машинный код будет фактически напрямую будет работать с массивом и указателями на его элементы. В результате, программа одновременно является и высокоуровневой, и быстродействующей.

Ссылку на полный вариант реализации композиции классов глава-книга с применением итераторов можно найти в конце лекции.