В практике разработки ПО зачастую встает задача динамической модификации программного кода в зависимости от текущих или настраиваемых значений параметров. Для решения этой задачи широко используются обратные вызовы. В языке C++ обратные вызовы реализуются различными способами, и далеко не всегда очевидно, какой из них лучший для конкретной ситуации. В книге рассмотрены теоретические и практические аспекты организации обратных вызовов, проанализированы достоинства и недостатки различных реализаций, выработаны рекомендации по выбору в зависимости от требований к проектируемому ПО. В первую очередь книга предназначена для программистов среднего (middle) уровня, т.е. тех, кто уже достаточно хорошо знает язык C++, но хотел бы расширить и углубить свои знания в области проектирования и дизайна. В определенной степени она также будет интересна опытным разработчикам, с одной стороны, как систематизация знаний, с другой стороны, как источник идей и методов для решения практических задач.
Приведённый ознакомительный фрагмент книги Обратные вызовы в C++ предоставлен нашим книжным партнёром — компанией ЛитРес.
Купить и скачать полную версию книги в форматах FB2, ePub, MOBI, TXT, HTML, RTF и других
3. Сравнительный анализ реализаций
3.1. Методологические подходы
3.1.1. Обобщенный алгоритм
Итак, мы рассмотрели различные способы реализации обратных вызовов. Какая из них наилучшим образом подходит для использования в конкретной ситуации? Чтобы ответить на этот вопрос, необходимо сравнить реализации, т. е. требуется сравнительный анализ.
Обобщенный алгоритм сравнительного анализа включает следующие шаги.
1. Выбрать объекты анализа.
2. Определить критерии сравнения.
3. Построить матрицу соответствия, в которой отобразить, насколько объекты анализа соответствуют выбранным критериям.
4. Проанализировать полученные результаты и выбрать объект, наилучшим образом удовлетворяющий совокупности критериев.
Рассмотрим указанные шаги подробнее.
1. Объект анализа — это некая сущность, которая будет подвергаться анализу. В нашем случае такими сущностями выступают реализации обратных вызовов.
2. Выбор критериев — пожалуй, самый сложный и в то же время наиболее важный этап сравнительного анализа. Критерии должны отражать значимость показателя, который они определяют; неверный выбор критериев приводит к неправильным результатам. Так, например, в качестве критерия можно выбрать количество строк кода, но насколько этот показатель значим при разработке? В нашем случае совершенно не значим: не имеет значения, займет реализация 10 или 50 строк, важно то, насколько она обеспечивает качество выполняемых функций. Качество, в свою очередь, определяется степенью выполнения требований, предъявляемых к проектируемой системе. По этой причине именно требования наилучшим образом подходят для использования в качестве критериев.
3. Матрица соответствия строится в виде таблицы. В заголовки строк таблицы вписываются критерии, в заголовки столбцов — объекты анализа. В ячейках таблицы для каждой пары «объект-критерий» выставляется степень соответствия объекта заданному критерию. Степень выполнения может быть качественной (выполняется/не выполняется) или количественной (выставляется оценка по заданной шкале).
4. Полученные результаты суммируются. Объект, набравший наибольшее количество положительных утверждений (качественная оценка), или наибольшее количество баллов (количественная оценка), будет оптимальным.
Итак, мы описали обобщенный алгоритм сравнительного анализа. Далее рассмотрим, как выполняются шаги алгоритма применительно к поставленной задаче — выбору оптимальной реализации для конкретного случая. Первый шаг — выбор объектов анализа — здесь тривиальный, объектами анализа являются реализации обратных вызовов. Перейдем ко второму шагу — определим критерии, в качестве которых выступают требования.
3.1.2. Требования как критерии
Обозначим требования, предъявляемые при разработке программного кода обратных вызовов. Состав требований не претендует на полноту, читатель может добавить свои, если посчитает их значимыми или актуальными для конкретного случая.
Простота. Показывает, насколько просто и быстро можно написать, отладить и сопровождать код.
Независимость компонентов. Показывает, нужно ли изменять код одного компонента при изменении другого. Чем меньше зависимости между компонентами (в нашем случае это инициатор и исполнитель), тем проще разработка и отладка программной системы. Кроме того, упрощается ее сопровождение и повышается надежность.
Отсутствие трансляции контекста. Отсутствие необходимости трансляции контекста упрощает разработку, улучшает прозрачность кода и повышает независимость компонентов. И наоборот, трансляция контекста усложняет код и заставляет инициатор выполнять дополнительные операции для хранения и передачи контекста
Безопасность. Показывает устойчивость системы к потенциальным ошибкам.
Гибкость. Показывает, насколько просто модифицировать код при появлении новых требований.
Полиморфизм. Показывает, поддерживается ли полиморфизм в реализации исполнителя. Поддержка полиморфизма упрощает разработку и повышает гибкость в рамках объектно-ориентированной парадигмы.
Быстродействие. Показывает, насколько быстро осуществляется вызов кода исполнителя.
Системный API. Показывает возможность реализации системных API.
C++ API. Показывает возможность реализации C++ API.
Итак, объекты анализа выбраны, критерии определены. Теперь нужно построить матрицу соответствия. Для начала мы будем использовать качественный анализ, поскольку он более простой в реализации.
3.2. Качественный анализ
3.2.1. Матрица соответствия
Матрица соответствия строится в виде таблицы. В строках выписываются требования, в столбцах — способы реализации, в ячейках — признаки, указывающие, насколько реализация поддерживает соответствующий критерий (Табл. 7.)
Табл. 7. Качественный анализ реализаций обратных вызовов
Легенда: ▪ полностью поддерживается; ▫ поддерживается частично; пустое поле — не поддерживается
По каким соображениям мы назначили оценки?
Простота. Самой сложной реализацией будет, пожалуй, указатель на метод-член класса: запутанный и не слишком наглядный синтаксис. Довольно сложной выглядит реализация лямбда-выражений, поскольку приходится использовать шаблоны. Несколько проще выглядит реализация с помощью указателей на функцию, но там немного запутывает необходимость приведения типов. На этом фоне остальные реализации выглядят достаточно простыми.
Независимость компонентов. Полностью независимыми будет реализация с помощью указателей на функцию: как бы мы не модифицировали код исполнителя, как бы не меняли используемый контекст, код инициатора остается неизменным, даже не требуется его перекомпиляция. Это одна из причин, почему указанная реализация подходит для построения системных API. Лямбда-выражения являются относительно независимыми: при любом изменении состава и типов захваченных переменных код инициатора остается неизменным, но он будет требовать перекомпиляции, поскольку реализован с использованием шаблонов. Указатели на методы классов являются частично независимыми, поскольку требуют предварительного объявления класса в инициаторе. Использование функциональных объектов порождает монолитную архитектуру, где инициатор и исполнитель зависят друг от друга.
Отсутствие трансляции контекста. Указатели на функции и статические методы требуют трансляции контекста, остальные реализации этого не требуют.
Безопасность. Самыми безопасными являются функциональные объекты и лямбда — выражения, потому что в инициаторе хранятся их копии, никак не зависящие от исполнителя. Указатели на методы класса поддерживают безопасность лишь частично: управление временем жизни экземпляра класса возлагается на исполнителя, и потенциально возможны ситуации, когда последний уничтожает экземпляр класса, указатель на который остается в инициаторе и может быть вызван. Указатель на функцию не является безопасным, поскольку исполнитель интерпретирует контекст приведением типов, и нет никакой возможности проверить полученный указатель.
Гибкость. Самым гибким является указатель на метод класса, поскольку здесь имеются несколько способов модификации поведения обработчика. Другие реализации не предлагают таких возможностей, а функциональные объекты в силу монолитной структуры гибкими не являются.
Полиморфизм. Указатель на метод-член класса поддерживает полиморфизм подтипов (наследование и виртуализация), лямбда-выражения поддерживают специализированный полиморфизм (различный код в зависимости от состава и типов захваченных переменных). Остальные реализации полиморфизм не поддерживают.
Конец ознакомительного фрагмента.
Приведённый ознакомительный фрагмент книги Обратные вызовы в C++ предоставлен нашим книжным партнёром — компанией ЛитРес.
Купить и скачать полную версию книги в форматах FB2, ePub, MOBI, TXT, HTML, RTF и других