Введение

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

Почему я должен использовать SOLID?

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

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

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

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

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

Как говорится, давайте перейдем к принципам…

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

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

В следующем примере у нас есть класс, который имеет много обязанностей и не следует принципу SRP:

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

Открытый закрытый принцип (OCP)

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

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

Классы, которые закрыты для модификации, обычно имеют меньше условий (if/else/switch…), потому что они возлагают на других ответственность за достижение чего-то.

Давайте воспользуемся нашим классом Car, чтобы продолжить показ примеров.
Ниже приведен пример класса, в котором не применяется принцип OCR.

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

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

Это означает, что каждый раз, когда мы добавляем новый тип автомобиля в нашу систему, мы должны добавлять еще одно условное выражение в метод GetFuelConsumption().

Представьте, если нам нужно проверить расход топлива сотни автомобилей?!? 😥
Наш метод/класс станет таким огромным, потому что для расчета потребления может использоваться другая переменная, поэтому для этого будут использоваться тонны строк кода.

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

Давайте посмотрим ниже на хороший пример принципа OCR:

Посмотрите, каким коротким и простым стал метод GetFuelConsumption().
Понятно, что он делает. И добавление новых типов автомобилей в систему не вызовет у нас никаких проблем. GetFuelConsumment изменять не нужно!

Сейчас он закрыт для модификации, но открыт для расширения.

Ниже я покажу вам, как мы можем добиться результата, показанного выше:

Теперь нашу систему проще обслуживать, если добавляется еще один тип автомобиля, нам достаточно создать свой калькулятор класса, например, SportFuelConsumptionCalculator : CarFuelConsumptionCalculator и включить его в класс завод.

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

Принцип замещения Лисков (LSP)

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

Если класс B является подтипом класса A, мы должны иметь возможность передать объект типа B в любое место, которое ожидает объект типа A, не нарушая работу системы. Все должно продолжать работать.

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

Допустим, у нас есть Транспортное средство Автомобиль в нашей системе, и у нас есть класс, который включает двигатели транспортных средств.

Когда мы пытаемся завести двигатель Автомобиля, все работает отлично, но в какой-то момент система попыталась завести двигатель велосипеда (это тоже транспортное средство), но это не моторизованный велосипед, и поведение было не таким. ожидал.

См. пример ниже:

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

Давайте исправим нашу проблему:

Наша программа:

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

Принцип разделения интерфейсов (ISP)

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

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

Проблема с большими интерфейсами заключается в том, что они вызывают большую зависимость. И еще зависимость означает:

  • Больше связи
  • Более хрупкий код
  • Более сложное тестирование
  • Более сложные развертывания

Это первый пример плохого использования универсального интерфейса:

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

Посмотрите на приведенные ниже примеры, все классы являются транспортными средствами, и они реализуют интерфейс IVehicle, но этим классам не нужны все методы, представленные в интерфейсе _3, и поэтому они реализуют нужные им методы и выдают исключение для тех, которые им не нужны. не нужно.

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

Здесь у нас гораздо более конкретные интерфейсы. Реализация одного из них не обязывает нас реализовывать все остальные методы.

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

Поскольку все классы являются транспортными средствами, все они являются производными от класса транспортных средств, реализующего интерфейс IVehicle. Это означает, что все они имеют конкретный метод Run.

Также у каждого транспортного средства есть свои особенности, и они реализуют только то, что должны.

Например, Airplane кроме Vehicle реализует интерфейс IAirplane. Он дает классу Airplane метод Vehicle Run() плюс метод IAirplane.Fly(). Теперь он может только Fly() или Run().

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

Это последний принцип SOLID.

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

Мы должны избегать создания экземпляров классов в нашем коде, то есть избегать использования New() слова (это вызывает связанность).

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

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

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

Кроме того, мы можем принять любой экземпляр, который реализует ICarPainter и ICarEngineAssembler вместо определенного и конкретного типа CarPainter и CarEngineAssembler. Это дает нам гибкость.

Посмотрим на результат рефакторинга (с применением DIP):

Нашему классу CarBuilder не нужно знать, как работают CarPainter и CarEngineAssembler, как реализуются их функции.
Что нужно CarBuilder, так это экземпляр каждого из них и пусть они делают свою работу.
Ничего о реализации, но все об абстракции.