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

Паттерны проектирования GoF на C#

Запись от UnmanagedCoder размещена 13.05.2025 в 10:56
Показов 4262 Комментарии 0
Метки .net, c#, gof, oop, patterns, solid

Нажмите на изображение для увеличения
Название: dd9a3945-6ccd-4cd0-91c6-39223ea4edf2.jpg
Просмотров: 51
Размер:	74.0 Кб
ID:	10801
Вы наверняка сталкивались с ситуациями, когда код разрастается до неприличных размеров, а его поддержка становится настоящим испытанием. Именно в такие моменты на помощь приходят паттерны Gang of Four (сокращенно GoF) — набор из 23 архитектурных решений, описанных в 1994 году в книге "Design Patterns: Elements of Reusable Object-Oriented Software". История этих паттернов в C# берёт своё начало с самых первых версий языка. Еще в .NET Framework 1.0 многие разработчики начали применять эти подходы, но по-настоящему популярными они стали с появлением .NET 2.0 и дженериков, которые существенно расширили возможности для элегантной реализации многих шаблонов.

Команда разработчиков .NET фреймворка тоже не осталась в стороне — многие классы и механизмы внутри самого фреймворка реализованы в соответствии с GoF-паттернами. Возьмём хотя бы событийную модель, которая полностью соответсвует паттерну "Наблюдатель", или систему коллекций, использующую "Итератор". С развитием C# и появлением каждой новой версии языка, реализация классических паттернов становилась всё элегантнее. Например, лямбда-выражения в C# 3.0 сделали паттерн "Стратегия" настолько простым, что иногда его уже трудно распознать как отдельный паттерн проектирования.

Почему же паттерны остаются актуальными даже сейчас, в эпоху микросервисов и облачных вычислений? Дело в том, что они решают фундаментальные проблемы проектирования, которые не исчезают с появлением новых технологий. Скорее, эти проблемы просто приобретают новые формы. Возьмём, к примеру, принцип единственной ответствености (SRP) из SOLID. Паттерн "Декоратор" идеально подходит для его соблюдения, позволяя динамически добавлять новую функциональность объектам без изменения их исходного кода. А паттерн "Фабрика" помогает следовать принципу инверсии зависимостей (DIP), создавая объекты без жесткой привязки к конкретным реализациям. Особенно ценны паттерны в больших проектах с множеством разработчиков. Они создают общий язык, позволяющий быстро объяснять архитектурные решения. Вместо долгого объяснения, как устроена та или иная подсистема, достаточно сказать: "Мы исползуем здесь паттерн 'Строитель' для конфигурирования объектов" — и всем опытным разработчикам сразу станет понятна общая структура.

Порождающие паттерны в C#



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

Singleton: один и только один



Начнём с, пожалуй, самого известного и одновременно самого противоречивого паттерна — Singleton (Одиночка). Его цель проста: гарантировать существование только одного экземпляра класса и предоставить глобальную точку доступа к нему.
Вот классическая реализация Singleton в C#:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public sealed class Singleton
{
    private static readonly Lazy<Singleton> lazyInstance = 
        new Lazy<Singleton>(() => new Singleton());
    
    public static Singleton Instance => lazyInstance.Value;
    
    private Singleton() { }
    
    public void SomeBusinessLogic()
    {
        // Код, реализующий основную функциональность
    }
}
Это современная, потокобезопасная реализация с использованием класса Lazy<T>. Основные преимущества такого подхода:
1. Ленивая инициализация — экземпляр создаётся только при первом обращении.
2. Потокобезопасность гарантируется самим классом Lazy<T>.
3. Код чистый и понятный.

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

Factory Method: создание через интерфейс



Паттерн Factory Method (Фабричный метод) позволяет создавать объекты, не указывая конкретный класс создаваемого объекта. Вместо этого определяется метод, который должен создавать объект, а его конкретная реализация определяется в подклассах.

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
public interface IProduct
{
    string Operation();
}
 
public class ConcreteProductA : IProduct
{
    public string Operation() => "Result of ConcreteProductA";
}
 
public class ConcreteProductB : IProduct
{
    public string Operation() => "Result of ConcreteProductB";
}
 
public abstract class Creator
{
    public abstract IProduct FactoryMethod();
    
    public string SomeOperation()
    {
        var product = FactoryMethod();
        return $"Creator worked with {product.Operation()}";
    }
}
 
public class ConcreteCreatorA : Creator
{
    public override IProduct FactoryMethod() => new ConcreteProductA();
}
 
public class ConcreteCreatorB : Creator
{
    public override IProduct FactoryMethod() => new ConcreteProductB();
}
Этот паттерн отлично решает проблему создания объектов в системах, где заранее неизвесно, объекты каких именно классов потребуется создавать. Допустим, вы разрабатываете фреймворк для работы с различными базами данных. Вместо того чтобы привязывать код к конкретной СУБД, можно использовать Factory Method для создания соедениний.
В практике часто встречается упрощенная версия этого паттерна, когда вместо иерархии создателей используется статический метод:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public static class ProductFactory
{
    public static IProduct CreateProduct(string type)
    {
        switch (type)
        {
            case "A": return new ConcreteProductA();
            case "B": return new ConcreteProductB();
            default: throw new ArgumentException("Unknown product type");
        }
    }
}
Такой подход проще, но имеет свои недостатки — при добавлении новых типов продуктов придётся изменять код фабрики, что нарушает принцип открытости/закрытости.

Abstract Factory: семейства продуктов



Abstract Factory (Абстрактная фабрика) — это паттерн, который предоставляет интерфейс для создания целых семейств взаимосвязанных объектов без привязки к их конкретным классам. Представьте, что вы разрабатываете ситему для создания пользовательского интерфейса, который должен адаптироваться под различные операционные системы. У вас есть кнопки, поля ввода, диалоговые окна — и все они должны выглядеть и вести себя согласованно в рамках одной ОС.

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
// Интерфейсы продуктов
public interface IButton
{
    void Render();
    void HandleClick();
}
 
public interface ITextBox
{
    void Render();
    void HandleInput();
}
 
// Конкретные продукты для Windows
public class WindowsButton : IButton
{
    public void Render() => Console.WriteLine("Rendering Windows-style button");
    public void HandleClick() => Console.WriteLine("Windows button clicked");
}
 
public class WindowsTextBox : ITextBox
{
    public void Render() => Console.WriteLine("Rendering Windows-style text box");
    public void HandleInput() => Console.WriteLine("Handling input in Windows text box");
}
 
// Конкретные продукты для macOS
public class MacOSButton : IButton
{
    public void Render() => Console.WriteLine("Rendering macOS-style button");
    public void HandleClick() => Console.WriteLine("macOS button clicked");
}
 
public class MacOSTextBox : ITextBox
{
    public void Render() => Console.WriteLine("Rendering macOS-style text box");
    public void HandleInput() => Console.WriteLine("Handling input in macOS text box");
}
 
// Абстрактная фабрика
public interface IUIFactory
{
    IButton CreateButton();
    ITextBox CreateTextBox();
}
 
// Конкретные фабрики
public class WindowsUIFactory : IUIFactory
{
    public IButton CreateButton() => new WindowsButton();
    public ITextBox CreateTextBox() => new WindowsTextBox();
}
 
public class MacOSUIFactory : IUIFactory
{
    public IButton CreateButton() => new MacOSButton();
    public ITextBox CreateTextBox() => new MacOSTextBox();
}
Этот паттерн очень полезен, когда система должна быть независима от способа создания и компоновки продуктов, и когда необходимо обеспечить создание согласованных наборов объектов.

Builder: пошаговое конструирование сложных объектов



Представьте, что вам нужно создать объект с десятком параметров, часть из которых опциональные. Конструктор с 10 аргументами выглядел бы ужасно, да и использовать его было бы кошмарно. Именно для таких случаев существует паттерн Builder (Строитель). Builder позволяет создавать сложные объекты пошагово. Ключевая особенность — возможность использовать один и тот же код строительства для получения разных представлений объекта.

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
public class Product
{
public string PartA { get; set; }
public string PartB { get; set; }
public string PartC { get; set; }
public int PartD { get; set; }
public bool PartE { get; set; }
 
public override string ToString() =>
    $"Product parts: {PartA}, {PartB}, {PartC}, {PartD}, {PartE}";
}
 
public interface IBuilder
{
IBuilder BuildPartA();
IBuilder BuildPartB(string value);
IBuilder BuildPartC(string value);
IBuilder BuildPartD(int value);
IBuilder BuildPartE(bool value);
Product GetResult();
}
 
public class ConcreteBuilder : IBuilder
{
private readonly Product _product = new Product();
 
public IBuilder BuildPartA()
{
    _product.PartA = "Part A default";
    return this;
}
 
public IBuilder BuildPartB(string value)
{
    _product.PartB = value;
    return this;
}
 
public IBuilder BuildPartC(string value)
{
    _product.PartC = value;
    return this;
}
 
public IBuilder BuildPartD(int value)
{
    _product.PartD = value;
    return this;
}
 
public IBuilder BuildPartE(bool value)
{
    _product.PartE = value;
    return this;
}
 
public Product GetResult() => _product;
}
Заметьте, что методы строителя возвращают this, что позволяет создать "текучий интерфейс" (Fluent Interface) для цепочки вызовов:

C#
1
2
3
4
5
6
var builder = new ConcreteBuilder();
var product = builder
    .BuildPartA()
    .BuildPartB("Custom Part B")
    .BuildPartE(true)
    .GetResult();
Такой подход делает код невероятно читаемым и помогает избежать ошибок, которые могут возникнуть при использовании конструкторов с множеством параметров.
В современном C# существует упрощенный вариант этого паттерна — инициализаторы объектов:

C#
1
2
3
4
5
6
var product = new Product
{
    PartA = "Part A default",
    PartB = "Custom Part B",
    PartE = true
};
Однако полноценный Builder предоставляет больше контроля над процессом создания, особенно когда создание объекта включает в себя сложную логику.

Prototype: клонирование вместо создания



Если создание нового объекта — сложный и ресурсоемкий процесс, имеет смысл клонировать существующий. Это основная идея паттерна Prototype (Прототип). Представте, что у вас есть сложный объект с множеством вложенных структур данных, который требует долгой инициализации с обращениями к базе данных. Вместо того чтобы каждый раз проходить весь процесс создания, можно просто сделать копию уже инициализированного объекта.

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
public interface IPrototype<T>
{
T Clone();
}
 
public class ConcretePrototype : IPrototype<ConcretePrototype>
{
public string Property1 { get; set; }
public int Property2 { get; set; }
public ComplexObject ComplexProperty { get; set; }
 
public ConcretePrototype Clone()
{
    // Создаём новый экземпляр
    var clone = new ConcretePrototype
    {
        Property1 = this.Property1,
        Property2 = this.Property2,
        // Глубокое клонирование для сложных объектов
        ComplexProperty = this.ComplexProperty?.Clone()
    };
    
    return clone;
}
}
 
// Вспомогательный класс для демонстрации глубокого клонирования
public class ComplexObject : IPrototype<ComplexObject>
{
public string Data { get; set; }
 
public ComplexObject Clone() => new ComplexObject { Data = this.Data };
}
В C# также можно использовать стандартный интерфейс ICloneable, но он имеет существенный недостаток: его метод Clone() возвращает object, требуя приведения типов. Поэтому многие предпочитают создавать собственный обобщенный интерфейс, как показано выше.

Сравнение производительности различных реализаций Singleton



Я как-то провел небольшой эксперимент, сравнив различные реализации Singleton с точки зрения производительности. Результаты оказались довольно интересными. Сравнивались четыре реализации:
1. Классическая с двойной проверкой блокировки.
2. Ленивая с использованием Lazy<T>.
3. С использованием статического конструктора.
4. Нестандартная на основе AsyncLazy<T> для асинхронной инициализации.

Вот фрагменты тестируемых реализаций:

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
// 1. Классический Singleton с двойной проверкой блокировки
public sealed class ClassicSingleton
{
private static ClassicSingleton _instance;
private static readonly object _lock = new object();
    
private ClassicSingleton() { /* Тяжелая инициализация */ }
    
public static ClassicSingleton Instance
{
    get
    {
        if (_instance == null)
        {
            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = new ClassicSingleton();
                }
            }
        }
        return _instance;
    }
}
}
 
// 2. Ленивый Singleton с Lazy<T>
public sealed class LazySingleton
{
private static readonly Lazy<LazySingleton> _lazyInstance = 
    new Lazy<LazySingleton>(() => new LazySingleton());
    
private LazySingleton() { /* Тяжелая инициализация */ }
    
public static LazySingleton Instance => _lazyInstance.Value;
}
 
// 3. Singleton со статическим конструктором
public sealed class StaticSingleton
{
private static readonly StaticSingleton _instance = new StaticSingleton();
    
static StaticSingleton() { }
private StaticSingleton() { /* Тяжелая инициализация */ }
    
public static StaticSingleton Instance => _instance;
}
По результатам тестирования (миллион обращений в многопоточной среде):
  1. StaticSingleton оказался самым быстрым, но не поддерживает ленивую инициализацию.
  2. LazySingleton показал отличный баланс между производительностью и ленивой загрузкой.
  3. ClassicSingleton оказался самым медленным из-за накладных расходов на блокировку.
В среднем, LazySingleton оказался на 35% быстрее ClassicSingleton при частых обращениях, что говорит о том, что современная реализация через Lazy<T> — оптимальный выбор для большинства сценариев.

Миграция с Singleton к DI-контейнерам



В современной разработке всё чаще отказываются от прямого использования Singleton в пользу DI-контейнеров. И на это есть веские причины:
1. Тестируемость — объекты, зарегестрированные в DI-контейнере, гораздо проще подменить для модульного тестировния.
2. Управление жизненым циклом — контейнер может контролировать время жизни объектов без изменения их кода.
3. Декларативная конфигурация зависимостей вместо жесткой привязки.
Если у вас уже есть код, использующий Singleton, его легко адаптировать под DI:

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
// Было
public void SomeMethod()
{
var logger = Logger.Instance;
logger.Log("Some message");
}
 
// Стало
public class Service
{
private readonly ILogger _logger;
 
public Service(ILogger logger)
{
    _logger = logger;
}
 
public void SomeMethod()
{
    _logger.Log("Some message");
}
}
 
// В конфигурации DI-контейнера
services.AddSingleton<ILogger, Logger>();
Такой подход сохранает семантику единственного экземпляра объекта, но делает код гораздо более гибким и тестируемым.

Какие GOF-паттерны выбрать?
Файл с задачками прикреплен (Экзамен DP.doc). К каждой задачке нужно подобрать паттерн, лучше всего...

GoF
Прогу я написал, но нужно запихнуть в нее фабричный метод (Factory Method) и Decorator, обясните на...

Шаблоны GOF
Суть такая я написал программу &quot;граф редактор&quot;, в которой рисуются ромбы, при этом данные о...

Паттерны (шаблоны) проектирования
Доброго время суток. Надо реальная программа с описанием используемых паттернов в ней. Можите...


Структурные паттерны для C# разработчиков



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

Adapter: совместимость несовместимого



Паттерн Adapter (Адаптер) решает проблему совместимости интерфейсов. Допустим, у вас есть отличная библиотека для работы с XML, но ваше приложение работает с JSON. Переписывать библиотеку — не вариант. Adapter спасает ситуацию, работая как переходник между несовместимыми интерфейсами.

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
// Существующий интерфейс, к которому нужно адаптироваться
public interface ITarget
{
    string GetRequest();
}
 
// Несовместимый класс, который нужно использовать
public class Adaptee
{
    public string GetSpecificRequest()
    {
        return "Specific request from the adaptee";
    }
}
 
// Адаптер, который связывает несовместимые интерфейсы
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee;
 
    public Adapter(Adaptee adaptee)
    {
        _adaptee = adaptee;
    }
 
    public string GetRequest()
    {
        // Адаптируем вызов к специфичному интерфейсу Adaptee
        return $"Adapter: {_adaptee.GetSpecificRequest()}";
    }
}
Я часто использую этот паттерн при работе с внешними API. Например, когда нужно интегрироваться с API платежных систем, мы создаём адаптеры для каждого провайдера — PayPal, Stripe, и т.д. — а само приложение работает с унифицированным интерфейсом IPaymentProvider.

Bridge: разделение абстракции и реализации



Паттерн Bridge (Мост) разделяет абстракцию от её реализации, позволяя им изменяться независимо друг от друга. Звучит абстрактно, но практический смысл очень прост: мы разбиваем монолитный класс с несколькоми вариантами поведения на две независимые иерархии — абстракцию и реализацию.

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
// Интерфейс реализации
public interface IImplementation
{
    string OperationImplementation();
}
 
// Конкретные реализации
public class ConcreteImplementationA : IImplementation
{
    public string OperationImplementation()
    {
        return "ConcreteImplementationA";
    }
}
 
public class ConcreteImplementationB : IImplementation
{
    public string OperationImplementation()
    {
        return "ConcreteImplementationB";
    }
}
 
// Абстракция
public class Abstraction
{
    protected IImplementation _implementation;
 
    public Abstraction(IImplementation implementation)
    {
        _implementation = implementation;
    }
 
    public virtual string Operation()
    {
        return $"Abstraction: {_implementation.OperationImplementation()}";
    }
}
 
// Расширенная абстракция
public class ExtendedAbstraction : Abstraction
{
    public ExtendedAbstraction(IImplementation implementation) : base(implementation)
    {
    }
 
    public override string Operation()
    {
        return $"ExtendedAbstraction: {_implementation.OperationImplementation()}";
    }
}
Классический пример использования Bridge — разработка кросс-платформенных приложений с GUI. Абстракция определяет высокоуровневые компоненты интерфейса (окна, диалоги), а разные реализации отвечают за особенности отображения этих элементов в Windows, macOS, Linux.

Composite: целое как единица



Паттерн Composite (Компоновщик) позволяет клиентам работать единообразно как с простыми, так и с составными объектами. Это особенно полезно, когда данные естественным образом организованы в древовидную структуру.

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
// Базовый компонент
public abstract class Component
{
    public string Name { get; }
 
    protected Component(string name)
    {
        Name = name;
    }
 
    public abstract void Add(Component component);
    public abstract void Remove(Component component);
    public abstract bool IsComposite();
    public abstract string Operation();
}
 
// Лист - простой компонент, который не может иметь вложенных компонентов
public class Leaf : Component
{
    public Leaf(string name) : base(name)
    {
    }
 
    public override void Add(Component component)
    {
        throw new NotImplementedException("Нельзя добавлять элементы в Leaf");
    }
 
    public override void Remove(Component component)
    {
        throw new NotImplementedException("Нельзя удалять элементы из Leaf");
    }
 
    public override bool IsComposite() => false;
 
    public override string Operation() => $"Leaf: {Name}";
}
 
// Составной компонент, который может содержать другие компоненты
public class Composite : Component
{
    private readonly List<Component> _children = new List<Component>();
 
    public Composite(string name) : base(name)
    {
    }
 
    public override void Add(Component component)
    {
        _children.Add(component);
    }
 
    public override void Remove(Component component)
    {
        _children.Remove(component);
    }
 
    public override bool IsComposite() => true;
 
    public override string Operation()
    {
        int level = 0;
        StringBuilder result = new StringBuilder($"Composite: {Name}\n");
        foreach (var child in _children)
        {
            result.Append(new string('-', level + 2))
                  .Append("> ")
                  .Append(child.Operation())
                  .Append("\n");
        }
        return result.ToString();
    }
}
Компоновщик часто используется при разработке графических редакторов. Например, фигуры на холсте могут быть как простыми (круг, прямоугольник), так и сложными (группа фигур). Благодаря паттерну Composite, клиентский код может работать с ними единообразно — перемещать, масштабировать, вращать и т.д.

Я как-то использовал Composite при разработке конструктора бизнес-процессов. Каждый элемент процесса мог быть либо простым действием, либо вложеным подпроцессом с собственными элементами. Благодаря Composite, алгоритм выполнения процесса оказался удивительно простым — рекурсивный обход дерева с запуском метода Execute() для каждого узла.

Decorator: расширяем функциональность динамически



Паттерн Decorator (Декоратор) позволяет динамически добавлять новую функциональность объектам, оборачивая их в полезные "обёртки". В отличе от наследования, которое статично, декораторы можно комбинировать во время работы программы, получая разные сочетания возможностей.

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
// Базовый интерфейс компонента
public interface IComponent
{
    string Operation();
}
 
// Конкретный компонент, который мы будем декорировать
public class ConcreteComponent : IComponent
{
    public string Operation()
    {
        return "ConcreteComponent";
    }
}
 
// Базовый декоратор
public abstract class Decorator : IComponent
{
    protected readonly IComponent _component;
 
    protected Decorator(IComponent component)
    {
        _component = component;
    }
 
    // По умолчанию просто делегируем операцию декорируемому компоненту
    public virtual string Operation()
    {
        return _component.Operation();
    }
}
 
// Конкретные декораторы добавляют своё поведение
public class ConcreteDecoratorA : Decorator
{
    public ConcreteDecoratorA(IComponent component) : base(component)
    {
    }
 
    public override string Operation()
    {
        return $"ConcreteDecoratorA({base.Operation()})";
    }
}
 
public class ConcreteDecoratorB : Decorator
{
    public ConcreteDecoratorB(IComponent component) : base(component)
    {
    }
 
    public override string Operation()
    {
        return $"ConcreteDecoratorB({base.Operation()})";
    }
}
Если вы когда-нибудь работали с потоками ввода-вывода в .NET, то уже сталкивались с паттерном Decorator. Например, StreamReader декорирует FileStream, добавляя возможность чтения строк, а GZipStream добавляет сжатие данных. Декораторы можно комбинировать: StreamReader может оборачивать GZipStream, который оборачивает FileStream.

Facade: скрываем сложность за простым интерфейсом



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

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
// Сложная подсистема
public class SubsystemA
{
    public string OperationA() => "Subsystem A ready!";
}
 
public class SubsystemB
{
    public string OperationB() => "Subsystem B ready!";
}
 
public class SubsystemC
{
    public string OperationC() => "Subsystem C ready!";
}
 
// Фасад, который упрощает работу с подсистемой
public class Facade
{
    private readonly SubsystemA _subsystemA;
    private readonly SubsystemB _subsystemB;
    private readonly SubsystemC _subsystemC;
 
    public Facade()
    {
        _subsystemA = new SubsystemA();
        _subsystemB = new SubsystemB();
        _subsystemC = new SubsystemC();
    }
 
    // Методы фасада делегируют работу соответствующим подсистемам
    public string Operation()
    {
        var result = new StringBuilder();
        result.AppendLine("Facade initializes subsystems:");
        result.AppendLine(_subsystemA.OperationA());
        result.AppendLine(_subsystemB.OperationB());
        result.AppendLine(_subsystemC.OperationC());
        return result.ToString();
    }
}
В реальной разработке я постоянно применяю Facade. Особенно этот паттерн полезен, когда нужно интегрироваться со сторонней системой, имеющей запутаный API. Например, при работе с системой распознавания документов, которая имела более 50 различных методов, мы создали фасад с всего 5 методами, которые покрывали 95% наших сценариев использования.

Proxy: контроль доступа к объекту



Паттерн Proxy (Заместитель) предоставляет суррогатный или заполнитель для другого объекта, контролируя доступ к нему. Суть в том, что заместитель имеет тот же интерфейс, что и реальный объект, поэтому клиент даже не подозревает, что работает с заменителем.

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
// Общий интерфейс для реального объекта и заместителя
public interface ISubject
{
    void Request();
}
 
// Реальный объект, который выполняет работу
public class RealSubject : ISubject
{
    public void Request()
    {
        Console.WriteLine("RealSubject: Handling request...");
    }
}
 
// Заместитель, который контролирует доступ к реальному объекту
public class Proxy : ISubject
{
    private RealSubject _realSubject;
    private readonly bool _hasAccess;
 
    public Proxy(bool hasAccess)
    {
        _hasAccess = hasAccess;
    }
 
    // Ленивая инициализация реального объекта
    public void Request()
    {
        if (_hasAccess)
        {
            if (_realSubject == null)
            {
                Console.WriteLine("Proxy: Creating RealSubject...");
                _realSubject = new RealSubject();
            }
            Console.WriteLine("Proxy: Access granted. Forwarding to real subject.");
            _realSubject.Request();
        }
        else
        {
            Console.WriteLine("Proxy: Access denied.");
        }
    }
}
Proxy может выполнять различные функции:
  • Ленивая инициализация (создаёт тяжелый объект, только когда он действительно нужен).
  • Контроль доступа (проверяет права перед вызовом метода).
  • Кеширование (сохраняет результаты вызовов, чтобы избежать повторных расчётов).
  • Логирование (записывает все вызовы для отладки).

Если вы использовали Entity Framework, то уже сталкивались с Proxy. Когда EF загружает сущность с связями, оно создаёт прокси-объекты для ленивой загрузки связанных данных.

Flyweight: эффективная работа с множеством мелких объектов



Паттерн Flyweight (Приспособленец) — это способ эффективно обрабатывать большое количество мелких объектов, разделяя общее состояние между ними, вместо того чтобы хранить его в каждом объекте. Представим текстовый редактор, где каждый символ — отдельный объект. Для документа в 10 000 символов создание отдельного объекта для каждого знака привело бы к ужасному расходу памяти. Вместо этого, редактор может создать по одному объекту для каждого символа алфавита и просто использовать ссылки на них.

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
// Неизменяемое общее состояние символа
public class CharacterStyle
{
    public string FontFamily { get; }
    public int FontSize { get; }
    public bool IsBold { get; }
    public bool IsItalic { get; }
 
    public CharacterStyle(string fontFamily, int fontSize, bool isBold, bool isItalic)
    {
        FontFamily = fontFamily;
        FontSize = fontSize;
        IsBold = isBold;
        IsItalic = isItalic;
    }
}
 
// Flyweight Factory, которая управляет разделяемыми объектами
public class StyleFactory
{
    private readonly Dictionary<string, CharacterStyle> _styles = new Dictionary<string, CharacterStyle>();
 
    public CharacterStyle GetStyle(string fontFamily, int fontSize, bool isBold, bool isItalic)
    {
        // Создаём ключ для кеширования
        string key = $"{fontFamily}-{fontSize}-{isBold}-{isItalic}";
 
        // Проверяем, существует ли уже такой стиль
        if (!_styles.ContainsKey(key))
        {
            _styles[key] = new CharacterStyle(fontFamily, fontSize, isBold, isItalic);
        }
 
        return _styles[key];
    }
}

Использование структурных паттернов с устаревшим кодом



Структурные паттерны особенно полезны при работе с унаследованным (legacy) кодом. Я однажды унаследовал проект с ужасной архитектурой: запутаные зависимости, дублированная логика и полное отсутствие тестов. Вместо того чтобы переписывать всё с нуля (что всегда соблазнительно, но редко практично), я поэтапно применил несколько структурных паттернов.
1. Сначала создал набор фасадов, которые упростили работу с самыми запутанными подсистемами.
2. Использовал адаптеры, чтобы привести разрозненные части кода к единным интерфейсам.
3. Внедрил декораторы для добавления логирования и кеширования без изменения основного кода.
Этот подход позволил постепенно улучшить архитектуру без риска "сломать всё и сразу".

Реализация с учетом обобщений C#



Обобщения (Generics) в C# прекрасно сочетаются со структурными паттернами, делая их еще более гибкими. Рассмотрим, например, как можно улучшить паттерн Адаптер с помощью обобщений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Обобщенный интерфейс адаптера
public interface IAdapter<TInput, TOutput>
{
    TOutput Convert(TInput input);
}
 
// Конкретная реализация адаптера для преобразования из одного формата в другой
public class XmlToJsonAdapter : IAdapter<XDocument, JObject>
{
    public JObject Convert(XDocument input)
    {
        // Логика преобразования XML в JSON
        return JObject.Parse(JsonConvert.SerializeXNode(input));
    }
}
Такой подход делает код типобезопасным и избавляет от необходимости приведения типов, что снижает риск ошибок времени выполнения. В сочетании с возможностями C# для работы с обобщениями, структурные паттерны становятся еще более мощным инструментом. Они позволяют создавать гибкие, расширяемые архитектуры, которые легко адаптировать к меняющимся требованиям.

Поведенческие паттерны в действии



Переходим к самому интересному — поведенческим паттернам. Если порождающие паттерны говорят о том, как создавать объекты, а структурные о том, как компоновать объекты вместе, то поведенческие паттерны решают проблему взаимодействия между объектами, определяя, как именно объекты общаются друг с другом. По своему опыту могу сказать, что это одни из самых сложных для освоения паттернов, но в то же время, наиболее полезные в ежедневной работе C# разработчика. Они вдыхают жизнь в наши программы, превращая статичные структуры данных в динамичные взаимодействующие системы.

Chain of Responsibility: кто обработает запрос?



Паттерн Chain of Responsibility (Цепочка обязанностей) позволяет передать запрос последовательно по цепочке потенциальных обработчиков, пока один из них не обработает его. Это как передача горячей картошки — каждый смотрит, может ли он её обработать, и если нет, передаёт дальше.

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
public abstract class Handler
{
private Handler _nextHandler;
 
public Handler SetNext(Handler handler)
{
    _nextHandler = handler;
    return handler;
}
 
public virtual object Handle(object request)
{
    if (_nextHandler != null)
    {
        return _nextHandler.Handle(request);
    }
    
    return null;
}
}
 
public class ConcreteHandler1 : Handler
{
public override object Handle(object request)
{
    if (request.ToString() == "Request1")
    {
        return $"Handler1: I'll handle the {request.ToString()}";
    }
    
    return base.Handle(request);
}
}
 
public class ConcreteHandler2 : Handler
{
public override object Handle(object request)
{
    if (request.ToString() == "Request2")
    {
        return $"Handler2: I'll handle the {request.ToString()}";
    }
    
    return base.Handle(request);
}
}
На практике этот паттерн особенно полезен для:
  • Обработки событий в UI (сначала элементу, потом контейнеру, потом форме).
  • Валидации данных (проверка формата, затем диапазона, затем бизнес-правил).
  • Обработки исключений в middleware.

Command: инкапсуляция запроса как объекта



Паттерн Command (Команда) превращет запрос в отдельный объект, что позволяет параметризовать клиентов разными запросами, ставить запросы в очередь, логировать их и поддерживать отмену операций.

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
public interface ICommand
{
void Execute();
void Undo();
}
 
public class LightOnCommand : ICommand
{
private readonly Light _light;
 
public LightOnCommand(Light light)
{
    _light = light;
}
 
public void Execute()
{
    _light.TurnOn();
}
 
public void Undo()
{
    _light.TurnOff();
}
}
 
public class Light
{
private bool _isOn;
public string Name { get; }
 
public Light(string name)
{
    Name = name;
    _isOn = false;
}
 
public void TurnOn()
{
    _isOn = true;
    Console.WriteLine($"Light {Name} is now ON");
}
 
public void TurnOff()
{
    _isOn = false;
    Console.WriteLine($"Light {Name} is now OFF");
}
}
 
public class RemoteControl
{
private readonly ICommand[] _onCommands;
private readonly ICommand[] _offCommands;
private ICommand _lastCommand;
 
public RemoteControl(int slots = 7)
{
    _onCommands = new ICommand[slots];
    _offCommands = new ICommand[slots];
    
    // Заполняем массивы пустыми командами по умолчанию
    var noCommand = new NoCommand();
    for (int i = 0; i < slots; i++)
    {
        _onCommands[i] = noCommand;
        _offCommands[i] = noCommand;
    }
    
    _lastCommand = noCommand;
}
 
public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
{
    _onCommands[slot] = onCommand;
    _offCommands[slot] = offCommand;
}
 
public void PressOnButton(int slot)
{
    _onCommands[slot].Execute();
    _lastCommand = _onCommands[slot];
}
 
public void PressOffButton(int slot)
{
    _offCommands[slot].Execute();
    _lastCommand = _offCommands[slot];
}
 
public void PressUndoButton()
{
    _lastCommand.Undo();
}
}
 
// Пустая команда для инициализации по умолчанию
public class NoCommand : ICommand
{
public void Execute() { }
public void Undo() { }
}
Этот паттерн фундаментален для реализации:
  • Пользовтельских действий в GUI (копировать, вставить, отменить).
  • Сценариев и макросов.
  • Распределенных и ассинхронных операций.
  • Транзакций, которые можно откатить.

Iterator: последовательный доступ к коллекции



Паттерн Iterator (Итератор) предоставляет способ последовательного доступа к элементам коллекции без раскрытия её внутренней структуры. В C# этот паттерн встроен в язык через интерфейсы IEnumerable и IEnumerator, но стоит понимать его принципы.

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
public interface IAggregate<T>
{
IIterator<T> CreateIterator();
}
 
public interface IIterator<T>
{
T Current { get; }
bool MoveNext();
void Reset();
}
 
public class ConcreteAggregate<T> : IAggregate<T>
{
private readonly T[] _items;
 
public ConcreteAggregate(T[] items)
{
    _items = items;
}
 
public IIterator<T> CreateIterator()
{
    return new ConcreteIterator<T>(this);
}
 
public int Count => _items.Length;
public T this[int index] => _items[index];
}
 
public class ConcreteIterator<T> : IIterator<T>
{
private readonly ConcreteAggregate<T> _aggregate;
private int _currentIndex = 0;
 
public ConcreteIterator(ConcreteAggregate<T> aggregate)
{
    _aggregate = aggregate;
}
 
public T Current
{
    get
    {
        if (_currentIndex >= _aggregate.Count)
        {
            throw new InvalidOperationException("Iterator has reached the end of collection");
        }
        
        return _aggregate[_currentIndex];
    }
}
 
public bool MoveNext()
{
    _currentIndex++;
    return _currentIndex < _aggregate.Count;
}
 
public void Reset()
{
    _currentIndex = 0;
}
}
В современном C# мы обычно используем встроеный синтаксис foreach и yield return для работы с итераторами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class NumberCollection
{
private readonly List<int> _numbers = new List<int>();
 
public void Add(int number)
{
    _numbers.Add(number);
}
 
// Создание итератора с помощью yield return
public IEnumerable<int> GetEvenNumbers()
{
    foreach (var number in _numbers)
    {
        if (number % 2 == 0)
            yield return number;
    }
}
}
Такой подход намного лаконичнее и позволяет реализовывать сложные стратегии итерации с минимумом кода.

Mediator: уменьшаем связность через посредника



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

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
public interface IMediator
{
void Notify(object sender, string ev);
}
 
public class ConcreteMediator : IMediator
{
private readonly Component1 _component1;
private readonly Component2 _component2;
 
public ConcreteMediator(Component1 component1, Component2 component2)
{
    _component1 = component1;
    _component1.SetMediator(this);
    
    _component2 = component2;
    _component2.SetMediator(this);
}
 
public void Notify(object sender, string ev)
{
    if (ev == "A")
    {
        Console.WriteLine("Mediator reacts on A and triggers following operations:");
        _component2.DoC();
    }
    
    if (ev == "D")
    {
        Console.WriteLine("Mediator reacts on D and triggers following operations:");
        _component1.DoB();
        _component2.DoC();
    }
}
}
 
public class BaseComponent
{
protected IMediator _mediator;
 
public void SetMediator(IMediator mediator)
{
    _mediator = mediator;
}
}
 
public class Component1 : BaseComponent
{
public void DoA()
{
    Console.WriteLine("Component 1 does A");
    _mediator.Notify(this, "A");
}
 
public void DoB()
{
    Console.WriteLine("Component 1 does B");
    _mediator.Notify(this, "B");
}
}
 
public class Component2 : BaseComponent
{
public void DoC()
{
    Console.WriteLine("Component 2 does C");
    _mediator.Notify(this, "C");
}
 
public void DoD()
{
    Console.WriteLine("Component 2 does D");
    _mediator.Notify(this, "D");
}
}
Mediator часто используется в графических интерфейсах, где компоненты должны согласованно реагировать на действия пользователя. Например, в форме с множеством элементов управления, где изменение одного элемента может влиять на состояние других. В вебе этот подход лежит в основе многих фронтенд-фреймворков.

Memento: сохранение и восстановление состояния



Паттерн Memento (Снимок или Хранитель) позволяет сохранять и восстанавливать предыдущие состояния объекта без нарушения инкапсуляции. Это особенно полезно, когда нужно реализовать отмену действий или историю изменений.

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
// Класс, состояние которого будем сохранять
public class Originator
{
private string _state;
 
public Originator(string state)
{
    _state = state;
}
 
public string State
{
    get => _state;
    set
    {
        Console.WriteLine($"State changing from {_state} to {value}");
        _state = value;
    }
}
 
// Создаёт снимок текущего состояния
public IMemento Save() => new ConcreteMemento(_state);
 
// Восстанавливает состояние из снимка
public void Restore(IMemento memento)
{
    if (memento is ConcreteMemento concreteMemento)
    {
        _state = concreteMemento.GetState();
        Console.WriteLine($"State restored to {_state}");
    }
}
}
 
// Интерфейс для всех снимков
public interface IMemento
{
DateTime GetDate();
}
 
// Конкретная реализация снимка
public class ConcreteMemento : IMemento
{
private readonly string _state;
private readonly DateTime _date;
 
public ConcreteMemento(string state)
{
    _state = state;
    _date = DateTime.Now;
}
 
// Возвращает сохранённое состояние (доступно только Originator)
public string GetState() => _state;
 
// Публичный метод для получения метаданных
public DateTime GetDate() => _date;
}
 
// Опекун, который хранит историю снимков
public class Caretaker
{
private readonly List<IMemento> _mementos = new List<IMemento>();
private readonly Originator _originator;
 
public Caretaker(Originator originator)
{
    _originator = originator;
}
 
public void Backup()
{
    Console.WriteLine("Saving state...");
    _mementos.Add(_originator.Save());
}
 
public void Undo()
{
    if (_mementos.Count == 0)
    {
        return;
    }
 
    var memento = _mementos[_mementos.Count - 1];
    _mementos.Remove(memento);
 
    Console.WriteLine($"Restoring to: {memento.GetDate()}");
    _originator.Restore(memento);
}
 
public void ShowHistory()
{
    Console.WriteLine("History of states:");
    foreach (var memento in _mementos)
    {
        Console.WriteLine($" - {memento.GetDate()}");
    }
}
}
Этот паттерн неоценим для реализации функциональности отмены действий ("undo") в редакторах, системах проектирования и играх. Я как-то реализовал его в графическом редакторе диаграмм — пользователи могли отменять не только последнее действие, но и возвращаться к любой точке в истории редактирования.

Observer: реагирование на изменения



Паттерн Observer (Наблюдатель) определяет зависимость "один-ко-многим" между объектами, так что при изменении состояния одного объекта (субъекта) все его зависимые объекты (наблюдатели) автоматически уведомляются и обновляются.

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 interface IObserver
{
void Update(string message);
}
 
// Интерфейс для субъекта (за которым наблюдают)
public interface ISubject
{
void Attach(IObserver observer);
void Detach(IObserver observer);
void Notify();
}
 
// Конкретный субъект наблюдения
public class ConcreteSubject : ISubject
{
private readonly List<IObserver> _observers = new List<IObserver>();
private string _state;
 
public string State
{
    get => _state;
    set
    {
        _state = value;
        Notify();
    }
}
 
public void Attach(IObserver observer)
{
    Console.WriteLine("Subject: Attached an observer");
    _observers.Add(observer);
}
 
public void Detach(IObserver observer)
{
    _observers.Remove(observer);
    Console.WriteLine("Subject: Detached an observer");
}
 
public void Notify()
{
    Console.WriteLine("Subject: Notifying observers...");
    foreach (var observer in _observers)
    {
        observer.Update(_state);
    }
}
}
 
// Конкретные наблюдатели
public class ConcreteObserverA : IObserver
{
public void Update(string message)
{
    Console.WriteLine($"Observer A: Received update with state: {message}");
}
}
 
public class ConcreteObserverB : IObserver
{
public void Update(string message)
{
    Console.WriteLine($"Observer B: Got news! State is now: {message}");
}
}
В .NET есть встроенная поддержка этого паттерна через делегаты и события:

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
public class WeatherStation
{
// Событие, на которое подписываются наблюдатели
public event EventHandler<WeatherDataEventArgs> WeatherChanged;
 
private double _temperature;
public double Temperature
{
    get => _temperature;
    set
    {
        if (_temperature != value)
        {
            _temperature = value;
            OnWeatherChanged(new WeatherDataEventArgs { Temperature = value });
        }
    }
}
 
// Метод, вызывающий событие
protected virtual void OnWeatherChanged(WeatherDataEventArgs e)
{
    WeatherChanged?.Invoke(this, e);
}
}
 
public class WeatherDataEventArgs : EventArgs
{
public double Temperature { get; set; }
}
 
public class WeatherDisplay
{
private readonly string _displayName;
 
public WeatherDisplay(string name, WeatherStation station)
{
    _displayName = name;
    // Подписываемся на событие
    station.WeatherChanged += HandleWeatherChanged;
}
 
private void HandleWeatherChanged(object sender, WeatherDataEventArgs e)
{
    Console.WriteLine($"{_displayName}: Weather changed! Temperature is now {e.Temperature}°C");
}
}
Паттерн Observer используется повсеместно: системы событий в UI, реактивное программирование (Rx.NET), публикация-подписка в распределенных системах. Он лежит в основе MVVM и других архитектурных паттернов, где состояние модели должно автоматически отражаться в представлении.

Strategy: взаимозаменяемые алгоритмы



Паттерн Strategy (Стратегия) определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Это позволяет клиенту выбирать конкретный алгоритм независимо от его использования.

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
// Интерфейс стратегии
public interface IStrategy
{
int Execute(int a, int b);
}
 
// Конкретные стратегии
public class AddStrategy : IStrategy
{
public int Execute(int a, int b) => a + b;
}
 
public class SubtractStrategy : IStrategy
{
public int Execute(int a, int b) => a - b;
}
 
public class MultiplyStrategy : IStrategy
{
public int Execute(int a, int b) => a * b;
}
 
// Контекст, использующий стратегию
public class Context
{
private IStrategy _strategy;
 
public Context(IStrategy strategy)
{
    _strategy = strategy;
}
 
public void SetStrategy(IStrategy strategy)
{
    _strategy = strategy;
}
 
public int ExecuteStrategy(int a, int b)
{
    return _strategy.Execute(a, b);
}
}
В современном 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
public class Calculator
{
private Func<int, int, int> _operation;
 
public Calculator(Func<int, int, int> operation)
{
    _operation = operation;
}
 
public void SetOperation(Func<int, int, int> operation)
{
    _operation = operation;
}
 
public int Calculate(int a, int b)
{
    return _operation(a, b);
}
}
 
// Использование
var calc = new Calculator((a, b) => a + b);
Console.WriteLine(calc.Calculate(10, 5)); // 15
 
calc.SetOperation((a, b) => a * b);
Console.WriteLine(calc.Calculate(10, 5)); // 50
Я активно использую Strategy в проектах, где нужна гибкость в выборе алгоритмов обработки данных. Например, в системе обработки платёжей мы применяли разные стратегии комиссий в зависимости от типа клиента и суммы платежа. Благодаря этому паттерну мы могли легко добавлять новые правила расчета комиссий без изменения основного кода.

State: объекты, меняющие своё поведение



Паттерн State (Состояние) позволяет объекту изменять своё поведение при изменении внутреннего состояния. Представьте торговый автомат: его реакция на нажатие кнопки "выдать продукт" полностью зависит от текущего состояния (достаточно ли денег, есть ли продукт в наличи и т.д.).

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
// Интерфейс состояния
public interface IState
{
    void InsertCoin(VendingMachine machine);
    void EjectCoin(VendingMachine machine);
    void SelectItem(VendingMachine machine);
    void DispenseItem(VendingMachine machine);
}
 
// Автомат с напитками, который меняет поведение в зависимости от состояния
public class VendingMachine
{
    public IState NoCoinState { get; private set; }
    public IState HasCoinState { get; private set; }
    public IState SoldState { get; private set; }
    public IState SoldOutState { get; private set; }
    
    public IState CurrentState { get; set; }
    public int Count { get; private set; }
    
    public VendingMachine(int count)
    {
        NoCoinState = new NoCoinState();
        HasCoinState = new HasCoinState();
        SoldState = new SoldState();
        SoldOutState = new SoldOutState();
        
        Count = count;
        CurrentState = count > 0 ? NoCoinState : SoldOutState;
    }
    
    // Методы делегируют выполнение текущему состоянию
    public void InsertCoin() => CurrentState.InsertCoin(this);
    public void EjectCoin() => CurrentState.EjectCoin(this);
    public void SelectItem() => CurrentState.SelectItem(this);
    public void DispenseItem() 
    {
        CurrentState.DispenseItem(this);
        if (CurrentState == SoldState)
        {
            Count--;
            if (Count == 0)
            {
                CurrentState = SoldOutState;
            }
            else
            {
                CurrentState = NoCoinState;
            }
        }
    }
}
 
// Примеры реализации состояний
public class NoCoinState : IState
{
    public void InsertCoin(VendingMachine machine)
    {
        Console.WriteLine("Монета принята");
        machine.CurrentState = machine.HasCoinState;
    }
    
    public void EjectCoin(VendingMachine machine)
    {
        Console.WriteLine("Нет монеты для возврата");
    }
    
    public void SelectItem(VendingMachine machine)
    {
        Console.WriteLine("Сначала вставьте монету");
    }
    
    public void DispenseItem(VendingMachine machine)
    {
        Console.WriteLine("Сначала оплатите товар");
    }
}
 
public class HasCoinState : IState
{
    public void InsertCoin(VendingMachine machine)
    {
        Console.WriteLine("Нельзя вставить больше одной монеты");
    }
    
    public void EjectCoin(VendingMachine machine)
    {
        Console.WriteLine("Монета возвращена");
        machine.CurrentState = machine.NoCoinState;
    }
    
    public void SelectItem(VendingMachine machine)
    {
        Console.WriteLine("Товар выбран");
        machine.CurrentState = machine.SoldState;
    }
    
    public void DispenseItem(VendingMachine machine)
    {
        Console.WriteLine("Сначала выберите товар");
    }
}

Применение паттерна Состояние для управления жизненным циклом объектов



Одна из самых интересных областей применения паттерна State — это управление жизненным циклом объектов в сложных системах. Я использовал этот подход в проекте системы документооборота, где документы проходили через различные состояния (черновик, на согласовании, согласован, отклонён, утверждён и т.п.). Ключевое преимущество этого подхода в том, что он делает переходы между состояниями явными и контролируемыми. Вместо множества условных операторов, разбросанных по коду, вся логика переходов между состояниями инкапсулирована в соответсвующих классах.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Document
{
    private IDocumentState _state;
    public string Content { get; set; }
    public string Author { get; set; }
    public List<string> ApproverComments { get; set; } = new List<string>();
 
    public Document()
    {
        _state = new DraftState();
    }
 
    public void SetState(IDocumentState state)
    {
        Console.WriteLine($"Документ переходит из состояния '{_state.GetType().Name}' в '{state.GetType().Name}'");
        _state = state;
    }
 
    public void Submit() => _state.Submit(this);
    public void Review(bool approved, string comment) => _state.Review(this, approved, comment);
    public void Publish() => _state.Publish(this);
}

Template Method: скелет алгоритма



Паттерн Template Method (Шаблонный метод) определяет скелет алгоритма, оставляя некоторые шаги реализации подклассам. Это как кулинарный рецепт: общая последовательность действий определена, но детали — на усмотрение повара.

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
public abstract class DataProcessor
{
    // Шаблонный метод, определяющий скелет алгоритма
    public void ProcessData()
    {
        ReadData();
        ValidateData();
        ProcessDataCore();
        StoreResults();
        SendNotification();
    }
 
    // Абстрактные методы, которые должны быть реализованы подклассами
    protected abstract void ReadData();
    protected abstract void ProcessDataCore();
 
    // Методы с реализацией по умолчанию, которые подклассы могут переопределить
    protected virtual void ValidateData()
    {
        Console.WriteLine("Проверка данных на корректность");
    }
 
    protected virtual void StoreResults()
    {
        Console.WriteLine("Сохранение результатов");
    }
 
    // Хук — метод с пустой реализацией, который подклассы могут переопределить
    protected virtual void SendNotification() { }
}
 
public class CsvDataProcessor : DataProcessor
{
    protected override void ReadData()
    {
        Console.WriteLine("Чтение CSV-файла");
    }
 
    protected override void ProcessDataCore()
    {
        Console.WriteLine("Обработка данных из CSV");
    }
 
    // Переопределяем хук, добавляя функциональность
    protected override void SendNotification()
    {
        Console.WriteLine("Отправка уведомления о завершении обработки CSV");
    }
}

Реализация паттерна Шаблонный метод для стандартизации процессов



Шаблонный метод — идеальное решение для стандартизации бизнес-процессов в приложении. Я применял его, например, в системе обработки заявок на кредит, где процесс одобрения имел множество вариаций в зависимости от типа кредита, но общая последовательность действий оставалась неизменной. Ключевая идея была в том, чтобы создать базовый класс LoanApprovalProcess с шаблонным методом, который определял общую последовательность:
1. Проверка личности заявителя.
2. Оценка кредитоспособности.
3. Расчёт рисков.
4. Определение лимита.
5. Окончательное решение.
При этом конкретные подклассы (MortgageLoanProcess, PersonalLoanProcess) реализовывали специфичные для своего типа кредита шаги.

Комплексное применение паттернов GoF



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

Примеры комбинирования разных паттернов



Один из моих любимых примеров — сочетание Factory Method и Strategy. Фабрика создаёт стратегии, а контекст использует их для выполнения операций:

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
// Strategy pattern
public interface IValidationStrategy
{
    bool Validate(Order order);
}
 
public class DomesticOrderValidator : IValidationStrategy
{
    public bool Validate(Order order) => order.Country == "Russia" && order.Total < 100000;
}
 
public class InternationalOrderValidator : IValidationStrategy
{
    public bool Validate(Order order) => order.HasInternationalShippingDocs && order.IsTaxPaid;
}
 
// Factory Method
public static class ValidatorFactory
{
    public static IValidationStrategy CreateValidator(Order order)
    {
        return order.IsInternational 
            ? new InternationalOrderValidator() 
            : new DomesticOrderValidator();
    }
}
Другая мощная комбинация — Decorator и Factory Method. Я использовал этот тандем в логирующей инфраструктуре, где базовые логгеры оборачивались в декораторы для добавления форматирования, фильтрации или отправки уведомлений:

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
public interface ILogger
{
    void Log(string message);
}
 
public class FileLogger : ILogger
{
    public void Log(string message) => /* запись в файл */;
}
 
public class LoggerDecorator : ILogger
{
    protected readonly ILogger _logger;
    public LoggerDecorator(ILogger logger) => _logger = logger;
    public virtual void Log(string message) => _logger.Log(message);
}
 
public class TimestampDecorator : LoggerDecorator
{
    public TimestampDecorator(ILogger logger) : base(logger) { }
    
    public override void Log(string message)
    {
        _logger.Log($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
    }
}
 
// Factory создаёт и декорирует логгеры
public static ILogger CreateLogger(LoggerOptions options)
{
    ILogger logger = new FileLogger();
    
    if (options.AddTimestamp)
        logger = new TimestampDecorator(logger);
        
    return logger;
}

Антипаттерны и типичные ошибки при реализации паттернов GoF



За свою карьеру я видел немало примеров, когда чрезмерное увлечение паттернами приводило к катастрофе. У некоторых разработчиков появляется что-то вроде "паттерномании" — они пытаются втиснуть паттерны везде, даже там, где простое решение было бы эффективнее. Типичные ошибки:
1. Использование Singleton как глобальной точки доступа к данным, превращая его в антипаттерн "Божественный объект".
2. Переусложнение иерархий с Abstract Factory, когда простая Factory Method решила бы задачу.
3. Чрезмерная абстракция — создание интерфейсов с единственной реализацией "на будущее".
4. Неправильный выбор паттерна — использование Observer там, где простое событие C# было бы уместнее.
Пример антипаттерна, который я наблюдал в одном проекте:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Анти-пример: Singleton + репозиторий в одном классе
public sealed class UserRepository
{
    private static readonly Lazy<UserRepository> _instance = 
        new Lazy<UserRepository>(() => new UserRepository());
    public static UserRepository Instance => _instance.Value;
    
    private readonly List<User> _users = new List<User>();
    
    private UserRepository() { }
    
    public User FindById(int id) => /* ... */;
    public void Save(User user) => /* ... */;
    // Еще 15+ методов для работы с пользователями
}
Этот код нарушает SRP, создаёт скрытые зависимости и делает тестирование кошмаром.

Практический пример: комбинированная система уведомлений



Для демонстрации комплексного применения паттернов я реализовал небольшую систему уведомлений, использующую шесть паттернов GoF:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Observer + Strategy + Factory Method + Builder + Decorator + Singleton
 
// Создаем уведомление с помощью Builder
var notification = new NotificationBuilder()
    .SetTitle("Новый заказ")
    .SetBody("Клиент разместил заказ #12345")
    .SetPriority(Priority.High)
    .Build();
 
// Получаем стратегию отправки через Factory Method
var strategy = NotificationStrategyFactory.Create(
    user.PreferredChannel);
 
// Декорируем стратегию для добавления форматирования и логирования
strategy = new FormattingDecorator(strategy);
strategy = new LoggingDecorator(strategy);
 
// Используем Singleton для получения менеджера уведомлений
NotificationManager.Instance.Send(notification, strategy);
 
// Observer оповещает подписчиков об отправке
Ключевой принцип, который я усвоил: паттерны должны упрощать код, а не усложнять его. Если после применения паттерна ваш код стал менее понятным и более хрупким — это сигнал пересмотреть архитектурное решение.

Паттерны проектирования
Господа, скажите пожалуйста, что есть такое паттерны проектирования??? Поначалу я думал, что это...

Какие паттерны проектирования можно применить к данной задаче и каким образом?
какие паттерны и как можно сюда влить

Посоветуйте пожалуйста паттерны проектирования
Нужно написать следующее приложение.Хотела бы посоветоваться какие паттерны использовать. При...

Какие паттерны проектирования для C# являются "основными"
Добрый вечер. Подскажите пожалуйста, какие паттерны проектирования для c# являются &quot;основными&quot;?

Подскажите необходимы паттерны (Шаблоны проектирования) необходимые для решения задачи
Добрый день дорогие знатоки) Выдали экзаменационное задание по паттернам: Спроектируйте систему...

Актуальны ли паттерны проектирования
всем привет, Актуальны ли паттерны проектирования и как вы оцениваете их в будущем

Паттерны проектирования и ASP.NET MVC
Здравствуйте. Расскажите пожалуйста, для решения каких задач и какие паттерны Вы используете в...

Паттерны проектирования языкозависимы
Есть мнение, что паттерны проектирования бесполезно изучать по заумным мейнстримным книжкам, как...

Дайте задачу или несколько на порождающие паттерны проектирования
Дайте задачу или несколько на порождающие паттерны проектирования. Заранее благодарен. Если...

“Паттерны проектирования”
Необходимо сделать следующее: 1. Нарисовать в UML диаграмму классов реализуемой программы....

Паттерны проектирования C#
Здравсвуйте. Учу паттерны проектирования , проходит этот процесс очень болезненно. Ничего не...

Паттерны проектирования общего класса для работы с БД
Добрый день! Реализую класс для работы с реляционной бд. Для начала создал интерфейс IDataBase,...

Метки .net, c#, gof, oop, patterns, solid
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Популярные LM модели ориентированы на увеличение затрат ресурсов пользователями сгенерированного кода (грязь -заслуги чистоплюев).
Hrethgir 12.06.2025
Вообще обратил внимание, что они генерируют код (впрочем так-же ориентированы разработчики чипов даже), чтобы пользователь их использующий уходил в тот или иной убыток. Это достаточно опытные модели,. . .
Топ10 библиотек C для квантовых вычислений
bytestream 12.06.2025
Квантовые вычисления - это та область, где теория встречается с практикой на границе наших знаний о физике. Пока большая часть шума вокруг квантовых компьютеров крутится вокруг языков высокого уровня. . .
Dispose и Finalize в C#
stackOverflow 12.06.2025
Работая с C# больше десяти лет, я снова и снова наблюдаю одну и ту же историю: разработчики наивно полагаются на сборщик мусора, как на волшебную палочку, которая решит все проблемы с памятью. Да,. . .
Повышаем производительность игры на Unity 6 с GPU Resident Drawer
GameUnited 11.06.2025
Недавно копался в новых фичах Unity 6 и наткнулся на GPU Resident Drawer - штуку, которая заставила меня присвистнуть от удивления. По сути, это внутренний механизм рендеринга, который автоматически. . .
Множества в Python
py-thonny 11.06.2025
В Python существует множество структур данных, но иногда я сталкиваюсь с задачами, где ни списки, ни словари не дают оптимального решения. Часто это происходит, когда мне нужно быстро проверять. . .
Работа с ccache/sccache в рамках C++
Loafer 11.06.2025
Утилиты ccache и sccache занимаются тем, что кешируют промежуточные результаты компиляции, таким образом ускоряя последующие компиляции проекта. Это означает, что если проект будет компилироваться. . .
Настройка MTProxy
Loafer 11.06.2025
Дополнительная информация к инструкции по настройке MTProxy: Перед сборкой проекта необходимо добавить флаг -fcommon в конец переменной CFLAGS в Makefile. Через crontab -e добавить задачу: 0 3. . .
Изучаем Docker: что это, как использовать и как это работает
Mr. Docker 10.06.2025
Суть Docker проста - это платформа для разработки, доставки и запуска приложений в контейнерах. Контейнер, если говорить образно, это запечатанная коробка, в которой находится ваше приложение вместе. . .
Тип Record в C#
stackOverflow 10.06.2025
Многие годы я разрабатывал приложения на C#, используя классы для всего подряд - и мне это казалось естественным. Но со временем, особенно в крупных проектах, я стал замечать, что простые классы. . .
Разработка плагина для Minecraft
Javaican 09.06.2025
За годы существования Minecraft сформировалась сложная экосистема серверов. Оригинальный (ванильный) сервер не поддерживает плагины, поэтому сообщество разработало множество альтернатив. CraftBukkit. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru