Чтобы было что обрабатывать, давайте представим, что нам нужно проверить список URLов на предмет того, живы ли они (вроде, достаточно жизненно).
В этом примере сначала код, потом объяснения :)
| //Вспомогательный метод. |
| //Возвращает набор портов, в первый из которых она |
| //положит успешный ответ сервера, а во второй - ошибку. |
| static PortSet<WebResponse, WebException> ProcessURL(string url) |
| { |
| //Сначала создадим порты |
| Port<WebResponse> responsePort = new Port<WebResponse>(); |
| Port<WebException> exceptionPort = new Port<WebException>(); |
| |
| //сформируем запрос |
| WebRequest request = HttpWebRequest.Create(url); |
| request.Method = "HEAD"; |
| |
| //Выполним запрос асинхронно. |
| //Получателя ответа напишем здесь же в виде анонимного метода. |
| request.BeginGetResponse( |
| delegate(IAsyncResult result) |
| { |
| try |
| { |
| //пробуем получить ответ. |
| //в случае успеха кладем его в соответствующий порт. |
| WebResponse response = request.EndGetResponse(result); |
| responsePort.Post(response); |
| } |
| catch (WebException ex) |
| { |
| //в случае ошибки кладем ее в порт ошибок. |
| //пусть с ней разбирается тот, кто "знает" |
| //что делать в такой ситуации. |
| exceptionPort.Post(ex); |
| } |
| }, |
| null //state нам не нужен. |
| ); |
| |
| return new PortSet<WebResponse, WebException>(responsePort, exceptionPort); |
| } |
| |
| static void Main(string[] args) |
| { |
| //Список адресов, которые мы хотим проверить. |
| string[] urls = new string[] { |
| "http://alexey.raga.name", |
| "http://error.error", |
| "http://photos.raga.name"}; |
| |
| //Создадим диспатчер и очередь, как в прошлом примере |
| using (Dispatcher disp = new Dispatcher(3, "Main Pool")) |
| { |
| DispatcherQueue queue = new DispatcherQueue("Main Queue", disp); |
| |
| //Запустим проверку по всем адресам |
| foreach (string url in urls) |
| { |
| //Правила для Арбитра: |
| //ожидается либо ответ сервера, либо ошибка. |
| Arbiter.Activate(queue, |
| Arbiter.Choice<WebResponse, WebException>( |
| ProcessURL(url), //Наш вспомогательный метод вернет пару портов. |
| HandleResponse, //Вызвать в случае получения "нормального" ответа |
| HandleError //Вызвать в случае получения ошибки. |
| ) |
| ); |
| } |
| } |
| } |
| |
| static void HandleResponse(WebResponse response) |
| { |
| // ... обработка результата ... |
| } |
| |
| static void HandleError(WebException exception) |
| { |
| // ... обработка ошибки ... |
| } |
Что мы тут видим.
Я сделал вспомогательный метод ProcessURL, в котором асинхронно выполняю запрос к веб-серверу. В качестве коллбека я передаю делегат, анонимный метод. В котором и помещаю результат (ответ сервера либо ошибку) в соответствующий порт.
Перед тем, как пойти дальше, объясню, почему я сразу не вызвал в ProcessURL методы HandleResponse и HandleError вместо того, чтобы класть результаты в порты.
Нет, не потому, что мне очень хотелось использовать CCR :)
Есть две другие, более веские причины.
- Методологическая. Вызов этих методов - не зона ответственности вспомогательного метода. Он - всего лишь хелпер, он умеет сделать запрос и вернуть результат его выполнения. В моем случае метод ProcessURL ничего не знает о внешней инфраструктуре, от нее не зависит, принимает известный ему параметр и возвращает результат, дальнейший путь обработки которого не в его компетенции.
- Техническая. Если я вызову в том анонимном методе эти обработчики, а в них еще другие функции, а в тех - еще, и т.д, то я получу своеобразную "спагетти", цепочку, в которой все действия выполняются внутри этого самого анонимного метода. Он не завершится до тех пор, пока не завершится вся цепочка.
Зачем мне это? Легче положить результат в порт и выйти из метода. Пусть там потом с ним кому надо, тот и разбирается :) Получаем как бы нормальный последовательный вызов функций: одна закончилась - началась вторая.
Ну, разобрались со вспомогательным методом, теперь по существу: сам цикл.
В нем я задаю правило для Арбитра, но уже не с помощью метода Receive<..>, а с помощью Choiсe<..>.
Для начала нужно иметь в виду тот факт, что с помощью метода Receive<..> можно "подписаться" на данные из порта "на постоянной основе" (то есть - зарегистрировали один раз и получаем всегда), а вот с помощью Choiсe<..> - только на получение одной порции данных.
Поэтому я регистрирую правило в цикле, каждый раз. А набор портов - он тоже каждый раз разный, его возвращает метод ProcessURL.
Кроме того, Choice<..> подразумевает, что данные будут получены либо в одном порту, либо во втором. И будет вызван соответствующий обработчик. После чего Арбитр просто забудет о существовании этого набора портов.
Все это сделано как раз для реализации этого "либо". Мне остается только знать, что будет вызван или один обработчик, или другой. Или "норма" или "ошибка".
Итак, все же к обработке ошибок. Она становится проще, и вот почему:
- Мне не нужно принимать решение о том, что делать с ошибкой там же, где я ее перехватил. В случае, если в месте возникновения исключения я ничего не могу с ним поделать (а в большинстве случаев это так, например метод ProcessURL вообще не может "знать" как быть в такой ситуации), мне не нужно ни подавлять его, ни пропускать выше, чтобы кто-то его (может быть) перехватил, ни даже возбуждать собственное исключение. Мне нужно просто... Смотрим пункт 2 :)
- С использованием данного подхода я просто получаю две "ветки" своего алгоритма. Одна исполняется в случае "нормального" поведения, другая - в случае "ненормального" (это, кстати, совсем не обязательно должен быть exception). То есть, я просто как бы говорю: "если все ОК - то пойдем сюда, если нет - то пойдем туда".
Ошибка становится не чем-то неожиданным и из ряда вон выходящим, а нормальным и, главное, ожидаемым и обрабатываемым событием системы. Мне, как программисту, уже существенно проще работать с таким событием и существенно сложнее его проигнорировать и, таким образом, накосячить.
Вдумайтесь. Мне не надо здесь и сейчас решать, что делать с ошибкой. Именно необходимость (и невозможность) подобного решения "здесь и сейчас" в основном приводит к тому, что ошибку вообще не обрабатывают, а, в крайнем случае, регистрируют в логе. Осмысленно же обработать ошибку бывает делом ой каким непростым (я уже писал об этом на старом блоге).
Но не в этом случае, согласитесь? :) Здесь в момент принятия решения (вызов Arbiter.Choice<..>) я четко знаю, что делал и имею достаточно информации для того, чтобы решить "а что же делать, если вызов не получился". И моя ошибка будет спокойно обрабатываться себе опять же в отдельном, заметьте, потоке.
Все.
В следующий раз я не стану трепаться о всякой там методологии, а просто покажу несколько фичей, которые могут понадобиться (и понадобятся) при реальной работе с CCR.
В частности объясню, почему мои примеры "не совсем работоспособны" :)