Хочется вспомнить SOLID принципы и рассмотреть, как можно их применять в разработке интерфейсов на примере React компонентов.
S: Single Responsibility Principle (Принцип единственной ответственности). Означает, что каждый класс/функция/компонент должны выполнять только одну конкретную задачу.
На примере React компонента: компонент, который отрисовывает пользовательский интерфейс, не должен содержать в себе логику авторизации этого пользователя.
O: Open-Closed Principle (Принцип открытости-закрытости). Означает, что класс/функция/компонент должны быть открыты для расширения, но закрыты для модификации. Чтобы их можно было расширять новым функционалом, не изменяя при этом исходный код.
На примере React компонента:
На примере выше мы имеем абстрактную базовую кнопку, которую мы не можем изменять, но можем расширять, за счет передачи таких значений как className
, onClick
и children
Еще один способ реализовать данный принцип в React – использовать HOC (читается не как нос, а как хок) (Higher-Order Component) — это функция, которая принимает компонент и возвращает новый компонент с добавленным поведением или функциональностью. Это отличный способ повторно использовать логику в разных компонентах.
L: LSP (Принцип подстановки Барбары Лисков). Это означает, что объекты базовых классов должны быть заменяемы объектами производных классов без изменения ожидаемого поведения программы. То есть производные классов должны мочь использоваться в тех местах, где используется базовый класс без каких-либо изменений
На примере React компонента:
В базовом классе Payment
мы определяем метод processPayment
, который будет обрабатывать платеж. Этот метод должен быть реализован в подклассах.
Оба подкласса (CreditCardPayment
и PayPalPayment
) реализуют метод processPayment
, каждый по-своему. Но оба класса сохраняют контракт, заданный базовым классом, и могут быть использованы взаимозаменяемо.
Функция processOrder
принимает объект paymentMethod
, который является экземпляром любого класса, наследующего Payment
, и вызывает метод processPayment
. Благодаря принципу подстановки Лисков, мы можем передавать как CreditCardPayment
, так и PayPalPayment
в функцию processOrder
, и она будет корректно обрабатывать любой тип платежа.
Тоже самое работает с примером с кнопками, каждый вид кнопки, который расширил базовую, может использоваться вместо базовой без каких-либо изменений, соответственно выполняется прицип LSP.
I: ISP (Принцип разделения интерфейса). Означает что классы/функции/компоненты не должны зависеть от интерфейсов, в которых они не нуждаются.
На примере React компонента: самое простое объяснение нарушения данного принципа — передавать в компонент огромный объект, а использовать только одно его поле.
В примере выше, стоит в компонент UserName
передавать только поле имени, а не весь объект
D: DIP (Принцип инверсии зависимостей). Означает что высокоуровневые модули не должны зависеть от низкоуровневых модулей
На примере React компонента: рассмотрим такой компонент
Мы имеем простой компонент с кнопкой, по нажатию на которую происходит логирование. В данном примере высокоуровневым компонентом у нас является кнопка, а низкоуровневым — функция логгер (ConsoleLogger
) ее нажатия. Как мы видим кнопка напрямую зависит от реализации данного логгера, а согласно принципу, она должна зависеть от абстракции. Проблема в том, что если придется использовать другой способ логирования, то придуется изменять код компонента
Посмотрим на компонент, соответствующий принципу DIP
Мы определили интерфейс функции логгера, создали несколько конкретных реализаций логгера, которые соответствуют определенному интерфейсу. Компонент UserService
теперь принимает логгер через пропс, что позволяет легко менять реализацию логгера, не изменяю при этом код компонента. Благодаря чему, мы можем использовать UserService
с любым логгером, который реализует интерфейс ILogger
Заключение
Таким образом, несмотря на то, что принципы SOLID были изначально разработаны и применялись для ООП разработки, оказывается, что их можно вполне успешно применять в функциональном программировании при разработке интерфейсов — круто
Комментарии (16)
markelov69
26.08.2024 15:19SOLID не работает на практике и в реальной жизни во фронтенд разработке, вообще никак, никогда, без вариантов. Да и не только на фронте, в принципе он очень сильно мешает комфортной разработке, и плюсы которые он дает никаким образом не перекрывают минусы. Слишком уж серьезные минусы.
На hello world'ах которые вы показываете и сами пробуете для тестов, это ещё более менее терпимо, но когда речь заходит о реальном проекте где множество страниц, логики, компонентов, взаимосвязей и т.п. то это всё, там начинается полнейшая дичь, видел я несколько таких попыток применения SOLID'a на реальных проектах. это нечто) Сложность на ровном месте просто повышается в разы, по сравнению с так сказать традиционными подходами.
И самые жирные минусы как раз кроются в том, какой "классный и удобный код" приходится писать если придерживаться всех этих принципов, бонусом как раз таки усложнение серьезное и увеличения кода как таковое и отсюда вытекает ещё один жирнющий минус, это время затраченное на написание кода.and-kushnir Автор
26.08.2024 15:19+3Если его использовать правильно и понимать, для чего ты его используешь, то все работает прекрасно, как на helloWorld примерах, так и на крупных проектах
Если использовать просто лишь бы использовать или потому что так делают все, то можно любую технологию/фреймворк/библиотеку подставить в ваш комментарий вместо слова "SOLID" и сказать, что это не работает
mint_K
26.08.2024 15:19просто с ходу солид применить достаточно сложно.
Сначала нужно проработать архитектуру приложения. К сожалению, реальность таков, что это не то что не возможно, а скорее просто нет на это времени.and-kushnir Автор
26.08.2024 15:19всегда проще сделать "побыстрее", а потом закопаться в тех долге, как раз подобные принципы говорят о том, что нужно продумывать свои решения, прежде, чем их реализовывать
Ну а вообще это же вопрос рефакторинга, который как говорил один известный деятель "Нужно делать ежедневно". Никто не говорит, что нужно взять и переписать огромный проект под какую-то определенную методологию сразу целиком.
Можно представить это как переход с одной устаревшей библиотеки на другую новомодную) Весь новый функционал пишем, используя новую, а в тех местах, где используем старую, в рамках тех долга или по личной инициативе не спеша тоже переводим на новую и в конечном счете в какой-то момент времени все обновится
mint_K
26.08.2024 15:19+2По разному бывает.
Мне приходилось работать с проектами, которые предпочли переписать с реакт на вью. Но, честно говоря, проще было бы рефакторинг сделать. Многие до сих пор ошибочно думают, что переписать проект - это серебренная пуля. Хотя на самом деле новый проект - это новые баги.
А еще хорошо, когда есть тз, но так тоже не везде и не всегда бывает.СЕ ЛЯ ВИ
OlegZH
26.08.2024 15:19В базовом классе
Payment
мы определяем методprocessPayment
, который будет обрабатывать платеж. Этот метод должен быть реализован в подклассах.А почему метод избыточно назван
processPayment
, а не простоprocess
?Оба подкласса (
CreditCardPayment
иPayPalPayment
) реализуют методprocessPayment
, каждый по-своему. Но оба класса сохраняют контракт, заданный базовым классом, и могут быть использованы взаимозаменяемо.А что мы будем делать, если придумаем ещё один вид платежа? Будем вводить ещё один класс? А если мы хотим сделать это (ввести новый вид платежа) без перекомпиляции приложения?
gun_dose
26.08.2024 15:19А почему метод избыточно назван processPayment, а не просто process?
Это уже DRY, а не SOLID :)
powernic
26.08.2024 15:19А что мы будем делать, если придумаем ещё один вид платежа? Будем вводить ещё один класс? А если мы хотим сделать это (ввести новый вид платежа) без перекомпиляции приложения?
В данной статье приводится пример расширения через использование паттерна "стратегия". Для ваше примера расширения нужно использовать другой подход, уже не используя расширение через ввод новых классов
OlegZH
26.08.2024 15:19+1В примере выше, стоит в компонент
UserName
передавать только поле имени, а не весь объектВ таком решении таится опасность: если кто-то захочет изменить логику работы компонента, то может потребоваться получить больше информации от объекта. Но это всё, лишь, взгляд со стороны.
and-kushnir Автор
26.08.2024 15:19Конечно, если компонент UserName станет более сложным и решит, что кроме имени, он будет еще показывать фамилию и дату рождения, то конечно же придется передать в компонент еще два поля.
Возможно в конечном счете данный компонент разрастется настолько, что в него уже проще будет передать весь объект, (даже если в нем будут лишние поля — да иногда не нужно слепо руководствоваться методологиями), чем 10 пропсов. Хотя в таком варианте уже следует пересмотреть реализацию компонента =)
adminNiochen
26.08.2024 15:19Пример про лисков какая-то фигня, там нету никакого реакт компонента
and-kushnir Автор
26.08.2024 15:19Возможно вам стоит перечитать более внимательно, чтобы понять и увидеть там компоненты)
monochromer
26.08.2024 15:19Принцип подстановки Барбары Лисков на примере React компонента:
А где тут React?
and-kushnir Автор
26.08.2024 15:19Тоже самое работает с примером с кнопками, каждый вид кнопки, который расширил базовую, может использоваться вместо базовой без каких-либо изменений, соответственно выполняется прицип LSP.
mint_K
Статья хорошая даже несмотря на то, что вызывает эффект дежавю. ;-)
and-kushnir Автор
Спасибо)
Соглашусь, что тема не новая, просто постарался как-то обобщить под свое видение