- •Ооп: Лекция 10. Более сложные формы композиции.
- •Типичные ошибки при реализации родительских классов
- •Рекомендованный стиль интерфейса родительских классов
- •Классы-родители с применением итераторов контейнеров
- •Умные указатели std::unique_ptr
- •Использование std::unique_ptr для композиции объектов
- •Итерирование дочерних элементов с учетом умных указателей
- •Использование средства std::function
- •Использование отображений при реализации композиции
- •Использование множеств
- •Композиция с кратностью многие-ко-многим
- •Полные примеры из лекции
Использование средства std::function
Проблему инкапсуляции конкретной структуры данных, используемой для хранения дочерних объектов в классе-родителе, можно решить несколько другим способом. Альтернативное решение состоит в отказе от идеи предоставления клиентскому коду итераторов на внутренние контейнеры в пользу передачи родителю интересующего действия. Действиет может быть задано в виде любой вызываемой сущности - функции, функционального объекта и лямбда-выражения.
Для решения этой задачи можно воспользоваться универсальной вызываемой сущностью std::function. Допустим, необходима вызываемая сущность, которая ничего не возвращает и принимает константную ссылку на объект Chapter. Объявление типа для такого объекта выглядит следующим образом:
std::function< void ( Chapter const & ) > f;
Такой объект может быть инициализирован любым из трех видов вызываемых сущностей:
// Обыкновенная функция
void printChapter ( Chapter const & _chapter )
{
std::cout << _chapter.getTitle() << std::endl;
}
// Инициализируем std::function обыкновенной функцией
std::function< void ( Chapter const & ) > f1 = & printChapter;
// Функциональный объект (функтор)
struct ChapterPrinter
{
void operator () ( Chapter const & _chapter )
{
std::cout << _chapter.getTitle() << std::endl;
}
};
// Инициализируем std::function функциональным объектом
std::function< void ( Chapter const & ) > f2 = ChapterPrinter();
// Инициализируем std::function лямбда-выражением
std::function< void ( Chapter const & ) > f3 =
[] ( Chapter const & _chapter )
{
std::cout << _chapter.getTitle() << std::endl;
};
Наличие такого мощного средства позволяет совершить радикальные изменения при реализации итерирования дочерних объектов в родительском классе. Суть предлагаемого подхода состоит в полном отказе от предоставления итераторов и методов, которые их возвращают - их можно удалить. Итераторы заменяются специальным методом для обхода дочерних объектов, принимающим желаемое действие на каждом из элементов в виде объекта std::function. Ниже представлена очередная версия класса Book с применением предложенного варианта:
class Book
{
public:
// Функция обхода глав - принимает действие, которое применяется ко всем главам
void forEachChapter ( std::function< void ( Chapter const & ) > _action ) const
{
// Обходим контейнер глав и вызываем переданное действие на каждой из глав.
// Каждый элемент chapterPtr - это std::unique_ptr< Chapter > const &
for ( auto const & chapterPtr : m_chapters )
_action( * chapterPtr );
// ^
// клиент получит ссылку на главу
}
// ...
};
Тестовый код видоизменяется следующим образом - в качестве действия для элементов передается лямбда выражение, принимающее ссылку на главу:
pBook->forEachChapter(
[] ( Chapter const & _chapter )
{
std::cout << _chapter.getTitle() << std::endl;
}
);
В итоге, получается на редкость удачное решение - и клиентский код, и реализация средства выглядят просто и воспринимаются легко. Сложно лишь представить как сообщество программистов пришло к такому балансу простоты записи, строгой инкапсуляции и легкости адаптации под любые желаемые изменения. Изменение способа хранения в этом способе абсолютно не влияет ни на клиентский код, ни на набор открытых операций - лишь на реализацию части закрытых методов.
