Функции обратного вызова, анонимные функции и механизм замыканий
Да, анонимные функции относятся не только к области объектно-ориентированного программирования, однако обойти их стороной было бы странно - настолько они полезны и удобны.
Анонимные функции очень активно используются в ООП наряду с функциями обратного вызова (так называемые callback-функции). Ну что ж, давайте обратимся к примерам.
Для начала определим два класса.
class Product { public $name; public $price; function __construct( $name, $price ) { $this->name = $name; $this->price = $price; } } class ProcessSale { private $callbacks; function registerCallback( $callback ) { if ( ! is_callable( $callback ) ) { throw new Exception( "callback not callable" ); } $this->callbacks[] = $callback; } function sale( $product ) { print "{$product->name}: processing <br>"; foreach ( $this->callbacks as $callback ) { call_user_func( $callback, $product ); } } } ?>
Данный код предназначен для запуска нескольких функций обратного вызова. В классе Product просто сохраняются значения свойств $name и $price. Для упрощения конструкции они объявлены открытыми, хотя в боевом проекте следует сделать их закрытыми (private) или защищенными (protected) и создать для этих свойств методы доступа.
В классе ProcessSale определены два метода. Методу registerCallback() передается обычная скалярная переменная безо всяких уточнений. После ее проверки, она добавляется в массив функций обратного вызова $callbacks. Процесс тестирования при этом выполняется с помощью встроенной функции is_callable().
В результате гарантируется, что методу registerCallback() будет передано имя функции, которую можно вызвать с помощью таких функций, как call_user_func() или array_walk().
Методу sale() передается объект типа Product. Метод выводит об этом информацию и затем в цикле выполняет перебор элементов массива $callback.
Каждый элемент вместе с объектом типа Product передается функции call_user_func(), которая, собственно, и вызывает код, написанный пользователем.
Чем же полезны функции обратного вызова? Они позволяют в процессе работы скрипта добавлять компоненту новый функционал, который напрямую не связан с его изначальной функциональностью.
— Регулярная проверка качества ссылок по более чем 100 показателям и ежедневный пересчет показателей качества проекта.
— Все известные форматы ссылок: арендные ссылки, вечные ссылки, публикации (упоминания, мнения, отзывы, статьи, пресс-релизы).
— SeoHammer покажет, где рост или падение, а также запросы, на которые нужно обратить внимание.
SeoHammer еще предоставляет технологию Буст, она ускоряет продвижение в десятки раз, а первые результаты появляются уже в течение первых 7 дней. Зарегистрироваться и Начать продвижение
Предусмотрев в объекте работу с функциями обратного вызова, вы тем самым позволите другим разработчикам расширять функциональность вашего кода, если в этом будет необходимость. Причем обстоятельства могут быть достаточно интересные.
Представьте себе, что через какое-то время некто, дорабатывающий ваш класс ProcessSale захочет создать журнал продаж. Если у человека есть доступ к исходному коду класса, он может просто дописать код для фиксации фактов сделок непосредственно в метод sale(). Но это не всегда будет удачным решением.
Если этот пользователь (вносящий изменения) не является владельцем пакета, в котором определен класс ProcessSale, то все его исправления будут затерты, когда выйдет его обновленная версия. Но даже если с этим моментом все хорошо, все равно, добавлять в метод sale() дополнительные ветки кода, решающие некие "случайные" задачи, неразумно.
Почему? Дело в том, что в конечном итоге такой подход приведет к изменению зоны ответственности класса и может снизить возможность его повторного использования в других проектах, а это нарушение одного из главных принципов веб-программирования - принципа повторного использования кода.
Несмотря на это, по счастливой случайности, мы заложим в класс ProcessSale возможность обратного вызова.
Давайте посмотрим на код функции обратного вызова, которая фиксирует все сделки в журнале продаж.
$logger = create_function('$product', 'print " Записываем ... ({$product->name})<br>";'); $processor = new ProcessSale(); $processor->registerCallback($logger); $processor->sale(new Product("Туфли",6)); print "<br>"; $processor->sale (new Product ("Кофе",6));
Здесь для создания функции обратного вызова мы воспользовались функцией create_function(). Как видите, ей передаются два параметра. Сначала указывается список параметров функции, а затем - тело самой функции.
В результате у нас получается конструкция, которую обычно называют анонимной функцией, поскольку при создании мы не присвоили ей имя, как это обычно бывает.
Вместо этого ссылка на вновь созданную функцию сохраняется в переменной, которую затем можно передать в качестве параметра другим функциям и методам. Как раз это мы и делаем в коде, приведенном выше: мы сохраняем ссылку на анонимную функцию в переменной $logger, которую затем передаем в качестве параметра методу ProcessSale::registerCallback().
В конце этого фрагмента кода мы создаем два объекта, описывающих товары, и передаем их по очереди методу sale(). О том, что произойдет дальше, вы уже, наверное, догадались: каждая сделка по продаже будет обработана (в нашем случае просто будет выведено сообщение о товаре), после чего произойдет вызов всех прописанных callback-функций.
— Разгрузит мастера, специалиста или компанию;
— Позволит гибко управлять расписанием и загрузкой;
— Разошлет оповещения о новых услугах или акциях;
— Позволит принять оплату на карту/кошелек/счет;
— Позволит записываться на групповые и персональные посещения;
— Поможет получить от клиента отзывы о визите к вам;
— Включает в себя сервис чаевых.
Для новых пользователей первый месяц бесплатно. Зарегистрироваться в сервисе
Туфли: обрабатывается... Записываем... (Туфли) Кофе: обрабатывается Записываем... (Кофе)
Давайте еще раз проанализируем пример кода с функцией create_function(). Она же выглядит просто кошмарно, заметили?))
При помещении исполняемого кода в строку всегда возникает головная боль. Для начала нам нужно выполнить экранирование всех символов "$" и "?", которые встречаются в теле скрипта. Более того, по мере роста тела функции (а почему-то это происходит всегда!), ее будет все труднее проанализировать и понять.
Было бы здорово найти какой-то более элегантный способ для создания таких функций. И он существует:) Мы можем просто объявить функцию как обычно, а затем присвоить ссылку на нее нужной нам переменной. И все это - в одном операторе! Ниже приведен предыдущий пример, в котором использован другой синтаксис.
$logger2 = function($product) { print " Записываем ({$product->name})<br>"; }; $processor = new ProcessSale(); $processor->registerCallback($logger2); $processor->sale(new Product("Туфли",6)); print "<br>"; $processor->sale (new Product ("Кофе",6));
Как видите, отличие здесь заключается в способе создания переменной, ссылающейся на анонимную функцию. Очевидно, что этот вариант кода не только понятнее, но и проще. Мы просто указываем в операторе присвоения ключевое слово function и не задаем имя функции.
При этом обратите внимание, что поскольку в операторе присваивания используется встроенная функция, в конце блока нужно обязательно поместить точку с запятой.
Само собой, что функции обратного вызова вовсе не обязаны быть анонимными. В качестве такой функции можно смело использовать имя обычной функции или даже ссылку на метод какого-нибудь объекта. Например:
class Mailer { function doMail ($product) { print " Упаковываем ({$product->name})<br>"; } } $processor = new ProcessSale(); $processor->registerCallback(array (new Mailer(), "doMail")); $processor->sale(new Product("Туфли",6)); print "<br>"; $processor->sale (new Product ("Кофе",6));
В коде выше мы создаем новый класс Mailer, содержащий метод doMail(). Данному методы мы передаем объект типа Product, о котором метод выводит сообщение.
Далее, при вызове метода registerCallback() мы передаем ему в качестве параметра массив, ане ссылку на функцию обратного вызова, как мы это делали раньше. Первым элементом данного массива является объект типа Mailer, а вторым - строка, содержащая имя метода, который мы хотим вызвать.
Помните, что в методе registerCallback() с помощью функции is_callable() выполняется проверка типа передаваемого аргумента? Отлично. Что интересно, эта функция достаточно интеллектуальна, чтобы распознать массивы подобного вида. Поэтому при указании функции обратного вызова в виде массива, в первом элементе такого массива должен находиться объект, содержащий вызываемый метод, а имя этого метода помещается в виде строки во второй элемент массива.
Таким образом, мы успешно проходим проверку типа аргумента, а наш код при отработке даст следующий результат:
Туфли: обрабатывается Упаковываем (Туфли) Кофе: обрабатывается Упаковываем (Кофе)
Разумеется, что анонимную функцию можно вернуть из метода, как показано ниже.
class Totalizer { static function warnAmount() { return function ($product) { if ($product->price > 5) { print " покупается дорогой товар: {$product->price}<br>"; } }; } } $processor = new ProcessSale(); $processor->registerCallback(Totalizer::warnAmount());
В этом примере нет ничего интересного, кроме того, что метод warnAmount() используется в качестве фабрики анонимной функции. Тем не менее, подобная структура позволяет нам делать гораздо больше, чем просто генерировать анонимную функцию. Она позволяет воспользоваться преимуществами механизма замыкания.
В анонимной функции нового типа могут использоваться переменные, объявленные в другой анонимной функции, находящейся в родительской области видимости. Данная концепция достаточно трудна, чтобы понять ее сразу. Но если говорить самым простым языком, то анонимная функция как бы запоминает контекст, в котором она была создана.
Предположим, мы хотим, чтобы метод warnAmount() выполнял следующее:
1. Чтобы ему можно было передавать пороговое значение стоимости товаров.
2. Чтобы он подсчитывал стоимость (т.е. сумму цен) проданного товара.
3. И, когда стоимость товара превысит некий порог, программа должна выполнить некое действие (для упрощения конструкции в нашем случае это не более, чем вывод сообщения)
Чтобы анонимная функция могла воспользоваться переменными, определенными в родительской области видимости, используется ключевое слово use, как показано в примере ниже.
class Totalizer { static function warnAmount($amt) { $count = 0; return function ($product) use ($amt, &$count) { $count += $product->price; print " сумма: $count<br>"; if ($count > $amt) { print " Продано товаров на сумму: {$count}<br>"; } }; } } $processor = new ProcessSale(); $processor->registerCallback(Totalizer::warnAmount(8)); $processor->sale(new Product("Туфли",6)); print "<br>"; $processor->sale (new Product ("Кофе",6));
В директиве use анонимной функции, которая возвращается методом Totalizer::warnAmount(), указаны две переменные. Первая из них - $amt, которая является аргументом, переданным методу warnAmount().
Вторая - замкнутая переменная $count. Она объявлена в теле метода warnAmount(), и начальное ее значение равно нулю. Обратите внимание на то, что перед именем переменой $count в директиве use указан символ амперсанда - "&".
Это значит, что данная переменная будет передаваться в анонимную функцию по ссылке, а не по значению. Дело в том, что в теле анонимной функции мы добавим к ней цену товар и затем сравним новую сумму со значением переменной $amt. Если будет достигнуто пороговое значение, выводится соответствующее сообщение, как показано ниже.
Туфли: обрабатывается Упаковываем (Туфли) Кофе: обрабатывается Упаковываем (Кофе) Продано товаров на сумму: 12
В этом примере было показано, что значение переменной $count сохраняется между вызовами функции обратного вызова. Обе переменые и $count, и $amt, остаются связанными с этой функцией, поскольку они указаны в контексте ее объявления и перечислены в директиве use.
На этом закругляемся, а в следующем материале поговорим о том, как получать информацию об объекте или классе, методах, свойствах и наследовании.
Понравился материал и хотите отблагодарить?
Просто поделитесь с друзьями и коллегами!
Смотрите также: