Паттерны проектирования на C#: Архитектура и лучшие практики

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

1. Основы архитектуры: Введение в паттерны и принципы SOLID

Основы архитектуры: Введение в паттерны и принципы SOLID

Добро пожаловать в курс «Паттерны проектирования на C#: Архитектура и лучшие практики». Это первая статья, которая заложит фундамент для всего дальнейшего обучения. Прежде чем мы начнем разбирать конкретные реализации вроде Singleton или Factory Method, нам необходимо понять, зачем вообще нужны паттерны и на каких принципах строится качественная архитектура программного обеспечения.

Многие начинающие разработчики считают, что главная цель программирования — написать код, который работает. Однако опытные инженеры знают: написать код, который работает, легко. Написать код, который можно поддерживать, тестировать и масштабировать годами — вот настоящее искусство.

Что такое паттерны проектирования?

Паттерн проектирования (или шаблон проектирования) — это часто встречающееся решение определенной проблемы при проектировании архитектуры программ.

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

!Сравнение паттерна с чертежом архитектора и рецептом повара: это руководство к действию, а не готовый продукт.

История появления

Понятие паттернов пришло в программирование из архитектуры зданий. В 1994 году четыре автора — Эрих Гамма, Ричард Хелм, Ральф Джонсон и Джон Влиссидес — выпустили книгу «Design Patterns: Elements of Reusable Object-Oriented Software». В сообществе разработчиков эту группу авторов называют «Банда четырех» (Gang of Four, GoF).

В своей книге они описали 23 классических паттерна, которые разделили на три большие группы:

  • Порождающие (Creational) — отвечают за удобное и безопасное создание объектов.
  • Структурные (Structural) — определяют, как классы и объекты компонуются в более крупные структуры.
  • Поведенческие (Behavioral) — заботятся об эффективном взаимодействии между объектами и распределении обязанностей.
  • > Хорошая архитектура делает систему легкой для понимания, разработки, поддержки и развертывания. — Роберт Мартин (Дядюшка Боб)

    Зачем изучать паттерны?

    Использование паттернов дает разработчику несколько ключевых преимуществ:

    * Проверенные решения. Вы используете решения, которые уже были опробованы миллионами разработчиков. Вам не нужно изобретать велосипед. Общий язык. Вместо того чтобы объяснять коллеге: «Ну, я создал класс, который единственный в системе, и к нему есть глобальная точка доступа», вы просто говорите: «Я использовал здесь Синглтон»*. * Предотвращение проблем. Паттерны часто учитывают неочевидные проблемы, которые могут возникнуть при наивной реализации.

    Фундамент архитектуры: Принципы SOLID

    Паттерны не существуют в вакууме. Большинство из них базируются на принципах объектно-ориентированного проектирования. Самый известный набор таких принципов называется SOLID. Это акроним, предложенный Робертом Мартином и Майклом Фезерсом.

    Понимание SOLID критически важно для работы с C#, так как этот язык предоставляет мощные инструменты (интерфейсы, абстрактные классы, дженерики) для их реализации.

    Разберем каждую букву акронима.

    S — Single Responsibility Principle (Принцип единственной ответственности)

    Принцип: У класса должна быть только одна причина для изменения.

    Это означает, что класс должен решать только одну задачу. Если класс занимается и расчетом зарплаты, и сохранением данных в БД, и отправкой отчетов на почту — это нарушение SRP. Такой класс называют God Object (Божественный объект).

    Пример нарушения:

    Правильный подход: Разделим ответственности на разные классы.

    O — Open/Closed Principle (Принцип открытости/закрытости)

    Принцип: Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.

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

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

    !Метафора принципа Open/Closed: розетка не меняется, но позволяет расширять функциональность сети новыми приборами.

    L — Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

    Принцип: Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.

    Если класс B наследуется от класса A, то мы должны иметь возможность использовать B везде, где используется A, и код не должен сломаться.

    Классический пример нарушения — проблема квадрата и прямоугольника. Если сделать Квадрат наследником Прямоугольника и изменить ширину квадрата, его высота тоже должна измениться. Но код, работающий с прямоугольником, не ожидает такого поведения (он ожидает, что ширина и высота меняются независимо).

    В C# нарушение этого принципа часто проявляется, когда в методе наследника вы пишете:

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

    I — Interface Segregation Principle (Принцип разделения интерфейса)

    Принцип: Клиенты не должны зависеть от методов, которые они не используют.

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

    Пример нарушения:

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

    Правильный подход:

    D — Dependency Inversion Principle (Принцип инверсии зависимостей)

    Принцип: Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

    Это самый сложный для понимания, но один из самых важных принципов. Он лежит в основе Dependency Injection (внедрения зависимостей).

    Допустим, у вас есть класс CustomerService (высокий уровень), который сохраняет данные. Если он внутри себя создает экземпляр SqlDatabase (низкий уровень), то он жестко связан с SQL. Вы не сможете легко переключиться на сохранение в файл или использовать мок-объект для тестов.

    Нарушение DIP:

    Соблюдение DIP:

    Теперь CustomerService не знает, куда сохраняются данные: в SQL, в текстовый файл или в облако. Он зависит только от абстракции IRepository.

    Заключение

    Паттерны проектирования и принципы SOLID — это инструменты, которые помогают бороться со сложностью программных систем.

    * SOLID помогает писать чистый код на уровне классов и модулей. * Паттерны предлагают типовые архитектурные решения для взаимодействия этих классов.

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

    Готовы перейти от теории к практике? Впереди нас ждут Singleton, Factory Method, Builder и многое другое.

    2. Порождающие паттерны: Singleton, Factory Method, Abstract Factory и Builder

    Порождающие паттерны: Singleton, Factory Method, Abstract Factory и Builder

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

    Сегодня мы переходим к практике и разберем первую группу паттернов из книги «Банды четырех» — Порождающие паттерны (Creational Patterns). Эти паттерны абстрагируют процесс инстанцирования (создания) объектов. Они помогают сделать систему независимой от того, как ее объекты создаются, компонуются и представляются.

    Мы детально разберем четыре самых популярных паттерна:

  • Singleton (Одиночка)
  • Factory Method (Фабричный метод)
  • Abstract Factory (Абстрактная фабрика)
  • Builder (Строитель)
  • ---

    Singleton (Одиночка)

    Это, пожалуй, самый известный и одновременно самый спорный паттерн.

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

    Решение: Скрыть конструктор класса (сделать его private) и создать статический метод, который возвращает единственный экземпляр.

    !Singleton гарантирует наличие только одного экземпляра класса.

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

    В мире C# существует множество способов реализации Singleton, но многие из них (например, классическая проверка на null без блокировок) не являются потокобезопасными.

    Лучшая практика в современном C# — использование класса Lazy<T>, который гарантирует ленивую инициализацию и потокобезопасность «из коробки».

    Когда использовать, а когда нет?

    Несмотря на популярность, Singleton часто называют антипаттерном. Почему?

  • Нарушение SRP: Класс управляет и своей основной работой, и своим жизненным циклом.
  • Глобальное состояние: Глобальные переменные — это зло. Изменение состояния Синглтона в одном месте программы может непредсказуемо повлиять на другое место.
  • Проблемы с тестированием: Синглтоны сложно подменять мок-объектами (mock) в юнит-тестах.
  • > Используйте Singleton только тогда, когда наличие нескольких экземпляров действительно может сломать логику программы (например, доступ к файловой системе или аппаратному порту).

    ---

    Factory Method (Фабричный метод)

    Этот паттерн решает проблему создания объектов без указания конкретного класса создаваемого объекта.

    Проблема: Представьте, что вы разрабатываете приложение для логистики. Сначала у вас были только грузовики (Truck). Весь код пестрит созданием new Truck(). Но бизнес растет, и нужно добавить морские перевозки (Ship). Вам придется переписывать весь код, добавляя проверки if (type == "Sea").

    Решение: Определить интерфейс для создания объекта, но позволить подклассам решать, какой класс инстанцировать.

    !Фабричный метод делегирует создание объекта наследникам.

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

    Преимущества: * Open/Closed Principle: Вы можете добавлять новые виды транспорта (например, AirLogistics и Plane), не ломая существующий код класса Logistics.

    ---

    Abstract Factory (Абстрактная фабрика)

    Если Фабричный метод создает один продукт, то Абстрактная фабрика создает семейства взаимосвязанных продуктов.

    Проблема: Вы пишете кроссплатформенное UI-приложение. У вас есть кнопки (Button) и чекбоксы (Checkbox). В Windows они выглядят в стиле WinForms, в macOS — в стиле Cocoa. Важно, чтобы кнопка Windows не появилась рядом с чекбоксом macOS.

    Решение: Создать интерфейс фабрики, который умеет производить все типы элементов (кнопку, чекбокс) для определенного семейства.

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

    Главное отличие от Фабричного метода: Абстрактная фабрика — это объект, который передается в клиентский код (через Dependency Injection), и клиент использует его для создания нужных ему компонентов.

    ---

    Builder (Строитель)

    Этот паттерн используется, когда создание объекта — это сложный пошаговый процесс.

    Проблема: Представьте класс House (Дом). У дома могут быть стены, двери, окна, крыша, гараж, бассейн, сад. Если делать все через конструктор, получится монстр: new House(4, 2, 4, true, true, false, true...). Это называется «Телескопический конструктор», и это ужасно нечитаемо.

    Решение: Вынести конструирование объекта за пределы его собственного класса, поручив это отдельному объекту — Строителю.

    !Builder позволяет создавать сложные объекты пошагово.

    Реализация на C# (Fluent Interface)

    В C# паттерн Builder часто реализуют с использованием «текучего интерфейса» (Fluent Interface), где методы возвращают сам объект строителя, позволяя выстраивать цепочки вызовов.

    Иногда в паттерне выделяют еще роль Director (Директор) — класс, который знает порядок вызова шагов строительства для получения стандартных конфигураций (например, Director.ConstructOfficePC(builder)).

    ---

    Итоги

    Мы рассмотрели четыре ключевых порождающих паттерна:

  • Singleton: Когда нужен строго один экземпляр (будьте осторожны с ним!).
  • Factory Method: Когда есть иерархия продуктов и создателей, и нужно делегировать создание наследникам.
  • Abstract Factory: Когда нужно создавать семейства совместимых объектов (темы оформления, кроссплатформенность).
  • Builder: Когда объект сложный и его нужно собирать по частям.
  • Эти паттерны помогают избавиться от жесткой привязки к конкретным классам в коде, делая архитектуру гибкой и расширяемой. В следующей статье мы перейдем к Структурным паттернам и узнаем, как собирать из простых классов сложные структуры, используя Adapter, Decorator и Facade.

    3. Структурные паттерны: Adapter, Decorator, Facade и Proxy

    Структурные паттерны: Adapter, Decorator, Facade и Proxy

    Добро пожаловать в третью часть курса «Паттерны проектирования на C#: Архитектура и лучшие практики».

    В прошлых статьях мы научились правильно создавать объекты, используя Порождающие паттерны (Singleton, Factory Method, Builder). Но создание объектов — это только полдела. В реальных приложениях объекты должны взаимодействовать друг с другом, образуя сложные системы. Здесь на сцену выходят Структурные паттерны (Structural Patterns).

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

    Сегодня мы разберем «великолепную четверку» структурных паттернов, которые чаще всего встречаются в разработке на C#:

  • Adapter (Адаптер)
  • Decorator (Декоратор)
  • Facade (Фасад)
  • Proxy (Заместитель)
  • ---

    Adapter (Адаптер)

    Адаптер — это паттерн, который позволяет объектам с несовместимыми интерфейсами работать вместе. Это один из самых полезных паттернов при интеграции легаси-кода или сторонних библиотек.

    Проблема

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

    Решение

    Вы создаете «переходник» — класс-адаптер. Он получает данные в XML (понятные вашему коду), конвертирует их в JSON и передает сторонней библиотеке.

    !Адаптер выступает мостом между двумя несовместимыми интерфейсами

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

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

    ---

    Facade (Фасад)

    Фасад — предоставляет простой интерфейс к сложной системе классов, библиотеке или фреймворку.

    Проблема

    Ваш код должен работать со сложной библиотекой конвертации видео. Чтобы сконвертировать видео, нужно: создать объект VideoFile, инициализировать BitrateReader, выбрать CodecFactory, смешать аудио через AudioMixer и запустить Converter.

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

    Решение

    Создать класс-фасад, который имеет один простой метод ConvertVideo(filename, format). Внутри себя фасад знает, какие классы библиотеки и в каком порядке нужно вызвать.

    !Фасад скрывает сложность системы за простым интерфейсом

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

    Когда использовать: * Когда нужно предоставить простой интерфейс к сложной подсистеме. * Когда нужно разложить подсистему на слои (Фасад может служить точкой входа для каждого уровня).

    ---

    Proxy (Заместитель)

    Proxy — это объект, который выступает в роли заместителя для другого объекта. Он перехватывает вызовы к оригинальному объекту, позволяя сделать что-то до или после передачи вызова оригиналу.

    Проблема

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

    Решение

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

    Другие сценарии использования: * Защита (Protection Proxy): Проверка прав доступа перед вызовом. * Кеширование (Caching Proxy): Возврат сохраненного результата вместо повторного тяжелого расчета. * Логирование: Запись запросов в журнал.

    !Прокси контролирует доступ к реальному объекту

    Реализация на C# (Виртуальный прокси)

    Сравнение паттернов

    Иногда структурные паттерны кажутся похожими, но у них разные цели:

    | Паттерн | Цель | | :--- | :--- | | Adapter | Меняет интерфейс одного объекта под другой. | | Decorator | Добавляет обязанности объекту, не меняя интерфейс. | | Facade | Упрощает интерфейс для работы со сложной системой. | | Proxy | Контролирует доступ к объекту, не меняя интерфейс. |

    Заключение

    Структурные паттерны помогают нам собирать гибкие системы из готовых блоков.

    * Используйте Adapter, когда нужно подружить несовместимое. * Используйте Decorator, когда нужно динамически навешивать функционал. * Используйте Facade, чтобы скрыть сложность и не пугать коллег (и себя в будущем). * Используйте Proxy, когда нужно лениво загружать ресурсы или защищать доступ.

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

    4. Поведенческие паттерны: Observer, Strategy, Command и State

    Поведенческие паттерны: Observer, Strategy, Command и State

    Добро пожаловать в четвертую часть курса «Паттерны проектирования на C#: Архитектура и лучшие практики».

    В предыдущих модулях мы научились создавать объекты, не привязываясь к конкретным классам (Порождающие паттерны), и собирать из них гибкие структуры (Структурные паттерны). Теперь перед нами встает новая задача: как организовать эффективное взаимодействие между этими объектами?

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

    За это отвечают Поведенческие паттерны (Behavioral Patterns). Они заботятся об алгоритмах и распределении обязанностей между объектами.

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

  • Observer (Наблюдатель)
  • Strategy (Стратегия)
  • Command (Команда)
  • State (Состояние)
  • ---

    Observer (Наблюдатель)

    Этот паттерн вы встречаете каждый день. Подписка на YouTube-канал, уведомление о новом письме, пуш от банка — всё это реализации Наблюдателя.

    Проблема

    Представьте, что у вас есть класс NewsAgency (Агентство новостей) и множество читателей. Читатели хотят узнавать новости сразу, как только они появляются. Если каждый читатель будет раз в секунду опрашивать агентство («Есть новости? А сейчас?»), сервер агентства упадет от нагрузки. Это называется polling (опрос), и это неэффективно.

    Решение

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

    !Издатель оповещает всех подписчиков о событии одновременно.

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

    В C# этот паттерн настолько популярен, что встроен в язык через ключевое слово event. Однако для понимания архитектуры полезно рассмотреть классическую реализацию через интерфейсы.

    Когда использовать: * Когда изменение состояния одного объекта требует изменения других, и вы не знаете заранее, сколько таких объектов будет. * Когда одни объекты должны следить за другими лишь ограниченное время.

    ---

    Strategy (Стратегия)

    Один из самых чистых и полезных паттернов для соблюдения принципа открытости/закрытости (Open/Closed Principle).

    Проблема

    Вы пишете навигатор. Сначала он прокладывал маршрут только для автомобилей. Потом добавили пешеходов. Потом общественный транспорт. Ваш класс Navigator превратился в гигантский if-else или switch:

    Любое изменение алгоритма требует правки основного класса, что чревато багами.

    Решение

    Выделите каждый алгоритм в отдельный класс с общим интерфейсом. Основной класс (Контекст) будет хранить ссылку на интерфейс стратегии и делегировать ему выполнение работы.

    !Навигатор использует сменные стратегии для построения маршрута.

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

    Теперь, чтобы добавить маршрут для велосипеда, вам не нужно трогать класс Navigator. Вы просто создаете BikeStrategy.

    ---

    Command (Команда)

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

    Проблема

    Вы делаете пульт управления умным домом. У вас есть кнопки. Если вы в коде кнопки напишете lamp.TurnOn(), то кнопка станет жестко привязана к лампе. А если вы захотите, чтобы эта же кнопка включала музыку? Или чтобы можно было нажать «Ctrl+Z» и отменить последнее действие?

    Решение

    Создайте прослойку. Кнопка не должна знать, кто и как выполнит действие. Она просто знает, что нужно выполнить некую «Команду».

    > Паттерн Команда часто сравнивают с заказом в ресторане. Вы (Клиент) даете заказ (Команду) официанту (Invoker). Официант не готовит еду, он передает заказ повару (Receiver). Вы не зависите от повара, повар не зависит от вас.

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

    Главная фишка: Благодаря методу Undo, мы можем хранить историю команд в стеке и реализовывать многоуровневую отмену действий в редакторах.

    ---

    State (Состояние)

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

    Проблема

    Возьмем класс Document. У него могут быть состояния: Draft (Черновик), Moderation (На модерации), Published (Опубликован).

    Метод Publish() будет выглядеть ужасно:

    Чем больше состояний, тем сложнее этот код.

    Решение

    Создать отдельные классы для каждого состояния и делегировать им логику переходов.

    !Объект делегирует поведение текущему активному состоянию.

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

    Теперь логика переходов размазана не по if-else, а инкапсулирована в конкретных классах состояний. Это делает код чистым и расширяемым.

    ---

    Итоги

    Мы рассмотрели четыре мощных инструмента управления поведением:

  • Observer — для реакции на события («один говорит — многие слушают»).
  • Strategy — для подмены алгоритмов на лету (Навигатор: авто/пешком/вело).
  • Command — для инкапсуляции действий и реализации Undo/Redo.
  • State — для объектов, чье поведение кардинально меняется в зависимости от статуса.
  • Эти паттерны помогают избежать жесткой связности и запутанных условных операторов. В следующей, заключительной статье курса, мы подведем итоги, рассмотрим оставшиеся важные паттерны (Template Method, Chain of Responsibility) и обсудим, как не переусердствовать с их применением.

    5. Практическое применение паттернов и рефакторинг существующего кода

    Практическое применение паттернов и рефакторинг существующего кода

    Поздравляю! Вы прошли долгий путь. Мы изучили основы архитектуры, разобрали порождающие, структурные и поведенческие паттерны. Теперь ваш инструментарий полон: от Singleton до Observer, от Adapter до Strategy.

    Но знать паттерны и уметь применять их в реальном проекте — это разные навыки. В учебниках примеры всегда идеальны: классы пустые, требования четкие, а код пишется с нуля. В реальности мы чаще сталкиваемся с легаси-кодом (наследием), запутанной логикой и горящими сроками.

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

    Когда нужен рефакторинг?

    Прежде чем бросаться переписывать код, нужно понять, что именно с ним не так. В программировании есть термин «Запахи кода» (Code Smells) — это признаки того, что архитектура системы нарушена.

    Типичные запахи, которые «лечатся» паттернами:

    * Длинные методы с кучей условий: Огромные конструкции if-else или switch, которые проверяют типы объектов. * Дублирование кода: Одинаковые куски логики в разных классах. * Жесткая связанность: Невозможно протестировать класс, не подняв базу данных или не отправив реальный запрос в сеть. * Божественный объект (God Object): Класс, который знает и умеет слишком много.

    > Любой дурак может написать код, понятный компьютеру. Хорошие программисты пишут код, понятный людям. — Мартин Фаулер

    Правила безопасности при рефакторинге

    Никогда не начинайте рефакторинг без модульных тестов (Unit Tests).

  • Напишите тесты для текущего «плохого» кода. Убедитесь, что они проходят.
  • Измените структуру, применив паттерн.
  • Запустите тесты. Если они упали — вы что-то сломали. Если прошли — поздравляю, рефакторинг успешен.
  • !Цикл безопасного рефакторинга: Тесты -> Рефакторинг -> Проверка.

    ---

    Кейс №1: Избавляемся от switch с помощью Стратегии и Фабрики

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

    До рефакторинга

    Проблемы:

  • Нарушен принцип Open/Closed. Чтобы добавить новый тип доставки, нужно менять этот класс.
  • Метод разрастается до гигантских размеров.
  • Сложно тестировать логику отдельного способа доставки.
  • Решение

    Мы применим паттерн Strategy (Стратегия) для выноса алгоритмов и Factory (Фабрика) для их создания.

    После рефакторинга

    Шаг 1: Создаем интерфейс стратегии.

    Шаг 2: Реализуем конкретные стратегии.

    Шаг 3: Создаем простую фабрику (можно использовать Dictionary).

    Шаг 4: Обновляем клиентский код.

    Теперь добавление нового способа доставки сводится к созданию нового класса и добавлению одной строчки в фабрику. Основной сервис OrderService больше не меняется.

    ---

    Кейс №2: Очистка бизнес-логики с помощью Декоратора

    Часто бизнес-логика «засоряется» техническими деталями: логированием, кешированием, проверкой прав доступа, транзакциями. Это называется Cross-Cutting Concerns (Сквозной функционал).

    До рефакторинга

    Проблемы:

  • Нарушен принцип Single Responsibility. Класс занимается и отчетами, и замерами производительности.
  • Код трудно читать из-за «шума».
  • Решение

    Используем паттерн Decorator (Декоратор). Оставим в сервисе только чистую логику, а логирование вынесем в обертку.

    После рефакторинга

    При сборке приложения (обычно в DI-контейнере) мы просто оборачиваем сервис:

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

    ---

    Кейс №3: Упрощение сложной системы через Фасад

    Иногда вы подключаете библиотеку, для работы с которой нужно инициализировать десять объектов в правильном порядке. Если этот код размазан по всему проекту, поддержка становится адом.

    Решение: Спрячьте всю сложность за паттерном Facade (Фасад).

    Вместо:

    Сделайте:

    Это делает ваш код независимым от конкретной библиотеки. Если вы решите сменить библиотеку конвертации, вам придется переписать только код внутри Фасада.

    ---

    Темная сторона: Паттерноз (Patternitis)

    Важно помнить: паттерны — это не самоцель.

    Существует болезнь начинающих архитекторов — желание впихнуть паттерны везде, где только можно. Это приводит к переусложнению (Over-engineering).

    Антипаттерн «Золотой молоток»

    Если у вас в руках молоток, все кажется гвоздем. Если вы только что выучили паттерн Singleton, вы начнете делать синглтонами всё подряд. Если выучили Factory, то будете создавать фабрики даже для простых строк.

    Принцип KISS и YAGNI

    * KISS (Keep It Simple, Stupid): Делай это проще. Если задачу можно решить простым методом без создания трех интерфейсов и пяти классов — сделайте это простым методом. * YAGNI (You Ain't Gonna Need It): Вам это не понадобится. Не создавайте гибкую архитектуру «на будущее», если сейчас в ней нет потребности. Вы просто потратите время на код, который никогда не будет использован.

    Пример избыточности:

    Заключение курса

    Мы завершаем наш курс по паттернам проектирования. Давайте подведем итоги:

  • Паттерны — это словарь. Они позволяют разработчикам быстро понимать друг друга. Фраза «здесь реализован Наблюдатель» заменяет 10 минут объяснений кода.
  • Паттерны решают типовые проблемы. Не изобретайте велосипед. Если проблема стандартная, скорее всего, для нее уже есть решение от «Банды четырех».
  • Архитектура — это компромисс. Любой паттерн добавляет сложность (новые классы, интерфейсы). Применяйте их только тогда, когда выгода от гибкости превышает цену сложности.
  • Развивайте свое архитектурное мышление. Читайте чужой код, ищите в нем паттерны, проводите рефакторинг своих старых проектов. И помните: лучший код — это тот, которого нет, а если он есть — он должен быть простым и понятным.

    Удачи в проектировании надежных систем на C#!