Created by AIImproved by people
Riqli · living documents · updated continuously
Where AI knowledge meets human practice.
Share with friends

Principles and Patterns of Object-Oriented Design (SOLID, GoF, GRASP). Hard skill. (OOP. SOLID. GRASP. GoF patterns. Architectural patterns. MVC. DDD. CQRS. Event Sourcing. Self-study. Q&A. Tutorials. Documentation.)
Введение
Аннотация. Объектно-ориентированное проектирование (ООП) обещает гибкость и переиспользуемость, но на практике часто приводит к хаосу, если не опираться на проверенные принципы и паттерны. Этот курс посвящён фундаменту профессиональной разработки: принципам SOLID, паттернам проектирования GoF (Банды Четырёх) и принципам GRASP. Мы разберём, как эти концепции превращают «спагетти-код» в элегантную, тестируемую и масштабируемую архитектуру. Курс создан для разработчиков, которые хотят не просто знать названия паттернов, а понимать, когда и как их применять, чтобы принимать осознанные архитектурные решения.
Цель курса. После прохождения курса вы сможете самостоятельно анализировать требования к программной системе, выбирать и обосновывать подходящие архитектурные и проектировочные решения на основе принципов SOLID, GRASP и каталога паттернов GoF, а также критически оценивать существующий код с точки зрения его гибкости, поддерживаемости и расширяемости.
Результаты обучения.
- Знать: пять принципов SOLID с примерами нарушений и рефакторинга; основные паттерны GoF (порождающие, структурные, поведенческие); ключевые принципы GRASP (Information Expert, Controller, Low Coupling, High Cohesion); роль архитектурных паттернов, таких как MVC, DDD, CQRS и Event Sourcing.
- Уметь: применять принципы SOLID для рефакторинга «сложного» кода; проектировать систему, используя подходящие порождающие, структурные и поведенческие паттерны; выбирать между альтернативными архитектурными решениями (например, CQRS vs CRUD) на основе контекста задачи.
- Владеть: навыками «белой доски» для моделирования архитектуры; техникой выявления «запахов» кода и замены их на паттерны; навыком документирования архитектурных решений с использованием стандартных нотаций.
Для кого этот курс.
Курс предназначен для разработчиков-самоучек, джуниоров и мидл-разработчиков, которые чувствуют неуверенность при проектировании «с нуля» или при рефакторинге большого легаси-проекта. Вы освоите общий язык архитектурных терминов, что сделает вас более эффективным участником команды и технических обсуждений.
Этот курс НЕ для вас, если вы ищете краткий справочник по синтаксису языка программирования. Для прохождения необходимы базовые знания ООП (наследование, инкапсуляция, полиморфизм) и практический опыт написания кода на любом объектно-ориентированном языке (Java, C#, Python, PHP, C++). Все примеры универсальны и не привязаны к конкретному языку.
Модуль 1. Принципы SOLID
Принцип единственной ответственности (SRP). SOLID начинается с SRP: «У класса должна быть только одна причина для изменения». Каждый класс должен решать строго одну задачу. Это не означает, что у класса должен быть только один метод, а то, что изменения в спецификации одной сущности предметной области не должны затрагивать другие аспекты системы. Например, класс Report не должен одновременно отвечать за генерацию данных, форматирование и отправку по электронной почте. Смешение ответственности приводит к тому, что изменение логики вычислений ломает форматирование и наоборот. Решение: выделите классы ReportDataCollector, ReportFormatter и ReportEmailSender. Следование SRP повышает связность внутри класса и снижает зацепление между модулями, делая код более понятным и тестируемым.
Принцип открытости/закрытости (OCP). «Программные сущности должны быть открыты для расширения, но закрыты для модификации» — второй принцип SOLID. Это означает, что вы должны иметь возможность изменять поведение системы без переписывания существующего кода. Достигается это через абстракции (интерфейсы или абстрактные классы). Классическая ошибка — добавление цепочки if-else для поддержки новых типов. Вместо этого используйте полиморфизм: создайте общий интерфейс и реализуйте конкретные классы для каждой вариации. Например, для системы расчета скидок создайте интерфейс DiscountPolicy и реализации NoDiscount, PercentDiscount, BlackFridayDiscount. Добавление новой скидки не затронет класс заказа, если он зависит только от абстракции.
Принцип подстановки Лисков (LSP). Третий принцип SOLID гласит: «Объекты в программе должны быть заменяемы экземплярами их подтипов без изменения правильности выполнения программы». Простыми словами, наследующий класс не должен нарушать поведение базового. Нарушение LSP — это когда вы проверяете тип наследника в коде (if (obj is ChildClass)) или когда наследник выбрасывает исключения, не объявленные в базовом классе. Классический пример — наследование Square от Rectangle с переопределением методов установки ширины и высоты, что нарушает математическую логику. Следуйте контракту базового класса: предусловия нельзя усиливать, постусловия — ослаблять, инварианты — нарушать. Соблюдение LSP делает код надежным при использовании полиморфизма.
Принцип разделения интерфейса (ISP). «Клиенты не должны зависеть от методов, которые они не используют». ISP борется с «толстыми» интерфейсами, где один метод не нужен большинству реализаций. Большой интерфейс с 10 методами заставляет разработчиков писать пустые заглушки, что усложняет поддержку. Вместо одного монолитного интерфейса IMachine с методами print, scan и fax создайте интерфейсы IPrinter, IScanner, IFax. Это позволяет реализовать только необходимую функциональность. Следуя ISP, вы добиваетесь высокой связности клиентов с узкими, специфичными интерфейсами, что упрощает изменения и уменьшает «связанность» системы.
Принцип инверсии зависимостей (DIP). Завершающий принцип SOLID формулируется так: «Зависимости должны строиться на абстракциях, а не на конкретных реализациях». Модули верхнего уровня (бизнес-логика) не должны зависеть от модулей нижнего уровня (работа с БД, API, файловой системой). Вместо этого оба уровня должны зависеть от абстракций. Внедрение зависимостей (Dependency Injection) — ключевая техника для реализации DIP. Вместо того чтобы класс UserService сам создавал внутри себя объект PostgreSQLRepository, он должен принимать через конструктор интерфейс IUserRepository. Это позволяет легко подменить базу данных на тестовую или переключиться на MongoDB без изменения сервиса. DIP — основа для слабой связанности и тестируемости.
Модуль 2. Паттерны проектирования GoF
Паттерны GoF: Порождающие паттерны (Singleton, Factory, Abstract Factory). Порождающие паттерны решают проблемы создания объектов. Singleton гарантирует, что у класса есть только один экземпляр, и предоставляет глобальную точку доступа (например, логгер, менеджер кэша). Однако злоупотребление синглтоном считается анти-паттерном из-за проблем с тестированием и скрытыми зависимостями. Factory Method делегирует создание объекта подклассам, определяя интерфейс метода createProduct. Это хороший выбор, когда заранее неизвестно, объекты какого класса нужно создавать. Abstract Factory предоставляет интерфейс для создания семейств взаимосвязанных объектов без указания их конкретных классов (например, создание элементов GUI для разных ОС). Все эти паттерны направлены на инкапсуляцию логики создания, следуя DIP и OCP.
Порождающие паттерны: Builder и Prototype. Builder отделяет конструирование сложного объекта от его представления, позволяя создавать разные представления одним и тем же процессом. Незаменим, когда объект имеет множество опциональных параметров (например, конфигурация сервера, запрос к API). Паттерн использует строителя (Builder) и директора (Director), который управляет процессом сборки. Это избавляет от «телескопического» конструктора с десятком аргументов. Prototype создает новые объекты путем клонирования существующего (прототипа), избегая затрат на создание объекта «с нуля». Используйте его, когда инициализация объекта требует значительных ресурсов или когда класс объекта определяется во время выполнения. Ключевой момент: реализация глубокого клонирования для сложных графов объектов.
Структурные паттерны: Adapter и Bridge. Структурные паттерны помогают совмещать несовместимые интерфейсы и строить гибкие иерархии. Adapter позволяет объектам с несовместимыми интерфейсами работать вместе, действуя как прослойка-переводчик. Например, интеграция нового API с устаревшим кодом, когда код ожидает интерфейс ILegacyPayment, а у нас есть класс StripeAPI. Паттерн Bridge разделяет абстракцию и реализацию, чтобы их можно было изменять независимо. Вместо создания иерархии классов для каждого типа устройства и пульта, создается иерархия для пультов и отдельно для устройств, а пульт содержит ссылку на интерфейс устройства. Это избегает взрывного роста числа классов.
Структурные паттерны: Composite и Decorator. Composite позволяет сгруппировать объекты в древовидную структуру для работы с единичными и составными объектами единообразно. Классика — GUI: элемент интерфейса и контейнер, содержащий другие элементы, реализуют общий интерфейс draw(). Decorator динамически добавляет объекту новые обязанности, являясь гибкой альтернативой наследованию. Оборачивая объект в декораторы, вы добавляете поведение, не меняя исходный код (в духе OCP). Например, обернуть поток FileStream декораторами BufferedStream, EncryptedStream, ZipStream. Комбинируя их, вы получаете нужную функциональность без создания сотен подклассов.
Поведенческие паттерны: Strategy и Observer. Поведенческие паттерны инкапсулируют алгоритмы и способы взаимодействия. Strategy определяет семейство алгоритмов, инкапсулирует их и делает их взаимозаменяемыми. Алгоритм выбирается во время выполнения (например, выбор алгоритма сортировки, компрессии или расчета скидки). Это прямая реализация принципа OCP. Observer определяет зависимость «один ко многим», когда при изменении состояния одного объекта (издателя) все зависимые объекты (подписчики) автоматически уведомляются. Широко используется в реализации событийно-ориентированных систем, GUI и реактивных подходах. Не забывайте про отписку, чтобы избежать утечек памяти.
Поведенческие паттерны: Command и Template Method. Command превращает запрос в объект, позволяя параметризовать клиентов запросами, ставить их в очередь, логировать и поддерживать отмену операций (Undo). Разделяет объект, инициирующий операцию, и объект, который знает, как ее выполнить. Template Method определяет «скелет» алгоритма в базовом классе, позволяя подклассам переопределять отдельные шаги без изменения структуры алгоритма. Используйте его для реализации инвариантов алгоритма в одном месте и избежания дублирования кода. Отличный пример — шаги загрузки данных из источника (открыть, прочитать, обработать, закрыть), где шаг «прочитать» варьируется в зависимости от формата.
Модуль 3. Принципы GRASP
Введение в GRASP: Information Expert. GRASP (General Responsibility Assignment Software Patterns) — это набор принципов для назначения ответственностей классам в объектно-ориентированном дизайне. Ключевой принцип — Information Expert: присваивайте ответственность тому классу, у которого есть вся необходимая информация для ее выполнения. Например, расчет общей стоимости заказа должна быть ответственностью класса Order, а не внешнего класса OrderCalculator, потому что именно Order содержит список позиций. Следование этому принципу напрямую повышает связность и инкапсуляцию, так как данные и методы, работающие с ними, находятся в одном месте.
GRASP: Controller и Low Coupling. Паттерн Controller определяет, кто отвечает за обработку системных событий (например, ввод пользователя). Делегируйте эту ответственность классу, представляющему либо всю систему (например, SystemController), либо конкретный вариант использования (CreateOrderHandler). Это защищает слой пользовательского интерфейса от бизнес-логики. Принцип Low Coupling — минимизируйте связанность между классами, чтобы изменения в одном классе как можно меньше затрагивали другие. Высокая связанность делает систему хрупкой и сложной для тестирования. Для достижения низкой связанности используйте внедрение зависимостей и интерфейсы.
GRASP: High Cohesion и другие принципы. High Cohesion — высокая связность внутри класса, означает, что методы класса выполняют логически связанные задачи и используют общие данные. Низкая связность порождает «божественные» классы, делающие всё (God Object). Creator — кто должен создавать объекты? Как правило, это класс, который содержит или агрегирует создаваемый объект, или тот, который имеет информацию для его инициализации. Polymorphism — назначайте ответственность на основе полиморфизма, когда поведение системы варьируется в зависимости от типа. Pure Fabrication — искусственно создавайте класс, не являющийся частью предметной области, для достижения высокой связанности и низкого зацепления (сервисы, репозитории).
Модуль 4. Архитектурные паттерны и современные подходы
Архитектурный паттерн Model-View-Controller (MVC). MVC разделяет приложение на три взаимосвязанных компонента: Model (данные и бизнес-логика), View (представление, пользовательский интерфейс) и Controller (обработка ввода, обновление Model и View). Это ключевой паттерн для веб-приложений и десктопных приложений. Основное преимущество — четкое разделение ответственности: дизайнеры могут работать над View, разработчики бизнес-логики — над Model, а интеграторы — над Controller. Современные веб-фреймворки (например, Spring MVC, Django, ASP.NET MVC) следуют этому паттерну, обеспечивая слабую связанность и высокую связность.
Domain-Driven Design (DDD) и стратегическое проектирование. DDD — это подход к проектированию сложных систем, в основе которого лежит предметная область. Ключевые концепции: Entity (идентифицируемый объект), Value Object (неизменяемый объект без идентичности), Aggregate (корень агрегата, транзакционная граница), Repository (паттерн для доступа к данным) и Factory (создание сложных объектов). Стратегическое проектирование в DDD включает выделение Bounded Context (ограниченного контекста) и Ubiquitous Language (единого языка). DDD идеально подходит для сложного бизнеса, где критично понимание логики, и менее эффективен для простых CRUD-приложений.
Паттерн CQRS (Command Query Responsibility Segregation). CQRS разделяет операции чтения (Queries) и записи (Commands) данных, предлагая использовать разные модели для них. Это позволяет оптимизировать каждую сторону независимо: модель записи может быть строгой, с проверкой бизнес-правил, а модель чтения — денормализованной, оптимизированной для получения данных. Часто CQRS сочетается с Event Sourcing. Сложность заключается в необходимости синхронизации моделей (обычно через события) и в увеличении объема кода. Используйте CQRS в высоконагруженных системах, где требования к чтению и записи сильно различаются, и не применяйте его без необходимости, так как это усложняет архитектуру.
Event Sourcing. Вместо хранения текущего состояния сущности, Event Sourcing хранит последовательность событий, которые привели к этому состоянию. Состояние восстанавливается путем воспроизведения (replay) всех событий из хранилища событий. Это дает полный аудит изменений, возможность отката во времени, и мощную основу для реализации CQRS. Однако Event Sourcing требует переосмысления логики: вместо обновления состояния вы генерируете событие (например, OrderPlaced, OrderShipped). Это усложняет чтение данных, если не использовать проекции (read models). Event Sourcing идеален для систем с высокими требованиями к аудиту, финансовых и аналитических систем.
Современные подходы: «Чистая архитектура» и «Гексагональная архитектура». Эти архитектуры ставят бизнес-логику в центр системы, изолируя её от деталей инфраструктуры (базы данных, UI, веб-фреймворки). «Чистая архитектура» (Uncle Bob) использует слои с зависимостью, направленной внутрь: Entities, Use Cases, Interface Adapters, Frameworks. «Гексагональная архитектура» (Ports & Adapters) рассматривает приложение как «ядро», которое взаимодействует с внешним миром через порты (интерфейсы) и адаптеры (конкретные реализации). Это обеспечивает максимальную тестируемость и гибкость: замена базы данных или веб-фреймворка превращается в замену адаптера, не затрагивая ядро.
Дизайн-системы и современные практики. В современной разработке особое внимание уделяется созданию дизайн-систем — набору компонентов, паттернов и руководств. Для управления кодом в масштабе компании используются монорепозитории и инструменты типа NX или Turborepo. Активно применяются API-первое проектирование (API-first) с использованием OpenAPI (Swagger) для документирования и генерации клиентов/серверов. Feature Flags позволяют включать и выключать функциональность без релиза. Все эти практики тесно связаны с принципами SOLID и GRASP: они направлены на снижение связанности, повышение переиспользуемости и управление сложностью.
Модуль 5. Практика: Выбор и применение паттернов
Как выбирать паттерн? Решение на основе проблемы. Не начинайте проектирование с паттернов. Сначала определите проблему: вам нужно создавать семейства продуктов? Используйте Abstract Factory. У вас изменяющийся алгоритм? Применяйте Strategy. Вы хотите добавить логирование к любому методу без изменения кода? Воспользуйтесь Decorator или аспектно-ориентированным программированием. Задайте себе вопросы: «Что изменяется?», «Как инкапсулировать это изменение?», «Как сделать код тестируемым?». Паттерны — это не цель, а средство. Критический совет: «Простота важнее, чем необходимость» (KISS). Не добавляйте паттерн, если можно обойтись простым условием.
Анти-паттерны при использовании SOLID и паттернов. Частая ошибка — «золотой молоток» (использование одного паттерна везде). Например, Singleton часто используется там, где нужен просто статический класс, а Factory — там, где достаточно конструктора. YAGNI (You Aren't Gonna Need It) предупреждает: не создавайте абстракций «на вырост», если они не нужны прямо сейчас. Другой анти-паттерн — «Analisis Paralysis» (паралич анализа), когда вы бесконечно проектируете паттерны вместо написания кода. Следуйте итеративной разработке, делайте рефакторинг «в сторону паттернов», когда необходимость становится очевидной.
Рефакторинг к легаси с использованием SOLID. Постепенный рефакторинг — наиболее безопасный путь. Начните с покрытия кода тестами (Characterization Tests), чтобы ничего не сломать. Затем ищите классы, нарушающие SRP (например, классы-«боги» с огромными методами). Используйте технику «Extract Class» для разделения ответственностей. Для внедрения DIP и снижения связанности начните с поиска прямых зависимостей (new) и замены их на внедрение через конструктор. Создавайте интерфейсы для внешних зависимостей (база данных, внешний API). После каждого небольшого шага запускайте тесты. Рефакторинг к чистым принципам — это постоянный процесс, а не одноразовое событие.
Практическое задание: анализ кода. Возьмите любой небольшой проект на GitHub (или код с работы) и проанализируйте его на предмет следования принципам SOLID. Выпишите нарушения по каждому пункту. Затем предложите план рефакторинга: как изменить иерархию наследования, чтобы соблюсти LSP? Какие интерфейсы нужно разделить (ISP)? Какие абстракции ввести для инверсии зависимостей (DIP)? Самостоятельная «инспекция» кода — лучший способ закрепить теорию.
Инструменты анализа и документации. Для контроля архитектуры используйте инструменты статического анализа, такие как SonarQube, который подсвечивает нарушения принципов SOLID и предоставляет метрики (покрытие тестами, технический долг). Для документирования архитектуры полезна методология C4 Model (уровни контекста, контейнеров, компонентов и кода). Используйте диаграммы классов UML для фиксации паттернов проектирования в вашем проекте. Современные IDE (IntelliJ IDEA, Visual Studio) предоставляют встроенные инструменты для рефакторинга и анализа зависимостей, которые помогут применить изученные концепции на практике.