Тот код, который я приводил, это код компонента приложения. Он собирает из повторно используемых компонентов конкретное окно, для решения конкретной задачи. this.state.showCloseButton — это его состояние, часть логики приложения. Откуда он взялся, это уже определяет бизнес-логика и предметная область.
Чтобы перевести мой пример на чистый js, давайте сделаем компоненты функциями. Функция-компонент принимает на вход параметры и массив других, дочерних компонентов. Возвращает функция свои html-теги, внутрь которых вставлено то, что вернули дочерние компоненты.
var html =
Form({onSend: this.onSend, isOpen: this.state.open}, [
FormHeader({}, [
RawText("MyForm") ]),
this.state.showCloseButton && FormCloseButton({onClick: this.onClose}),
StandardFormButtonPanel()
]);
Здесь функция Form ничего не знает о дочерних компонентах. Она может выглядеть примерно так:
function Form(options, children) {
return `<form onsubmit=${options.onSubmit}>${children.join("")}</form>`;
}
Это конечно все скорее псевдокод, но идея думаю ясна.
O (open-closed) — никогда, ни при каких обстоятельствах не модифицируем код компонентов, которые часто используются;
D (dependency inversion) — решение о том, какой из компонентов будет использован в каждом случае, должен принимать вызывающий код.
Из-за того, что нарушена D пришлось нарушить S. Решение о рендеринге FormCloseButton принимает Form, а не вызывающий код. Поэтому, когда нам понадобилось изменить это поведение, нам пришлось менять Form. А изменение повторно используемого компонента это зло.
В целом, Ваш Form по сути является оберткой, о которых я пишу. Однако, обертка не должна изменяться. Если нужно другое поведение, то нужно заменять обертку.
По поводу скрытия кнопки в моем варианте все проще:
Фишка в том, что я убрал CloseButton для моей формы, но при этом не поменял ни один из повторно-используемых компонентов. То есть, никакой код в других местах приложения, который использовал Form и т.д. не мог поломаться даже теоретически — мы не добавляли опций, не изменяли кода Form.
добавляешь что-то типа {parent: selector, controls: false}
Это описано в секции статьи "Как делать не надо":
Неизбежно разрастающееся количество опций у компонента — это "попахивает", но с этим еще как-то можно мириться. Вот что действительно ужасно, так это то, что каждый нетипичный случай использования компонента заставлял нас изменять компонент, который используется во многих других местах. При добавлении каждой опции мы правили код, правили верстку и этим теоретически могли сломать логику где-то еще, где используется то же модальное окно. То есть, добавление новых фич грозит появлением регрессий в самых неожиданных местах.
Описанный подход решает проблему следующим образом:
Ключевой момент здесь в том, что мы вместо изменения компонента, который используется во многих местах, просто заменять его на другой. Это важно для компонентов в веб, потому что любое изменение в стилях может повлечь нарушение верстки в каких-то обстоятельствах использования компонента. Единственный надежный способ обезопасить себя от этого — не изменять однажды написанные компоненты (если они многократно используются).
Ну а что если придется менять не Modal, а конкретную эту самую составную часть
Составные повторно используемые компоненты не должны изменяться, также как и простые. Если что-то не устраивает, то просто заменяешь его. Код составного компонента тривиален, ничего не стоит его заменить.
попробовать решения аля CSSModules,
CssModules очень хорошо ложится на эту концепцию. Стили каждого компонента изолируются, и это создает дополнительную защиту от говнокода. То есть, это вынуждает пользователя не менять компонент, а заменять.
правило трех
Да, что-то в этом роде. Если код используется в 3+ местах, то стоит подумать над созданием переиспользуемого компонента пускаясь во все тяжкие SOLID (это довольно дорого).
Проблема не столько в количество props (хотя это тоже плохо), сколько в стабильности. Каждый раз, когда нужно будет специальное окно, придется добавлять новую опцию и менять Modal. Весь описанный подход сводится к тому, чтобы не изменять компоненты, которые уже кем-то используются. Ты просто заменяешь те части, которые тебе не подходят, и делаешь любую кастомизацию не ломая уже существующий код.
Все это так важно для фронтенда именно потому, что здесь даже малейшее изменение font-size может сломать всю верстку самым непредсказуемым образом.
Тут ведь еще какой момент, если мы в одном месте подменим Backdrop на FadeBackdrop, то будет ли везде работать также как работало раньше? К сожалению, в веб без тестирования трудно это утверждать. Где-то кто-то может переопределить стили, и с FadeBackdrop там верстка посыпется. Поэтому делать такого рода гибкость во фронтенде я не вижу смысла. Лучше руками поправить везде и точно знать, где именно ты поправил и где нужно протестить.
DI я понимаю здесь именно в том смысле, что каждый LEGO кусочек не должен предполагать с кем именно он будет взаимодействовать. Очень легко попасть в эту ловушку — пишешь Menu, и подразумеваешь, что внутри будут MenuItem (специальные компоненты). Завязываешь стили внутри, так что кроме MenuItem ничего внутри быть не может. Подход заключается именно в том, что Menu должен быть максимально agnostic относительно своих итемов, чтобы туда можно было положить SuperMenuItem. Это сложно, но это окупается с лихвой. Ну и тем более, Menu сам явно не должен создавать MenuItem, а передавать эту обязанность вызыввающему коду.
Конечно, все принципы здесь применены очень в неявном виде. Здесь скорее дух этих принципов, чем они сами. Например, Seemann пишет, что в общем случае DI is a passing an argument. Именно это я и делаю, передаю дочерние компоненты как параметры (children или аттрибуты).
По моему опыту для "популярных" компонентов (вроде модального окна), это мега актуально. С первых дней внедрения я начал использовать всю эту гибкость на 100% (возможно потому, что именно недостаток этой гибкости в прошлой реализации и подвинул меня ко всему этому). Постоянно нужны какие-то особые модальные окна, без кнопки закрытия, или которые не закрываются по щелчку на бэкдроп, или с полоской вверху цветной — очень много вариантов.
Там выше NimElennar упоминал, что хорошо бы использвать facade. Мне казалось это очевидным, но пожалуй допишу в статью. Дело в том, что не обязательно составлять каждый раз окно из самых маленьких кирпичиков. Можно составить из них чуть более крупные, и использовать уже их. Это никак не нарушает принципа, т.к. всегда можно выкинуть эти крупные сборки, и сделать другие (или использовать исходные, маленькие составляющие).
То есть, если есть стандартные случаи, то для них можно сделать обобщающие компоненты с более простым интерфейсом. В нестандартных случаях можно будет вернуться ко всей этой сложности.
Смотрите, Вы разделили один интерфейс на несколько, более простых. Я разделяю компоненты на несколько, более простых (содержащих меньше опций, т.к. у компонентов кроме опций ничего нет — это их интерфейс). То есть, тоже самое.
Про DI, я понимаю о чем Вы, я даже делал свою либу для TypeScript. Seeman пишет, что лучший DI это через конструктор. Он еще не очень любит DI-контейнеры, и в CompositionRoot предпочитает кидать зависимости в конструктор вручную (Poor Man's DI). В этом случае весь DI сводится к тому, что вы создаете объекты и передаете их параметрами в конструктор другим объектам.
В случае с React все немного иначе. Во-первых, мы не можем использовать конструктор, реакт сам создает объекты. Вместо этого, мы передаем зависимости либо через аттрибуты, либо через children. Последнее выглядит так:
Здесь ModalDialogContent передается как children для ModalDialog. ModalDialog при этом задает требования к children — они должны быть реакт-компонентами. То есть, реализовывать соответствующий интерфейс. Он может, конечно, потребовать более конкретный интерфейс, но часто этого не нужно.
К сожалению, настоящий CompositionRoot здесь затруднительно сделать, и вообще не нужно. Вместо этого, какждый компонент приложения выступает в качестве мини-composition-root, и собирает, допустим, модальное окно для своих нужд. Для фронтенда такой гибкости в управлении зависимости обычно достаточно.
Компоненты, о которых я говорю, в основном только версткой и занимаются. Бывают компоненты с логикой, там рефакторинг и unit-тесты возможны, да.
Легче один раз написать и отладить helper (в общепринятой терминологии facade), чем помнить как, с какими параметрами и в какой последовательности нужно выполнить 5-10 простых, казалось бы, операций.
Разумно, соглашусь.
Принцип interface segregation советует разделить эти два варианта на разные классы/компоненты, так как по отдельности ими будет легче пользоваться.
Вроде именно об этом я и пишу. Подскажите, в чем отличие?
может динамически (то есть в runtime) выбрать один из нескольких вариантов
Когда резолвить зависимости — runtime или compile-time это уже не так принципиально, и зависит от потребностей в каждом случае. Среди задач которые мне встречались, динамическое сопоставление зависимостей привнесло бы только лишнюю сложность и не дало никакой выгоды. Можно, конечно, для каждого LEGO придумать фабрику и т.д., но это уже будет over-проектирование. Для фронтенда я такого не встречал.
Дело в том, что в web даже если ты немножко меняешь верстку, то это может сильно повлиять на тех, кто использует. Кроме того, верстку очень трудно покрывать unit-тестами (почти невозможно). Поэтому я ужесточаю правило.
single responsibility несёт в себе несколько иной смысл
Полностью с Вами согласен, но все-же статью править не буду. Именно из-за специфики веба, размер здесь особенно важен.
я бы вынес в отдельный factory-класс
Да, можно делать хелперы, которые принимают набор параметров, и собирают из них LEGO нужной конфигурации. Я не стал это описывать, мне кажется это очевидным. Но использовать эти хелперы не обязательно, всегда можно заюзать компоненты напрямую. В этом смысл всего подхода.
но в самом решении конструирования диалогового окна из кубиков принцип I вообще не соблюдается.
Поясните, не пойму эту мысль. Когда мы конструируем окно, то речь уже идет о "компонентах приложения". Как я описываю в последней части статьи, здесь уже не применяется SOLID, там уже играют роль другие соображения — целостность логики и т.д.
Принципы L и D я в предложенном решении не вижу
L: Вы же сами пишите — "компонент может быть заменен на любой другой" — именно это и фигурирует в названии статьи "заменяй и властвуй". D: вызывающий код выбирает, из каких компонентов он собирает окно. Вызывающий код является внешним для компонентов LEGO, то есть зависимости резолвятся из вне. Что это если не DI?
Спасибо, можете еще добавить, что из этого не соответствует представленному в статье описанию? Не обязательно маленькие, здесь я согласен. Но в случае компонентов в веб они чаще всего маленькие. Если они большие, то скорее всего там слишком много ответственности. В букве О я делаю чуть более жесткие требования — не рекомендую вообще менять, даже рефакторить. D — разве суть DI не в том, чтобы зависимости резолвились из вне? Или все что не Composition Root это не DI?
Я выше уже писал, это статья не о SOLID. Я, как и Павел, просто используем идеи из SOLID. Или Вы считаете, что если у меня функция вместо класса, то принципы SOLID никак нельзя применить?
Спасибо за развернутые комментарии. Эта статья не о SOLID, и тем более не об ООП. Эта статья о фронтенде. Если Вы внимательно посмотрите, то компоненты это вообще функции, тут даже классов нет. Я не преследовал цель описать SOLID в классическом понимании, я описываю как писать универсальные компоненты, руководствуясь идеями, заимствоваными из SOLID подхода.
В абзаце который Вы привели речь идет о конкретном меню пользователя, которое специфично конкретно для данного сайта, для его предметной области. Это меню может быть реализовано через повторно используемые компоненты — меню, иконка и т.д. Поэтому я и говорю о разделении компонентов:
Отсюда следует, что нужно четко разделять повторно используемые компоненты и компоненты бизнес-логики.
Все правильно, модальное окно это повторно-используемый компонент, который не должен быть связан с бизнес-логикой текущего приложения. Его можно оформить в виде отдельного npm-пакета, как это, например, делают эти парни или использовать готовый. Я где-то написал, что модальное окно это бизнес-логика?
Тот код, который я приводил, это код компонента приложения. Он собирает из повторно используемых компонентов конкретное окно, для решения конкретной задачи. this.state.showCloseButton — это его состояние, часть логики приложения. Откуда он взялся, это уже определяет бизнес-логика и предметная область.
Чтобы перевести мой пример на чистый js, давайте сделаем компоненты функциями. Функция-компонент принимает на вход параметры и массив других, дочерних компонентов. Возвращает функция свои html-теги, внутрь которых вставлено то, что вернули дочерние компоненты.
Здесь функция Form ничего не знает о дочерних компонентах. Она может выглядеть примерно так:
Это конечно все скорее псевдокод, но идея думаю ясна.
В вашей реализации нарушены буквы O и D из SOLID:
Из-за того, что нарушена D пришлось нарушить S. Решение о рендеринге FormCloseButton принимает Form, а не вызывающий код. Поэтому, когда нам понадобилось изменить это поведение, нам пришлось менять Form. А изменение повторно используемого компонента это зло.
В целом, Ваш Form по сути является оберткой, о которых я пишу. Однако, обертка не должна изменяться. Если нужно другое поведение, то нужно заменять обертку.
По поводу скрытия кнопки в моем варианте все проще:
До (будем считать, что использованы некоторые обертки):
После:
Фишка в том, что я убрал CloseButton для моей формы, но при этом не поменял ни один из повторно-используемых компонентов. То есть, никакой код в других местах приложения, который использовал Form и т.д. не мог поломаться даже теоретически — мы не добавляли опций, не изменяли кода Form.
Это описано в секции статьи "Как делать не надо":
Описанный подход решает проблему следующим образом:
Если в каком-то случае Вам нужна была новая фича от Form, которой не было предусмотрено. Что в этом случае Вы делали?
Спасибо за конструктивный диалог, дополнил статью.
Составные повторно используемые компоненты не должны изменяться, также как и простые. Если что-то не устраивает, то просто заменяешь его. Код составного компонента тривиален, ничего не стоит его заменить.
CssModules очень хорошо ложится на эту концепцию. Стили каждого компонента изолируются, и это создает дополнительную защиту от говнокода. То есть, это вынуждает пользователя не менять компонент, а заменять.
Да, что-то в этом роде. Если код используется в 3+ местах, то стоит подумать над созданием переиспользуемого компонента пускаясь во все тяжкие SOLID (это довольно дорого).
Проблема не столько в количество props (хотя это тоже плохо), сколько в стабильности. Каждый раз, когда нужно будет специальное окно, придется добавлять новую опцию и менять Modal. Весь описанный подход сводится к тому, чтобы не изменять компоненты, которые уже кем-то используются. Ты просто заменяешь те части, которые тебе не подходят, и делаешь любую кастомизацию не ломая уже существующий код.
Все это так важно для фронтенда именно потому, что здесь даже малейшее изменение font-size может сломать всю верстку самым непредсказуемым образом.
Тут ведь еще какой момент, если мы в одном месте подменим Backdrop на FadeBackdrop, то будет ли везде работать также как работало раньше? К сожалению, в веб без тестирования трудно это утверждать. Где-то кто-то может переопределить стили, и с FadeBackdrop там верстка посыпется. Поэтому делать такого рода гибкость во фронтенде я не вижу смысла. Лучше руками поправить везде и точно знать, где именно ты поправил и где нужно протестить.
DI я понимаю здесь именно в том смысле, что каждый LEGO кусочек не должен предполагать с кем именно он будет взаимодействовать. Очень легко попасть в эту ловушку — пишешь Menu, и подразумеваешь, что внутри будут MenuItem (специальные компоненты). Завязываешь стили внутри, так что кроме MenuItem ничего внутри быть не может. Подход заключается именно в том, что Menu должен быть максимально agnostic относительно своих итемов, чтобы туда можно было положить SuperMenuItem. Это сложно, но это окупается с лихвой. Ну и тем более, Menu сам явно не должен создавать MenuItem, а передавать эту обязанность вызыввающему коду.
Конечно, все принципы здесь применены очень в неявном виде. Здесь скорее дух этих принципов, чем они сами. Например, Seemann пишет, что в общем случае DI is a passing an argument. Именно это я и делаю, передаю дочерние компоненты как параметры (children или аттрибуты).
По моему опыту для "популярных" компонентов (вроде модального окна), это мега актуально. С первых дней внедрения я начал использовать всю эту гибкость на 100% (возможно потому, что именно недостаток этой гибкости в прошлой реализации и подвинул меня ко всему этому). Постоянно нужны какие-то особые модальные окна, без кнопки закрытия, или которые не закрываются по щелчку на бэкдроп, или с полоской вверху цветной — очень много вариантов.
Там выше NimElennar упоминал, что хорошо бы использвать facade. Мне казалось это очевидным, но пожалуй допишу в статью. Дело в том, что не обязательно составлять каждый раз окно из самых маленьких кирпичиков. Можно составить из них чуть более крупные, и использовать уже их. Это никак не нарушает принципа, т.к. всегда можно выкинуть эти крупные сборки, и сделать другие (или использовать исходные, маленькие составляющие).
То есть, если есть стандартные случаи, то для них можно сделать обобщающие компоненты с более простым интерфейсом. В нестандартных случаях можно будет вернуться ко всей этой сложности.
Смотрите, Вы разделили один интерфейс на несколько, более простых. Я разделяю компоненты на несколько, более простых (содержащих меньше опций, т.к. у компонентов кроме опций ничего нет — это их интерфейс). То есть, тоже самое.
Про DI, я понимаю о чем Вы, я даже делал свою либу для TypeScript. Seeman пишет, что лучший DI это через конструктор. Он еще не очень любит DI-контейнеры, и в CompositionRoot предпочитает кидать зависимости в конструктор вручную (Poor Man's DI). В этом случае весь DI сводится к тому, что вы создаете объекты и передаете их параметрами в конструктор другим объектам.
В случае с React все немного иначе. Во-первых, мы не можем использовать конструктор, реакт сам создает объекты. Вместо этого, мы передаем зависимости либо через аттрибуты, либо через children. Последнее выглядит так:
Здесь ModalDialogContent передается как children для ModalDialog. ModalDialog при этом задает требования к children — они должны быть реакт-компонентами. То есть, реализовывать соответствующий интерфейс. Он может, конечно, потребовать более конкретный интерфейс, но часто этого не нужно.
К сожалению, настоящий CompositionRoot здесь затруднительно сделать, и вообще не нужно. Вместо этого, какждый компонент приложения выступает в качестве мини-composition-root, и собирает, допустим, модальное окно для своих нужд. Для фронтенда такой гибкости в управлении зависимости обычно достаточно.
Компоненты, о которых я говорю, в основном только версткой и занимаются. Бывают компоненты с логикой, там рефакторинг и unit-тесты возможны, да.
Разумно, соглашусь.
Вроде именно об этом я и пишу. Подскажите, в чем отличие?
Когда резолвить зависимости — runtime или compile-time это уже не так принципиально, и зависит от потребностей в каждом случае. Среди задач которые мне встречались, динамическое сопоставление зависимостей привнесло бы только лишнюю сложность и не дало никакой выгоды. Можно, конечно, для каждого LEGO придумать фабрику и т.д., но это уже будет over-проектирование. Для фронтенда я такого не встречал.
Дело в том, что в web даже если ты немножко меняешь верстку, то это может сильно повлиять на тех, кто использует. Кроме того, верстку очень трудно покрывать unit-тестами (почти невозможно). Поэтому я ужесточаю правило.
Полностью с Вами согласен, но все-же статью править не буду. Именно из-за специфики веба, размер здесь особенно важен.
Да, можно делать хелперы, которые принимают набор параметров, и собирают из них LEGO нужной конфигурации. Я не стал это описывать, мне кажется это очевидным. Но использовать эти хелперы не обязательно, всегда можно заюзать компоненты напрямую. В этом смысл всего подхода.
Поясните, не пойму эту мысль. Когда мы конструируем окно, то речь уже идет о "компонентах приложения". Как я описываю в последней части статьи, здесь уже не применяется SOLID, там уже играют роль другие соображения — целостность логики и т.д.
L: Вы же сами пишите — "компонент может быть заменен на любой другой" — именно это и фигурирует в названии статьи "заменяй и властвуй". D: вызывающий код выбирает, из каких компонентов он собирает окно. Вызывающий код является внешним для компонентов LEGO, то есть зависимости резолвятся из вне. Что это если не DI?
Спасибо, можете еще добавить, что из этого не соответствует представленному в статье описанию? Не обязательно маленькие, здесь я согласен. Но в случае компонентов в веб они чаще всего маленькие. Если они большие, то скорее всего там слишком много ответственности. В букве О я делаю чуть более жесткие требования — не рекомендую вообще менять, даже рефакторить. D — разве суть DI не в том, чтобы зависимости резолвились из вне? Или все что не Composition Root это не DI?
Я выше уже писал, это статья не о SOLID. Я, как и Павел, просто используем идеи из SOLID. Или Вы считаете, что если у меня функция вместо класса, то принципы SOLID никак нельзя применить?
Спасибо за развернутые комментарии. Эта статья не о SOLID, и тем более не об ООП. Эта статья о фронтенде. Если Вы внимательно посмотрите, то компоненты это вообще функции, тут даже классов нет. Я не преследовал цель описать SOLID в классическом понимании, я описываю как писать универсальные компоненты, руководствуясь идеями, заимствоваными из SOLID подхода.
Впрочем я понял Вас, я выбрал неудачный термин "компоненты бизнес-логики". Пожалуй да, стоит заменить на "компоненты приложения".
В абзаце который Вы привели речь идет о конкретном меню пользователя, которое специфично конкретно для данного сайта, для его предметной области. Это меню может быть реализовано через повторно используемые компоненты — меню, иконка и т.д. Поэтому я и говорю о разделении компонентов:
Все правильно, модальное окно это повторно-используемый компонент, который не должен быть связан с бизнес-логикой текущего приложения. Его можно оформить в виде отдельного npm-пакета, как это, например, делают эти парни или использовать готовый. Я где-то написал, что модальное окно это бизнес-логика?