|
Программирование >> Обобщенные обратные вызовы
2. Избегайте чрезмерного ограничения функциональности. Если вы пишете некоторое программное средство, которое делает X и Y, то что, если некий пользователь захочет сделать Z, и это Z не слишком отличается от Y? Иногда вы будете хотеть сделать ваш код достаточно гибким для поддержки Z, иногда нет. Частью хорошего обобщенного дизайна является выбор путей и средств для настройки или расширения вашего кода. То, что это важно в обобщенном дизайне, не должно оказаться сюрпризом, поскольку тот же принцип применим и к дизайну класса в объектно-ориентированном програм-мировании. Дизайн, основанный на стратегии, представляет собой одну из нескольких важных методик, которые обеспечивают подключаемое поведение обобщенного кода. Примеры такого дизайна вы можете найти в нескольких главах в [AlexandrescuOl]; начать можно с глав, где описываются SmartPtr и singleton. Это приводит нас к следующему вопросу, 3. Избегайте чрезмерно монолитного дизайна. Этот важный вопрос не возникает непосредственно при рассмотрении приведенного примера, но сам по себе заслуживает столь пристального внимания, что ему посвящена даже не одна задача, а целая мини-серия - задачи с 37 по 40. Во всех трех пунктах рефреном звучит слово чрезмерно . Это означает именно то, что я хотел сказать, - хорошее решение лежит между чрезмерно малой (синдром я уверен, что никто не будет использовать этот код с чем-то, кроме char ) и чрезмерно большой обобщенностью (нездоровые фантазии А если кто-то захочет применить эту программу дисплея тостера на межпланетной станции? ). Правильное решение, как и истина, всегда где-то посредине. Разбор обобиленных обратных вызовов 2. Показанный далее код представляет интересную и очень полезную идиому для оболочек функций обратного вызова. Более детальное описание вы можете найти в оригинальной статье [KalevOl]. Этот код представлен ниже. template < class т, void (т::*F)() > class callback publi с: Присваивание члену object callback(т& t) : object(t) {} Запуск функции обратного вызова void execute О {(object.*F)();} private: T& object; Итак, сколько способов ошибиться есть в простом классе со всего двумя однострочными функциями-членами? Как оказывается, чрезмерная простота и является частью проблемы. Этот шаблон класса не должен быть тяжеловесным, но и такая легковесность - это тоже перебор. Улучшение стиля Отрецензируйте предложенный код и определите: а) возможные стилистические улучшения дизайна для лучшего идиоматического использования С++. Сколько возможных улучшений вы обнаружили? Вот что имел в виду я. 4. Конструктор должен быть описан как explicit. Автор, вероятно, не намеревался обеспечить неявное преобразование т в cal 1 Ьаск<т>. Правильные классы не позволяют себе создавать такие потенциальные проблемы для своих пользователей. Так что на самом деле нам нужно следующее. Присваивание члену object explicit call back(т& t) : object(t) {} To, что мы обнаружили в рассматриваемой строке, -- вопрос стилистики, который сам по себе не является вопросом дизайна, но заслуживает отдельной рекомендации. > Рекомендация Предпочтительно описывать конструкторы как explicit, кроме тех случаев, когда вы действительно хотите обеспечить возможность преобразования типов. (Мелочь). Комментарий не верен. Слово присваивание в комментарии не верно и вводит в заблуждение. Более точно в конструкторе мы привязываем ссылку к объекту типа т. Словом, будет лучще, если комментарий будет таким, как показано ниже привязка к реальному объекту explicit call back(т& t) : object(t) {} Но в этом случае все, что говорит комментарий, сказано кодом, который предельно прост и в комментариях не нуждается, так что лучще оставить его и вовсе без комментария. explicit callback(T& t) : object(t) {} 5. Функция execute должна быть const. В конце концов, функция execute никак не влияет на состояние объекта cal 1 Ьаск<т>! Это возвращает нас к азам - рекомендация о корректном использовании const, как вино, с возрастом становится только лучше. Значение корректного использования const известно в С и С++ как минимум с начала 1980-х годов, и это значение никуда не делось и в новом тысячелетии, и на него не влияет массовое использование шаблонов. запуск функции обратного вызова void executeO const {(object.*F)();} > Рекомендация He забывайте о корректном использовании const. Теперь, когда мы разобрались с функцией execute, перейдем к более серьезной идиоматической проблеме. 6. (Идиома). Функция execute должна быть оператором operatorO. В С++ использование оператора вызова функции для выполнения операций в стиле функции идиоматично. Кстати, тогда комментарий, и гак несколько излишний, становится совершенно ненужным и может быть удален. Код теперь идиоматически комментирует сам себя. void operator О () const { (object.*F)(); } Но, - можете удивиться вы, -- если мы обеспечиваем оператор вызова функции, значит, перед нами разновидность функционального объекта? Отличный вопрос, который приводит нас к наблюдению, что обратный вызов также можно рассматривать как функциональный объект. > Рекомендация Идиоматические функциональные объекты следует обеспечивать оператором operatorO вместо именованной функции вызова. Ловушка: (Идиома). Должен ли данный обратный вызов быть производным от std: :ипагу function? См. раздел 36 в [MeyersOI], где более подробно изложено обсуждение адаптируемости и почему в общем случае это Хорошая Вещь. Увы, в данном примере имеются две отличные причины не порождать обратный вызов от std: :unary function, как минимум, не сейчас. Это не унарная функция. У нее нет параметров, в то время, как у унарной функции - один параметр (voi d не в счет!). Порождение от std: :unary function, в любом случае, не улучшает расширяемость. Позже мы увидим, что обратный вызов должен работать и с другими видами сигнатур функций, и в зависимости от количества параметров может не оказаться соответствующего стандартного базового класса. Например, если мы поддерживаем функции обратного вызова с тремя параметрами, то стандартного класса std:: ternary function, от которого мы могли бы породить собственный класс, не существует. Порождение от std::unary function или std::binary function - удобный способ снабдить обратный вызов небольшим количеством важных определений typedef, которые могут использоваться различными профаммными средствами, такими как замыкания, но это имеет значение, только если вы используете их как настоящие функциональные объекты. Вряд ли это окажется необходимо в силу природы обратного вызова и его предназначения. (Если в будущем дело обернется так, что обратные вызовы должны будут использоваться таким образом с одним или двумя параметрами, то соответствующие версии можно будет породить от std: :unary f uncti on и std::binary function.) Исправление механических ошибок и ограничений б) механические ограничения полезности данного средства 7. Рассмотрим возможность передачи функции обратного вызова в качестве обычного (не шаблонного) параметра. Параметры шаблонов, не являющиеся типами, редко дают столь значительные преимущества, чтобы фиксировать их во время компиляции. То есть мы можем изменить исходный текст следующим образом. template < class т > class callback { public: typedef void (т::*Func)(); Связывание с реальным объектом call back(т& t, Func func) : object(t), f(func) {} Выполнение функции обратного вызова void operator()() const { (object.*f)(); } private: T& object; Func f; Теперь используемая функция может изменяться в процессе выполнения программы; к коду легко добавить функцию-член, которая позволит пользователю изменить функцию, с которой связан существующий объект cal 1 back, что было невозможно в предыдущей версии кода. > Рекомендация Предпочтительно использовать параметры, не являющиеся типами, в качестве обычных параметров функций, за исключением случаев, когда они действительно должны быть параметрами шаблонов.
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |