SOLID — это аббревиатура, обозначающая первые пять принципов объектно-ориентированного программирования, сформулированные Робертом С. Мартином (также известным как дядя Боб).
Примечание. Хотя эти принципы применимы к разным языкам программирования, в этой статье мы приведем примеры для языка PHP.
Эти принципы устанавливают практики, помогающие создавать программное обеспечение, которое можно обслуживать и расширять по мере развития проекта. Применение этих практик также поможет избавиться от плохого кода, оптимизировать код и создавать гибкое или адаптивное программное обеспечение.
SOLID включает следующие принципы:
В этой статье мы расскажем о каждом из принципов SOLID, которые помогут вам стать лучшим программистом и избавиться от плохого кода.
Принцип единственной ответственности (SRP) гласит:
У класса должна быть одна и только одна причина для изменения, то есть у класса должна быть только одна работа.
Рассмотрим в качестве примера приложение, которое берет набор фигур, состоящий из кругов и квадратов, и рассчитывает сумму площадей всех фигур в наборе.
Для начала мы создадим классы фигур и используем конструкторы для настройки требуемых параметров.
В случае квадратов необходимо знать длину
стороны:
В случае кругов необходимо знать радиус
:
Далее следует создать класс AreaCalculator
и написать логику для суммирования площадей всех заданных фигур. Площадь квадрата равна значению длины в квадрате. Площадь круга равняется значению радиуса в квадрате, умноженному на число пи.
Чтобы использовать класс AreaCalculator
, нужно создать экземпляр класса, передать в него массив фигур и вывести результат внизу страницы.
Вот пример с набором из трех фигур:
Проблема с методом вывода заключается в том, что класс AreaCalculator
использует логику для вывода данных.
Давайте рассмотрим сценарий, в котором вывод необходимо конвертировать в другой формат, например, JSON.
Вся логика будет обрабатываться классом AreaCalculator
. Это нарушит принцип единственной ответственности. Класс AreaCalculator
должен отвечать только за вычисление суммы площадей заданных фигур. Он не должен учитывать, что пользователь хочет получить результат в формате JSON или HTML.
Для решения этой проблемы вы можете создать отдельный класс SumCalculatorOutputter
и использовать этот новый класс для обработки логики, необходимой для вывода данных пользователю:
Класс SumCalculatorOutputter
должен работать следующим образом:
Логика, необходимая для вывода данных пользователю, обрабатывается классом SumCalculatorOutputter
.
Это соответствует принципу единственной ответственности.
Принцип открытости/закрытости гласит:
Объекты или сущности должны быть открыты для расширения, но закрыты для изменения.
Это означает, что у нас должна быть возможность расширять класс без изменения самого класса.
Давайте вернемся к классу AreaCalculator
и посмотрим на метод sum
:
Рассмотрим сценарий, когда пользователю нужно получать сумму
площадей дополнительных фигур, таких как треугольники, пятигранники, шестигранники и т. д. В этом случае нам бы пришлось постоянно редактировать этот файл и добавлять в него дополнительные блоки if
/else
. Это нарушит принцип открытости/закрытости.
Однако мы можем улучшить метод sum
, убрав логику расчета площади каждой фигуры из метода класса AreaCalculator
и прикрепив ее к классу каждой фигуры.
Вот метод area
, определенный в классе Square
:
Вот метод area
, определенный в классе Circle
:
В этом случае метод sum
класса AreaCalculator
можно переписать так:
Теперь вы можете создавать новые классы фигур и передавать их для расчета суммы без нарушения кода.
Однако при этом возникает другая проблема. Как определить, что передаваемый в класс AreaCalculator
объект действительно является фигурой, или что для этой фигуры задан метод area
?
Кодирование в интерфейс является неотъемлемой частью принципов SOLID.
Создайте ShapeInterface
, поддерживающий метод area
:
Измените классы фигур, чтобы реализовать
интерфейс ShapeInterface
.
Вот обновление класса Square
:
А вот обновление класса Circle
:
В методе sum
класса AreaCalculator
вы можете проверить, являются ли фигуры экземплярами ShapeInterface
; а если это не так, программа выдаст исключение:
Это соответствует принципу открытости/закрытости.
Принцип подстановки Лисков гласит:
Пусть q(x) будет доказанным свойством объектов x типа T. Тогда q(y) будет доказанным свойством объектов y типа S, где S является подтипом T.
Это означает, что каждый подкласс или производный класс должен быть заменяемым на базовый класс или родительский класс.
Возьмем класс AreaCalculator
из нашего примера и рассмотрим новый класс VolumeCalculator
, расширяющий класс AreaCalculator
:
Помните, что класс SumCalculatorOutputter
выглядит примерно так:
Если мы попробуем выполнить такой пример:
Когда мы вызовем метод HTML
для объекта $output2
, мы получим сообщение об ошибке E_NOTICE
, информирующее нас о преобразовании массива в строку.
Чтобы исправить это, вместо вывода массива из метода sum класса VolumeCalculator
мы будем возвращать $summedData
:
Значение $summedData
может быть дробным числом, двойным числом или целым числом.
Это соответствует принципу подстановки Лисков.
Принцип разделения интерфейса гласит:
Клиент никогда не должен быть вынужден реализовывать интерфейс, который он не использует, или клиенты не должны вынужденно зависеть от методов, которые они не используют.
Возьмем предыдущий пример с ShapeInterface
. Допустим, нам нужно добавить поддержку новых трехмерных фигур Cuboid
и Spheroid
, и для этих фигур также требуется рассчитывать объем
.
Давайте посмотрим, что произойдет, если мы изменим ShapeInterface
, чтобы добавить новый контракт:
Теперь все создаваемые фигуры должны иметь метод volume
, но мы знаем, что квадраты — двухмерные фигуры, и у них нет объема. В результате этот интерфейс принуждает класс Square
реализовывать метод, который он не может использовать.
Это нарушает принцип разделения интерфейса. Вместо этого мы можем создать новый интерфейс ThreeDimensionalShapeInterface
, в котором имеется контракт volume
, и трехмерные фигуры смогут реализовывать этот интерфейс:
Этот подход намного лучше, но здесь нужно следить за правильностью выбора интерфейса. Вместо использования интерфейса ShapeInterface
или ThreeDimensionalShapeInterface
мы можем создать еще один интерфейс, например ManageShapeInterface
, и реализовать его и для двухмерных, и для трехмерных фигур.
Так мы получим единый API для управления фигурами:
Теперь в классе AreaCalculator
мы можем заменить вызов метода area
вызовом метода calculate
и проверить, является ли объект экземпляром класса ManageShapeInterface
, а не ShapeInterface
.
Это соответствует принципу разделения интерфейса.
Принцип инверсии зависимостей гласит:
Сущности должны зависеть от абстракций, а не от чего-то конкретного. Это означает, что модуль высокого уровня не должен зависеть от модуля низкого уровня, но они оба должны зависеть от абстракций.
Этот принцип открывает возможности разъединения.
Вот пример модуля PasswordReminder
, подключаемого к базе данных MySQL:
Во-первых, MySQLConnection
— это модуль низкого уровня, а PasswordReminder
— модуль высокого уровня, однако определение D в принципах SOLID гласит: зависимость от абстракций, а не от чего-то конкретного. В приведенном выше фрагменте этот принцип нарушен, потому что класс PasswordReminder
вынужденно зависит от класса MySQLConnection
.
Если впоследствии вам потребуется изменить систему базы данных, вам также будет нужно изменить класс PasswordReminder
, а это нарушит принцип открытости/закрытости.
Класс PasswordReminder
не должен зависеть от того, какую базу данных использует ваше приложение. Чтобы решить эти проблемы, вы можете запрограммировать интерфейс, поскольку модули высокого уровня и низкого уровня должны зависеть от абстракции:
Интерфейс содержит метод connect, и класс MySQLConnection
реализует этот интерфейс. Вместо того, чтобы прямо указывать тип класса MySQLConnection
в конструкторе PasswordReminder
, мы указываем тип класса DBConnectionInterface
, и в этом случае, какую бы базу данных ни использовало ваше приложение, класс PasswordReminder
сможет подключиться к этой базе данных без каких-либо проблем, и принцип открытости/закрытости не будет нарушен.
В этом коде модули высокого уровня и модули низкого уровня зависят от абстракции.
В этой статье мы рассказали о пяти принципах SOLID, применяемых в объектно-ориентированном программировании. Проекты, соответствующие принципам SOLID, можно передавать коллегам, расширять, модифицировать, тестировать и перерабатывать с меньшим количеством сложностей.
Чтобы продолжить обучение, прочитайте о других практиках Agile и разработки адаптивного программного обеспечения.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
SOLID: 5 принципов объектно-ориентированного программирования | DigitalOcean
Hi! Maybe it need to change class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface { public function area() { // calculate the surface area of the cuboid }
}
to: … public function calculate() { return $this->volume(); } …