Solid principles
Solid principles

Хочется вспомнить SOLID принципы и рассмотреть, как можно их применять в разработке интерфейсов на примере React компонентов.

S: Single Responsibility Principle (Принцип единственной ответственности). Означает, что каждый класс/функция/компонент должны выполнять только одну конкретную задачу.

На примере React компонента: компонент, который отрисовывает пользовательский интерфейс, не должен содержать в себе логику авторизации этого пользователя.

O: Open-Closed Principle (Принцип открытости-закрытости). Означает, что класс/функция/компонент должны быть открыты для расширения, но закрыты для модификации. Чтобы их можно было расширять новым функционалом, не изменяя при этом исходный код.

На примере React компонента:

Open-Closed Principle
Open-Closed Principle

На примере выше мы имеем абстрактную базовую кнопку, которую мы не можем изменять, но можем расширять, за счет передачи таких значений как className, onClick и children

Еще один способ реализовать данный принцип в React – использовать HOC (читается не как нос, а как хок) (Higher-Order Component) — это функция, которая принимает компонент и возвращает новый компонент с добавленным поведением или функциональностью. Это отличный способ повторно использовать логику в разных компонентах.

Пример использования HOC с компонентом кнопки
Пример использования HOC с компонентом кнопки

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

На примере React компонента:

LSP
LSP

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

Оба подкласса (CreditCardPayment и PayPalPayment) реализуют метод processPayment, каждый по-своему. Но оба класса сохраняют контракт, заданный базовым классом, и могут быть использованы взаимозаменяемо.

Функция processOrder принимает объект paymentMethod, который является экземпляром любого класса, наследующего Payment, и вызывает метод processPayment. Благодаря принципу подстановки Лисков, мы можем передавать как CreditCardPayment, так и PayPalPayment в функцию processOrder, и она будет корректно обрабатывать любой тип платежа.

Тоже самое работает с примером с кнопками, каждый вид кнопки, который расширил базовую, может использоваться вместо базовой без каких-либо изменений, соответственно выполняется прицип LSP.

I: ISP (Принцип разделения интерфейса). Означает что классы/функции/компоненты не должны зависеть от интерфейсов, в которых они не нуждаются.

На примере React компонента: самое простое объяснение нарушения данного принципа — передавать в компонент огромный объект, а использовать только одно его поле.

Нарушение принципа ISP
Нарушение принципа ISP

В примере выше, стоит в компонент UserName передавать только поле имени, а не весь объект

Соответствие принципу ISP
Соответствие принципу ISP

D: DIP (Принцип инверсии зависимостей). Означает что высокоуровневые модули не должны зависеть от низкоуровневых модулей

На примере React компонента: рассмотрим такой компонент

Пример нарушения DIP
Пример нарушения DIP

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

Посмотрим на компонент, соответствующий принципу DIP

Компонент, который соответствует принципу DIP
Компонент, который соответствует принципу DIP

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

Пример использования
Пример использования

Заключение

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

Комментарии (16)


  1. mint_K
    26.08.2024 15:19

    Статья хорошая даже несмотря на то, что вызывает эффект дежавю. ;-)


    1. and-kushnir Автор
      26.08.2024 15:19

      Спасибо)

      Соглашусь, что тема не новая, просто постарался как-то обобщить под свое видение


  1. markelov69
    26.08.2024 15:19

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

    На hello world'ах которые вы показываете и сами пробуете для тестов, это ещё более менее терпимо, но когда речь заходит о реальном проекте где множество страниц, логики, компонентов, взаимосвязей и т.п. то это всё, там начинается полнейшая дичь, видел я несколько таких попыток применения SOLID'a на реальных проектах. это нечто) Сложность на ровном месте просто повышается в разы, по сравнению с так сказать традиционными подходами.

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


    1. and-kushnir Автор
      26.08.2024 15:19
      +3

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

      Если использовать просто лишь бы использовать или потому что так делают все, то можно любую технологию/фреймворк/библиотеку подставить в ваш комментарий вместо слова "SOLID" и сказать, что это не работает


    1. mint_K
      26.08.2024 15:19

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


      1. and-kushnir Автор
        26.08.2024 15:19

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

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

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


        1. mint_K
          26.08.2024 15:19
          +2

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

          А еще хорошо, когда есть тз, но так тоже не везде и не всегда бывает.

          СЕ ЛЯ ВИ


  1. OlegZH
    26.08.2024 15:19

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

    А почему метод избыточно назван processPayment, а не просто process?

    Оба подкласса (CreditCardPayment и PayPalPayment) реализуют метод processPayment, каждый по-своему. Но оба класса сохраняют контракт, заданный базовым классом, и могут быть использованы взаимозаменяемо.

    А что мы будем делать, если придумаем ещё один вид платежа? Будем вводить ещё один класс? А если мы хотим сделать это (ввести новый вид платежа) без перекомпиляции приложения?


    1. gun_dose
      26.08.2024 15:19

      А почему метод избыточно назван processPayment, а не просто process?

      Это уже DRY, а не SOLID :)


    1. powernic
      26.08.2024 15:19

      А что мы будем делать, если придумаем ещё один вид платежа? Будем вводить ещё один класс? А если мы хотим сделать это (ввести новый вид платежа) без перекомпиляции приложения?

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


  1. OlegZH
    26.08.2024 15:19
    +1

    В примере выше, стоит в компонент UserName передавать только поле имени, а не весь объект

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


    1. and-kushnir Автор
      26.08.2024 15:19

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

      Возможно в конечном счете данный компонент разрастется настолько, что в него уже проще будет передать весь объект, (даже если в нем будут лишние поля — да иногда не нужно слепо руководствоваться методологиями), чем 10 пропсов. Хотя в таком варианте уже следует пересмотреть реализацию компонента =)


  1. adminNiochen
    26.08.2024 15:19

    Пример про лисков какая-то фигня, там нету никакого реакт компонента


    1. and-kushnir Автор
      26.08.2024 15:19

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


  1. monochromer
    26.08.2024 15:19

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

    А где тут React?


    1. and-kushnir Автор
      26.08.2024 15:19

      Тоже самое работает с примером с кнопками, каждый вид кнопки, который расширил базовую, может использоваться вместо базовой без каких-либо изменений, соответственно выполняется прицип LSP.