Chain of Responsibility (Цепочка обязянностей) — это поведенческий шаблон проектирования, который позволяет передавать запросы последовательно по цепочке обработчиков. Каждый последующий обработчик решает, может ли он обработать запрос сам и стоит ли передавать запрос дальше по цепи.
Особенности:
Многие зададут вопрос, а что такое запрос? В данном контексте запрос - это любая задача, которая должна быть обработана несколькими объектами (или одним если есть только один обработчик). Например мы должны записать сообщение в лог - то это конкретная задача т.е. запрос. Получатель в таком случае - это тот класс, который будет обрабатывать запрос (задачу).
Как было описано выше суть паттерна в том, чтобы уменьшить связанность компонентов системы т.е. сделать так, чтобы при отправке запроса мы не знали какой конкретно объект получатель (компонент) будет его обрабатывать, но в тоже время он должен быть корректно обработан даже, если обработчик не найден.
Реализация
Давайте для примера напишем систему обработки документов, на вход обработчиков (объекты-получатели) приходит название документа, а задача обработчика распарсить его содержимое основываясь на его расширении. Начнём с проектирования обработчика, создадим абстрактный класс с методом parse
:
abstract class DocParser { public function parse($fileName) { // тут мы определяем является ли документ допустимым // для текущего обработчика } }
От этого класса будут наследоваться обработчики документов. Всего в цепочке будет четыре обработчика для документов: xml, json, csv, txt. Под каждый обработчик мы создам свои классы: XmlDocParser
, JsonDocParser
, CsvDocParser
, TxtDocParser
у каждого класса будет свой собственный способ обработки документа (метод parse
).
Итак, у нас есть коллекция обработчиков и теперь необходимо сделать так, чтобы их можно было объеденить в цепочку т.к. основа шаблона Цепочка обязанностей - это обработка запроса по цепочке.
На этом этапе у нас возникает следующая задача: в методе parse
мы должны проверять можно ли обработать текущий документ и если можно то выполнить обработку, а если нельзя, то необходимо передавать текущий документ следующему обработчику в цепочке.
Реализуем эту логику:
/**
* Class DocParser
*/
abstract class DocParser
{
/**
* @var DocParser
*/
protected $successor;
/**
* @param DocParser $successor
*/
public function __construct(DocParser $successor = null)
{
$this->successor = $successor;
}
/**
* @param $fileName
* @return mixed
*/
public function parse($fileName)
{
$successor = $this->getSuccessor();
if (!is_null($successor)) {
$successor->parse($fileName);
} else {
print("Unable to find the correct parser for the file: {$fileName}\n");
}
}
/**
* @return DocParser
*/
public function getSuccessor()
{
return $this->successor;
}
/**
* @param $fileName
* @param $format
* @return bool
*/
public function canHandleFile($fileName, $format)
{
if ($fileName) {
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
if ($extension == $format) {
return true;
}
}
return false;
}
}
Чтобы передать запрос дальше по цепочке, текущий обработчик должен содержать в себе объект следующего обработчика, для этого было добавлено свойство successor
. При инициализации каждого обработчика, нужно будет передать в конструкторе объект следующего обработчика, а как это сделать будет показано ниже.
Метод canHandleFile
проверяет является ли текущий файл допустимым для обработки. Соответственно изменился метод parse
, теперь он проверяет, что если есть хоть какой-то обработчик, то необходимо передать ему документ на обработку . Если же нету обработчика, то необходимо вывести сообщение (или бросить исключение), о том что корректный обработчик не найден.
Теперь нам надо реализовать метод parse
в каждом из классов обрабочиков:
/** * Class CsvDocParser */ class CsvDocParser extends DocParser { /** * @param $fileName * @return mixed|void */ public function parse($fileName) { if ($this->canHandleFile($fileName, 'csv')) { print("A CSV parser is handling the file: {$fileName}\n"); } else { parent::parse($fileName); } } }
/** * Class JsonDocParser */ class JsonDocParser extends DocParser { /** * @param $fileName * @return mixed|void */ public function parse($fileName) { if ($this->canHandleFile($fileName, 'json')) { print("A JSON parser is handling the file: {$fileName}\n"); } else { parent::parse($fileName); } } }
/** * Class TextDocParser */ class TextDocParser extends DocParser { /** * @param $fileName * @return mixed|void */ public function parse($fileName) { if ($this->canHandleFile($fileName, 'txt')) { print("A TEXT parser is handling the file: {$fileName}\n"); } else { parent::parse($fileName); } } }
/** * Class XmlDocParser */ class XmlDocParser extends DocParser { /** * @param $fileName * @return mixed|void */ public function parse($fileName) { if ($this->canHandleFile($fileName, 'xml')) { print("A XML parser is handling the file: {$fileName}\n"); } else { parent::parse($fileName); } } }
Вы видите, что в методе parse
вызывается метод canHandleFile
для проверки документа, сам метод может быть реализован как угодно, можно например проверять не по расширению, а по MIME типу документа. В нашем случае, если парсер может обработать тип документа, то он выводит сообщение, но конечно в реальном приложении, мы бы обрабатывали этот документ по другому.
Вроде всё сделали, теперь каждый обработчик может определить есть ли следующий обработчик в цепочке. Тут вы меня спросите, а где же эта цепочка и куда передавать название файла (запрос)? И я отвечу, можно вызывать напрямую соотествующий класс обработчик, но так как мы используем шаблон проектирования Цепочка обязанностей, нам нужно ослабить связь и абстрагироваться от конкретного обработчика. Для этого создадим класс, в который и будем передавать запрос и строить цепочку обработчиков:
require_once('DocParser.php'); require_once('XmlDocParser.php'); require_once('JsonDocParser.php'); require_once('TextDocParser.php'); require_once('CsvDocParser.php'); /** * ChainOfResponsibility * * Class DocParserProcessor */ class DocParserProcessor { /** * @param $files */ public static function run($files) { // Это и есть цепочка обязанностей $xmlParser = new XmlDocParser(); $jsonParser = new JsonDocParser($xmlParser); $csvParser = new CsvDocParser($jsonParser); $textParser = new TextDocParser($csvParser); if (is_array($files)) { foreach ($files as $file) { $textParser->parse($file); } } else { $textParser->parse($files); } } }
В данном примере файлы подключены вручную (через require), в реальном приложении они будут автоматически загружаться по namespace.
Итак, в этом классе и таится вся магия, в метод run
мы передаём ЗАПРОС! А запрос в данном случае это набор документов. В этом методе создаём цепочку из объектов обработчиков, в конструктор метода run
, можно передать как набор названий документов, так и просто название одного документа и он будет обработан.
А теперь посмотрим, как использовать этот шаблон на практике:
require_once('DocParserProcessor.php'); $files = [ 'file.txt', 'file.json', 'file.xml', 'file.csv', 'file.doc' ]; DocParserProcessor::run($files); DocParserProcessor::run('somefile.xml');
Смотрите, в любом месте вашей программы, вы можете передать любое количество файлов, и вам не надо заботиться о том, какой обработчик вызвать, потому что класс DocParserProcessor
сам об этом позаботится, он найдёт соотвествущий обработчик, а если не найдёт, то бросит исключение и мы сможем грамотно обработать ошибку.
Вот вывод вышеприведённого листинга:
A TEXT parser is handling the file: file.txt A JSON parser is handling the file: file.json A XML parser is handling the file: file.xml A CSV parser is handling the file: file.csv Unable to find the correct parser for the file: file.doc A XML parser is handling the file: somefile.xml
Как видите, каждый парсер определил свой тип документа и обработал его! На этом всё, если нашли ошибку или у вас другое мнение по поводу этого шаблона проектирования, то пишите в комментариях.