Независимость компонентов. Полностью независимыми будет реализация с помощью указателей на функцию: как бы мы не модифицировали код исполнителя, как бы не меняли используемый контекст, код инициатора остается неизменным, даже не требуется его перекомпиляция. Это одна из причин, почему указанная реализация подходит для построения системных API. Лямбда-выражения являются относительно независимыми: при любом изменении состава и типов захваченных переменных код инициатора остается неизменным, но он будет требовать перекомпиляции, поскольку реализован с использованием шаблонов. Указатели на методы классов являются частично независимыми, поскольку требуют предварительного объявления класса в инициаторе. Использование функциональных объектов порождает монолитную архитектуру, где инициатор и исполнитель зависят друг от друга.
Отсутствие трансляции контекста. Указатели на функции и статические методы требуют трансляции контекста, остальные реализации этого не требуют.
Безопасность. Самыми безопасными являются функциональные объекты и лямбда- выражения, потому что в инициаторе хранятся их копии, никак не зависящие от исполнителя. Указатели на методы класса поддерживают безопасность лишь частично: управление временем жизни экземпляра класса возлагается на исполнителя, и потенциально возможны ситуации, когда последний уничтожает экземпляр класса, указатель на который остается в инициаторе и может быть вызван. Указатель на функцию не является безопасным, поскольку исполнитель интерпретирует контекст приведением типов, и нет никакой возможности проверить полученный указатель.
Гибкость. Самым гибким является указатель на метод класса, поскольку здесь имеются несколько способов модификации поведения обработчика. Другие реализации не предлагают таких возможностей, а функциональные объекты в силу монолитной структуры гибкими не являются.
Полиморфизм. Указатель на метод-член класса поддерживает полиморфизм подтипов (наследование и виртуализация), лямбда-выражения поддерживают специализированный полиморфизм (различный код в зависимости от состава и типов захваченных переменных). Остальные реализации полиморфизм не поддерживают.
Быстродействие. Самым быстродействующим является функциональный объект, практически не отстает от него и лямбда-выражение. Это связано с тем, что их код может встраиваться в точку вызова. Несколько медленнее работают указатели на функцию и на статический метод, поскольку их код выполняется через вызов функции13. Медленнее всего работает указатель на метод-член класса, поскольку ему необходимо обращение к таблице виртуальных функций.
Системный API. Указатель на функцию единственный способ, с помощью которого можно использовать обратные вызовы при проектировании системных API.
C++ API. Лямбда-выражения не подходят для использования в C++ API: хотя инициатор не требует изменений при модификации исполнителя, но ему требуется перекомпиляция. Не подходят для С++ API также функциональные объекты, поскольку изменение функционального объекта затрагивает как инициатор, так и исполнитель.
3.2.2. Выбор реализации
Итак, мы построили матрицу соответствия, проанализировали, насколько реализации соответствуют выбранным критериям. Что же выбрать для конкретного случая? Для решения этого вопроса необходимо определить, какой критерий сейчас является наиболее важным, и выбрать реализацию по этому критерию. Так, например, если самым важным является возможность проектирования системного API, то следует выбрать указатели на функцию. Если самым важным является быстродействие, то следует выбрать функциональные объекты. Если самым важным является гибкость, то следует выбрать указатели на член класса.
А как сделать выбор, если нам одновременно важны несколько критериев, причем некоторые из них противоречат друг другу (а чаще всего именно так и происходит)? У нас появляется проблема многокритериального выбора, решить которую позволяет метод интегральных оценок.
3.3. Метод интегральных оценок
3.3.1. Количественные оценки
По своей сути метод интегральных оценок повторяет качественный анализ, но с одним отличием в матрице соответствия вместо качественных вводятся количественные оценки. В ячейках матрицы проставляются числовые значения, отражающие, насколько объект анализа поддерживает (другими словами, в какой степени реализует) соответствующее требование. Диапазон возможных значений задается шкалой оценок, которая зависит от точности, которую мы хотим получить. Примеры различных шкал оценок изображены на Рис. 16.
3.3. Метод интегральных оценок
3.3.1. Количественные оценки
По своей сути метод интегральных оценок повторяет качественный анализ, но с одним отличием в матрице соответствия вместо качественных вводятся количественные оценки. В ячейках матрицы проставляются числовые значения, отражающие, насколько объект анализа поддерживает (другими словами, в какой степени реализует) соответствующее требование. Диапазон возможных значений задается шкалой оценок, которая зависит от точности, которую мы хотим получить. Примеры различных шкал оценок изображены на Рис. 16.
Рис. 16. Шкалы оценки реализуемости требований
Итак, строим матрицу соответствия, в ячейках выставляем числовые оценки, суммируем оценки по столбцам. Реализация, набравшая наибольшее количество баллов, является оптимальной.
Пример интегральных оценок по трем критериям с использованием трехбалльной шкалы приведен в Табл. 8. Здесь наибольшее количество балов набирает реализация с использованием функционального объекта, которая для конкретного случая является оптимальной.
Табл. 8. Интегральные оценки по трехбалльной шкале
3.3.2. Коэффициенты важности
Зачастую оказывается, что некоторые требования являются более важными, чем остальные. Например, быстродействие важно, но в то же время гибкость еще важнее; в свою очередь, безопасность является приоритетным фактором. Чтобы учесть такие ситуации, вводятся коэффициенты важности.
Каждому требованию присваивается коэффициент, который отражает, насколько данное требование является важным для обеспечения качества функционирования системы в конкретном случае. При расчете числовых оценок каждое значение в ячейке таблицы умножается на этот коэффициент; таким образом вносятся поправки в итоговые значения. Целесообразно предварительно ранжировать требования по важности: наименее важному присвоить коэффициент 1, и для каждого требования, более важного, чем предыдущее, увеличивать значение на единицу.
Введем коэффициенты важности для предыдущего примера. Ранжируем требования: считаем, что наименее важным для нас является простота, наиболее важным безопасность. Результаты приведены в Табл. 9.
Табл. 9. Ранжирование требований
Пересчитаем показатели с учетом коэффициентов важности. Для коэффициентов важности вводим отдельный столбец, где проставляем соответствующие значения. В ячейках в скобках отображаются значения оценки без учета коэффициента, без скобок отображаются новые значения с учетом поправок (Табл. 10).
Табл. 10. Интегральные оценки с учетом коэффициентов важности.
Как видим, после введения коэффициентов важности результаты изменились: теперь максимальное количество балов набирают две реализации указатель на функцию и лямбда-выражение.
3.3.3. Учет прогнозных показателей
Как мы видели в предыдущем примере, может оказаться, что по результатам расчетов несколько реализаций имеют одинаковое количество балов. В этом случае целесообразно заглянуть в будущее.
Из списка требований выбираем те, которые не актуальны сейчас, но которые, возможно, станут актуальны в последствии. Сводим эти требования в таблицу, аналогично предыдущему примеру, но для числовых значений используем инверсную шкалу: если реализация полностью поддерживает соответствующее требование, выставляем 0, если не поддерживает, то выставляем минимальное отрицательное значение14. Так, например, если используется трехбалльная шкала, то 0 превращается в -2, 1 превращается в -1, а 2 превращается в 0. Инверсная шкала показывает, насколько сильно новые требования ухудшают текущую интегральную оценку: чем меньше значение15, тем в большей степени уменьшается текущая оценка.
Далее, полученные оценки суммируются, получившаяся отрицательная интегральная оценка для каждого столбца суммируется с соответствующей текущей оценкой, внося, таким образом, поправки. Из получившихся итоговых значений выбирается реализация, у которой количество балов после коррекции получается наибольшим.
Вернемся к примеру из предыдущего параграфа. Представим, что мы поразмыслили и решили, что в будущем для нас может стать актуальным быстродействие и необходимость реализации C++ API. Сводим эти критерии в таблицу с инверсной шкалой, считаем, что важность этих критериев одинакова. Подсчитываем сумму (Табл. 11).