Назад в блог

SOLID: Первые 5 принципов объектно-ориентированного проектирования?

SOLID: Первые 5 принципов объектно-ориентированного проектирования?

Введение

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

Акроним SOLID расшифровывается как:

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

O – Принцип открытости/закрытости

L – Принцип подстановки Барбары Лисков

I – Принцип разделения интерфейса

D – Принцип инверсии зависимостей

В этой статье мы подробно разберем каждый принцип, чтобы понять, как он помогает писать более качественный код. Кроме того, мы добавим фрагменты кода для каждого принципа, чтобы показать, как применять их на практике, а также чего следует избегать в архитектуре чистого кода. Для демонстрации концепций мы будем использовать Kotlin programming язык, разработанный JetBrains и участниками сообщества Open-source.

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

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

Рассмотрим следующий фрагмент кода в качестве примера того, как этот принцип можно применить в Kotlin:

Ключевое слово data в Kotlin указывает на то, что этот класс является классом данных, а это значит, что он предназначен для хранения данных и не содержит сложной логики. Используя этот класс данных, мы можем создать класс UserService  для управления пользователями, как показано ниже:

В этом примере класс UserService имеет одну обязанность: управление пользователями в базе данных. Каждый пользователь представлен кодом data class User , представленным ранее. Все методы в классе UserService связаны с этой обязанностью и, следовательно, согласованы. Это упрощает понимание и поддержку класса, поскольку очевидно, что все его методы связаны с одной четко определенной задачей.

Метод, нарушающий принцип единственной ответственности (SRP) в классе UserService , — это метод, не связанный с основной обязанностью класса, то есть с управлением пользователями в базе данных. Например, рассмотрим следующий вариант класса UserService :

В этом примере метод sendEmail  не связан с основной обязанностью класса UserService, который заключается в управлении пользователями в базе данных. Этот метод отвечает за отправку электронных писем, что является отдельной задачей, не связанной с управлением пользователями. В результате этот метод нарушает принцип SRP, так как он вводит вторую причину для изменения класса UserService.

Чтобы соответствовать принципу единственной ответственности, было бы лучше выделить функциональность отправки электронной почты в отдельный класс, например EmailService . Это позволило бы классу UserService сосредоточиться на своей основной обязанности по управлению пользователями, а классу EmailService — сосредоточиться на своей обязанности по отправке писем.

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

Принцип открытости-закрытости

Принцип открытости-закрытости (Open-Closed Principle, OCP) — это принцип проектирования программного обеспечения, который гласит, что программные сущности (такие как классы, модули или функции) должны быть открыты для расширения, но закрыты для изменения. Это означает, что должна быть возможность добавлять новую функциональность в класс или модуль без изменения его существующего кода.

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

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

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

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

Принцип подстановки Барбары Лисков

Принцип подстановки Барбары Лисков (LSP) — это принцип проектирования программного обеспечения, который утверждает, что объекты суперкласса должны иметь возможность заменяться объектами подкласса без влияния на корректность программы. Это означает, что подкласс должен быть допустимой заменой своего суперкласса и должен вести себя так же, как суперкласс, при использовании в том же контексте.

Мы продолжим демонстрацию, используя User и UserService классы из предыдущих примеров. Чтобы разрешить расширение класса User, мы используем ключевое слово open  в Kotlin:

Вот исходный класс UserService, который использует класс данных User выше:

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

В этом примере класс AdminUser является допустимой заменой класса User, так как он ведет себя так же, как класс User, и может использоваться везде, где ожидается объект User. Например, мы можем использовать класс AdminUser с классом UserService следующим образом:

Этот код корректен, так как класс AdminUser является допустимой заменой класса User и может использоваться так же, как объект User.

Важно отметить, что принцип подстановки Барбары Лисков касается не только наследования. Речь идет о том, чтобы объекты подкласса вели себя так же, как объекты суперкласса, независимо от того, как реализован подкласс. Например, если бы класс AdminUser вел себя как-то иначе, чем класс User, это нарушило бы принцип подстановки Барбары Лисков, так как он не был бы допустимой заменой класса User.

Принцип разделения интерфейса

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

Вот пример того, как можно применить этот принцип, переписав классы User и UserService из предыдущих примеров:

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

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

Ваш код не будет работать, если вы также не реализуете этот метод во всех классах, реализующих UserService интерфейс:

Хотя этот подход работает, он нарушает принцип разделения интерфейса (Interface Segregation Principle), так как заставляет DatabaseUserService  класс реализовывать метод ( searchUsersByEmail ) который ему может быть не нужен или который он не использует.

Более правильным подходом было бы создать отдельный интерфейс  для функции поиска по электронной почте:

Теперь у нас есть отдельные, небольшие и специализированные интерфейсы, а именно UserService  и UserSearchServiceкоторые имеют одну обязанность. Класс, которому требуются все функции этих интерфейсов, может реализовать их, как показано во фрагменте кода ниже:

Это соответствует принципу разделения интерфейса (Interface Segregation Principle), так как гарантирует, что клиенты (такие как класс DatabaseUserService) не вынуждены зависеть от интерфейсов, которые они не используют.

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

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

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

Принцип инверсии зависимостей (Dependency Inversion Principle, DIP) — это принцип проектирования программного обеспечения, который гласит, что модули верхнего уровня не должны зависеть от модулей нижнего уровня; оба типа модулей должны зависеть от абстракций. Это означает, что в целом рекомендуется проектировать программное обеспечение таким образом, чтобы компоненты верхнего уровня не были привязаны к конкретным реализациям компонентов нижнего уровня, а зависели от абстракций (таких как интерфейсы или абстрактные классы), которые могут быть реализованы различными способами.

Давайте рассмотрим пример того, как этот принцип можно применить к классам User и UserService, использовавшимся в предыдущих фрагментах кода:

В этом примере класс UserService зависит от абстракции под названием интерфейс UserRepository, а не от конкретной реализации репозитория пользователей. Это позволяет нам реализовать интерфейс UserRepository различными способами, например, с помощью базы данных или хранилища в памяти, не затрагивая класс UserService.

Например, вот реализация интерфейса UserRepository, которая использует базу данных:

Вот еще одна реализация интерфейса UserRepository, которая использует хранилище в памяти:

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

Заключение

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

Приятного программирования!

author

Preslav Dobrev

Автор · CloudSigma

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

Комментарии

Комментариев пока нет. Будьте первым.