Форум программистов, компьютерный форум, киберфорум UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Паттерн Цепочка ответственности в C#

Запись от UnmanagedCoder размещена 17.03.2025 в 17:44
Показов 942 Комментарии 0

Нажмите на изображение для увеличения
Название: 2eecc03d-1299-460d-815c-8304ae375038.jpg
Просмотров: 23
Размер:	181.7 Кб
ID:	10439
Цепочка ответственности — это поведенческий паттерн проектирования, который позволяет передавать запросы последовательно по цепочке потенциальных обработчиков, пока один из них не обработает запрос. Ключевая идея паттерна заключается в том, что отправитель запроса не знает, какой именно объект в итоге обработает этот запрос. Это создаёт слабую связанность между компонентами системы, поскольку отправитель запроса не привязан жёстко к конкретному получателю.

Среди поведенческих паттернов Цепочка ответственности занимает особое место. В отличие от Стратегии, которая позволяет выбирать алгоритм выполнения во время выполнения, или Наблюдателя, который передаёт информацию множеству заинтересованных объектов, Цепочка ответственности строго следует линейной модели передачи запроса от одного обработчика к другому.

Чем же привлекателен этот паттерн для разработчиков? Прежде всего своей гибкостью. Вы можете:
  • Динамически изменять цепочку обработчиков.
  • Добавлять новые обработчики без изменения существующего кода.
  • Изменять порядок обработки запросов.
  • Реализовать различные стратегии обработки ошибок.

Примечательно, что Цепочка ответственности отлично работает в сценариях, где заранее неизвестно, какой обработчик должен взяться за задачу, или когда все запросы должны проходить через ряд предварительных проверок. Например, в контексте веб-разработки, когда HTTP-запрос приходит в систему, он может последовательно проходить через множество обработчиков: аутентификацию, авторизацию, валидацию данных, логирование и т.д. И только после этого попадать к основному обработчику бизнес-логики.

Сравнивая с другими поведенческими паттернами, можно отметить, что Команда, например, фокусируется на инкапсуляции запроса в объект, а Посредник централизует взаимодействия между объектами. Цепочка же ответственности, напротив, децентрализует процесс обработки, распределяя ответственность между множеством объектов.

При работе с C# этот паттерн приобретает дополнительные возможности благодаря событиям, делегатам и гибкой системе типов языка. Фактически, многие механизмы .NET, включая обработку исключений и мидлвары ASP.NET Core, в своей основе используют принципы Цепочки ответственности.

Структура паттерна



Паттерн Цепочка ответственности имеет достаточно чёткую и логичную структуру, которая позволяет эффективно распределять обработку запросов между несколькими объектами. В классическом варианте паттерн Цепочка ответственности включает следующие ключевые компоненты:
1. Обработчик (Handler) — интерфейс или абстрактный класс, определяющий метод для обработки запросов и метод для установки следующего обработчика в цепочке.
2. Конкретный обработчик (ConcreteHandler) — класс, который реализует интерфейс Обработчика. Каждый конкретный обработчик принимает решение, может ли он сам обработать запрос, и если нет — передаёт его дальше по цепочке.
3. Клиент (Client) — класс, который создаёт и настраивает цепочку обработчиков, а затем отправляет запросы первому обработчику в цепочке.

Схематично это выглядит следующим образом:

Code Скопировано
1
2
3
+-------------+      +----------------+      +----------------+
|   Клиент    |----->| Обработчик A   |----->| Обработчик B   |----->...
+-------------+      +----------------+      +----------------+
На языке UML диаграмма классов паттерна выглядит так:

Code Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
+----------------+      +----------------+
|    Handler       |<-----|    Client      |
+----------------+      +----------------+
| + SetNext()    |
| + HandleRequest|
+-------^--------+
        |
        |
+----------------+      +----------------+
|ConcreteHandlerA|      |ConcreteHandlerB|
+----------------+      +----------------+
| + HandleRequest|      | + HandleRequest|
+----------------+      +----------------+
Когда мы говорим о передаче ответственности в цепочке, то здесь существует два основных варианта:
1. Прерывающая цепочка — как только один из обработчиков полностью обрабатывает запрос, процесс останавливается. Этот вариант подходит, когда нам нужно, чтобы запрос обработал только один компонент и на этом всё закончилось. Например, авторизация пользователя: если она прошла успешно, то дальнейшая проверка уже не требуется.
2. Непрерывная цепочка — запрос проходит через все обработчики, независимо от того, был он обработан или нет. Это полезно, когда нам нужно выполнить несколько действий последовательно для одного запроса. Например, при обработке HTTP-запроса — логирование, проверка безопасности, расширение данных запроса и т.д.

Последовательность выполнения в Цепочке ответственности имеет строго линейную природу. Запрос всегда начинается с первого обработчика в цепи и либо завершается где-то в цепи (прерывающая модель), либо проходит через всю цепь (непрерывная модель). При этом каждый обработчик сам принимает решение о дальнейшей судьбе запроса:

C# Скопировано
1
2
3
4
Обработчик:
1. Проверяет, может ли он обработать запрос.
2. Если может - обрабатывает (и возможно завершает цепочку).
3. Если не может - передаёт следующему обработчику.
Стоит отметить, что внутренняя логика каждого обработчика может сильно отличаться. Некоторые обработчики могут выполнять сложные вычисления, другие — просто проверять условия и передавать запрос дальше. Единственное, что их объединяет — они все соответствуют интерфейсу Обработчика и знают, как передавать запросы по цепочке. Для простого примера на C# базовая структура может выглядеть так:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// Интерфейс обработчика
public interface IHandler
{
    IHandler SetNext(IHandler handler);
    object Handle(object request);
}
 
// Базовый класс обработчика с общей функциональностью
public abstract class Handler : IHandler
{
    private IHandler _nextHandler;
 
    public IHandler SetNext(IHandler handler)
    {
        _nextHandler = handler;
        return handler; // Возвращаем обработчик для возможности цепочки вызовов
    }
    
    public virtual object Handle(object request)
    {
        if (_nextHandler != null)
            return _nextHandler.Handle(request);
            
        return null; // Если следующего обработчика нет, возвращаем null
    }
}
 
// Конкретные обработчики
public class ConcreteHandlerA : Handler
{
    public override object Handle(object request)
    {
        if (CanHandle(request))
        {
            // Обработка запроса
            return "Обработано в ConcreteHandlerA";
        }
        else
        {
            // Передача запроса следующему обработчику
            return base.Handle(request);
        }
    }
 
    private bool CanHandle(object request)
    {
        // Логика проверки, может ли этот обработчик обработать запрос
        return true;
    }
}
В современных реализациях Цепочки ответственности часто добавляются дополнительные возможности, которые делают паттерн ещё более гибким:
1. Приоритеты обработчиков — обработчики могут иметь разные уровни приоритета, и запросы сначала направляются к обработчикам с более высоким приоритетом.
2. Обработка отказов — в цепочке может быть предусмотрен механизм обработки ситуаций, когда ни один обработчик не смог обработать запрос.
3. Параллельная обработка — в некоторых случаях запросы могут обрабатываться несколькими обработчиками параллельно, а результаты объединяются позже.

В контексте асинхронного программирования, которое широко используется в современных C#-приложениях, паттерн Цепочка ответственности также может быть адаптирован для работы с асинхронными методами и задачами. Это позволяет эффективно обрабатывать запросы, которые требуют I/O-операций или длительных вычислений, не блокируя основной поток выполнения.
В асинхронной реализации интерфейс обработчика может выглядеть примерно так:

C# Скопировано
1
2
3
4
5
public interface IAsyncHandler
{
    IAsyncHandler SetNext(IAsyncHandler handler);
    Task<object> HandleAsync(object request);
}
А базовая реализация:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class AsyncHandler : IAsyncHandler
{
    private IAsyncHandler _nextHandler;
 
    public IAsyncHandler SetNext(IAsyncHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }
 
    public virtual async Task<object> HandleAsync(object request)
    {
        if (_nextHandler != null)
            return await _nextHandler.HandleAsync(request);
            
        return null;
    }
}
Типичные сценарии применения паттерна Цепочка ответственности довольно разнообразны и встречаются во многих областях разработки. Рассмотрим некоторые из наиболее распространённых:
1. Проверка и фильтрация — часто используется для реализации последовательных проверок входных данных. Например, когда пользователь заполняет форму на сайте, данные могут проходить через цепочку валидаторов, каждый из которых проверяет определенный аспект введённой информации.
2. Middleware в веб-фреймворках — в ASP.NET Core, Express.js и других современных веб-фреймворках промежуточное ПО (middleware) фактически реализует Цепочку ответственности. Запрос проходит через серию компонентов, каждый из которых может обработать запрос, изменить его или передать дальше.
3. Обработка событий в GUI — во многих графических интерфейсах события (клики, нажатия клавиш и т.д.) обрабатываются с использованием Цепочки ответственности. Событие сначала передаётся самому вложенному элементу интерфейса, и если он не обрабатывает его, событие "всплывает" вверх по иерархии элементов.
4. Системы логирования — когда сообщение должно быть записано в лог, оно может проходить через цепочку обработчиков, каждый из которых решает, нужно ли ему обрабатывать это сообщение на основе его уровня важности или других критериев.
5. Утверждение расходов — как в нашем вступительном примере, система утверждения расходов часто реализуется с помощью Цепочки ответственности, где каждый уровень руководства имеет определённый лимит на утверждение расходов.
6. Обработка команд — в системах, работающих с командами, запрос может проходить через цепочку обработчиков команд, пока не найдётся тот, который может выполнить эту конкретную команду.
7. Фильтрация контента — в системах модерации контента запрос может проходить через серию фильтров, каждый из которых проверяет контент на определённый тип нежелательной информации.
8. Транзакционная обработка — в финансовых системах транзакции могут проходить через цепочку проверок и корректировок, прежде чем будут окончательно обработаны.

Интересно, что паттерн Цепочка ответственности хорошо сочетается с другими паттернами проектирования. Например:
С Компоновщиком — когда каждый обработчик может содержать внутри себя группу вложенных обработчиков.
С Командой — когда запросы, передаваемые по цепочке, инкапсулированы в объектах-командах.
С Шаблонным методом — для определения скелета алгоритма обработки в базовом классе обработчика, оставляя реализацию конкретных шагов алгоритма подклассам.

Стоит обратить внимание на исследование "паттерны обработки запросов в распределенных системах", проведенное Грегором Хохпе и коллегами, которые описывают интеграцию паттерна Цепочки ответственности в современных архитектурах. Они утверждают, что этот паттерн остается критически важным в эпоху микросервисов и событийно-ориентированных архитектур.

Еще одним преимуществом Цепочки ответственности является возможность динамически настраивать процесс обработки запросов. Например, в зависимости от текущих требований системы или прав пользователя, можно включать или исключать определённые обработчики из цепочки. Также стоит упомянуть о том, что Цепочка ответственности часто применяется в реализации паттерна "Спецификация" (Specification), когда сложные условные выражения разбиваются на отдельные спецификации, объединённые в цепочку.

На практике вы можете столкнуться с разными вариациями этого паттерна. Некоторые реализации могут отходить от классического определения, но сохранять основную идею — последовательную передачу запроса от одного обработчика к другому. Например, цепочка может быть не линейной, а иметь разветвления, что превращает её в направленный граф обработчиков.

Одна из частых задач при реализации Цепочки ответственности — обеспечение правильной очерёдности обработки запросов. В некоторых случаях порядок обработчиков критически важен для правильной работы системы. Поэтому необходимо тщательно проектировать цепочку и документировать ограничения на порядок обработчиков.

Почему паттерн абстрактная фабрика - паттерн уровня объектов, если в нём могут быть статические отношения?
Взято из Шевчук А., Охрименко Д., Касьянов А. Design Patterns via C#. Приемы объектно-ориентированного проектирования (2015): Почему паттерн...

Дизайн в соответствии с принципом единственной ответственности
Учу SOLID. Вопрос касательно первого принципа, но также буду рад любым другим поправкам. Программа является симуляцией экосистемы. Экосистема...

Нарушает ли кусок кода принцип Единства ответственности?
Доброго времени суток! Подскажите, кто знает... Искал и читал объяснения принципов SOLID, но вот четкого ответа на свой вопрос не нашел пока что. А...

Разделить класс ReportMaker по ответственности за оформление отчета и за исчисление показателей: Как угодить NUnitLite ?
Program.cs /*Завдання 1 Петро розробив генератор звітів в проекті Delegates.Reports, який рахує просту статистику про погоду за кількома...


Реализация на C#



Теперь, когда мы разобрались с теоретической частью, давайте перейдем к практической реализации паттерна Цепочка ответственности на C#. Я покажу вам базовый пример, а затем мы рассмотрим несколько интересных модификаций, которые можно применить в реальных проектах.
Начнем с базового примера. Представим, что мы разрабатываем систему обработки платежей, которая должна проверять различные аспекты платежа перед его выполнением. Вот как может выглядеть реализация этой системы с использованием паттерна Цепочка ответственности:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// Класс платежа, который будет проходить через цепочку
public class Payment
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
    public string Source { get; set; }
    public string Destination { get; set; }
 
    public Payment(decimal amount, string currency, string source, string destination)
    {
        Amount = amount;
        Currency = currency;
        Source = source;
        Destination = destination;
    }
}
 
// Базовый интерфейс обработчика
public interface IPaymentHandler
{
    IPaymentHandler SetNext(IPaymentHandler handler);
    bool Process(Payment payment);
}
 
// Базовый класс для всех обработчиков
public abstract class PaymentHandler : IPaymentHandler
{
    protected IPaymentHandler _nextHandler;
 
    public IPaymentHandler SetNext(IPaymentHandler handler)
    {
        _nextHandler = handler;
        return handler; // Возвращаем обработчик для создания цепочки вызовов
    }
 
    public virtual bool Process(Payment payment)
    {
        if (_nextHandler != null)
            return _nextHandler.Process(payment);
 
        // По умолчанию, если нет следующего обработчика, считаем процесс успешным
        return true;
    }
}
 
// Конкретные обработчики
public class CurrencyValidator : PaymentHandler
{
    private readonly string[] _supportedCurrencies = { "USD", "EUR", "GBP" };
 
    public override bool Process(Payment payment)
    {
        Console.WriteLine("Проверка валюты...");
        
        if (!_supportedCurrencies.Contains(payment.Currency))
        {
            Console.WriteLine($"Валюта {payment.Currency} не поддерживается.");
            return false;
        }
        
        Console.WriteLine("Валюта поддерживается.");
        return base.Process(payment);
    }
}
 
public class AmountValidator : PaymentHandler
{
    public override bool Process(Payment payment)
    {
        Console.WriteLine("Проверка суммы платежа...");
        
        if (payment.Amount <= 0)
        {
            Console.WriteLine("Сумма платежа должна быть положительной.");
            return false;
        }
        
        if (payment.Amount > 1000000)
        {
            Console.WriteLine("Превышен лимит платежа в 1 000 000.");
            return false;
        }
        
        Console.WriteLine("Сумма платежа корректна.");
        return base.Process(payment);
    }
}
 
public class FraudDetector : PaymentHandler
{
    private readonly string[] _blacklistedSources = { "blacklisted1", "blacklisted2" };
 
    public override bool Process(Payment payment)
    {
        Console.WriteLine("Проверка на мошенничество...");
        
        if (_blacklistedSources.Contains(payment.Source.ToLower()))
        {
            Console.WriteLine("Источник платежа в черном списке.");
            return false;
        }
        
        Console.WriteLine("Проверка на мошенничество пройдена.");
        return base.Process(payment);
    }
}
 
public class PaymentProcessor : PaymentHandler
{
    public override bool Process(Payment payment)
    {
        Console.WriteLine("Обработка платежа...");
        Console.WriteLine($"Переведено {payment.Amount} {payment.Currency} от {payment.Source} к {payment.Destination}");
        return true;
    }
}
Теперь давайте посмотрим, как это все будет работать вместе:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
    static void Main()
    {
        // Создаем цепочку обработчиков
        var currencyValidator = new CurrencyValidator();
        var amountValidator = new AmountValidator();
        var fraudDetector = new FraudDetector();
        var paymentProcessor = new PaymentProcessor();
 
        // Устанавливаем порядок обработки
        currencyValidator.SetNext(amountValidator)
                        .SetNext(fraudDetector)
                        .SetNext(paymentProcessor);
 
        // Создаем платеж
        var payment = new Payment(100, "USD", "Alice", "Bob");
        
        // Запускаем обработку
        bool result = currencyValidator.Process(payment);
        
        Console.WriteLine(result 
            ? "Платеж успешно обработан." 
            : "Произошла ошибка при обработке платежа.");
    }
}
Что тут происходит? Мы создали цепочку из четырех обработчиков:
1. Валидатор валюты.
2. Валидатор суммы.
3. Детектор мошенничества.
4. Процессор платежа.

Запрос на обработку платежа проходит через каждый обработчик по порядку. Если какой-то обработчик возвращает false, цепочка прерывается. Если все обработчики возвращают true, платеж считается успешно обработанным.
Это базовая реализация, но на практике вы часто будете сталкиваться с более сложными сценариями. Давайте рассмотрим некоторые модификации:

1. Использование Generic-типов для универсальных обработчиков

Generic-типы позволяют создавать более гибкие обработчики, которые могут работать с разными типами запросов:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IHandler<T>
{
    IHandler<T> SetNext(IHandler<T> handler);
    bool Handle(T request);
}
 
public abstract class Handler<T> : IHandler<T>
{
    protected IHandler<T> _nextHandler;
 
    public IHandler<T> SetNext(IHandler<T> handler)
    {
        _nextHandler = handler;
        return handler;
    }
 
    public virtual bool Handle(T request)
    {
        if (_nextHandler != null)
            return _nextHandler.Handle(request);
        return true;
    }
}
2. Асинхронные обработчики для работы с I/O операциями

В современных приложениях часто требуется выполнять асинхронные операции. Вот как может выглядеть асинхронная версия нашего обработчика:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IAsyncHandler<T>
{
    IAsyncHandler<T> SetNext(IAsyncHandler<T> handler);
    Task<bool> HandleAsync(T request);
}
 
public abstract class AsyncHandler<T> : IAsyncHandler<T>
{
    protected IAsyncHandler<T> _nextHandler;
 
    public IAsyncHandler<T> SetNext(IAsyncHandler<T> handler)
    {
        _nextHandler = handler;
        return handler;
    }
 
    public virtual async Task<bool> HandleAsync(T request)
    {
        if (_nextHandler != null)
            return await _nextHandler.HandleAsync(request);
        return true;
    }
}
3. Использование делегатов и функционального подхода

Для более лаконичной реализации можно использовать делегаты:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FunctionalPaymentProcessor
{
    private readonly List<Func<Payment, bool>> _handlers = new List<Func<Payment, bool>>();
 
    public void AddHandler(Func<Payment, bool> handler)
    {
        _handlers.Add(handler);
    }
 
    public bool Process(Payment payment)
    {
        return _handlers.All(handler => handler(payment));
    }
}
 
// Использование:
var processor = new FunctionalPaymentProcessor();
processor.AddHandler(p => p.Currency == "USD");
processor.AddHandler(p => p.Amount > 0 && p.Amount < 1000000);
processor.AddHandler(p => !blacklistedSources.Contains(p.Source));
Этот подход особенно хорош для простых проверок, но при сложной логике обработки классический объектно-ориентированный подход может быть предпочтительнее.

4. Создание цепочки с использованием DI-контейнеров

В современных приложениях часто используются контейнеры внедрения зависимостей (Dependency Injection). Вот как можно зарегистрировать и использовать обработчики с Microsoft.Extensions.DependencyInjection:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Регистрация обработчиков
services.AddTransient<IPaymentHandler, CurrencyValidator>();
services.AddTransient<IPaymentHandler, AmountValidator>();
services.AddTransient<IPaymentHandler, FraudDetector>();
services.AddTransient<IPaymentHandler, PaymentProcessor>();
 
// Регистрация фабрики цепочки
services.AddTransient<Func<IEnumerable<IPaymentHandler>>>(sp => 
    () => sp.GetServices<IPaymentHandler>());
 
// В сервисе, который будет использовать цепочку:
public class PaymentService
{
    private readonly Func<IEnumerable<IPaymentHandler>> _handlersFactory;
 
    public PaymentService(Func<IEnumerable<IPaymentHandler>> handlersFactory)
    {
        _handlersFactory = handlersFactory;
    }
 
    public bool ProcessPayment(Payment payment)
    {
        // Получаем все зарегистрированные обработчики
        var handlers = _handlersFactory().ToList();
        
        // Соединяем их в цепочку
        for (int i = 0; i < handlers.Count - 1; i++)
        {
            handlers[i].SetNext(handlers[i + 1]);
        }
        
        // Запускаем обработку
        return handlers.First().Process(payment);
    }
}
Использование DI-контейнера позволяет легко добавлять новые обработчики в цепочку без изменения существующего кода. Вы просто регистрируете новый обработчик в контейнере, и он автоматически включается в цепочку.

5. Улучшение с помощью декораторов

Интересный подход к реализации Цепочки ответственности — использование паттерна Декоратор. Это позволяет добавлять функциональность к обработчикам динамически:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Декоратор для логирования
public class LoggingHandlerDecorator<T> : IHandler<T>
{
    private readonly IHandler<T> _handler;
    private readonly ILogger _logger;
 
    public LoggingHandlerDecorator(IHandler<T> handler, ILogger logger)
    {
        _handler = handler;
        _logger = logger;
    }
 
    public IHandler<T> SetNext(IHandler<T> handler)
    {
        _handler.SetNext(handler);
        return handler;
    }
 
    public bool Handle(T request)
    {
        _logger.Log($"Обработка запроса типа {typeof(T).Name}");
        bool result = _handler.Handle(request);
        _logger.Log($"Результат обработки: {result}");
        return result;
    }
}
6. Создание расширяемой системы плагинов

Цепочка ответственности идеально подходит для создания расширяемой системы плагинов:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface IPlugin
{
    string Name { get; }
    bool CanHandle(object data);
    bool Handle(object data);
}
 
public class PluginManager
{
    private readonly List<IPlugin> _plugins = new List<IPlugin>();
 
    public void RegisterPlugin(IPlugin plugin)
    {
        _plugins.Add(plugin);
    }
 
    public bool ProcessData(object data)
    {
        foreach (var plugin in _plugins.Where(p => p.CanHandle(data)))
        {
            if (plugin.Handle(data))
                return true;
        }
        return false;
    }
}
Пожалуй, самым нестандартным, но крайне эффективным применением паттерна Цепочка ответственности можно считать его интеграцию с реактивным программированием. Например, с использованием библиотеки Reactive Extensions (Rx.NET) можно создать поток обработки, где каждый обработчик представляет собой оператор в потоке:

C# Скопировано
1
2
3
4
5
6
7
IObservable<Payment> paymentStream = Observable.FromAsync(() => GetPaymentAsync());
 
var processedPayments = paymentStream
    .Where(p => _supportedCurrencies.Contains(p.Currency)) // CurrencyValidator
    .Where(p => p.Amount > 0 && p.Amount <= 1000000)      // AmountValidator
    .Where(p => !_blacklistedSources.Contains(p.Source))   // FraudDetector
    .Do(p => ProcessPayment(p));                          // PaymentProcessor
В этом примере операторы .Where() и .Do() формируют последовательность обработки, аналогичную цепочке ответственности.

Все эти подходы и модификации демонстрируют гибкость паттерна Цепочка ответственности и его способность адаптироваться к различным требованиям и условиям. Выбор конкретной реализации зависит от контекста использования и требований к системе.

Практические примеры



Я расскажу о нескольких практических примерах, которые вы можете сразу же применить в своих проектах.

Обработка запросов в веб-приложениях



В мире современной веб-разработки обработка HTTP-запросов часто требует выполнения ряда стандартных действий: аутентификация, авторизация, кэширование, сжатие, логирование и т.д. Посмотрите на этот пример middleware в ASP.NET Core:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class CustomApiMiddleware
{
    private readonly RequestDelegate _next;
 
    public CustomApiMiddleware(RequestDelegate next)
    {
        _next = next;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        // Проверяем заголовок API-ключа
        if (!context.Request.Headers.TryGetValue("Api-Key", out var apiKey))
        {
            context.Response.StatusCode = StatusCodes.Status401Unauthorized;
            await context.Response.WriteAsync("API ключ отсутствует");
            return;
        }
 
        // Проверяем валидность API-ключа
        if (!IsValidApiKey(apiKey))
        {
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            await context.Response.WriteAsync("Недействительный API ключ");
            return;
        }
 
        // Всё в порядке, передаём управление следующему middleware
        await _next(context);
    }
 
    private bool IsValidApiKey(string apiKey)
    {
        // Логика проверки ключа
        return apiKey == "valid-api-key-123";
    }
}
 
// Регистрация в Startup.cs
app.UseMiddleware<CustomApiMiddleware>();
В этом примере middleware работает как звено в цепочке ответственности. Если API-ключ отсутствует или недействителен, обработка останавливается. В противном случае запрос передается дальше по цепочке.

Система логирования с разными уровнями



Другой классический пример — многоуровневая система логирования. Вы можете определить различные обработчики для различных уровней логирования:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public enum LogLevel
{
    Debug,
    Info,
    Warning,
    Error,
    Critical
}
 
public class LogEntry
{
    public LogLevel Level { get; set; }
    public string Message { get; set; }
    public DateTime Timestamp { get; set; }
 
    public LogEntry(LogLevel level, string message)
    {
        Level = level;
        Message = message;
        Timestamp = DateTime.Now;
    }
}
 
public abstract class LogHandler
{
    protected LogHandler _nextHandler;
    protected LogLevel _level;
 
    public LogHandler SetNext(LogHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }
 
    public virtual void Log(LogEntry entry)
    {
        if (entry.Level >= _level)
        {
            WriteLog(entry);
        }
 
        if (_nextHandler != null)
        {
            _nextHandler.Log(entry);
        }
    }
 
    protected abstract void WriteLog(LogEntry entry);
}
 
// Конкретные обработчики
public class ConsoleLogHandler : LogHandler
{
    public ConsoleLogHandler(LogLevel level)
    {
        _level = level;
    }
 
    protected override void WriteLog(LogEntry entry)
    {
        Console.WriteLine($"[{entry.Timestamp}][{entry.Level}] {entry.Message}");
    }
}
 
public class FileLogHandler : LogHandler
{
    private string _filePath;
 
    public FileLogHandler(LogLevel level, string filePath)
    {
        _level = level;
        _filePath = filePath;
    }
 
    protected override void WriteLog(LogEntry entry)
    {
        File.AppendAllText(_filePath, 
            $"[{entry.Timestamp}][{entry.Level}] {entry.Message}\n");
    }
}
 
public class EmailLogHandler : LogHandler
{
    private string _emailAddress;
 
    public EmailLogHandler(LogLevel level, string emailAddress)
    {
        _level = level;
        _emailAddress = emailAddress;
    }
 
    protected override void WriteLog(LogEntry entry)
    {
        // В реальном коде здесь была бы отправка email
        Console.WriteLine($"Отправка лога на {_emailAddress}: [{entry.Level}] {entry.Message}");
    }
}
Использование этой системы логирования будет выглядеть так:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var consoleLogger = new ConsoleLogHandler(LogLevel.Debug);
var fileLogger = new FileLogHandler(LogLevel.Info, "app.log");
var emailLogger = new EmailLogHandler(LogLevel.Critical, "admin@example.com");
 
consoleLogger.SetNext(fileLogger).SetNext(emailLogger);
 
// Это увидят все обработчики
consoleLogger.Log(new LogEntry(LogLevel.Critical, "Критическая ошибка!"));
 
// Это увидят только консоль и файл
consoleLogger.Log(new LogEntry(LogLevel.Info, "Информационное сообщение"));
 
// Это увидит только консоль
consoleLogger.Log(new LogEntry(LogLevel.Debug, "Отладочная информация"));
Это отличный пример непрерывной цепочки, где запрос проходит через все обработчики, но каждый из них решает, нужно ли его обрабатывать.

Фильтрация данных в многоуровневой системе



Представьте, что у вас есть сервис поиска, который должен отфильтровывать неподходящий контент перед отправкой результатов пользователю:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class SearchResult
{
    public string Content { get; set; }
    public string Category { get; set; }
    public int Rating { get; set; }
 
    public SearchResult(string content, string category, int rating)
    {
        Content = content;
        Category = category;
        Rating = rating;
    }
}
 
public interface IContentFilter
{
    IContentFilter SetNext(IContentFilter filter);
    List<SearchResult> Filter(List<SearchResult> results);
}
 
public abstract class ContentFilter : IContentFilter
{
    protected IContentFilter _nextFilter;
 
    public IContentFilter SetNext(IContentFilter filter)
    {
        _nextFilter = filter;
        return filter;
    }
 
    public virtual List<SearchResult> Filter(List<SearchResult> results)
    {
        if (_nextFilter != null)
            return _nextFilter.Filter(results);
        
        return results;
    }
}
 
public class AdultContentFilter : ContentFilter
{
    public override List<SearchResult> Filter(List<SearchResult> results)
    {
        var filtered = results.Where(r => r.Category != "Adult").ToList();
        return base.Filter(filtered);
    }
}
 
public class LowRatingFilter : ContentFilter
{
    public override List<SearchResult> Filter(List<SearchResult> results)
    {
        var filtered = results.Where(r => r.Rating >= 3).ToList();
        return base.Filter(filtered);
    }
}
 
public class ProfanityFilter : ContentFilter
{
    private readonly string[] _profanityWords = { "bad", "word", "offensive" };
 
    public override List<SearchResult> Filter(List<SearchResult> results)
    {
        var filtered = results.Where(r => 
            !_profanityWords.Any(word => r.Content.ToLower().Contains(word))).ToList();
        return base.Filter(filtered);
    }
}
Использование:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var adultFilter = new AdultContentFilter();
var ratingFilter = new LowRatingFilter();
var profanityFilter = new ProfanityFilter();
 
adultFilter.SetNext(ratingFilter).SetNext(profanityFilter);
 
var searchResults = new List<SearchResult>
{
    new SearchResult("Хороший контент", "General", 4),
    new SearchResult("Контент с bad словом", "General", 5),
    new SearchResult("Нейтральный контент", "Adult", 3),
    new SearchResult("Слабый контент", "General", 2)
};
 
var filteredResults = adultFilter.Filter(searchResults);
// Результат будет содержать только "Хороший контент"

Валидация входных данных в веб-API



Валидация входных данных — ещё одна область, где Цепочка ответственности может быть очень полезна:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
public class User
{
    public string Username { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}
 
public class ValidationResult
{
    public bool IsValid { get; set; } = true;
    public List<string> Errors { get; set; } = new List<string>();
}
 
public interface IValidator<T>
{
    IValidator<T> SetNext(IValidator<T> validator);
    ValidationResult Validate(T entity);
}
 
public abstract class Validator<T> : IValidator<T>
{
    protected IValidator<T> _nextValidator;
 
    public IValidator<T> SetNext(IValidator<T> validator)
    {
        _nextValidator = validator;
        return validator;
    }
 
    public virtual ValidationResult Validate(T entity)
    {
        ValidationResult result = new ValidationResult();
        
        if (_nextValidator != null)
        {
            ValidationResult nextResult = _nextValidator.Validate(entity);
            
            // Объединяем ошибки
            result.IsValid = result.IsValid && nextResult.IsValid;
            result.Errors.AddRange(nextResult.Errors);
        }
        
        return result;
    }
}
 
public class UsernameValidator : Validator<User>
{
    public override ValidationResult Validate(User user)
    {
        var result = new ValidationResult();
        
        if (string.IsNullOrWhiteSpace(user.Username))
        {
            result.IsValid = false;
            result.Errors.Add("Имя пользователя не может быть пустым");
        }
        else if (user.Username.Length < 3)
        {
            result.IsValid = false;
            result.Errors.Add("Имя пользователя должно быть не менее 3 символов");
        }
        
        var nextResult = base.Validate(user);
        
        // Объединяем результаты
        result.IsValid = result.IsValid && nextResult.IsValid;
        result.Errors.AddRange(nextResult.Errors);
        
        return result;
    }
}
 
public class EmailValidator : Validator<User>
{
    public override ValidationResult Validate(User user)
    {
        var result = new ValidationResult();
        
        if (string.IsNullOrWhiteSpace(user.Email))
        {
            result.IsValid = false;
            result.Errors.Add("Email не может быть пустым");
        }
        else if (!user.Email.Contains("@"))
        {
            result.IsValid = false;
            result.Errors.Add("Некорректный формат email");
        }
        
        var nextResult = base.Validate(user);
        
        result.IsValid = result.IsValid && nextResult.IsValid;
        result.Errors.AddRange(nextResult.Errors);
        
        return result;
    }
}
 
public class PasswordValidator : Validator<User>
{
    public override ValidationResult Validate(User user)
    {
        var result = new ValidationResult();
        
        if (string.IsNullOrWhiteSpace(user.Password))
        {
            result.IsValid = false;
            result.Errors.Add("Пароль не может быть пустым");
        }
        else if (user.Password.Length < 8)
        {
            result.IsValid = false;
            result.Errors.Add("Пароль должен содержать не менее 8 символов");
        }
        
        var nextResult = base.Validate(user);
        
        result.IsValid = result.IsValid && nextResult.IsValid;
        result.Errors.AddRange(nextResult.Errors);
        
        return result;
    }
}
Использование:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var usernameValidator = new UsernameValidator();
var emailValidator = new EmailValidator();
var passwordValidator = new PasswordValidator();
 
usernameValidator.SetNext(emailValidator).SetNext(passwordValidator);
 
var user = new User
{
    Username = "jo",
    Email = "notvalidemail",
    Password = "123"
};
 
var validationResult = usernameValidator.Validate(user);
 
if (!validationResult.IsValid)
{
    Console.WriteLine("Пользователь не прошел валидацию:");
    foreach (var error in validationResult.Errors)
    {
        Console.WriteLine($"- {error}");
    }
}

Реализация бизнес-правил в корпоративных приложениях



В корпоративных приложениях часто требуется реализовать сложные бизнес-правила, которые определяют, может ли определённая операция быть выполнена. Цепочка ответственности — отличный способ организовать такие правила:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class Loan
{
    public decimal Amount { get; set; }
    public int TermInMonths { get; set; }
    public decimal InterestRate { get; set; }
    public int CreditScore { get; set; }
    public decimal Income { get; set; }
    public decimal ExistingDebt { get; set; }
}
 
public class LoanApprovalResult
{
    public bool IsApproved { get; set; }
    public string Reason { get; set; }
}
 
public abstract class LoanApprovalHandler
{
    protected LoanApprovalHandler _nextHandler;
 
    public LoanApprovalHandler SetNext(LoanApprovalHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }
 
    public abstract LoanApprovalResult Approve(Loan loan);
}
 
public class CreditScoreHandler : LoanApprovalHandler
{
    public override LoanApprovalResult Approve(Loan loan)
    {
        if (loan.CreditScore < 600)
        {
            return new LoanApprovalResult
            {
                IsApproved = false,
                Reason = "Низкий кредитный рейтинг"
            };
        }
 
        return _nextHandler?.Approve(loan) ?? new LoanApprovalResult { IsApproved = true };
    }
}
 
public class DebtToIncomeHandler : LoanApprovalHandler
{
    public override LoanApprovalResult Approve(Loan loan)
    {
        decimal monthlyPayment = loan.Amount * (loan.InterestRate / 12) / 
                               (1 - (decimal)Math.Pow((double)(1 + loan.InterestRate / 12), -loan.TermInMonths));
        
        decimal dti = (loan.ExistingDebt + monthlyPayment) / loan.Income;
        
        if (dti > 0.43m)
        {
            return new LoanApprovalResult
            {
                IsApproved = false,
                Reason = "Слишком высокое соотношение долга к доходу"
            };
        }
 
        return _nextHandler?.Approve(loan) ?? new LoanApprovalResult { IsApproved = true };
    }
}
 
public class LoanAmountHandler : LoanApprovalHandler
{
    public override LoanApprovalResult Approve(Loan loan)
    {
        if (loan.Amount > loan.Income * 5)
        {
            return new LoanApprovalResult
            {
                IsApproved = false,
                Reason = "Сумма кредита слишком высока относительно годового дохода"
            };
        }
 
        return _nextHandler?.Approve(loan) ?? new LoanApprovalResult { IsApproved = true };
    }
}
Использование:

C# Скопировано
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var creditScoreHandler = new CreditScoreHandler();
var dtiHandler = new DebtToIncomeHandler();
var loanAmountHandler = new LoanAmountHandler();
 
creditScoreHandler.SetNext(dtiHandler).SetNext(loanAmountHandler);
 
var loan = new Loan
{
    Amount = 200000,
    TermInMonths = 360,
    InterestRate = 0.035m,
    CreditScore = 720,
    Income = 60000,
    ExistingDebt = 10000
};
 
var result = creditScoreHandler.Approve(loan);
Console.WriteLine(result.IsApproved 
    ? "Кредит одобрен" 
    : $"Кредит не одобрен: {result.Reason}");
Все эти примеры демонстрируют, насколько разнообразным может быть применение паттерна Цепочка ответственности. От обработки HTTP-запросов и логирования до фильтрации данных, валидации и реализации бизнес-правил — этот паттерн проявляет свою гибкость и удобство использования в различных сценариях.

Подводные камни и ограничения



Несмотря на всю гибкость и полезность паттерна Цепочка ответственности, при его использовании можно столкнуться с рядом проблем и ограничений. Рассмотрим основные из них, чтобы вы могли избежать типичных ловушек.

Проблемы производительности



Одной из главных проблем при использовании длинных цепочек обработчиков является снижение производительности. Каждый обработчик в цепочке добавляет дополнительный слой косвенности, что может привести к замедлению работы вашего приложения. Представьте ситуацию: у вас есть цепочка из 20 обработчиков, и запрос проходит через всю цепочку. Если каждый обработчик выполняет даже небольшую работу, совокупное время обработки может стать значительным. Эта проблема особенно заметна в высоконагруженных системах. Для решения этой проблемы можно:
  • Комбинировать несколько близких по смыслу обработчиков в один.
  • Использовать асинхронный подход для параллельной обработки, где это возможно.
  • Применять кэширование результатов работы обработчиков.
  • Внедрить механизм пропуска обработчиков для определенных типов запросов.

Гарантия обработки



Другая распространенная проблема — отсутствие гарантии обработки запроса. Если ни один из обработчиков в цепочке не способен обработать запрос, он может быть просто проигнорирован без какого-либо явного сообщения об ошибке. Чтобы избежать этой проблемы, рекомендуется:
  • Добавлять в конец цепочки обработчик по умолчанию, который генерирует исключение или возвращает сообщение об ошибке.
  • Реализовать механизм логирования, который отслеживает путь запроса через цепочку.
  • Использовать какой-то флаг или свойство в запросе, которое указывает, был ли он обработан.

Отладка и тестирование



Отладка и тестирование приложений с использованием Цепочки ответственности может стать настоящей головной болью. Когда запрос проходит через множество обработчиков, может быть трудно понять, где именно произошла ошибка или почему запрос не был обработан ожидаемым образом. Для упрощения отладки:
  • Используйте подробное логирование в каждом обработчике.
  • Реализуйте механизм трассировки, который записывает путь запроса через цепочку.
  • Пишите модульные тесты для каждого обработчика отдельно.
  • Создавайте интеграционные тесты для проверки работы всей цепочки.

Тестирование цепочки обработчиков тоже имеет свои особенности. Важно проверять не только работу отдельных обработчиков, но и их взаимодействие в цепочке. Для этого можно использовать техники имитации (mocking) для создания тестовых обработчиков, которые эмулируют различные сценарии.

Сложность конфигурации



С увеличением количества обработчиков растет и сложность конфигурации цепочки. Особенно это заметно, когда порядок обработчиков имеет критическое значение для правильной работы системы.
Для управления сложностью:
  • Документируйте зависимости между обработчиками и требования к их порядку.
  • Используйте фабрики или строители для создания предварительно настроенных цепочек.
  • Рассмотрите возможность хранения конфигурации цепочки во внешнем источнике.
  • Разрабатывайте инструменты визуализации для отображения структуры цепочки.

Альтернативные решения



В некоторых случаях Цепочка ответственности может оказаться не самым оптимальным решением. Иногда лучше использовать альтернативные подходы:
1. Паттерн Стратегия — если вы знаете, какой именно алгоритм нужен для обработки конкретного запроса. Стратегия позволяет выбирать алгоритм напрямую, без прохождения через цепочку.
2. Паттерн Команда — если вам нужна возможность отмены операций или их постановки в очередь на выполнение.
3. Паттерн Посредник — когда вам требуется централизованное управление взаимодействиями между объектами.
4. Обычный switch/case или словарь обработчиков — в простых случаях, когда цепочка излишне усложнит код.

Выбор между этими паттернами зависит от конкретных требований вашей системы, включая такие факторы как производительность, гибкость, расширяемость и поддерживаемость.

В заключение, паттерн Цепочка ответственности — мощный инструмент в арсенале разработчика, но как и любой другой инструмент, он должен применяться с учетом его сильных и слабых сторон. Понимание потенциальных проблем и ограничений поможет вам эффективно использовать этот паттерн в своих проектах и избежать распространенных ошибок.

Цепочка форм
1.На главной форме есть таблица в которую при помощи кнопки добавить открывается вторая форма в которой можно ввести данные которые в свою очередь...

Цепочка слов(метаграмма)
/*На выходе программа должна вывести в консоль путь от исходного слова к конечному, по одному слову на одной строке. Пример Начальное слово:...

Цепочка корутин из xml
Добрый день. Нужна помощь опытных программистов, так как сам работаю с C# недавно. Мне нужно из xml файла сформировать цепочку методов, которые...

Полином, цепочка слов
Добрый день! Прошу помочь... Необходимо написать 2 программы. Мне нужна помощь с основным кодом.. 1) Последовательность многочленов определяется...

Логическая цепочка выбора
Добрый день постояльцы. Прошу помощи в усовершенствовании кода. Есть на форме ComboBox и в него подхватывается из TXT собственно коллекция (фамилии...

длинная неубывающая цепочка чисел
помогите пожалуйста сделать задание. просто нужно сегодня его сдать, но не хватает времени. В цикле с клавиатуры вводятся 15 целых чисел....

Системное программирование, цепочка из Task
Как создать цепочку из 3х Task, чтобы этот Task1 принимал целое число, инкриминирует его 1000 раз в цикле и возвращает полученный результат? вот...

Цепочка вызовов и передача аргументов
Если делегат ссылается на цепочку методов (в примере ниже на методы one и two), то всем методам передается начальное значение аргумента? Эксперимент...

Из собеседования - вложенная цепочка try catch
Доброго времени суток. Предлагаю следующий вопрос для обсуждения. Есть некий код try { //some code here that may throw...

Ожидание выполнения метода [цепочка методов]
Здравствуйте. Мне нужно чтобы методы выполнялись друг за другом, ожидая выполнения предыдущего и не подключаясь к БД. Я использую БД SqLlite и нижний...

Непрерывная цепочка повторяющихся символов в файле
здравствуйте) очень нужна помощь с разбором задачи. Готова учиться, направьте) вот суть задачи: Найдите самую длинную непрерывную цепочку...

Цепочка объектов на основе обобщенного класса
Здравствуйте ! Помогите,пожалуйста, разобраться с задачкой из книжки. Напишите программу, в которой создаётся цепочка из объектов, созданных на...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Сопоставление с образцом (Pattern Matching) в Python: Списки и словари
py-thonny 19.03.2025
Программисты любят, когда код говорит сам за себя. Представьте, что вы можете просмотреть структуру данных и мгновенно понять, что с ней делать — без сложных условий и вложенных проверок. Именно эту. . .
Работа с метаданными EXIF и JPEG в JavaScript
run.dev 19.03.2025
Работа с изображениями в веб-разработке нередко выходит за рамки простого отображения картинки. Современные задачи требуют глубокого понимания структуры файлов и умения манипулировать их внутренними. . .
Чеклист для Kubernetes в продакшене: Лучшие практики для SRE
Mr. Docker 19.03.2025
Когда сталкиваешься с запуском Kubernetes в продакшене, невольно задаешься вопросом: почему то, что так гладко работало в тестовой среде, вдруг начинает вызывать головную боль на боевых системах?. . .
Разработка продвинутого ИИ в Unity с использованием Behavior Graph
GameUnited 19.03.2025
В разработке игр искусственный интеллект персонажей часто становится тем элементом, который превращает хорошую игру в выдающуюся. До недавнего времени разработчикам под Unity приходилось либо писать. . .
Словари в Python: методы работы, оптимизация, сериализация
py-thonny 19.03.2025
Каждый хотя бы раз сталкивался с необходимостью хранить связанные данные, где важна не только сама информация, но и их взаимосвязь. В дебрях Python словари — это тот универсальный инструмент, который. . .
Реализация паттерна CQRS с Event Sourcing в PHP
Jason-Webb 19.03.2025
CQRS (Command Query Responsibility Segregation) — это архитектурный паттерн, который разделяет операции чтения и записи данных в приложении. Если вы столкнулись с ситуацией, когда ваше PHP-приложение. . .
std::span в C++: Подпредставлени­я и срезы
NullReferenced 18.03.2025
Если вы когда-нибудь работали с большими объемами данных в C++, то наверняка сталкивались с необходимостью манипулировать отдельными частями массивов или контейнеров. Традиционные подходы часто. . .
std::span в C++: Доступ к элементам и итерирование
NullReferenced 18.03.2025
В C++ каждый разработчик сталкивается с проблемой эффективного управления последовательностями данных. Представьте: вы работаете с массивом, передаете его в функцию, а затем в другую, и каждый раз. . .
Утечки памяти в C#
UnmanagedCoder 18.03.2025
Когда мы говорим о разработке приложений на C#, то часто успокаиваем себя мыслью, что сборщик мусора решит все наши проблемы с памятью. "Память управляется автоматически" — эта мантра прочно засела в. . .
std::span в C++: Введение в невладеющее представление
NullReferenced 18.03.2025
С появлением стандарта C++20 у нас появился новый инструмент — std::span, который представляет собой невладеющее представление для работы с последовательностями данных. std::span — это легковесный. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru
Выделить код Копировать код Сохранить код Нормальный размер Увеличенный размер