Назначение паттерна?
- Паттерн Chain of Responsibility (Цепочка обязянностей) позволяет избежать жесткой зависимости отправителя запроса от его получателя (т.е. обеспечивается слабая связанность компонентов системы), при этом запрос может быть обработан несколькими объектами.
- Объекты-получатели связываются в цепочку. Запрос передается по этой цепочке, пока не будет обработан.
- Вводит конвейерную обработку для запроса с множеством возможных обработчиков.
Многие зададут вопрос, а что такое запрос? В данном контексте запрос — это любая задача, которая должна быть обработана несколькими объектами (или одним если есть только один обработчик). Например мы должны записать сообщение в лог — то это конкретная задача т.е. запрос. Получатель в таком случае — это тот класс, который будет обрабатывать запрос (задачу).
Как было описано выше суть паттерна в том, чтобы уменьшить связанность компонентов системы т.е. сделать так, чтобы при отправке запроса мы не знали какой конкретно объект получатель (компонент) будет его обрабатывать, но в тоже время он должен быть корректно обработан даже, если обработчик не найден.
Реализация
Давайте для примера напишем систему обработки документов, на вход обработчиков (объекты-получатели) приходит название документа, а задача обработчика распарсить его содержимое основываясь на его расширении. Начнём с проектирования обработчика, создадим абстрактный класс с методом parse
:
<?php
abstract class DocParser
{
public function parse($fileName)
{
// тут мы определяем является ли документ допустимым
// для текущего обработчика
}
}
От этого класса будут наследоваться обработчики документов. Всего в цепочке будет четыре обработчика для документов: xml, json, csv, txt. Под каждый обработчик мы создам свои классы: XmlDocParser
, JsonDocParser
, CsvDocParser
, TxtDocParser
у каждого класса будет свой собственный способ обработки документа (метод parse
).
Итак, у нас есть коллекция обработчиков и теперь необходимо сделать так, чтобы их можно было объеденить в цепочку т.к. основа шаблона Цепочка обязанностей — это обработка запроса по цепочке.
На этом этапе у нас возникает следующая задача: в методе parse
мы должны проверять можно ли обработать текущий документ и если можно то выполнить обработку, а если нельзя, то необходимо передавать текущий документ следующему обработчику в цепочке.
Реализуем эту логику:
<?php
/**
* 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
в каждом из классов обрабочиков:
<?php
/**
* 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);
}
}
}
<?php
/**
* 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);
}
}
}
<?php
/**
* 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);
}
}
}
<?php
/**
* 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 типу документа. В нашем случае, если парсер может обработать тип документа, то он выводит сообщение, но конечно в реальном приложении, мы бы обрабатывали этот документ по другому.
Вроде всё сделали, теперь каждый обработчик может определить есть ли следующий обработчик в цепочке. Тут вы меня спросите, а где же эта цепочка и куда передавать название файла (запрос)? И я отвечу, можно вызывать напрямую соотествующий класс обработчик, но так как мы используем шаблон проектирования Цепочка обязанностей, нам нужно ослабить связь и абстрагироваться от конкретного обработчика. Для этого создадим класс, в который и будем передавать запрос и строить цепочку обработчиков:
<?php
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), в реальном приложении они будут автоматически загружаться по неймспайсам.
Итак, в этом классе и таится вся магия, в метод run
мы передаём ЗАПРОС! А запрос в данном случае это набор документов. В этом методе создаём цепочку из объектов обработчиков, в конструктор метода run
, можно передать как набор названий документов, так и просто название одного документа и он будет обработан.
А теперь посмотрим, как использовать этот шаблон на практике:
<?php
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
Как видите, каждый парсер определил свой тип документа и обработал его! На этом всё, если нашли ошибку или у вас другое мнение по поводу этого шаблона проектирования, то пишите в комментариях.