Skip to content

Latest commit

 

History

History
439 lines (267 loc) · 31.2 KB

File metadata and controls

439 lines (267 loc) · 31.2 KB

Лекция 14. Проектирование. Паттерны. SOLID.

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

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

Виды проектирования

  1. Архитектурное проектирование: Определение общей структуры системы, ее компонентов и их взаимодействий.
  2. Проектирование компонентов: Разработка отдельных модулей или компонентов системы.
  3. Проектирование интерфейсов: Определение способов взаимодействия между компонентами системы.

Принципы хорошего проектирования

  1. Простота: Проект должен быть максимально простым и понятным.
  2. Модульность: Система должна состоять из отдельных, легко заменяемых модулей.
  3. Расширяемость: Легкость добавления нового функционала.
  4. Повторное использование: Возможность использования компонентов в других проектах.

OOA --> OOD --> OOP

OOA - объектно-ориентированный анализ. Если страшной терминологией, то это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, выявленных в предметной области.

Если человеческими словами, то это о том, что бы понять какие у нас вообще будут объекты, будут ли они между собой взаимодействовать, и если будут, то какие и как

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

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

Как это работает?

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

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

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

Подход к проектированию

Вне зависимости от того, что вы разрабатываете, всегда можно применять два принципа:

KISS = Keep it simple, stupid (Чем проще, тем лучше! Если всё можно описать двумя классами, в которых 3 метода, то не надо описывать 10 классов с 30-ю методами.)

DRY = Don't repeat yourself (Не повторяйся! Если ты используешь один и тот же код в разных местах, сделай из него функцию или метод)

Существует очень много различных подходов к разработке. Разбирать их детально, мы не будем.

Но посмотрим на два распространенных подхода Data-driven development и Test-driven development.

Data-driven development

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

Все действия описаны на схеме

  1. Планируем что-то сделать
  2. Анализируем полученный на этапе планирования запрос
  3. Проектируем проект
  4. Имплементируем (пишем код)
  5. Пишем тесты
  6. Поддерживаем приложение

Test-driven development

Test-driven development - это подход к разработке, когда тесты пишутся до кода. Смысл в том, что если проектирование было проведено правильно, то ты заранее знаешь, какие в твоей системе будут действия, и как система должна реагировать на разные действия. Тогда можно написать тесты, которые будут отвечать требованиям проектирования, и только после этого писать код, который будет соответствовать уже написанным тестам.

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

SOLID

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

Принципы

SOLID принципы советуют, как проектировать модули.

Цель принципов — проектировать модули, которые:

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

SRP: The Single Responsibility Principle (S)

A module should be responsible to one, and only one, actor.

Старая формулировка: A module should have one, and only one, reason to change.

Принцип единой ответственности

Часто ее трактовали следующим образом: Модуль должен иметь только одну обязанность.

Каждый класс должен решать одну конкретную задачу. Программист не должен одновременно быть уборщицей и поваром.

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

OCP: The Open Closed Principle (O)

A software artifact should be open for extension but closed for modification.

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

LSP: The Liskov Substitution Principle (L)

Принцип подстановки Барбары Лисков - класс потомок должен иметь возможность заменить родителя.

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

Если класс родитель делал кофе, а класс потомок начал продавать наркотики, то что-то у нас пошло не так.

ISP: The Interface Segregation Principle (I)

Make fine grained interfaces that are client specific.

Приницип разделения интерфесов - Представте себе кофемашину, у которой всего одна кнопка(интерфейс) и что-бы сделать капучино нужно нажать на нее 4 раза. А что бы латте, надо зажать кнопку два раза по 5 секунд. Удобно таким пользоваться? Я думаю что катастрофически нет.

Если есть метод(функция), который при разных входных данных ведёт себя по-разному, то лучше написать несколько методов( функций).

Чем меньше нагружены функции/методы, тем проще их поддерживать и тестировать.

DIP: The Dependency Inversion Principle (D)

Depend on abstractions, not on concretions.

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

Если нормальным языком. В вашей реализации должно быть как можно меньше деталей до момента когда эти детали необходимы.

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

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

Еще раз на всякий случай

Принципы SOLID стремятся свести изменение модулей к их добавлению и удалению.

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

Таким образом:

  • Принцип единственной ответственности (Single responsibility)

На каждый объект должна быть возложена одна единственная обязанность

Для этого проверяем, сколько у нас есть причин для изменения класса — если больше одной, то следует разбить данный класс.

  • Принцип открытости/закрытости (Open-closed)

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

Для этого представляем наш класс как «черный ящик» и смотрим, можем ли в таком случае изменить его поведение.

  • Принцип подстановки Барбары Лисков (Liskov substitution)

Объекты в программе могут быть заменены их наследниками без изменения свойств программы

Для этого проверяем, не усилили ли мы предусловия и не ослабили ли постусловия. Если это произошло — то принцип не соблюдается.

  • Принцип разделения интерфейса (Interface segregation)

Много специализированных интерфейсов лучше, чем один универсальный

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

  • Принцип инверсии зависимостей (Dependency Invertion)

Зависимости должны строиться относительно абстракций, а не деталей

Проверяем, зависят ли классы от каких-то других классов (непосредственно инстанцируют объекты других классов и т. д.) и если эта зависимость имеет место, заменяем на зависимость от абстракции.

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

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

Например, декоратор, итератор, генератор (нет, не все паттерны заканчиваются на -ратор). Но это те вещи которые вы уже видели. Декоратор, это реализация паттерна. Если вы запускали цикл фор, то вы запускали паттерн итератор. А если у вас там был написан range то вы использовали паттерн генератор.

Паттернов существует просто огромное количество, настолько огромное, что существуют сотни книг по паттернам проектирования. Тут можно посмотреть на многие из них, но далеко не на все.

Что такое паттерн?

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

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

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

Если привести аналогии, то алгоритм — это кулинарный рецепт с чёткими шагами, а паттерн — инженерный чертёж, на котором нарисовано решение, но не конкретные шаги его реализации.

Из чего состоит паттерн?

Описания паттернов обычно очень формальны и чаще всего состоят из таких пунктов:

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

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

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

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

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

  • Проверенные решения. Вы тратите меньше времени, используя готовые решения, вместо повторного изобретения велосипеда. До некоторых решений вы смогли бы додуматься и сами, но многие могут быть для вас открытием.

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

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

Классификация паттернов

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

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

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

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

  • Структурные паттерны показывают различные способы построения связей между объектами.

  • Поведенческие паттерны заботятся об эффективной коммуникации между объектами

Тут шикарный сайт с описанием некоторых паттернов и их реализации на разных языках программирования.

Рассмотрим некоторые паттерны

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

Одиночка (Singleton)

Описание:

Паттерн Одиночка гарантирует, что класс имеет только один экземпляр, и предоставляет глобальную точку доступа к этому экземпляру.

Применение:

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

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

Описание:

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

Применение:

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

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

Описание:

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

Применение:

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

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

Описание:

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

Применение:

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

Прототип (Prototype)

Описание:

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

Применение:

  • В системах, где создание объектов требует значительных затрат ресурсов (например, клонирование объектов в графических редакторах).
  • В играх для клонирования игровых объектов с определенными характеристиками.

Адаптер (Adapter)

Описание:

Паттерн Адаптер позволяет объектам с несовместимыми интерфейсами работать вместе. Он преобразует интерфейс одного класса в интерфейс, который ожидает клиент.

Применение:

  • Для интеграции старого кода в новые системы без изменения самого старого кода.
  • Для работы с классами, интерфейсы которых отличаются от необходимых.

Мост (Bridge)

Описание:

Паттерн Мост разделяет абстракцию и реализацию, позволяя им изменяться независимо друг от друга.

Применение:

  • В приложениях, где необходимо изменить реализацию абстракции во время выполнения.
  • Для разработки кроссплатформенных GUI-приложений.

Компоновщик (Composite)

Описание:

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

Применение:

  • В графических приложениях для построения сложных сцен из простых объектов.
  • В системах управления файлами для работы с файловыми системами (директории и файлы).

Декоратор (Decorator)

Описание:

Паттерн Декоратор позволяет динамически добавлять новые функциональности к объектам, не изменяя их классов.

Применение:

  • В текстовых редакторах для добавления форматирования к тексту.
  • В веб-приложениях для добавления новых возможностей к существующим объектам запроса и ответа.

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

Описание:

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

Применение:

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

Практика

На этом материал заканчивается, давайте переходить ко второму модулю

Важно! Перед следующим занятием обязательно выполните все что описано по вот этой ссылке