Паттерн Chain of Responsibility
Когда в приложении начинают сыпаться десятки разных запросов — обработка превращается в настоящий хаос. Знакомая ситуация? 😱
Как сделать так, чтобы код оставался чистым, а новые обработчики добавлялись без боли и слёз?
Здесь на помощь приходит паттерн Chain of Responsibility — один из самых элегантных способов организовать поток обработки данных в приложении.
🔗 Укрощаем хаос: Chain of Responsibility в Dart
В этом руководстве мы разберем, как применить этот паттерн в Dart: от базовых принципов до продвинутых примеров. Поехали! 🚀
🤔 Что такое Chain of Responsibility?
Chain of Responsibility (Цепочка обязанностей) — это поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке обработчиков. Каждый последующий обработчик решает, может ли он обработать запрос сам, или должен передать его дальше по цепи.
Проще говоря: запрос путешествует по цепочке, пока не найдётся обработчик, который сможет с ним справиться или цепочка не закончится.
![Представьте эстафету, где палочка — это ваш запрос ⚡]
💡 Зачем нужен Chain of Responsibility?
Паттерн Цепочка обязанностей решает следующие задачи:
- Разделение ответственности 🧩: каждый обработчик выполняет только одну конкретную задачу.
- Динамичность обработки 🔄: легко изменять порядок обработки и добавлять новые обработчики.
- Снижение связанности 🔓: отправитель запроса не знает, кто конкретно его обработает.
- Соблюдение принципа SRP 👮: каждый класс фокусируется на одной задаче.
👨💻 Реализация Chain of Responsibility в Dart
В Dart существует несколько способов реализации этого паттерна. Давайте посмотрим на самые практичные подходы:
1. Классическая реализация с абстрактным классом
// Абстрактный обработчик
abstract class Handler {
Handler? _nextHandler;
// Установка следующего обработчика в цепочке
Handler setNext(Handler handler) {
_nextHandler = handler;
return handler; // Возвращаем handler для цепочки вызовов
}
// Метод для обработки запроса
void handle(String request) {
if (canHandle(request)) {
process(request);
} else if (_nextHandler != null) {
_nextHandler!.handle(request);
} else {
print('Запрос "$request" не обработан.');
}
}
// Определяем, может ли этот обработчик обработать запрос
bool canHandle(String request);
// Фактическая обработка запроса
void process(String request);
}
// Конкретные обработчики
class EmailHandler extends Handler {
@override
bool canHandle(String request) {
return request.contains('@');
}
@override
void process(String request) {
print('EmailHandler: обработка запроса "$request"');
}
}
class PhoneHandler extends Handler {
@override
bool canHandle(String request) {
return request.contains(RegExp(r'^\+?\d+$'));
}
@override
void process(String request) {
print('PhoneHandler: обработка запроса "$request"');
}
}
class DefaultHandler extends Handler {
@override
bool canHandle(String request) {
return true; // Этот парень обработает что угодно! 😎
}
@override
void process(String request) {
print('DefaultHandler: обработка запроса "$request"');
}
}
Как использовать:
void main() {
// Создаём обработчики
final emailHandler = EmailHandler();
final phoneHandler = PhoneHandler();
final defaultHandler = DefaultHandler();
// Строим цепочку обработчиков (как Lego! 🧱)
emailHandler.setNext(phoneHandler).setNext(defaultHandler);
// Обрабатываем запросы
emailHandler.handle('user@example.com'); // Обработает EmailHandler
emailHandler.handle('+12345678'); // Обработает PhoneHandler
emailHandler.handle('Hello'); // Обработает DefaultHandler
}
2. Функциональный подход с использованием замыканий
typedef RequestHandler = bool Function(String request);
class FunctionalChain {
final Map<RequestHandler, void Function(String)> _handlers = {};
final void Function(String)? _defaultHandler;
FunctionalChain({void Function(String)? defaultHandler})
: _defaultHandler = defaultHandler;
// Добавляем обработчик в цепочку
void addHandler(
RequestHandler canHandle, void Function(String) processHandler) {
_handlers[canHandle] = processHandler;
}
// Обработка запроса
void handle(String request) {
for (final entry in _handlers.entries) {
if (entry.key(request)) {
entry.value(request);
return;
}
}
// Если ни один обработчик не сработал, используем план Б 🧐
if (_defaultHandler != null) {
_defaultHandler!(request);
} else {
print('Запрос "$request" не обработан.');
}
}
}
Как использовать:
void main() {
final chain = FunctionalChain(
defaultHandler: (request) => print('Default: обработка "$request"'),
);
// Добавляем обработчики — просто и элегантно! ✨
chain.addHandler(
(request) => request.contains('@'),
(request) => print('Email: обработка "$request"'),
);
chain.addHandler(
(request) => request.contains(RegExp(r'^\+?\d+$')),
(request) => print('Phone: обработка "$request"'),
);
// Обрабатываем запросы
chain.handle('user@example.com'); // Email обработчик
chain.handle('+12345678'); // Phone обработчик
chain.handle('Hello'); // Default обработчик
}
📊 Быстрая таблица: классический vs функциональный подход
Характеристика | Классический подход | Функциональный подход |
---|---|---|
Гибкость | Средняя | Высокая |
Простота использования | Требует создания классов | Требует минимум кода |
Читаемость | Хорошая для сложных задач | Отличная для простых задач |
Расширяемость | Через наследование | Через замыкания |
Поддержка состояния | Встроенная | Требует дополнительных усилий |
🔍 Практический пример: система логирования
Хватит теории! Давайте посмотрим на реальный пример, который вы можете сразу использовать — систему логирования с разными уровнями важности:
enum LogLevel {
debug,
info,
warning,
error, // Когда всё горит! 🔥
}
abstract class LoggerHandler {
LoggerHandler? _nextHandler;
final LogLevel _level;
LoggerHandler(this._level);
LoggerHandler setNext(LoggerHandler handler) {
_nextHandler = handler;
return handler;
}
void log(LogLevel level, String message) {
if (level.index >= _level.index) {
writeLog(message);
}
// Передаём запрос дальше по цепочке
if (_nextHandler != null) {
_nextHandler!.log(level, message);
}
}
void writeLog(String message);
}
class ConsoleLogger extends LoggerHandler {
ConsoleLogger(LogLevel level) : super(level);
@override
void writeLog(String message) {
print('Консоль: $message');
}
}
class FileLogger extends LoggerHandler {
FileLogger(LogLevel level) : super(level);
@override
void writeLog(String message) {
print('Запись в файл: $message');
// В реальном приложении здесь был бы код записи в файл
}
}
class EmailAlertLogger extends LoggerHandler {
EmailAlertLogger(LogLevel level) : super(level);
@override
void writeLog(String message) {
print('Отправка Email-уведомления: $message');
// В реальном приложении здесь был бы код отправки email
}
}
Как использовать:
void main() {
// Создаём и настраиваем цепочку логгеров
final consoleLogger = ConsoleLogger(LogLevel.debug); // Всегда срабатывает
final fileLogger = FileLogger(LogLevel.info); // Со второго уровня
final emailLogger = EmailAlertLogger(LogLevel.error); // Только ошибки!
// Строим цепочку
consoleLogger.setNext(fileLogger).setNext(emailLogger);
// Логирование сообщений разного уровня
consoleLogger.log(LogLevel.info, 'Информационное сообщение');
// Выведет:
// Консоль: Информационное сообщение
// Запись в файл: Информационное сообщение
consoleLogger.log(LogLevel.error, 'Критическая ошибка!');
// Выведет:
// Консоль: Критическая ошибка!
// Запись в файл: Критическая ошибка!
// Отправка Email-уведомления: Критическая ошибка!
}
🎯 Когда использовать Chain of Responsibility?
Применение этого паттерна оправдано в следующих случаях:
- Когда программа должна обрабатывать разнообразные запросы несколькими способами.
- Когда порядок обработчиков важен, но может динамически меняться.
- Когда набор объектов, способных обработать запрос, определяется во время выполнения.
- Для реализации middleware в веб-приложениях.
- В системах обработки событий и исключений.
⚠️ Недостатки Chain of Responsibility
Без недостатков никуда. Давайте честно посмотрим на обратную сторону медали:
-
Гарантии обработки отсутствуют 🤷♂️ Запрос может не найти подходящего обработчика и остаться необработанным.
-
Возможная избыточность ⏱️ Для длинных цепочек может потребоваться больше времени для прохождения всей цепи.
-
Сложность отладки 🐛 Отслеживание прохождения запроса может быть затруднительным.
-
Риск циклических зависимостей 🔄 При неправильной настройке цепочки можно случайно создать бесконечный цикл.
🎬 Заключение
Паттерн Chain of Responsibility — мощный инструмент для построения гибких систем обработки запросов. Его сила в том, что код становится проще в поддержке, а цепочку обработчиков можно настраивать как конструктор.
Главное правило: применяйте этот паттерн, когда нужна гибкость и чистое разделение обязанностей между компонентами.
Чем сложнее проект, тем чаще цепочка обязанностей спасает архитектуру от хаоса.
Попробуйте реализовать его в своих проектах — и вы почувствуете, насколько легче становится развивать приложение без боли. 💪
📚 Полезные ссылки
- Refactoring.Guru: Chain of Responsibility
- Официальная документация Dart: Callable Classes
- Шаблоны проектирования на Dart (GitHub)
🏋️♂️ Домашнее задание
Базовый уровень 🐣: Создайте систему валидации формы с использованием Chain of Responsibility. Обработчики:
EmptyValidator
: проверка на пустые поля.EmailValidator
: проверка корректности email.PasswordValidator
: проверка сложности пароля.
(Подсказка: каждый обработчик должен или передавать запрос дальше, или прерывать цепочку при ошибке.)
Средний уровень 🦊: Реализуйте HTTP middleware цепочку для обработки запросов. Обработчики:
AuthMiddleware
: проверка авторизации.LoggingMiddleware
: логирование запроса.CorsMiddleware
: проверка CORS.RequestHandler
: финальная обработка запроса.
(Подсказка: используйте класс для представления HTTP-запроса.)
Продвинутый уровень 🦁: Создайте систему фильтрации контента с использованием функционального подхода Chain of Responsibility. Фильтры:
- Фильтр нецензурных слов.
- Фильтр ссылок.
- Фильтр HTML-тегов.
- Фильтр длины текста.
(Подсказка: каждый фильтр должен изменять текст и передавать его следующему обработчику.)
Экспертный уровень 🐉: Разработайте систему обработки транзакций с цепочкой проверок:
- Проверка лимита транзакции.
- Проверка баланса счета.
- Антифрод система.
- Проверка валюты.
- Обработка комиссии.
Реализуйте возможность динамического построения цепочки в зависимости от типа транзакции.
(Подсказка: создайте класс TransactionContext для хранения всех данных о транзакции.)